ERC20重要補充之approveAndCall

什麼是ERC20

ERC20是以太坊上爲token提供的一種協議,也能夠理解成一種token的共同標準。遵循ERC20協議的token均可以兼容以太坊錢包,讓用戶在錢包中能夠查看token餘額以及操做token轉帳,而不須要本身再手動與token合約交互。git

ERC20規定了如下基本方法:github

contract ERC20 {
    // 方法
    function name() view returns (string name);
    function symbol() view returns (string symbol);
    function decimals() view returns (uint8 decimals);
    function totalSupply() view returns (uint256 totalSupply);
    function balanceOf(address _owner) view returns (uint256 balance);
    function transfer(address _to, uint256 _value) returns (bool success);
    function transferFrom(address _from, address _to, uint256 _value) returns (bool success);
    function approve(address _spender, uint256 _value) returns (bool success);
    function allowance(address _owner, address _spender) view returns (uint256 remaining);
    // 事件
    event Transfer(address indexed _from, address indexed _to, uint256 _value);
    event Approval(address indexed _owner, address indexed _spender, uint256 _value);
}

能夠看到,經過上面的幾種方法,規定了一種token的基本信息、轉帳以及受權操做。這些操做基本能夠覆蓋貨幣使用的絕大部分場景,該協議一經提出後,立獲得了開發者的接納。安全

ERC20的侷限

ERC20雖然廣受開發者喜好,可是依然有本身侷限的一面。

讓咱們先從一個你們十分熟悉的場景開始談起。假設某一天,星巴克忽然宣佈爲了擁抱區塊鏈技術,再也不接受法幣買咖啡了,你們之後能夠用以太幣或者星巴克本身發行的星星幣來買咖啡。app

首先,咱們來看用以太幣來買咖啡的流程。框架

1. 用以太幣買咖啡

簡單寫一個買咖啡的合約(注:僞代碼,僅表示邏輯)區塊鏈

contract BuyCoffee {
    function buy() public payable {
        starbucks.transfer(msg.value);
        COFFEE.transfer(msg.sender);
    }
}

(熟悉ERC721的小夥伴確定看出來了,這裏的COFFEE是遵照ERC721的NFT token,本文重點講解的是ERC20,所以就不在贅述ERC721的實現了)。測試

整個調用過程以下圖:ui

coffee-starcoin-Page-1

客戶直接調用buy()方法,輸入買咖啡須要的以太幣數量,BuyCoffee合約就把本身有的COFFEE轉給客戶。整個過程只須要一步。this

2. 用星星幣買咖啡

星巴克本身發行了token,取名StarCoin,遵循ERC20協議。編碼

那麼BuyCoffee合約就要作一些小修改:(注:僞代碼,僅表示邏輯)

contract BuyCoffee {
    // 一杯咖啡的StarCoin價格
    uint constant COFFEE_PRICE;
    //@param _fee - 用戶買咖啡須要支付的StarCoin數量
    function buy(uint _fee) public payable {
        require(_fee >= COFFEE_PRICE);
        StarCoin.transferFrom(msg.sender, address(this), _fee);
        COFFEE.transfer(msg.sender);
    }
}

整個買咖啡的過程以下圖:

coffee-starcoin-Copy of Page-1

圖中能夠看到,由於StarCoinBuyCoffee是兩個合約,分別有本身獨立的地址,因此客戶買咖啡就要通過兩次操做:

  • 先要把買咖啡的starcoin數量受權給BuyCoffee
  • 而後調用BuyCoffee中的buy(uint)方法買咖啡;

3. 以太幣 vs 星星幣

經過上面的分析能夠看到,若是要使用星巴克發行的StarCoin進行付款的話,買一杯咖啡要操做兩次,無疑這增長了操做成本,而且很反常識。一個很好的辦法就是把StarCoinBuyCoffee合二爲一,若是token邏輯和業務邏輯都在同一個合約裏的話,就不存在上述問題了。

這看上去是一個不錯的辦法,然而治標不治本。萬一之後星巴克還宣佈可使用星星幣買積分、參加優惠活動甚至直接參與星巴克公司分成,鑑於智能合約不可更改的特色,這麼多業務邏輯不可能一開始就所有規劃好,之後的新業務依然面臨屢次操做的問題。

approveAndCall

approveAndCall方法能夠完美地解決上述問題,把兩次操做合併爲一次,讓用戶在付款時感受不到這些複雜的操做。

使用approveAndCall方法以後,整個操做的流程以下:

  1. 用戶在token合約 (StarCoin) 中受權一筆token給業務合約 (BuyCoffee), 經過token合約中的approveAndCall方法;
  2. token合約通知業務合約,它已經被受權能夠操做用戶的一筆token,經過調用業務合約的receiveApproval方法;
  3. 業務合約就能夠把用戶的token轉給本身,而後本身再去完成相關的業務邏輯(好比把咖啡轉給用戶,或者本身再作一些轉帳操做)。

整個過程就以下圖:

approveandcall-Page-2

這就須要在token合約裏建立approveAndCall方法,以下:

function approveAndCall(address _to, uint256 _value, bytes _extraData) {
    approve(_to, _value);
    ApproveAndCallFallBack(_to).receiveApproval(
        msg.sender,
        _value,
        extraData)
}

(參數的個數能夠根據須要自行選擇,例如能夠加上address(tokenContract))

而後在service合約中建立receiveApproval方法,以下:

function receiveApproval(address _sender, uint256 _value, bytes _extraData) {
    require(msg.sender == tokenContract);
    // do something by breaking down _extraData
    ...
}

approveAndCall使用注意事項

爲何要使用approveAndCall以及怎樣使用它,上文已經解釋清楚了。有些可能以爲再多寫一個ApproveAndCallFallBack接口有些畫蛇添足,不如直接使用address(_to).call(...)來的簡單直接。

ConsenSys的疏忽

ConsenSys公司的思路也是這樣的,如下代碼就是Consensys的approveAndCall方法:

/* Approves and then calls the receiving contract */
    function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
        allowed[msg.sender][_spender] = _value;
        Approval(msg.sender, _spender, _value);

        //call the receiveApproval function on the contract you want to be notified. This crafts the function signature manually so one doesn't have to include a contract in here just for this.
        //receiveApproval(address _from, uint256 _value, address _tokenContract, bytes _extraData)
        //it is assumed that when does this that the call *should* succeed, otherwise one would use vanilla approve instead.
        if(!_spender.call(bytes4(bytes32(sha3("receiveApproval(address,uint256,address,bytes)"))), msg.sender, _value, this, _extraData)) { throw; }
        return true;
    }
}
想看所有源碼的能夠訪問: https://github.com/ConsenSys/...

可是你們若是稍加嘗試就會發現,若是這裏的_extraData超過32個字節,就會報錯。

緣由就在於address(_to).call(...)這樣的調用,並不會對所傳數據作ABI.encode編碼,而bytes做爲動態數據類型,它的ABI編碼方式和基礎的、固定長度類型的變量是不同的。

舉個例子:

下面是長度爲64字節的bytes (換行只是爲了讓你們看着不費力) :

0x0000000000000000000000000000000100000000000000000000000000000001
  000000000000000000000000964633feef5a290be634c2e718353b98def350be

它的ABI編碼以下 (換行只是爲了讓你們看着不費力) :

0x0000000000000000000000000000000100000000000000000000000000000060
  0000000000000000000000000000000100000000000000000000000000000040
  0000000000000000000000000000000100000000000000000000000000000001
  000000000000000000000000964633feef5a290be634c2e718353b98def350be
  • 第一行(第一個32byte):距離參數開始位置的偏移量;
  • 第二行(第二個32byte):bytes參數的長度;
  • 第三行和第四行(最後64個byte):bytes參數的內容;

因此上面的bytes參數若是超過32byte長度,第二個32byte就會被當成bytes參數的長度,最後由於out of gas而致使調用失敗。

以上錯誤的修復方式

針對上面的ConsenSys公司的代碼,正確寫法應該是:

/* Approves and then calls the receiving contract */
    function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
        approve(_spender, _value); //若是該token遵循ERC20的話
             if(!_spender.call(bytes4(keccak256("receiveApproval(address,uint256,address,bytes)")), abi.encode(msg.sender, _value, this, _extraData)) { throw; }
        return true;
    }
}

address(_spender).call(...)方法中,使用abi.encode()方法對參數進行ABI編碼,能夠防止出現上述錯誤。

approveAndCall的正確打開方式

接着上面的代碼繼續說,除了上面的abi.encode對參數進行ABI編碼的例子,還可使用abi.encodeWithSelector(...)方法:

/* Approves and then calls the receiving contract */
    function approveAndCall(address _spender, uint256 _value, bytes _extraData) returns (bool success) {
        approve(_spender, _value); //若是該token遵循ERC20的話
             if(!_spender.call(abi.encodeWithSelector(bytes4(keccak256("receiveApproval(address,uint256,address,bytes)")),msg.sender, _value, this, _extraData)) { throw; }
        return true;
    }
}

abi.encodeWithSelector會自動忽略前四個字節,對後面的內容進行ABI編碼。

還有一個使代碼看上去更加簡潔的代碼方式就是上面提到的,增長ApproveAndCallFallBack接口:

interface ApproveAndCallFallBack {
    function receiveApproval(address from, uint256 _amount, address _token, bytes _data) public;
}

以後approveAndCall方法內的實現變爲:

function approveAndCall(address _spender, uint256 _amount, bytes _extraData
    ) returns (bool success) {
        if (!approve(_spender, _amount)) throw;

        ApproveAndCallFallBack(_spender).receiveApproval(
            msg.sender,
            _amount,
            this,
            _extraData
        );

        return true;
    }
以上代碼貢獻自:

https://github.com/evolutionl...

注:這是一個以太坊上的沙盤遊戲。其中RING token的設計目的之一就是爲了在遊戲中買賣地塊,感興趣的同窗能夠詳細研究其中的erc20和erc721token之間的交互方式。

寫在最後

這一篇解釋了爲何使用approveAndCall以及怎樣更好地使用它。區塊鏈是一個更新迭代迅速同時又極其強調安全的領域,對於權威組織給出的代碼,咱們也不能簡單地copy-and-paste,審計和測試是必須的。

至於ERC20爲何沒有把approveAndCall添加進協議中,可能早期在以太坊上流通的大部分多爲token合約,尚未可以創建去較爲複雜的應用強的程序,所以更增強調的是token做爲貨幣具備的流通手段的職能;隨着以太坊生態的發展出現了愈來愈多的應用,這時ERC20 token的支付手段的職能才被你們重視起來。

也可能由於approveAndCall和業務的聯繫過於緊密,ERC20做爲一個框架性的協議,這些細節並不在考慮範圍以內。

鑑於智能合約的不可更改性,但願從此的發行token的組織機構或者我的,在實現ERC20的基礎上,能夠儘量安全地實現approveAndCall方法,使得基於token的應用生態更加魯棒。

最後提醒,ERC223的 tokenFallback方法也有相似的效果,若是你們感興趣也能夠本身作進一步的研究。

友情提醒:ERC223的tokenFallback方法在以前提到的https://github.com/evolutionl...,感興趣的朋友能夠自行參考。

相關文章
相關標籤/搜索