跳到主要内容

交易所

加密货币

什么是代币:

代币可以在以太坊中表示任何东西:

  • 在线平台中的信誉积分
  • 金融资产类似于公司股份的资产
  • 像美元一样的法定货币
  • 一盎司黄金
  • 及更多...

ERC-20就是一套基于以太坊网络的标准代币发行协议。有了ERC-20,开发者们得以高效、可靠、低成本地创造专属自己项目的代币,我们甚至可以将ERC-20视为以太坊网络为早期区块链世界做出的最重要贡献

ERC-20的功能示例包括:

  • 将代币从一个帐户转到另一个帐户
  • 获取帐户的当前代币余额
  • 获取网络上可用代币的总供应量
  • 批准一个帐户中一定的代币金额由第三方帐户使用

A standard interface for tokens,通过不同的标准接口实现自己的代币,将来部署在区块链上,因为他本来就是智能合约

代币合约

接下来使用ERC-20写我们的代币(合约)

需要实现ERC-20要求的方法,对外公开的状态变量会自动生成getter方法

DolToken.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
// 导入安全数学方法
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
contract DolToken {
using SafeMath for uint256;//为uint256后面使用sub,add方法
string public name = 'DolToken';//自定生成getter方法
string public symbol = 'DOL';//令牌的名称
uint256 public decimals = 18;//令牌使用的小数位数
uint256 public totalSupply;//供应量
// mapping类型
mapping(address => uint256) public balanceOf;//返回地址为一个帐户的帐户余额
// 事件在合约中一旦被触发,会把传递进来的参数存储到交易的日志中,与合约的地址关联,合并到区块链中,区块可以访问交易的日志,前端可以对日志进行追踪,如前端可以监听一笔交易完成后在请求获取余额,还可以订阅transfer事件
event Transfer(address indexed _from, address indexed _to, uint256 _value);//本身是在代币协议中必须要求的,不可更改,公开透明,以太坊也会订阅这个事件

constructor(){
totalSupply = 1000000 * (10 ** decimals);
// 部署智能合约需要一个账号从里面扣款,从配置中拿到from里填写的账户,测试时不填默认是第一账户
balanceOf[msg.sender] = totalSupply;//部署账号拥有所有代币
}
function transfer(address _to, uint256 _value) public returns (bool success){
require(_to != address(0));//需要有效地址
_transfer(msg.sender, _to, _value);
return true;
}
function _transfer(address _from,address _to,uint256 _value) internal {
require(balanceOf[_from] >= _value);//true正常执行,否则抛出错误,会被以太坊的接口接收纳入到日志中, 同时退回gas
// 从哪个账号发起的调用者
balanceOf[_from] = balanceOf[_from].sub(_value);
balanceOf[_to] = balanceOf[_to].add(_value);
// 触发事件
emit Transfer(_from, _to, _value);
}
}

另外需要安装一个库

npm i openzeppelin-solidity

注意在使用库的使用,SafeMath.sol约束solidity编译器的版本要小于8.0,目前默认是0.8.19,需要降低,修改配置文件中的compilers.solc.version0.7.5或者其他适合的版本

测试代币

测试方案就是在账户间互相转账,默认给了第1个账户全部资产,就测试从它往其他账户转账

testToken.js
const Contacts = artifacts.require("DolToken.sol")
const fromWei = (bn)=>{
return web3.utils.fromWei(bn,"ether");
}
const toWei = (number)=>{
return web3.utils.toWei(number.toString(),"ether");
}
module.exports = async function(callback){
const token = await Contacts.deployed()
// 转账
await token.transfer('0x72e32551B6eF58bE2b125F09c4754Daa760d12f7',toWei(10000),{
from:'0xD1Bc206C1Dba42Cf01A1427dA56e61Dda31dd2bf'
})
// 获取第一个账号
const firstCount = await token.balanceOf
('0xD1Bc206C1Dba42Cf01A1427dA56e61Dda31dd2bf')
console.log('第一',fromWei(firstCount))
// 获取第二个账号
const secondCount = await token.balanceOf
('0x72e32551B6eF58bE2b125F09c4754Daa760d12f7')
console.log('第二',fromWei(secondCount))
callback()
}
truffle exec ./scripts/testToken.js

多次执行测试,转账成功,金额会变化

image-20230312130118724

添加资产

除了在控制台打印资产,还可以到插件里添加资产

image-20230312130411644

填写必要信息

  • 合约地址:build目录的json文件中networks里的address字段
  • 其他两项会自动生成,否则填入配置

image-20230312130629622

添加时可以看到余额,添加后应当在主页看到,目前不可以

image-20230312131011231

委托交易

把一定数量的资产授权给交易所,然后可以把不大于授权金额的资金存到交易所,即转账给交易所账号

转账到交易所需要交易所调用代币合约中的transferFrom,从用户转账到交易所一定的金额

授权

approve这个函数很重要,一旦授权给不良的合约,钱就可能被转走

事件是以太坊中特殊的数据结构,传的参数都会被永久的记录,将来可以回溯拿到这些事件,前端也可以订阅事件,只要授权就会收到对应的通知

mapping(address => mapping(address => uint256)) public allowance;
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
function approve(address _spender, uint256 _value) public returns (bool success){
// msg.sender 当前网页登录账号
// _spender 第三方交易所的账号地址
// _value 授权钱数
require(_spender != address(0));
allowance[msg.sender][_spender] = _value;
emit Approval(msg.sender,_spender,_value);
return true;
/*
不同账号授权给不同交易所的额度的Js数据结构
{
user1:{
A:100,
B:200,
}
user2:{
A:100,
C:200,
}
}
*/
}

从账户A转到账户B

实现transferFrom方法,被授权的交易所调用

function transferFrom(address _from, address _to, uint256 _value) public returns (bool success){
// _from某个放款账号
// _to 收款账号
// msg.sender 交易所账号地址
require(balanceOf[_from] >= _value);
require(allowance[_from][msg.sender] >= _value);//账户和授权了的金额不小于当前转账金额
// 从授权金额中减去准备转账的值
allowance[_from][msg.sender] = allowance[_from][msg.sender].sub(_value);
// 转账
_transfer(_from,_to,_value);
return true;
}

交易所

存款

我们定义一个交易所的合约Exchange.sol,交易所存货币的数据结构json表示如下

{
"以太币地址":{
"A用户":100,
"B用户":200,
},
"DOL币地址":{
"A用户":100,
"B用户":200,
},
}

交易所本身的地址是一个转账地址,用户授权后把钱转给交易所,同时还需要配置另外的收费账户地址,如收取手续费,还需要配置费率

下面实现了存储以太币和其他货币的方法

Exchange.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
// 导入安全数学方法
import "openzeppelin-solidity/contracts/math/SafeMath.sol";
import "./DolToken.sol";
contract Exchange {
using SafeMath for uint256;//为uint256后面使用sub,add方法
// 收费账户地址
address public feeAccount;
uint256 public feePercent;// 费率
mapping(address => mapping(address => uint256)) public tokens;
address constant ETHER = address(0);
constructor(address _feeAccount,uint256 _feePercent){
feeAccount = _feeAccount;
feePercent = _feePercent;
}
event Deposit(address token,address user,uint256 amount,uint256 balance);
// 以太币
function depositEther() payable public{
// 有payable才能在调用方法的时候,从msg.sender中划msg.value到合约部署的地址,注意:真的会转
tokens[ETHER][msg.sender] = tokens[ETHER][msg.sender].add(msg.value);
emit Deposit(ETHER, msg.sender, msg.value, tokens[ETHER][msg.sender]);
}
// 存其他货币
function depositToken(address _token,uint256 _amount) public{
// _token要存的类型货币的地址,_amount要存的金额
require(_token != ETHER);
// address(this)当前交易所地址,表示把钱转到当前交易所,msg.sender当前存钱用户
require(DolToken(_token).transferFrom(msg.sender,address(this),_amount));
// 转账成功后在交易所里记录值
tokens[_token][msg.sender] = tokens[_token][msg.sender].add(_amount);
emit Deposit(_token, msg.sender, _amount, tokens[_token][msg.sender]);
}
}

部署,写到一起的方法,可以获取web3的api,传入第1个账号,费率为2

3_Token.js
const Contacts = artifacts.require("DolToken.sol")
const Exchange = artifacts.require("Exchange.sol")
module.exports = async function(deployer){
const accounts = await web3.eth.getAccounts()
await deployer.deploy(Contacts)
await deployer.deploy(Exchange,accounts[0],2)
}

存款测试

testExchange.js
const DolToken = artifacts.require("DolToken.sol")
const Exchange = artifacts.require("Exchange.sol")

const fromWei = (bn)=>{
return web3.utils.fromWei(bn,"ether");
}
const toWei = (number)=>{
return web3.utils.toWei(number.toString(),"ether");
}
const ETHER_ADDRESS = '0x0000000000000000000000000000000000000000';//address(0)默认地址0x后40个0
module.exports = async function(callback){
const token = await DolToken.deployed()
const exchange = await Exchange.deployed()
const accounts = await web3.eth.getAccounts()
// 存储以太坊币测试
/* await exchange.depositEther({
from:accounts[0],
value:toWei(10)
})
let ret = await exchange.tokens(ETHER_ADDRESS,accounts[0])
console.log(fromWei(ret)) */
// 1.授权
await token.approve(exchange.address,toWei(10000),{
from:accounts[0]
})
// 2.存储
await exchange.depositToken(token.address,toWei(10000),{
from:accounts[0]
})
// 3.查看,每执行一次累计存10000,还可以添加自定义代币查看剩余
let ret1 = await exchange.tokens(token.address,accounts[0])
let ret2 = await token.balanceOf(accounts[0])
console.log(fromWei(ret1),fromWei(ret2))

callback()
}

取款

交易所取款逻辑

Exchange.sol
event WithDraw(address token,address user,uint256 amount,uint256 balance);
// 提取以太币
function withdrawEther(uint256 _amount) public {
// 当前账号有不小于该数额的钱
require(tokens[ETHER][msg.sender] >= _amount);
// 把记录减少
tokens[ETHER][msg.sender] = tokens[ETHER][msg.sender].sub(_amount);
// 使用payable转账,把当前交易所账户的_amount的以太币转给当前账户
payable(msg.sender).transfer(_amount);
// 触发WithDraw事件
emit WithDraw(ETHER, msg.sender, _amount, tokens[ETHER][msg.sender]);
}
// 提取其他币
function withdrawToken(address _token,uint256 _amount) public {
// _token要取的类型货币的地址,_amount要取的金额
require(_token != ETHER);
// 当前账号有不小于该数额的钱
require(tokens[_token][msg.sender] >= _amount);
// 把记录减少
tokens[_token][msg.sender] = tokens[_token][msg.sender].sub(_amount);
// 转账,拿到合约货币的实例,当前合约调用该方法,对应transfer内的msg.sender就是交易所账户
require(DolToken(_token).transfer(msg.sender, _amount));
// 触发WithDraw事件
emit WithDraw(_token, msg.sender, _amount, tokens[_token][msg.sender]);
}
// 添加查余额方法对标代币查询方法
function balanceOf(address _token,address _user) public view returns (uint256) {
return tokens[_token][_user];
}

测试方案,首先使用testExchange脚本存款,然后使用testExchangeDraw脚本取款

取款脚本如下

testExchangeDraw.js
const DolToken = artifacts.require("DolToken.sol")
const Exchange = artifacts.require("Exchange.sol")

const fromWei = (bn)=>{
return web3.utils.fromWei(bn,"ether");
}
const toWei = (number)=>{
return web3.utils.toWei(number.toString(),"ether");
}
const ETHER_ADDRESS = '0x0000000000000000000000000000000000000000';//address(0)默认地址0x后40个0
module.exports = async function(callback){
const token = await DolToken.deployed()
const exchange = await Exchange.deployed()
const accounts = await web3.eth.getAccounts()
// 提取以太坊币测试
await exchange.withdrawEther(toWei(2),{
from:accounts[0],
})
let ret = await exchange.balanceOf(ETHER_ADDRESS,accounts[0])
console.log(fromWei(ret))
// 提取不需要授权
await exchange.withdrawToken(token.address,toWei(500),{
from:accounts[0]
})
// 3.查看结果
let ret1 = await exchange.balanceOf(token.address,accounts[0])
let ret2 = await token.balanceOf(accounts[0])
console.log(fromWei(ret1),fromWei(ret2))

callback()
}

image-20230314193910973

流动池

创建订单,包含订单id,创建人,创建时间,准备兑换货币类型,准备兑换货币金额,目标兑换货币类型,目标兑换货币金额,订单状态

Exchange.sol
// 订单结构体
struct _Order {
uint256 id;
address createUser;
address tokenFrom;
uint256 amountFrom;
address tokenTo;
uint256 amountTo;
uint256 timestamp;
uint16 status; //0创建1完成2取消
}
// _Order[] orderList;//动态数组形式
mapping(uint256 => _Order) public orders; //maping结构
uint256 public orderCount; //记录总的订单数
// 创建订单事件
event Order(
uint256 id,
address createUser,
address tokenFrom,
uint256 amountFrom,
address tokenTo,
uint256 amountTo,
uint256 timestamp
);

// 创建订单
function makeOrder(
address _tokenFrom,
uint256 _amountFrom,
address _tokenTo,
uint256 _amountTo
) public {
require(balanceOf(_tokenFrom, msg.sender) >= _amountFrom,unicode'创建订单时余额不足');
orderCount = orderCount.add(1);
orders[orderCount] = _Order(
orderCount,
msg.sender,
_tokenFrom,
_amountFrom,
_tokenTo,
_amountTo,
block.timestamp,
0
);
// 发出订单事件
emit Order(
orderCount,
msg.sender,
_tokenFrom,
_amountFrom,
_tokenTo,
_amountTo,
block.timestamp
);
}

取消订单

修改状态为2

Exchange.sol
//取消订单事件
event Cancel(
uint256 id,
address createUser,
address tokenFrom,
uint256 amountFrom,
address tokenTo,
uint256 amountTo,
uint256 timestamp
);
// 取消订单
function cancelOrder(uint _id) public {
_Order memory myorder = orders[_id];
require(myorder.id == _id);
require(myorder.createUser == msg.sender);
// 修改状态
orders[_id] = _Order(
myorder.id,
myorder.createUser,
myorder.tokenFrom,
myorder.amountFrom,
myorder.tokenTo,
myorder.amountTo,
myorder.timestamp,
2
);//创建时间不变
// 发出取消事件
emit Cancel(
myorder.id,
myorder.createUser,
myorder.tokenFrom,
myorder.amountFrom,
myorder.tokenTo,
myorder.amountTo,
block.timestamp
);//取消时间
}

完成交易

涉及修改状态,改变在交易所的金额,收取小费,需优化冻结金额或者判断

Exchange.sol
// 完成订单事件
event Trade(
uint256 id,
address createUser,
address tokenFrom,
uint256 amountFrom,
address tokenTo,
uint256 amountTo,
uint256 timestamp
);
// 完成订单
function fillOrder(uint _id) public {
_Order memory myorder = orders[_id];
require(myorder.id == _id);
// msg.sender是要完成兑换的人
// 交易&手续费
require(
_trade(
myorder.createUser,
myorder.tokenFrom,
myorder.amountFrom,
myorder.tokenTo,
myorder.amountTo,
msg.sender
)
);
// todo 完成交易,可以补充完成交易的时间和用户,另外存储或改造结构体
// 修改状态
orders[_id] = _Order(
myorder.id,
myorder.createUser,
myorder.tokenFrom,
myorder.amountFrom,
myorder.tokenTo,
myorder.amountTo,
myorder.timestamp,
1
);
emit Trade(
myorder.id,
msg.sender,
myorder.tokenFrom,
myorder.amountFrom,
myorder.tokenTo,
myorder.amountTo,
block.timestamp
);//完成人,完成时间
}

// 交易:创建人,准备兑换货币类型,准备兑换货币金额,目标兑换货币类型,目标兑换货币金额,完成人
function _trade(
address _createUser,
address _tokenFrom,
uint256 _amountFrom,
address _tokenTo,
uint256 _amountTo,
address _fillUser
) internal returns (bool) {
// 计算小费,完成订单的人支付,从目标兑换货币金额中计算
uint256 feeAmount = _amountTo.mul(feePercent).div(100);
// 两方的存款要足够
require(balanceOf(_tokenFrom, _createUser) >= _amountFrom,unicode'创建订单用户余额不足');
require(balanceOf(_tokenTo, _fillUser) >= _amountTo.add(feeAmount),unicode'完成订单余额不足');
// todo优化,创建人不足会产生坏账单,不能交换,甚至发起人创建订单后资源可以扩展冻结
// 针对下单人
tokens[_tokenFrom][_createUser] = tokens[_tokenFrom][_createUser].sub(
_amountFrom
);
tokens[_tokenTo][_createUser] = tokens[_tokenTo][_createUser].add(
_amountTo
);
// 针对完成订单的人
tokens[_tokenFrom][_fillUser] = tokens[_tokenFrom][_fillUser].add(
_amountFrom
);
tokens[_tokenTo][_fillUser] = tokens[_tokenTo][_fillUser].sub(
_amountTo.add(feeAmount)
); //多减费用
// 把小费给配置的账户
tokens[_tokenTo][feeAccount] = tokens[_tokenTo][feeAccount].add(
feeAmount
);
return true;
}

测试订单

故事:账户1想拿1ETH兑换100DOL,交易后资金会存放在交易所

初始化时默认10个账号各自有1000ETH,前面测试交易可能有些变动,第一个账户有默认有全部的DOL币,转给账户2一定的DOL币

  1. 账户1存到交易所一定量ETH,如50
  2. 账户2存到交易所一定量DOL,如10000
  3. 账户1创建订单1ETH兑换100DOL,查看订单状态0
  4. 账户1取消订单,查看订单状态2
  5. 账户1再次创建新订单1ETH兑换100DOL,查看订单状态0
  6. 账户2完成订单,订单状态为1
  7. 查看账号1和2在交易所的金额,注意小费扣除(配置了2%),交易完成后交易所里账户1剩余【49ETH、100DOL】,账户2剩余【1ETH、10000 - (100 + 100*2%)】,收费账号配置了第一个账户,会把小费给第一个账户,就会变成102DOL

测试代码如下

testOrder.js
const DolToken = artifacts.require("DolToken.sol")
const Exchange = artifacts.require("Exchange.sol")

const fromWei = (bn)=>{
return web3.utils.fromWei(bn,"ether");
}
const toWei = (number)=>{
return web3.utils.toWei(number.toString(),"ether");
}
const ETHER_ADDRESS = '0x0000000000000000000000000000000000000000';//address(0)默认地址0x后40个0
module.exports = async function(callback){
const token = await DolToken.deployed()
const exchange = await Exchange.deployed()
const accounts = await web3.eth.getAccounts()
const [one,two] = accounts
try {
// 初始化查询
await token.transfer(two,toWei(100000),{
from: one
})
const initAssets = await getAssets(accounts,exchange,token)
console.log(initAssets,'数据😎😎😎initAssets');
// 1. 账户1存到交易所一定量ETH,如50
await exchange.depositEther({
from: one,
value: toWei(50)
})
// 2. 账户2存到交易所一定量DOL,如10000
await token.approve(exchange.address,toWei(10000),{
from:two
})//授权
await exchange.depositToken(token.address,toWei(10000),{
from: two,
})//转账
const deposit = await getAssets(accounts,exchange,token)
console.log(deposit,'数据😎😎😎deposit');
// 3. 账户1创建订单1ETH兑换100DOL,查看订单状态0
const order1 = await exchange.makeOrder(ETHER_ADDRESS,toWei(1),token.address,toWei(100),{
from: one
})
const id1 = order1.logs[0].args.id; // 获取信息的方式
const initOrder1 = await getOrder(exchange,id1)
console.log(initOrder1,'数据😎😎😎initOrder1');
// 4. 账户1取消订单,查看订单状态2
await exchange.cancelOrder(id1,{
from: one
})
const order1Cancel = await getOrder(exchange,id1)
console.log(order1Cancel,'数据😎😎😎order1Cancel');
// 5. 账户1再次创建新订单1ETH兑换100DOL,查看订单状态0
const order2 = await exchange.makeOrder(ETHER_ADDRESS,toWei(1),token.address,toWei(100),{
from: one
})
const id2 = order2.logs[0].args.id; // 获取信息的方式
const initOrder2 = await getOrder(exchange,id2)
console.log(initOrder2,'数据😎😎😎initOrder2');
// 6. 账户2完成订单,订单状态为1
await exchange.fillOrder(id2,{
from:two
})
const Order2Fill = await getOrder(exchange,id2)
console.log(Order2Fill,'数据😎😎😎Order2Fill');
// 7. 查看账号1和2在交易所的金额,注意小费扣除(配置了2%),交易完成后交易所里账户1剩余【49ETH、100DOL】,账户2剩余【1ETH、10000 - (100 + 100*2%)】,第一个账号也会累计2%小费
const finalAssets = await getAssets(accounts,exchange,token)
console.log(finalAssets,'数据😎😎😎finalAssets');
} catch (error) {
console.log(error,'数据😎😎😎error');
}
callback()
}

const getAssets = async (accounts,exchange,token) => {
const [one,two] = accounts
// 账户1和2在交易所的ETH和DOL
const oneExchangeETH = await exchange.balanceOf(ETHER_ADDRESS,one)
const twoExchangeETH = await exchange.balanceOf(ETHER_ADDRESS,two)
const oneExchangeDOL = await exchange.balanceOf(token.address,one)
const twoExchangeDOL = await exchange.balanceOf(token.address,two)
// 账户1和2本身的ETH和DOL
const oneSelfDOL = await token.balanceOf(one)
const twoSelfDOL = await token.balanceOf(two)
const oneSelfETH = await web3.eth.getBalance(one);
const twoSelfETH = await web3.eth.getBalance(two);

return {
oneExchangeETH: fromWei(oneExchangeETH),
twoExchangeETH: fromWei(twoExchangeETH),
oneExchangeDOL: fromWei(oneExchangeDOL),
twoExchangeDOL: fromWei(twoExchangeDOL),
oneSelfDOL: fromWei(oneSelfDOL),
twoSelfDOL: fromWei(twoSelfDOL),
oneSelfETH: fromWei(oneSelfETH),
twoSelfETH: fromWei(twoSelfETH),
}
}
const getOrder = async (exchange,id) =>{
return exchange.orders(id)
}

image-20230315144441001