场景:
用户中心对接钱包,安装Geth钱包提供RPC接口供后台使用,通过RPC接口为每个用户生成了钱包地址,且保存了钱包密码。

需求:
需要将用户充值进来的ETH或基于ERC20的代币归集(归总)转出到线下冷钱包。

步骤:

  1. var tokenContract = eth.contract('abi');
  2. var balance = tokenContract.at('contract').balanceOf('address');
  3. if (balance == 0) return;
  4. personal.unlockAccount(eth.accounts[0])
  5. eth.sendTransaction({from:eth.accounts[0],to:'address',value:'feeAmount', data:web3.toHex('fee')});
  6. personal.unlockAccount('address')
  7. var token = tokenContract.at('contract');
  8. token.approve(eth.accounts[0], 'num');
  9. token.transferFrom('address', 'cool wallet', 'num')

只是列出了大概思路,详细说明见:https://waphe.com/index.php/archives/ERC20-contract.html

扫码添加我的微信或QQ一起讨论更优化文案。

由以太坊代幣(Token)的標準 — ERC20 Token來介紹代幣的撰寫及使用方式,和其周邊的附加功能。

原文:https://medium.com/taipei-ethereum-meetup/erc20-token%E4%BD%BF%E7%94%A8%E6%89%8B%E5%86%8A-3d7871c58bea

ERC20

ERC20 Token的EIP約在兩年前就已經提出,經過歷時長久的社群討論,在九月的時候確定了最終版本。

以下是一個標準的ERC20合約所需要具備的函式及行為:

代幣的全名

function name() constant returns (string name)
回傳代幣的全名,例如 OutrichTrumpICOToken。注意,此函式目的為增加可讀性,非必要,使用者或合約請勿指望此函式一定會有回傳值。

代幣的縮寫

function symbol() constant returns (string symbol)
回傳代幣的縮寫,例如 OTICO,縮寫並無規定一定要為三個字元。同樣請勿指望此函式一定會有回傳值。

代幣的最小單位

function decimals() constant returns (uint8 decimals)
回傳使用者看到的最小單位,為一個數值,表示此代幣最多可細分到小數點後幾位數。例如假設數值為3,表示最後使用者看到的擁有代幣數量會是像這樣123.456。看代幣的用途,對應到現實世界中或現有的資產各會有不同的最小單位。沒特別需求可對應到以太幣的單位,設為18。同樣請勿指望此函式一定會有回傳值。

代幣的總量

function totalSupply() constant returns (uint256 totalSupply)
回傳代幣的發行總量。

查詢某帳戶的代幣餘額

function balanceOf(address _owner) constant returns (uint256 balance)
參數為欲查詢的帳戶地址,回傳值為一正整數。注意,代幣餘額皆會是正整數,要搭配 decimals函式來顯示單位給使用者。

移轉代幣給他人

function transfer(address _to, uint256 _value) returns (bool success)
參數1為接收代幣的地址,參數2為數量,回傳值為布林值 — 1表示成功,0表示失敗。

  1. 若成功移轉代幣,則一定要觸發Transfer event(見下一點)。如果送出代幣者的餘額不足,則應該要觸發throw,復原一切更動。
  2. 若是因為產生新代幣而呼叫此函式,則應該要將Transfer事件的_from設為0x0,表示為新產生的代幣。
  3. 若移轉代幣數量為零,仍視為正常的代幣移轉並照常觸發Transfer事件。

移轉代幣觸發事件

event Transfer(address indexed _from, address indexed _to, uint256 _value)
參數1為送出代幣的地址,參數2為接受代幣的地址,參數3為代幣的數量。

從A移轉代幣給B

function transferFrom(address _from, address _to, uint256 _value) returns (bool success)
參數1為送出代幣的地址,參數2為接受代幣的地址,參數3為數量。回傳值為布林值 — 1表示成功,0表示失敗。

  1. 如果沒有送出代幣者的授權(見下一點)或餘額不足,則應該要觸發throw,復原一切更動。若成功移轉代幣,則一定要觸發Transfer event。
  2. 若移轉代幣數量為零,仍視為正常的代幣移轉並照常觸發Transfer事件。

批准自己的代幣移轉

function approve(address _spender, uint256 _value) returns (bool success)
參數1為可以領走自己代幣的對象的地址,參數2為數量,回傳值為布林值 — 1表示成功,0表示失敗。透過呼叫這個函式來批准某對象可以藉由transferFrom函式領走自己最多_value數量的代幣。

  1. _value是自訂的,可以設為超過自己的餘額的值。
  2. 批准並不代表將代幣鎖住直到對方領走為止,使用者仍可在對方領走之前先領走代幣,這樣對方在呼叫函式時會因為餘額不足而失敗。若成功批准,則一定要觸發Approval事件。
  3. 為了避免Front Running而導致Double Spend的攻擊,使用者在更改一個批准的數量時,應該要先送出一個數量為零的批准再進行正常的批准。

A批准給B的代幣數量(必要)

function allowance(address _owner, address _spender) constant returns (uint256 remaining)
參數1為代幣擁有者的地址,參數2為可以領走代幣的地址,回傳值為一正整數。

代幣批准觸發事件(必要)

event Approval(address indexed _owner, address indexed _spender, uint256 _value)
參數1為代幣擁有者的地址,參數2為可以領走代幣的地址,參數3為代幣的數量。
transferFrom及approve兩個函式為一個組合,目的是提供用領取的方式來轉移代幣(相對於使用送出方式的transfer)。會使用領取模式是因為如果代幣接收方是一個合約,則在發生transfer時合約不會收到通知,即合約沒辦法知道代幣轉到它身上。

也因為這個原因,讓某些應用場景無法實現。例如一個合約C的某個功能需要收取某代幣才能執行,如果合約沒辦法知道transfer到底發生了沒,就沒辦法知道代幣是否成功移轉給它,也就沒辦法繼續執行它的功能。如果改用領取模式 — 使用者在使用合約C的功能前先批准一定量的代幣給C,合約C執行的時候用transferFrom領取代幣 ,領取成功就繼續執行它的功能,否則退出執行 — 這讓合約接受代幣做為報酬的方式得以實現。

路印协议 - LRC

路印协议新一代区块链资产交易协议和交易所。它采用去中心化技术,提供零风险的代币交易所模式,并允许多家交易所通过竞争,对同样的订单进行链外撮合及链上清结算。

https://loopring.io/#/

0x协议 - ZRX

0x是一个点对点交易的开源协议,以促进以太坊区块链中ERC20代币的交易。该协议旨在作为开放标准和通用构建模块,推动包括交易所功能的去中心化应用(DApps)之间的互操作性。交易由以太坊智能合约系统执行,可以公开访问,免费使用,且任何DApp都可以接入。 建立在协议之上的DApps可以访问公共流动资金池或创建自己的流动资金池,并对其交易量收取交易费用。该协议不会把成本强加于用户之上,也不会任意地从一组用户中获取价值来惠及其他用户。去中心化管理用于将更新以持续而安全的方式集成到基本协议中,而不会中断更高级别的进程。

https://www.0xproject.com/

KyberNetwork - KNC
KyberNetwork,它是一个具备高流动性的数字资产(如各类加密代币)以及加密数字货币(例如以太坊、比特币和 ZCash)即时交易和兑换的链上协议。

https://kyber.network/

原文:https://busy.org/@chaimyu/3c72ac-gusd

ERC20 token在转移时都需要有以太币做为燃料费,GUSD也是ERC20代币,但GUSD提供了一种不需要以太币进行资金归集的方法,简单来看一看。

授权资金归集

function enableSweep(uint8[] _vs, bytes32[] _rs, bytes32[] _ss, address _to) public onlySweeper {
    require(_to != address(0));
    require((_vs.length == _rs.length) && (_vs.length == _ss.length));

    uint256 numSignatures = _vs.length;
    uint256 sweptBalance = 0;

    for (uint256 i=0; i<numSignatures; ++i) {
      address from = ecrecover(sweepMsg, _vs[i], _rs[i], _ss[i]);

      // ecrecover returns 0 on malformed input
      if (from != address(0)) {
        sweptSet[from] = true;

        uint256 fromBalance = erc20Store.balances(from);

        if (fromBalance > 0) {
          sweptBalance += fromBalance;

          erc20Store.setBalance(from, 0);

          erc20Proxy.emitTransfer(from, _to, fromBalance);
        }
      }
    }

    if (sweptBalance > 0) {
      erc20Store.addBalance(_to, sweptBalance);
    }
}

如果要允许地址余额被归集,需要用这个地址对合约中变量“sweepMsg”进行签名,把签名数据给Sweeper。

Sweeper用签名后的数据调用enableSweep()函数,波函数中会通过“ecrecover”验证签名数据,验证通过则把这个地址放入可归集地址中(sweptSet[from]),同时将地址余额转移到函数参数“_to”指定的地址。

可以看出,只要签名同意过Sweeper进行资金归集,这个授权就是永久的,Sweeper有权将地址余额转移到任何地址。

重新归集

function replaySweep(address[] _froms, address _to) public onlySweeper {
    require(_to != address(0));
    uint256 lenFroms = _froms.length;
    uint256 sweptBalance = 0;

    for (uint256 i=0; i<lenFroms; ++i) {
        address from = _froms[i];

        if (sweptSet[from]) {
            uint256 fromBalance = erc20Store.balances(from);

            if (fromBalance > 0) {
                sweptBalance += fromBalance;

                erc20Store.setBalance(from, 0);

                erc20Proxy.emitTransfer(from, _to, fromBalance);
            }
        }
    }

    if (sweptBalance > 0) {
        erc20Store.addBalance(_to, sweptBalance);
    }
}

这个函数比较简单,Sweeper可以调用这个函数,传入一堆地址,如果这些地址是授权可以被归集的,则将地址余额转移到函数参数“_to”指定的地址中。

结论

GUSD通过一次签名永久授权的方式,提供了一种 ERC20 代币资金归集的方法,可以借鉴。

遗憾的是只能自己写的 ERC20 合约才可以这样做,已经发布的 ERC20 代币进行资金归集还是需要以太币的,不知道 ERC 新标准里有没有类似的方法。

转处:https://zhuanlan.zhihu.com/p/38526169

1.背景

网上已经有不少关于以太坊智能合约、Solidity编程的文章,但是针对性比较强的并不多。

学习一门新技术最好的方式就是“读开源代码+动手实践”。

我用了几周的时间学习了一下区块链、比特币、以太坊、智能合约等内容。选了一个刚刚上线不久的数字货币交易所dex.top的智能合约进行整理分析,一来作为这个阶段学习内容的总结,二来也希望能帮助到凑巧也在学习相关内容的大家。

2.资料

本篇文章并不适合“零基础”入门,因此推荐大家一些比较好的资料。

比特币及区块链:

以太坊及智能合约:

dex.top交易所相关:

其他:

3.简介

DEx.top 是基于区块链智能合约(包括但不限于以太坊、Rootstock)的去中心化交易平台。平台 首创 ROC 机制(Replayed On Chain),采用链下、链上双账本的模式确保用户资产安全,支持 即时高并发交易。DEx.top 致力于为用户提供安全、高效、低成本的数字资产交易体验,打造对普 通交易者、专业交易者、量化交易者等各类用户友好的去中心化交易平台。

本文分析DEx.top的基于以太坊技术的“链上账本”部分使用的智能合约的Solidity实现,分块讲解Solidity代码实现,并把每一部分涉及到的知识点重点介绍。对于一些智能合约的基础概念、Solidity编程的基础知识需要读者自行熟悉。

4.interface部分

代码分析

interface Token {
  function transfer(address to, uint256 value) external returns (bool success);
  function transferFrom(address from, address to, uint256 value) external returns (bool success);
}

定义了Token接口。
接口中有两个方法定义,分别是transfer()和transferFrom()。
以上两个方法的签名跟ERC20代币合约标准中的定义完全相同。

Solidity知识点:Interface

Interface跟“抽象合约”(abstract contract)的概念很像,一定程度上可以类比Java中的抽象类和接口。

官方文档里并没有把用法写的很清楚,但是一般智能合约里都是按照如下方法使用的。先不深究,了解用法就好。

DEX2合约中,interface使用示例代码:

interface Token {
  function transfer(address to, uint256 value) external returns (bool success);
  function transferFrom(address from, address to, uint256 value) external returns (bool success);
}
contract Dex2 {
  function depositToken(address traderAddr, uint16 tokenCode, uint originalAmount) external {
    // ...
​
    // Need to make approval by calling Token(address).approve() in advance for ERC-20 Tokens.
    if (!Token(tokenInfo.tokenAddr).transferFrom(msg.sender, this, originalAmount)) revert();
    
    //...
  }
}

以上面代码为例,interface用法如下:

  • 定义Token接口,以及transfer()和transferFrom()方法,与ERC20标准合约中的对应方法,签名完全一致。
  • 在Dex2合约中,通过tokenInfo.tokenAddr(address类型)初始化Token。其中,tokenInfo.tokenAddr为实现了ERC-20 Token标准的智能合约。
  • 调用对应合约的transferFrom方法。

参考网址:

ERC20代币合约标准

ERC20代币合约标准规定了一个以太坊代币合约所需要实现的函数功能和事件记录。该标准满足了代币作为数字资产所必须具备的一些基本功能和要求,如注明代币名称、代币转账、本账户中允许链上第三方使用的代币限额等。ERC20标准使得不同代币能够被更多的DApp(去中心化应用)和交易所兼容(通过以上使用的接口方式)。

ERC,全称为Ethereum Request for Comment,20是它的编号。ERC20是以太坊平台上的代币合约标准,2017年国内特别火爆的ICO好多都是基于ERC20代币合约标准做的。另外个比较知名的代币合约标准是ERC721,该标准规定了一种不可替代的代币合约接口,此类型的代币最小单位为个,之前网上比较火的区块链养猫游戏使用的是这种类型的合约。

下面对标准接口简略做一下介绍,重点介绍dex.top合约中用到的部分。

ERC20合约包含的方法(Method)

name()

// 返回string类型的ERC20代币的名字,例如“MC Token”。
// 该方法为可选方法。
function name() constant returns (string name);

symbol()

// 返回string类型的ERC20代币的符号,也就是代币的简称,例如“MCT”。
// 该方法为可选方法。
function symbol() constant returns (string symbol);

decimals()

// 返回该类型Token支持小数点后几位。
// 该方法为可选方法。
function decimals() constant returns (uint8 decimals);

totalSupply()

// 返回Token发行的总量。
function totalSupply() view returns (uint256 totalSupply)

balanceOf(address _owner)

// 返回_owner地址对应账户的代币账户余额。
function balanceOf(address _owner) view returns (uint256 balance)

transfer(address _to, uint256 _value)

// 方法调用者把自己地址中_value数量的Token转移到_to地址。
// 该方法必须触发Transfer事件。
// 转账0个Token需要正确处理,同样触发Transfer事件。
function transfer(address _to, uint256 _value) returns (bool success)

transferFrom()

// 方法调用者把_from地址中_value数量的Token转移到_to地址。
// 该方法必须触发Transfer事件。
// 调用者需要经过授权(由_from地址授权给该调用者)。
// 与approve搭配使用,approve批准之后,调用transferFrom函数来转移token。
function transferFrom(address _from, address _to, uint256 _value) returns (bool success)

approve()

// 批准_spender从调用者地址中转移_value个Token(通过transferFrom()方法)。
// 可以分多次转移,允许转移的总量为_value
// 可以多次调用该方法,如重复调用则允许转移的token个数覆盖。
function approve(address _spender, uint256 _value) returns (bool success)

allowance()

// 查询_spender地址还能从_owner地址转移多少个token。
function allowance(address _owner, address _spender) constant returns (uint256 remaining)

ERC20中的方法分为必须实现和可选两类,其中name、symbol、decimals为可选方法,其余为必须实现。另外,以上提到的最后三个方法是相互关联的。其中,approve方法用于授权,allowance方法用于查询剩余的授权金额,transferFrom方法用于代理转账。通常的流程为:

  • A地址通过approve()方法授权B地址可转账数量
  • B地址想要通过transferFrom方法转移A地址中的Token时,首先通过allowance()方法查询是否已经经过A授权,以及可供转账的金额是否满足条件
  • 如果已经经过授权,并且剩余授权金额满足条件,则可以通过transferFrom()方法把一定数量的Token由A地址转移到C地址。

ERC20合约包含的事件

Transfer

// 当成功转移token时,一定要触发Transfer事件。包括0个token转移。
// 创建新token(铸币)也要触发Transfer事件,此时_from地址设置为0x0。
event Transfer(address indexed _from, address indexed _to, uint256 _value)

Approval

// 当调用approval成功时,一定要触发Approval事件
event Approval(address indexed _owner, address indexed _spender, uint256 _value)

Solidity的Event非常想nodejs中的Event,后面再做介绍。

5.Struct Definitions部分

代码分析

TokenInfo

  struct TokenInfo {
    string  symbol;       // e.g., "ETH", "ADX"
    address tokenAddr;    // ERC20 token address
    uint64  scaleFactor;  // <original token amount> = <scaleFactor> x <DEx amountE8> / 1e8
    uint    minDeposit;   // mininum deposit (original token amount) allowed for this token
  }

dex.top平台上可交易Token的信息。

symbol,代币的名称缩写。

tokenAddr,token代币智能合约的地址。

scaleFactor,比例因子,与token合约的decimal设置有关。scaleFactor=10**decimal。

minDeposit,该类型token在Dex2合约之上的,最小充值金额。

dex.top平台上可以交易的货币都是符合ERC20代币合约标准的代币。

TraderInfo

  struct TraderInfo {
    address withdrawAddr;
    uint8   feeRebatePercent;  // range: [0, 100]
  }

dex.top平台上交易账户信息。

withdrawAddr,提现地址

feeRebatePercent,根据后面代码逻辑,该参数表示手续费“返点”(部分减免),相当于针对某个address的交易账户的设置手续费优惠。如果“返点”设置为100%,则不需要手续费了

TokenAccount

  struct TokenAccount {
    uint64 balanceE8;          // available amount for trading
    uint64 pendingWithdrawE8;  // the amount to be transferred out from this contract to the trader
  }

Token账户信息。

balanceE8,token账户中可用于交易的余额。

pendingWithdrawE8,token账户中可用于提现的余额。

“E8”的含义是该参数存储的数值是实际值的10^8倍,实际数值为该数值除以10^8。

Order

  struct Order {
    uint32 pairId;  // <cashId>(16) <stockId>(16)
    uint8  action;  // 0 means BUY; 1 means SELL
    uint8  ioc;     // 0 means a regular order; 1 means an immediate-or-cancel (IOC) order
    uint64 priceE8;
    uint64 amountE8;
    uint64 expireTimeSec;
  }

挂单信息。

pairId,该参数为32bits无符号变量,前16bits代表cashId,后16bits代表stockId。例如,对于希望使用ETH买入LOOM的挂单,cashId为ETH在该合约上的编号(ETH的tokenCode为0),stockId为LOOM这种Token在该合约上的编号。对于卖出单,情况类似。

action,0代表买入,1代表卖出

ioc,immediate-or-cancel,0代表普通Order,1代表IOC Order。根据后面方法的代码逻辑理解,如果为IOC类型的挂单,则进行一次撮合交易(MatchOrder)后,挂单金额直接清零,即使仍有剩余;否则的话,该挂单的金额如果有剩余,仍然可以进行下一次撮合交易。

priceE8,价格。

amountE8,数量。

expireTimeSec,该Order过期时间。

挂单信息代表了用户希望以一定的价格交易一定量的token的意图。链上记录所有的挂单信息,并由dex.top平台负责撮合交易,然后向合约发指令完成交易。dex.top的链上账本和链下账本会一直同步,所以直接根据链下账本中的挂单信息进行撮合,然后向合约发送matchOrder操作指令然后进行同步即可。

Deposit

  struct Deposit {
    address traderAddr;
    uint16  tokenCode;
    uint64  pendingAmountE8;   // amount to be confirmed for trading purpose
  }

充值信息。

traderAddr,交易账户地址。

tokenCode,token类型编号。

pendingAmountE8,充值数量。

代表了traderAddr账户,tokenCode代表的类型的token的一次充值,充值的数量为pendingAmountE8。需要确认后(Comfirm)方可作为账户的余额使用。

DealInfo

  struct DealInfo {
    uint16 stockCode;          // stock token code
    uint16 cashCode;           // cash token code
    uint64 stockDealAmountE8;
    uint64 cashDealAmountE8;
  }

成交信息。根据变量的名字理解即可。

ExeStatus

  struct ExeStatus {
    uint64 logicTimeSec;       // logic timestamp for checking order expiration
    uint64 lastOperationIndex; // index of the last executed operation
  }

运行状态,跟执行Operation序列相关。

logicTimeSec,该参数用于检查Order是否过期。

lastOperationIndex,上次操作的索引。

6.Constants部分

代码分析

  //----------------- Constants: -------------------------------------------------------------------
​
  uint constant MAX_UINT256 = 2**256 - 1;
  uint16 constant MAX_FEE_RATE_E4 = 60;  // upper limit of fee rate is 0.6% (60 / 1e4)
​
  // <original ETH amount in Wei> = <DEx amountE8> * <ETH_SCALE_FACTOR> / 1e8
  uint64 constant ETH_SCALE_FACTOR = 10**18;
​
  uint8 constant ACTIVE = 0;
  uint8 constant CLOSED = 2;
​
  bytes32 constant HASHTYPES =
      keccak256('string title', 'address market_address', 'uint64 nonce', 'uint64 expire_time_sec',
                'uint64 amount_e8', 'uint64 price_e8', 'uint8 immediate_or_cancel', 'uint8 action',
                'uint16 cash_token_code', 'uint16 stock_token_code');

MAX_UINT256,256位无符号整形变量代表的最大值。

MAX_FEE_RATE_E4,最高的费率为万分之60,E4代表该数值需要除以10000,才是实际上的费率。

ETH_SCALE_FACTOR,ETH的比例因子为10^18,因为1Ether = 10^18Wei。

ACTIVE,CLOSED代表交易所的运行状态,一共分为三种情况:运行状态、暂停状态、关闭状态(一旦处于关闭状态,则不可逆)。

HASHTYPES,byte32类型,用于校验。

7.States部分

代码分析

  //----------------- States that cannot be changed once set: --------------------------------------
​
  address public admin;                         // admin address, and it cannot be changed
  mapping (uint16 => TokenInfo) public tokens;  // mapping of token code to token information
​
  //----------------- Other states: ----------------------------------------------------------------
​
  uint8 public marketStatus;        // market status: 0 - Active; 1 - Suspended; 2 - Closed
​
  uint16 public makerFeeRateE4;     // maker fee rate (* 10**4)
  uint16 public takerFeeRateE4;     // taker fee rate (* 10**4)
  uint16 public withdrawFeeRateE4;  // withdraw fee rate (* 10**4)
​
  uint64 public lastDepositIndex;   // index of the last deposit operation
​
  ExeStatus public exeStatus;       // status of operation execution
​
  mapping (address => TraderInfo) public traders;     // mapping of trade address to trader information
  mapping (uint176 => TokenAccount) public accounts;  // mapping of trader token key to its account information
  mapping (uint224 => Order) public orders;           // mapping of order key to order information
  mapping (uint64  => Deposit) public deposits;       // mapping of deposit index to deposit information

admin,创建该智能合约的账户地址,一旦设置不能修改。有一些外部函数,是仅供admin调用的。

tokens,tokenCode到TokenInfo的映射,tokenCode为uint16类型,TokenInfo为struct,上文已经介绍。

marketStatus,代表交易所的运行状态,分别为运行状态,暂停状态,关闭状态。

makerFeeRateE4、takerFeeRateE4、withdrawFeeRateE4,分别代表买入、卖出、提现的手续费费率。

lastDepositIndex,上一次充值操作的索引

exeStatus,操作的运行状态,用于记录上一次操作的时间和索引

traders,address到TraderInfo的映射,根据账户的address可以查询到TraderInfo。TraderInfo为struct类型,上文已经介绍。

accounts,unit176到TokenAccount的映射,其中unit176类型的变量由tokenCode(16) + traderAddr(160)拼接组成,根据tokenCode和账户的address可以查询到TokenAccount信息。TokenAccount为struct类型,上文已经介绍。

orders,unit224到Order的映射,其中uint224为orderNonce(64)+traderAddress(160)拼接组成。Order为struct类型,代表挂单信息,上文已经介绍。orderNonce为64bits的时间戳。

deposits,充值的索引值(uint64)到Deposit的映射。Deposit为struct类型,代表依次充值的信息,上文已经介绍。

Solidity知识点:变量类型

Solidity是一门静态类型语言,每一个变量都必须指定变量的类型。变量类型分成两大类值类型(Value Types)和引用类型(Reference Types)

值类型(Value Types)

值类型在每次赋值或者作为参数传递时,都会创建一份拷贝。对于值类型和引用类型的复制和传参时的处理,跟Java语言类似。值类型包括布尔类型、整数类型、地址类型、枚举类型、固定长度字节数组等。在DEX2合约中,主要用到了整数类型和地址类型两种。

整数类型

int表示有符号整数,uint表示无符号整数。通过后缀可以指明变量使用多少位进行存储,后缀必须是8~256范围内8的整数倍,比如uint8、uint16、uint224等。如果没有显式指明后缀,int默认表示int256,uint默认表示uint256.

地址类型

address表示地址类型,长度为20bytes,与以太坊账户地址长度一致。地址类型有一些成员变量和方法。

  • address.balance,类型为uint,表示账户余额,单位为wei。1eth=10^18wei
  • address.transfer(uint256 amount),发送amount数量的以太币给address表示的账户,单位是wei,失败会抛出异常。
  • address.send(uint256 amount) returns(bool),与transfer方法的区别在于,send方法如果执行失败则返回false;transfer方法如果执行失败则抛出异常。

引用类型

应用类型包括数组类型、结构体类型(struct)、映射(Mapping)。其中,映射类型可以看做是一个散列表。

Solidity知识点:存储位置

对于引用类型来说,存储位置分为内存(memory)和存储器(storage)两类,其中内存中不是永久存储,存储器是永久存储的。个人理解,对于值类型,不需要关心存储位置的概念,因为每一次赋值和调用都是一个传值的过程。

根据不同的上下文环境,变量都会有默认的存储位置;同时,也可以通过memory和storage关键字,显式地制定变量存储的位置。函数参数的默认值(包括返回参数)是在内存上,局部变量的默认存储位置是在存储器上,状态变量是的默认存储位置也是存储器上。

数据的存储位置会影响变量赋值的行为,一下代码是官方文档中的例子,根据自己的理解重新写了注释。可以简单理解为同为storage上的赋值操作,仅传递引用;如果是memory和storage之间的赋值操作,则需要对真个对象做一个新的拷贝。

pragma solidity ^0.4.0;
​
contract C {
    uint[] x; // x为状态变量(state variable),存储位置为storage
​
    // memoryArray为函数参数,默认存储位置为memory。
    function f(uint[] memoryArray) public {
        x = memoryArray; // 此次复制操作,会把整个数组复制到storage中
        var y = x; // y为局部变量(local variable),默认存储位置为storage。此次赋值操作,仅把x的“指针”赋值给y,x和y指向storage中的同一个数组。
        
        
        y[7]; // fine, returns the 8th element
        y.length = 2; // fine, modifies x through y
        delete x; // fine, clears the array, also modifies y
        
        // The following does not work; it would need to create a new temporary /
        // unnamed array in storage, but storage is "statically" allocated:
        // y = memoryArray;
        // This does not work either, since it would "reset" the pointer, but there
        // is no sensible location it could point to.
        // delete y;
        
        
        g(x); // g()函数中的参数有storage关键字修饰,因此此次调用仅把引用传递给g()方法
        h(x); // h()函数中的参数没有制定存储位置,此次调用会在memory中创建一个临时的独立拷贝。
    }
​
    function g(uint[] storage storageArray) internal {}
    function h(uint[] memoryArray) public {}
}```

官方文档给出的总结:

强制数据位置:

-   外部函数的参数(不返回):calldata(类似memory,不可改变,不持久存储)
-   状态变量:存储器

默认数据位置:

-   函数(有返回)的参数:内存
-   其他所有局部变量:存储器

## **8.Event部分**

## **代码分析**

//------------------------------ Dex2 Events: ----------------------------------------------------

event DeployMarketEvent();
event ChangeMarketStatusEvent(uint8 status);
event SetTokenInfoEvent(uint16 tokenCode, string symbol, address tokenAddr, uint64 scaleFactor, uint minDeposit);
event SetWithdrawAddrEvent(address trader, address withdrawAddr);

event DepositEvent(address trader, uint16 tokenCode, string symbol, uint64 amountE8, uint64 depositIndex);
event WithdrawEvent(address trader, uint16 tokenCode, string symbol, uint64 amountE8, uint64 lastOpIndex);
event TransferFeeEvent(uint16 tokenCode, uint64 amountE8, address toAddr);

// balanceE8 is the total balance after this deposit confirmation
event ConfirmDepositEvent(address trader, uint16 tokenCode, uint64 balanceE8);
// amountE8 is the post-fee initiated withdraw amount
// pendingWithdrawE8 is the total pending withdraw amount after this withdraw initiation
event InitiateWithdrawEvent(address trader, uint16 tokenCode, uint64 amountE8, uint64 pendingWithdrawE8);
event MatchOrdersEvent(address trader1, uint64 nonce1, address trader2, uint64 nonce2);
event HardCancelOrderEvent(address trader, uint64 nonce);
event SetFeeRatesEvent(uint16 makerFeeRateE4, uint16 takerFeeRateE4, uint16 withdrawFeeRateE4);
event SetFeeRebatePercentEvent(address trader, uint8 feeRebatePercent);
`

  • Event部分定义了Dex2智能合约在调用和运行过程中,需要记录的事件。

Solidity知识点:Event

事件是使用EVM日志内置功能的方便工具,在DAPP的接口中,它可以反过来调用Javascript的监听事件的回调。

当被调用时,会触发参数存储到交易的日志中(一种区块链上的特殊数据结构)。这些日志与合约的地址关联,并合并到区块链中,只要区块可以访问就一直存在。日志和事件在合约内不可直接被访问,即使是创建日志的合约。

可以最多有三个参数被设置为indexed,来设置是否被索引。设置为索引后,可以允许通过这个参数来查找日志,甚至可以按特定的值过滤。

如果数组(包括string和bytes)类型被标记为索引项,会用它对应的Keccak-256哈希值做为topic。

所有未被索引的参数将被做为日志的一部分被保存起来。

被索引的参数将不会保存它们自己,你可以搜索他们的值,但不能检索值本身。

以上内容,翻译自Solidity官方文档。可以通过web3提供的JavaScript API监听合约不同的事件,代码略,可以通过下面给出的网址查看API的使用方法和更多内容。另外,<span class="invisible">http://</span><span class="visible">etherscan.io</span><span class="invisible"></span>上可以查看Dex2合约上的所有的Events记录。

参考网址:

9.Contract Initialization部分

代码分析

  //------------------------------ Contract Initialization: ----------------------------------------
​
  function Dex2(address admin_) public {
    admin = admin_;
    setTokenInfo(0 /*tokenCode*/, "ETH", 0 /*tokenAddr*/, ETH_SCALE_FACTOR, 0 /*minDeposit*/);
    emit DeployMarketEvent();
  }
​```

这部分相当于Dex2合约的“构造函数”,在发起合约的时候执行并且只执行一次,通常是对合约状态的一些初始化的操作。

初始化的过程为:

1.  把合约发起人的地址赋值给admin,之后的一些函数仅供admin调用,方便做一些权限控制。
2.  调用setTokenInfo()方法,设置ETH的的tokenCode为0,最小充值金额为0(不做任何限制)。
3.  触发DeployMarketEvent事件。

## **10.External Functions部分**

Dex2合约的外部函数主要提供了如下功能

*   FallBack Function定义
*   改变交易所状态(仅admin)
*   设置提现地址
*   充值以太币或其他类型Token
*   提现以太币或其他类型Token
*   手续费提现(仅admin)
*   顺序执行链下账本上的操作(仅admin)

下面分别分析一下各个函数的功能,其中涉及到的一些Solidity知识已经分别在后面做了介绍,不明白的可以查阅对应的部分。

## **代码分析**

_FallBack Function_

function() external {
revert();
}


FallBack函数,当Dex2合约收到无法匹配任何函数名的函数调用或者仅仅用于转账的交易时,fallback函数将会被自动执行。调用的函数名错误时,直接状态回退;如果给该合约地址转账,同样直接状态回退。

关于FallBack函数的详细介绍,参考“Solidity知识点:Fallback函数”部分。

_changeMarketStatus_,修改交易所状态。

function changeMarketStatus(uint8 status_) external {

if (msg.sender != admin) revert();
if (marketStatus == CLOSED) revert();  // closed is forever

marketStatus = status_;
emit ChangeMarketStatusEvent(status_);

}


权限控制:仅admin可以调用

触发事件:ChangeMarketStatusEvent

修改交易所的状态,状态包括:运行中、暂停、关闭,如果交易所已经关闭,则不可以重新进入运行状态。

通过`if (msg.sender != admin) revert();`限制该函数只能由合约发起人admin调用,admin为“构造函数”中初始化的合约发起人的address,msg.sender为调用者的address。

_setWithdrawAddr_,设置交易账户的提现地址。

// Each trader can specify a withdraw address (but cannot change it later). Once a trader's
// withdraw address is set, following withdrawals of this trader will go to the withdraw address
// instead of the trader's address.
function setWithdrawAddr(address withdrawAddr) external {

if (withdrawAddr == 0) revert();
if (traders[msg.sender].withdrawAddr != 0) revert();  // cannot change withdrawAddr once set
traders[msg.sender].withdrawAddr = withdrawAddr;
emit SetWithdrawAddrEvent(msg.sender, withdrawAddr);

}


权限控制:任何地址可以调用,仅能修改对应的TraderInfo信息。

触发事件:SetWithdrawAddrEvent

设置交易账户(trader)的提现地址,仅能设置一次,之后不可以修改。同时,不可以设置为0地址。

`traders[msg.sender].withdrawAddr = withdrawAddr;`,通过调用者msg.sender地址获取到对应的TraderInfo信息,对其中的withdrawAddr变量进行赋值。也就是说,只能设置msg.sender交易账户的提现地址。

_depositEth_,充值以太币

// Deposit ETH from msg.sender for the given trader.
function depositEth(address traderAddr) external payable {

if (marketStatus != ACTIVE) revert();
if (traderAddr == 0) revert();
if (msg.value < tokens[0].minDeposit) revert();
if (msg.data.length != 4 + 32) revert();  // length condition of param count

uint64 pendingAmountE8 = uint64(msg.value / (ETH_SCALE_FACTOR / 10**8));  // msg.value is in Wei
if (pendingAmountE8 == 0) revert();

uint64 depositIndex = ++lastDepositIndex;
setDeposits(depositIndex, traderAddr, 0, pendingAmountE8);
emit DepositEvent(traderAddr, 0, "ETH", pendingAmountE8, depositIndex);

}


权限控制:任何地址可以调用

触发事件:DepositEvent

该方法完成从msg.sender到traderAddr地址充值Eth,msg.sender和traderAddr可以是同一个地址。该方法设置了payable关键字,可以接受以太币转账。

`if (msg.value &lt; tokens[0].minDeposit) revert();`,充值金额不可以小于以太币的minDeposit,以太币的tokenCode为0。

`setDeposits(depositIndex, traderAddr, 0, pendingAmountE8);`,调用私有方法setDeposit()完成充值。

完整的过程为:msg.sender转移一定数量的Eth到Dex2智能合约,Dex2智能合约的balance中增加相应的Eth,然后给traderAddr的Eth账户上做相应的记录。

_depositToken_,充值其他类型的Token

// Deposit token (other than ETH) from msg.sender for a specified trader.
//
// After the deposit has been confirmed enough times on the blockchain, it will be added to the
// trader's token account for trading.
function depositToken(address traderAddr, uint16 tokenCode, uint originalAmount) external {

if (marketStatus != ACTIVE) revert();
if (traderAddr == 0) revert();
if (tokenCode == 0) revert();  // this function does not handle ETH
if (msg.data.length != 4 + 32 + 32 + 32) revert();  // length condition of param count

TokenInfo memory tokenInfo = tokens[tokenCode];
if (originalAmount < tokenInfo.minDeposit) revert();
if (tokenInfo.scaleFactor == 0) revert();  // unsupported token

// Need to make approval by calling Token(address).approve() in advance for ERC-20 Tokens.
if (!Token(tokenInfo.tokenAddr).transferFrom(msg.sender, this, originalAmount)) revert();

if (originalAmount > MAX_UINT256 / 10**8) revert();  // avoid overflow
uint amountE8 = originalAmount * 10**8 / uint(tokenInfo.scaleFactor);
if (amountE8 >= 2**64 || amountE8 == 0) revert();

uint64 depositIndex = ++lastDepositIndex;
setDeposits(depositIndex, traderAddr, tokenCode, uint64(amountE8));
emit DepositEvent(traderAddr, tokenCode, tokens[tokenCode].symbol, uint64(amountE8), depositIndex);

}


权限控制:任何地址可以调用

触发事件:DepositEvent

该方法完成从msg.sender到traderAddr地址充值tokenCode类型的Token。

`TokenInfo memory tokenInfo = tokens[tokenCode];`,根据tokenCode获取到相应的TokenInfo。使用memory关键字,完成从storage存储到memory的完整拷贝。

`if (!Token(tokenInfo.tokenAddr).transferFrom(msg.sender, this, originalAmount)) revert();`,通过tokenInfo.tokenAddr“实例化”Token接口,然后调用ERC20代币合约标准接口的transferFrom()方法,完成从msg.sender到该合约的Token转移。

`setDeposits(depositIndex, traderAddr, tokenCode, uint64(amountE8));`,通过调用setDeposits私有方法完成充值。

完整的过程为:把msg.sender地址上的Token代币转移到Dex2智能合约,智能合约的balance增加相应数量的该类型Token,然后给traderAddr的Token账户上做相应的记录。

_withdrawEth_,从ETH账户中提现

// Withdraw ETH from the contract.
function withdrawEth(address traderAddr) external {

if (traderAddr == 0) revert();
if (msg.data.length != 4 + 32) revert();  // length condition of param count

uint176 accountKey = uint176(traderAddr);
uint amountE8 = accounts[accountKey].pendingWithdrawE8;
if (amountE8 == 0) return;

// Write back to storage before making the transfer.
accounts[accountKey].pendingWithdrawE8 = 0;

uint truncatedWei = amountE8 * (ETH_SCALE_FACTOR / 10**8);
address withdrawAddr = traders[traderAddr].withdrawAddr;
if (withdrawAddr == 0) withdrawAddr = traderAddr;
withdrawAddr.transfer(truncatedWei);
emit WithdrawEvent(traderAddr, 0, "ETH", uint64(amountE8), exeStatus.lastOperationIndex);

}


权限控制:任何地址可以调用

触发事件:WithdrawEvent

该函数将traderAddr账户中的待提现ETH(pendingWithdrawE8)完成提现流程。

`uint176 accountKey = uint176(traderAddr);`,构造accountKey。accountKey由16bits的tokenCode加上160bits的traderAddr拼接组成,ETH的tokenCode为0,因此直接通过类型转换即可生成ETH对应的accountKey。

`address withdrawAddr = traders[traderAddr].withdrawAddr;`,通过traderAddr拿到TraderInfo中的提现地址withdrawAddr(如果之前设置过的话,否则跟traderAddr一致)。

`withdrawAddr.transfer(truncatedWei);`,调用address的transfer方法,完成向提现地址withdrawAddr转移ETH。

**问题:任何人都可以向合约发送针对任意traderAddr的提现操作吗?**

_withdrawToken_,从Token账户中提现

// Withdraw token (other than ETH) from the contract.
function withdrawToken(address traderAddr, uint16 tokenCode) external {

if (traderAddr == 0) revert();
if (tokenCode == 0) revert();  // this function does not handle ETH
if (msg.data.length != 4 + 32 + 32) revert();  // length condition of param count

TokenInfo memory tokenInfo = tokens[tokenCode];
if (tokenInfo.scaleFactor == 0) revert();  // unsupported token

uint176 accountKey = uint176(tokenCode) << 160 | uint176(traderAddr);
uint amountE8 = accounts[accountKey].pendingWithdrawE8;
if (amountE8 == 0) return;

// Write back to storage before making the transfer.
accounts[accountKey].pendingWithdrawE8 = 0;

uint truncatedAmount = amountE8 * uint(tokenInfo.scaleFactor) / 10**8;
address withdrawAddr = traders[traderAddr].withdrawAddr;
if (withdrawAddr == 0) withdrawAddr = traderAddr;
if (!Token(tokenInfo.tokenAddr).transfer(withdrawAddr, truncatedAmount)) revert();
emit WithdrawEvent(traderAddr, tokenCode, tokens[tokenCode].symbol, uint64(amountE8),
                   exeStatus.lastOperationIndex);

}


权限控制:任何地址可以调用

触发事件:WithdrawEvent

该函数将traderAddr账户中的待提现tokenCode对应类型的Token完成提现流程。

整个过程与withdrawEth()类似,对ERC20类型Token的体现操作是通过调用transfer()接口实现的。

`if (!Token(tokenInfo.tokenAddr).transfer(withdrawAddr, truncatedAmount)) revert();`。

_transferFee_,转移手续费,用于交易所把手续费提现。

// Transfer the collected fee out of the contract.
function transferFee(uint16 tokenCode, uint64 amountE8, address toAddr) external {

if (msg.sender != admin) revert();
if (toAddr == 0) revert();
if (msg.data.length != 4 + 32 + 32 + 32) revert();

TokenAccount memory feeAccount = accounts[uint176(tokenCode) << 160];
uint64 withdrawE8 = feeAccount.pendingWithdrawE8;
if (amountE8 < withdrawE8) {
  withdrawE8 = amountE8;
}
feeAccount.pendingWithdrawE8 -= withdrawE8;
accounts[uint176(tokenCode) << 160] = feeAccount;

TokenInfo memory tokenInfo = tokens[tokenCode];
uint originalAmount = uint(withdrawE8) * uint(tokenInfo.scaleFactor) / 10**8;
if (tokenCode == 0) {  // ETH
  toAddr.transfer(originalAmount);
} else {
  if (!Token(tokenInfo.tokenAddr).transfer(toAddr, originalAmount)) revert();
}
emit TransferFeeEvent(tokenCode, withdrawE8, toAddr);

}


权限控制:仅admin可以调用

触发事件:TransferFeeEvent

该方法完成某种类型(tokenCode对应的)的Token手续费提现的过程。

`TokenAccount memory feeAccount = accounts[uint176(tokenCode) &lt;&lt; 160];`,tokenCode类型对应的所有的手续费全部存储在tokenCode+“0地址”的账户中。

针对ETH和其他类型的ERC20 Token,分别执行相应的transfer方法完成代币的转移。

_exeSequence_,顺序执行“链下”(off-chain)账本上的交易序列。

// Replay the trading sequence from the off-chain ledger exactly onto the on-chain ledger.
function exeSequence(uint header, uint[] body) external {

if (msg.sender != admin) revert();

uint64 nextOperationIndex = uint64(header);
if (nextOperationIndex != exeStatus.lastOperationIndex + 1) revert();  // check sequence index

uint64 newLogicTimeSec = uint64(header >> 64);
if (newLogicTimeSec < exeStatus.logicTimeSec) revert();

for (uint i = 0; i < body.length; nextOperationIndex++) {
  uint bits = body[i];
  uint opcode = bits & 0xFFFF;
  bits >>= 16;
  if ((opcode >> 8) != 0xDE) revert();  // check the magic number

  // ConfirmDeposit: <depositIndex>(64)
  if (opcode == 0xDE01) {
    confirmDeposit(uint64(bits));
    i += 1;
    continue;
  }

  // InitiateWithdraw: <amountE8>(64) <tokenCode>(16) <traderAddr>(160)
  if (opcode == 0xDE02) {
    initiateWithdraw(uint176(bits), uint64(bits >> 176));
    i += 1;
    continue;
  }

  //-------- The rest operation types are allowed only when the market is active ---------
  if (marketStatus != ACTIVE) revert();

  // MatchOrders
  if (opcode == 0xDE03) {
    uint8 v1 = uint8(bits);
    bits >>= 8;            // bits is now the key of the maker order

    Order memory makerOrder;
    if (v1 == 0) {         // order already in storage
      if (i + 1 >= body.length) revert();  // at least 1 body element left
      makerOrder = orders[uint224(bits)];
      i += 1;
    } else {
      if (orders[uint224(bits)].pairId != 0) revert();  // order must not be already in storage
      if (i + 4 >= body.length) revert();  // at least 4 body elements left
      makerOrder = parseNewOrder(uint224(bits) /*makerOrderKey*/, v1, body, i);
      i += 4;
    }

    uint8 v2 = uint8(body[i]);
    uint224 takerOrderKey = uint224(body[i] >> 8);
    Order memory takerOrder;
    if (v2 == 0) {         // order already in storage
      takerOrder = orders[takerOrderKey];
      i += 1;
    } else {
      if (orders[takerOrderKey].pairId != 0) revert();  // order must not be already in storage
      if (i + 3 >= body.length) revert();  // at least 3 body elements left
      takerOrder = parseNewOrder(takerOrderKey, v2, body, i);
      i += 4;
    }

    matchOrder(uint224(bits) /*makerOrderKey*/, makerOrder, takerOrderKey, takerOrder);
    continue;
  }

  // HardCancelOrder: <nonce>(64) <traderAddr>(160)
  if (opcode == 0xDE04) {
    hardCancelOrder(uint224(bits) /*orderKey*/);
    i += 1;
    continue;
  }

  // SetFeeRates: <withdrawFeeRateE4>(16) <takerFeeRateE4>(16) <makerFeeRateE4>(16)
  if (opcode == 0xDE05) {
    setFeeRates(uint16(bits), uint16(bits >> 16), uint16(bits >> 32));
    i += 1;
    continue;
  }

  // SetFeeRebatePercent: <rebatePercent>(8) <traderAddr>(160)
  if (opcode == 0xDE06) {
    setFeeRebatePercent(address(bits) /*traderAddr*/, uint8(bits >> 160) /*rebatePercent*/);
    i += 1;
    continue;
  }
} // for loop

setExeStatus(newLogicTimeSec, nextOperationIndex - 1);

} // function exeSequence


权限控制:仅admin可以调用

`function exeSequence(uint header, uint[] body) external {}`

操作序列用uint256的数组表示`uint[] body`,可能一个uint256代表一个操作,也可能若干个uint256代表一个操作。第一个uint256的最后16bits代表opcode,根据不同的opcode执行不同的操作。简略列举如下

*   ConfirmDeposit,确认充值
*   InitiateWithdraw,初始化提现
*   MatchOrders,撮合交易
*   HardCancelOrder,取消挂单
*   SetFeeRates,设置费率
*   SetFeeRebatePercent,设置返点

参考网址:

*   [https://github.com/dexDev/DEx.top/blob/master/smart-contract/dextop-spec.md](http://link.zhihu.com/?target=https%3A//github.com/dexDev/DEx.top/blob/master/smart-contract/dextop-spec.md)

## **Solidity知识点:Fallback函数**

在合约中,有一个默认隐式存在的函数叫做fallback函数。fallback函数不能接受任何参数并且不能拥有返回值。

当一个合约收到无法匹配任何函数名的函数调用或者仅仅用于转账的交易时,fallback函数将会被自动执行,默认的行为是抛出异常。我们可以使用function () { ... }这样的方式重写fallback函数。在Solidity 0.4.0以后的版本中,如果我们想让合约以简单的Transfer方式进行以太币转账,则需要像“function() payable { }”这样实现fallback函数,给函数加上payable修饰词。

contract Test {

// 这个合约收到任何函数调用都会触发fallback函数(因为没有其他函数)
// 向这个合约发送以太币会触发异常,因为fallback函数没有payable修饰词
function() { x = 1; }
uint x;

}
contract Caller {

function callTest(Test test) {
    test.call(0xabcdef01); // 对应的函数不存在
    // 触发test的fallback函数,导致test.x的值变为1
    // 下面这句话不会通过编译
    // 即使某个交易向test发送了以太币,也会触发异常并且退回以太币
    test.send(2 ether);
}

}
`

当手动实现fallback函数时,需要特别注意Gas消耗,因为fallback函数只拥有非常少的Gas(2300Gas)。比起fallback函数的Gas限制,一个触发了fallback函数的交易会消耗更多的Gas,因为大约有21000或者更多的Gas会用于签名验证等过程。

在部署合约之前,必须充分测试fallback函数,确保fallback函数的执行消耗少于2300Gas。

Solidity知识点:异常处理

以太坊使用状态回退机制处理异常。如果发生了异常,当前消息调用和子消息的调用产生的所有状态变化都将被撤销并且返回调用者一个报错信息。

Solidity提供了两个函数来检查条件,分别是assert和require,当条件不满足的时候抛出一个异常。具体用法和区别暂时先不讲了,Dex2合约主要使用了revert()函数的方式。

revert函数可以触发异常,可以包含一条发生错误的信息给调用者。throw关键字同样可以触发异常,现已deprecated。

看一下Dex2合约的changeMarketStatus()方法:

  // Change the market status of DEX.
  function changeMarketStatus(uint8 status_) external {
    if (msg.sender != admin) revert();
    if (marketStatus == CLOSED) revert();  // closed is forever
​
    marketStatus = status_;
    emit ChangeMarketStatusEvent(status_);
  }

以上代码通过rever()的方式完成状态回退。

如果调用者不是admin,则通过revert()触发异常。如果交易所以及处于关闭状态了,则通过revert()触发异常。

参考网址:

http://solidity.readthedocs.io/en/develop/control-structures.html#error-handling-assert-require-revert-and-exceptions

Solidity知识点:function、view、pure、payable

通常,一个函数的签名长下面这个样子:

function (<parameter types>) {internal|external} [pure|constant|view|payable] [returns (<return types>)]

view

代表该函数承诺进行的是只读操作,不会修改状态。这里的状态包括合约状态变量的修改,以及其他一些情况,包括:

  1. Writing to state variables.
  2. Emitting events.
  3. Creating other contracts.
  4. Using selfdestruct.
  5. Sending Ether via calls.
  6. Calling any function not marked view or pure.
  7. Using low-level calls.
  8. Using inline assembly that contains certain opcodes.

constant是view关键字的别名,但是之后会deprecated。

所有的Getter函数都是默认声明为view。可以关注一下Solidity最新变化。

编译器不强制,只给出警告。

pure

代表该函数承诺不会修改和读取状态。除了view函数的中一些限制,pure函数要求更加严格,以下情况也是不允许的。

  1. Reading from state variables.
  2. Accessing this.balance or &lt;address&gt;.balance.
  3. Accessing any of the members of block, tx, msg (with the exception of msg.sig and msg.data).
  4. Calling any function not marked pure.
  5. Using inline assembly that contains certain opcodes.

同样是非强制的,可以关注一下Solidity最新变化。

constant

constant是view关键字的别名,但是之后会deprecated。

payable

只能向声明为payable类型的函数发送以太币ether,否则会抛出异常。

What happens if you send ether along with a function call to a contract?

It gets added to the total balance of the contract, just like when you send ether when creating a contract. You can only send ether along to a function that has the `payable` modifier, otherwise an exception is thrown.

参考网址:

http://solidity.readthedocs.io/en/develop/contracts.html#view-functions

http://solidity.readthedocs.io/en/develop/contracts.html#pure-functions

Solidity知识点:函数和状态变量的可见性

对于一个合约中的状态变量和函数,一共存在4中关于可见性的声明:external、public、internal、private。对于函数(function)来说,默认为public;对于状态变量(state variables)来说,默认为internal,并且不可以声明为external。

external

external修饰函数时,表示该函数式一个外部函数,外部函数是合约接口的一部分。外部函数只能通过从其他合约(或者地址)发送交易的方式调用。外部函数不可以通过内部方式调用,直接使用f()方式不能正常调用,使用this.f()可以正常调用,但通常不会这么用。

External functions are sometimes more efficient when they receive large arrays of data.

官方文档中提到外部函数在接收大数组数据时比较高效,后面举例说明。

external不能修饰状态变量。

public

public修饰函数时,公有函数也是合约接口的一部分。公有函数可以通过合约外部发送消息的方式调用,也可以合约内部直接调用。

public修饰状态变量时,会自动生成Getter函数,Getter函数是external的。

internal

internal修饰函数和状态变量时,代表该函数/状态变量只能通过内部方式访问。当前合约,以及继承了当前合约的其他合约都适用。

private

private修饰函数和状态变量时,代表该函数/状态变量仅对当前合约可见,仅在当前合约中可以通过内部方式访问。

public vs external

根据上文的内容,外部函数(external)和公有函数(public)同样都能通过外部方式调用,而公有函数还可以通过内部方式调用。那是不是把所有的希望外部访问的函数都声明为public的就可以了呢?这样岂不是很省事儿?事实上并不是这样的。

要理解这个问题,首先要明确开发智能合约(DApp,分布式应用)与开发普通应用(中心化应用)的区别。由于智能合约的状态变量需要在所有的节点上存储,智能合约的运行逻辑也需要在所有节点上的EVM上执行以及同步,因此同样的代码逻辑对于DAPP来说从整体上看会消耗更多的资源。为了不让DApp消耗过多的计算和存储资源,以太坊引入了Gas、Gas Price、GasLimit的机制作为运行DApp的费用消耗,具体的机制可以自己查下资料,不复杂。

这里需要明确的是,对于智能合约中的不同代码逻辑,执行起来会消耗不同数量的Gas。因此智能合约的开发者自然是希望在实现同样功能的同时,尽量少地消耗Gas。在某些情况下,针对public函数的调用会比对同样功能的external函数的调用,消耗更多的Gas。

因此这里的最佳实践是:

如果一个函数可以只提供外部访问的方式,建议声明为external;如果一个函数同时需要外部访问和内部访问,建议声明为public。同时应尽量避免通过this.f()的方式访问public函数,可能会有更多的Gas消耗。具体分析,参考这里

参考网址:

http://solidity.readthedocs.io/en/develop/contracts.html#visibility-and-getters
https://ethereum.stackexchange.com/questions/19380/external-vs-public-best-practices

Solidity知识点:Block and Transaction Properties

简单列一些block、msg、tx自带的属性和方法。

  • block.blockhash(uint blockNumber) returns (bytes32): hash of the given block - only works for 256 most recent, excluding current, blocks - deprecated in version 0.4.22 and replaced by blockhash(uint blockNumber).
  • block.coinbase (address): current block miner’s address
  • block.difficulty (uint): current block difficulty
  • block.gaslimit (uint): current block gaslimit
  • block.number (uint): current block number
  • block.timestamp (uint): current block timestamp as seconds since unix epoch
  • gasleft() returns (uint256): remaining gas
  • msg.data (bytes): complete calldata
  • msg.gas (uint): remaining gas - deprecated in version 0.4.21 and to be replaced by gasleft()
  • msg.sender (address): sender of the message (current call)
  • msg.sig (bytes4): first four bytes of the calldata (i.e. function identifier)
  • msg.value (uint): number of wei sent with the message
  • now (uint): current block timestamp (alias for block.timestamp)
  • tx.gasprice (uint): gas price of the transaction
  • tx.origin (address): sender of the transaction (full call chain)

参考网址:

[http://solidity.readthedocs.io/en/develop/units-and-global-variables.html

](http://link.zhihu.com/?target=http%3A//solidity.readthedocs.io/en/develop/units-and-global-variables.html)

11.Public Functions

setTokenInfo

  //------------------------------ Public Functions: -----------------------------------------------
​
  // Set information of a token.
  function setTokenInfo(uint16 tokenCode, string symbol, address tokenAddr, uint64 scaleFactor,
                        uint minDeposit) public {
    if (msg.sender != admin) revert();
    if (marketStatus != ACTIVE) revert();
    if (scaleFactor == 0) revert();
​
    TokenInfo memory info = tokens[tokenCode];
    if (info.scaleFactor != 0) {  // this token already exists
      // For an existing token only the minDeposit field can be updated.
      tokens[tokenCode].minDeposit = minDeposit;
      emit SetTokenInfoEvent(tokenCode, info.symbol, info.tokenAddr, info.scaleFactor, minDeposit);
      return;
    }
​
    tokens[tokenCode].symbol = symbol;
    tokens[tokenCode].tokenAddr = tokenAddr;
    tokens[tokenCode].scaleFactor = scaleFactor;
    tokens[tokenCode].minDeposit = minDeposit;
    emit SetTokenInfoEvent(tokenCode, symbol, tokenAddr, scaleFactor, minDeposit);
  }

权限控制:仅admin可以调用

触发事件:SetTokenInfoEvent

设置TokenInfo信息,包括symbol、tokenAddr、scaleFactor、minDeposit等。如果已经存在tokenCode对应的信息,只能修改minDeposit。

12.Private Functions

代码分析

这部分为私有方法,除了setDeposits()方法用来设置充值信息,其他的各个方法几乎都是给EXESequence()方法提供支持的。

  • setDeposits:设置充值信息
  • ‘setExeStatus:设置运行状态
  • confirmDeposit:确认充值,把DepositInfo中的钱“转移”到balance。
  • initiateWithdraw:初始化提现,增加账户中pendingWithdrawE8的数量。
  • calcFeeE8:计算手续费
  • hardCancelOrder:取消交易
  • setFeeRates:设置不同交易的手续费(买入、卖出、提现)
  • setFeeRebatePercent:设置某个交易账户的手续费“返点”
  • getDealInfo:获取撮合交易的信息
  • settleAccounts:完成买卖双方的账户修改
  • setOrders:设置挂单信息,可用于对已有挂单信息的修改
  • matchOrder:撮合买卖双方的挂单信息
  • parseNewOrder:生成新的挂单信息。
  //------------------------------ Private Functions: ----------------------------------------------
​
  function setDeposits(uint64 depositIndex, address traderAddr, uint16 tokenCode, uint64 amountE8) private {
    deposits[depositIndex].traderAddr = traderAddr;
    deposits[depositIndex].tokenCode = tokenCode;
    deposits[depositIndex].pendingAmountE8 = amountE8;
  }
​
  function setExeStatus(uint64 logicTimeSec, uint64 lastOperationIndex) private {
    exeStatus.logicTimeSec = logicTimeSec;
    exeStatus.lastOperationIndex = lastOperationIndex;
  }
​
  function confirmDeposit(uint64 depositIndex) private {
    Deposit memory deposit = deposits[depositIndex];
    uint176 accountKey = (uint176(deposit.tokenCode) << 160) | uint176(deposit.traderAddr);
    TokenAccount memory account = accounts[accountKey];
​
    // Check that pending amount is non-zero and no overflow would happen.
    if (account.balanceE8 + deposit.pendingAmountE8 <= account.balanceE8) revert();
    account.balanceE8 += deposit.pendingAmountE8;
​
    deposits[depositIndex].pendingAmountE8 = 0;
    accounts[accountKey].balanceE8 += deposit.pendingAmountE8;
    emit ConfirmDepositEvent(deposit.traderAddr, deposit.tokenCode, account.balanceE8);
  }
​
  function initiateWithdraw(uint176 tokenAccountKey, uint64 amountE8) private {
    uint64 balanceE8 = accounts[tokenAccountKey].balanceE8;
    uint64 pendingWithdrawE8 = accounts[tokenAccountKey].pendingWithdrawE8;
​
    if (balanceE8 < amountE8 || amountE8 == 0) revert();
    balanceE8 -= amountE8;
​
    uint64 feeE8 = calcFeeE8(amountE8, withdrawFeeRateE4, address(tokenAccountKey));
    amountE8 -= feeE8;
​
    if (pendingWithdrawE8 + amountE8 < amountE8) revert();  // check overflow
    pendingWithdrawE8 += amountE8;
​
    accounts[tokenAccountKey].balanceE8 = balanceE8;
    accounts[tokenAccountKey].pendingWithdrawE8 = pendingWithdrawE8;
​
    // Note that the fee account has a dummy trader address of 0.
    if (accounts[tokenAccountKey & (0xffff << 160)].pendingWithdrawE8 + feeE8 >= feeE8) {  // no overflow
      accounts[tokenAccountKey & (0xffff << 160)].pendingWithdrawE8 += feeE8;
    }
​
    emit InitiateWithdrawEvent(address(tokenAccountKey), uint16(tokenAccountKey >> 160) /*tokenCode*/,
                               amountE8, pendingWithdrawE8);
  }
​
  function getDealInfo(uint32 pairId, uint64 priceE8, uint64 amount1E8, uint64 amount2E8)
      private pure returns (DealInfo deal) {
    deal.stockCode = uint16(pairId);
    deal.cashCode = uint16(pairId >> 16);
    if (deal.stockCode == deal.cashCode) revert();  // we disallow homogeneous trading
​
    deal.stockDealAmountE8 = amount1E8 < amount2E8 ? amount1E8 : amount2E8;
​
    uint cashDealAmountE8 = uint(priceE8) * uint(deal.stockDealAmountE8) / 10**8;
    if (cashDealAmountE8 >= 2**64) revert();
    deal.cashDealAmountE8 = uint64(cashDealAmountE8);
  }
​
  function calcFeeE8(uint64 amountE8, uint feeRateE4, address traderAddr)
      private view returns (uint64) {
    uint feeE8 = uint(amountE8) * feeRateE4 / 10000;
    feeE8 -= feeE8 * uint(traders[traderAddr].feeRebatePercent) / 100;
    return uint64(feeE8);
  }
​
  function settleAccounts(DealInfo deal, address traderAddr, uint feeRateE4, bool isBuyer) private {
    uint16 giveTokenCode = isBuyer ? deal.cashCode : deal.stockCode;
    uint16 getTokenCode = isBuyer ? deal.stockCode : deal.cashCode;
​
    uint64 giveAmountE8 = isBuyer ? deal.cashDealAmountE8 : deal.stockDealAmountE8;
    uint64 getAmountE8 = isBuyer ? deal.stockDealAmountE8 : deal.cashDealAmountE8;
​
    uint176 giveAccountKey = uint176(giveTokenCode) << 160 | uint176(traderAddr);
    uint176 getAccountKey = uint176(getTokenCode) << 160 | uint176(traderAddr);
​
    uint64 feeE8 = calcFeeE8(getAmountE8, feeRateE4, traderAddr);
    getAmountE8 -= feeE8;
​
    // Check overflow.
    if (accounts[giveAccountKey].balanceE8 < giveAmountE8) revert();
    if (accounts[getAccountKey].balanceE8 + getAmountE8 < getAmountE8) revert();
​
    // Write storage.
    accounts[giveAccountKey].balanceE8 -= giveAmountE8;
    accounts[getAccountKey].balanceE8 += getAmountE8;
​
    if (accounts[uint176(getTokenCode) << 160].pendingWithdrawE8 + feeE8 >= feeE8) {  // no overflow
      accounts[uint176(getTokenCode) << 160].pendingWithdrawE8 += feeE8;
    }
  }
​
  function setOrders(uint224 orderKey, uint32 pairId, uint8 action, uint8 ioc,
                     uint64 priceE8, uint64 amountE8, uint64 expireTimeSec) private {
    orders[orderKey].pairId = pairId;
    orders[orderKey].action = action;
    orders[orderKey].ioc = ioc;
    orders[orderKey].priceE8 = priceE8;
    orders[orderKey].amountE8 = amountE8;
    orders[orderKey].expireTimeSec = expireTimeSec;
  }
​
  function matchOrder(uint224 makerOrderKey, Order makerOrder,
                      uint224 takerOrderKey, Order takerOrder) private {
    // Check trading conditions.
    if (marketStatus != ACTIVE) revert();
    if (makerOrderKey == takerOrderKey) revert();  // the two orders must not have the same key
    if (makerOrder.pairId != takerOrder.pairId) revert();
    if (makerOrder.action == takerOrder.action) revert();
    if (makerOrder.priceE8 == 0 || takerOrder.priceE8 == 0) revert();
    if (makerOrder.action == 0 && makerOrder.priceE8 < takerOrder.priceE8) revert();
    if (takerOrder.action == 0 && takerOrder.priceE8 < makerOrder.priceE8) revert();
    if (makerOrder.amountE8 == 0 || takerOrder.amountE8 == 0) revert();
    if (makerOrder.expireTimeSec <= exeStatus.logicTimeSec) revert();
    if (takerOrder.expireTimeSec <= exeStatus.logicTimeSec) revert();
​
    DealInfo memory deal = getDealInfo(
        makerOrder.pairId, makerOrder.priceE8, makerOrder.amountE8, takerOrder.amountE8);
​
    // Update accounts.
    settleAccounts(deal, address(makerOrderKey), makerFeeRateE4, (makerOrder.action == 0));
    settleAccounts(deal, address(takerOrderKey), takerFeeRateE4, (takerOrder.action == 0));
​
    // Update orders.
    if (makerOrder.ioc == 1) {  // IOC order
      makerOrder.amountE8 = 0;
    } else {
      makerOrder.amountE8 -= deal.stockDealAmountE8;
    }
    if (takerOrder.ioc == 1) {  // IOC order
      takerOrder.amountE8 = 0;
    } else {
      takerOrder.amountE8 -= deal.stockDealAmountE8;
    }
​
    // Write orders back to storage.
    setOrders(makerOrderKey, makerOrder.pairId, makerOrder.action, makerOrder.ioc,
              makerOrder.priceE8, makerOrder.amountE8, makerOrder.expireTimeSec);
    setOrders(takerOrderKey, takerOrder.pairId, takerOrder.action, takerOrder.ioc,
              takerOrder.priceE8, takerOrder.amountE8, takerOrder.expireTimeSec);
​
    emit MatchOrdersEvent(address(makerOrderKey), uint64(makerOrderKey >> 160) /*nonce*/,
                          address(takerOrderKey), uint64(takerOrderKey >> 160) /*nonce*/);
  }
​
  function hardCancelOrder(uint224 orderKey) private {
    orders[orderKey].pairId = 0xFFFFFFFF;
    orders[orderKey].amountE8 = 0;
    emit HardCancelOrderEvent(address(orderKey) /*traderAddr*/, uint64(orderKey >> 160) /*nonce*/);
  }
​
  function setFeeRates(uint16 makerE4, uint16 takerE4, uint16 withdrawE4) private {
    if (makerE4 > MAX_FEE_RATE_E4) revert();
    if (takerE4 > MAX_FEE_RATE_E4) revert();
    if (withdrawE4 > MAX_FEE_RATE_E4) revert();
​
    makerFeeRateE4 = makerE4;
    takerFeeRateE4 = takerE4;
    withdrawFeeRateE4 = withdrawE4;
    emit SetFeeRatesEvent(makerE4, takerE4, withdrawE4);
  }
​
  function setFeeRebatePercent(address traderAddr, uint8 feeRebatePercent) private {
    if (feeRebatePercent > 100) revert();
​
    traders[traderAddr].feeRebatePercent = feeRebatePercent;
    emit SetFeeRebatePercentEvent(traderAddr, feeRebatePercent);
  }
​
  function parseNewOrder(uint224 orderKey, uint8 v, uint[] body, uint i) private view returns (Order) {
    // bits: <expireTimeSec>(64) <amountE8>(64) <priceE8>(64) <ioc>(8) <action>(8) <pairId>(32)
    uint240 bits = uint240(body[i + 1]);
    uint64 nonce = uint64(orderKey >> 160);
    address traderAddr = address(orderKey);
    if (traderAddr == 0) revert();  // check zero addr early since `ecrecover` returns 0 on error
​
    // verify the signature of the trader
    bytes32 hash1 = keccak256("\x19Ethereum Signed Message:\n70DEx2 Order: ", address(this), nonce, bits);
    if (traderAddr != ecrecover(hash1, v, bytes32(body[i + 2]), bytes32(body[i + 3]))) {
      bytes32 hashValues = keccak256("DEx2 Order", address(this), nonce, bits);
      bytes32 hash2 = keccak256(HASHTYPES, hashValues);
      if (traderAddr != ecrecover(hash2, v, bytes32(body[i + 2]), bytes32(body[i + 3]))) revert();
    }
​
    Order memory order;
    order.pairId = uint32(bits); bits >>= 32;
    order.action = uint8(bits); bits >>= 8;
    order.ioc = uint8(bits); bits >>= 8;
    order.priceE8 = uint64(bits); bits >>= 64;
    order.amountE8 = uint64(bits); bits >>= 64;
    order.expireTimeSec = uint64(bits);
    return order;
  }

13.总结和问题

这部分是我自己的一些疑问和总结,整理如下:

  1. 最后一部分private函数没有详细展开讲解,这部分主要是用于支持各种operation的执行的。逻辑不复杂,但是涉及一些细节的操作,如果大家对整体架构比较清楚的话还比较容易理解。
  2. Dex2合约中好多函数的代码逻辑中用到了memory关键字,猜测是跟“解约Gas”有关,后面再继续研究。
  3. setTokenInfo方法为什么要设置为public类型的呢?合约内部并没有调用。这个疑问也没有解决。
  4. 传统的股票交易所,用户通过券商挂单,然后买卖单在交易所(上证、深证)完成交易,而交易所由证监会进行监管。但是对于dex.top来讲虽然链上账本的挂单、撮合、交易信息都是不可篡改和可追溯的,但是链下账本其实是一个中心化的服务,这部分如果没有监管的话会不会存在一些问题?
  5. 对于function withdrawEth(address traderAddr) externalfunction withdrawToken(address traderAddr, uint16 tokenCode) external两个用于提现的外部方法,根据代码逻辑的限制,应该是允许任何地址发起对任何traderAddr的提现请求,这样是不是存在问题?是不是应该限制为admin及本人才有权限?