ERC777 功能型Token最佳實(shí)踐


轉(zhuǎn)自 https://learnblockchain.cn/2019/09/27/erc777/

想必很多人都已經(jīng)使用過ERC20 創(chuàng)建過代幣,或許已經(jīng)被老板要求在ERC20代幣上實(shí)現(xiàn)一些附加功能搞的焦頭爛額,如果還有選擇,一定要選擇 ERC777 。

ERC20 的問題

以下是一個(gè)遇到很多次的場(chǎng)景:有一天老板過來找你(開發(fā)者),最近存幣生息很火,我們也做一個(gè)合約吧, 用戶打幣過來給他計(jì)算利息, 看起來是一個(gè)很簡(jiǎn)單的需求,你滿口答應(yīng)說好,結(jié)果自己一研究發(fā)現(xiàn),使用 ERC20 標(biāo)準(zhǔn)沒辦法在合約里記錄是誰發(fā)過來多少幣,從而沒法計(jì)算利息(因?yàn)榻邮照吆霞s并不知道自己接收到ERC20代幣)。

ERC20 標(biāo)準(zhǔn)下,可以通過一個(gè)變通的辦法,采用兩個(gè)交易組合完成,方法是:第1步:先讓用戶把要轉(zhuǎn)移的金額用 ERC20 的approve 授權(quán)的存幣生息合約(這步通常稱為解鎖),第2步:再次讓用戶調(diào)用存幣生息合約的計(jì)息函數(shù),計(jì)息函數(shù)中通過 transferFrom 把代幣從用戶手里轉(zhuǎn)移的合約內(nèi),并開始計(jì)息。

同樣由于ERC20 標(biāo)準(zhǔn)沒有一個(gè)轉(zhuǎn)賬通知機(jī)制,很多ERC20代幣誤轉(zhuǎn)到合約之后,再也沒有辦法把幣轉(zhuǎn)移出來,已經(jīng)有大量的ERC20 因?yàn)檫@個(gè)原因被鎖死,如鎖死的QTUM,鎖死的EOS

另外一個(gè)問題是ERC20 轉(zhuǎn)賬時(shí),無法攜帶額外的信息,例如:我們有一些客戶希望讓用戶使用 ERC20 代幣購(gòu)買商品,因?yàn)檗D(zhuǎn)賬沒法攜帶額外的信息, 用戶的代幣轉(zhuǎn)移過來,不知道用戶具體要購(gòu)買哪件商品,從而展加了線下額外的溝通成本。

ERC777很好的解決了這些問題,同時(shí)ERC777 也兼容 ERC20 標(biāo)準(zhǔn)。因此強(qiáng)烈建議新開發(fā)的代幣使用ERC777標(biāo)準(zhǔn)。

ERC777 在 ERC20的基礎(chǔ)上定義了 send(dest, value, data) 來轉(zhuǎn)移代幣, send函數(shù)額外的參數(shù)用來攜帶其他的信息,send函數(shù)會(huì)檢查持有者和接收者是否實(shí)現(xiàn)了相應(yīng)的鉤子函數(shù),如果有實(shí)現(xiàn)(不管是普通用戶地址還是合約地址都可以實(shí)現(xiàn)鉤子函數(shù)),則調(diào)用相應(yīng)的鉤子函數(shù)。

ERC1820 接口注冊(cè)表合約

即便是一個(gè)普通用戶地址,同樣可以實(shí)現(xiàn)對(duì) ERC777 轉(zhuǎn)賬的監(jiān)聽, 聽起來有點(diǎn)神奇,其實(shí)這是通過 ERC1820 接口注冊(cè)表合約來是實(shí)現(xiàn)的。

ERC1820 如此的重要,以至于ERC777單獨(dú)把它拆出來作為一個(gè)EIP。

ERC1820 是一個(gè)全局的合約,有一個(gè)唯一在以太坊鏈上都相同的合約地址,它總是 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 ,這個(gè)合約是通過非常巧妙的方式進(jìn)行部署的,有興趣的同學(xué)可以閱讀EIP1820文檔。

ERC 1820 合約的官方實(shí)現(xiàn)代碼在ERC1820文檔可以查閱,這里說明合約實(shí)現(xiàn)的主要內(nèi)容。

ERC1820合約提過了兩個(gè)主要接口:

  • setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer)
    用來設(shè)置地址(_addr)的接口(_interfaceHash 接口名稱的 keccak256 )由哪個(gè)合約實(shí)現(xiàn)(_implementer)。
  • getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address)
    這個(gè)函數(shù)用來查詢地址(_addr)的接口由哪個(gè)合約實(shí)現(xiàn)。

setInterfaceImplementer函數(shù)會(huì)參數(shù)信息記錄到下面這個(gè)interfaces映射里:

// 記錄 地址(第一個(gè)鍵) 的接口(第二個(gè)鍵)的實(shí)現(xiàn)地址(第二個(gè)值)
mapping(address => mapping(bytes32 => address)) interfaces;

相對(duì)應(yīng)的 getInterfaceImplementer() 通過 interfaces 這個(gè)mapping 來獲得接口的實(shí)現(xiàn)。

ERC777 使用 send轉(zhuǎn)賬時(shí)會(huì)分別在持有者和接收者地址上使用ERC1820 的getInterfaceImplementer函數(shù)進(jìn)行查詢,查看是否有對(duì)應(yīng)的實(shí)現(xiàn)合約,ERC777 標(biāo)準(zhǔn)規(guī)范里預(yù)定了接口及函數(shù)名稱,如果有實(shí)現(xiàn)則進(jìn)行相應(yīng)的調(diào)用。

ERC777 標(biāo)準(zhǔn)規(guī)范

ERC777 接口

ERC777 為了在實(shí)現(xiàn)上可以兼容ERC20,除了查詢函數(shù)和ERC20一致外,操作接口均采用的獨(dú)立的命名(避免相同的命令無法分辨是哪個(gè)標(biāo)準(zhǔn)),ERC777的接口定義如下,要求所有的ERC777代幣合約都必須實(shí)現(xiàn)這些接口:

interface ERC777Token {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function totalSupply() external view returns (uint256);
    function balanceOf(address holder) external view returns (uint256);

    // 定義代幣最小的劃分粒度
    function granularity() external view returns (uint256);

    // 操作員 相關(guān)的操作(操作員是可以代表持有者發(fā)送和銷毀代幣的賬號(hào)地址)
    function defaultOperators() external view returns (address[] memory);
    function isOperatorFor(
        address operator,
        address holder
    ) external view returns (bool);
    function authorizeOperator(address operator) external;
    function revokeOperator(address operator) external;

    // 發(fā)送代幣
    function send(address to, uint256 amount, bytes calldata data) external;
    function operatorSend(
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

    // 銷毀代幣
    function burn(uint256 amount, bytes calldata data) external;
    function operatorBurn(
        address from,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;

    // 發(fā)送代幣事件
    event Sent(
        address indexed operator,
        address indexed from,
        address indexed to,
        uint256 amount,
        bytes data,
        bytes operatorData
    );

    // 鑄幣事件
    event Minted(
        address indexed operator,
        address indexed to,
        uint256 amount,
        bytes data,
        bytes operatorData
    );

    // 銷毀代幣事件
    event Burned(
        address indexed operator,
        address indexed from,
        uint256 amount,
        bytes data,
        bytes operatorData
    );

    // 授權(quán)操作員事件
    event AuthorizedOperator(
        address indexed operator,
        address indexed holder
    );

    // 撤銷操作員事件
    event RevokedOperator(address indexed operator, address indexed holder);
}

接口定義在 openzeppelin代碼庫(kù) 里找到,路徑為:contracts/token/ERC777/IERC777.sol 。

接口說明與實(shí)現(xiàn)約定

所有的ERC777 合約除了必須實(shí)現(xiàn)上述接口,還有一些其他的必須遵守的約定(直接導(dǎo)致了ERC777官方文檔又長(zhǎng)又臭...哭~)。

ERC777 合約必須要通過 ERC1820 注冊(cè) ERC777Token 接口,這樣任何人都可以查詢合約是否是ERC777標(biāo)準(zhǔn)的合約,注冊(cè)方法是: 調(diào)用ERC1820 注冊(cè)合約的 setInterfaceImplementer 方法,參數(shù) _addr 及 _implementer 均是合約的地址,_interfaceHash 是 ERC777Token 的 keccak256 哈希值(0xac7fbab5...177054)

如果 ERC777 要實(shí)現(xiàn)ERC20標(biāo)準(zhǔn),還必須通過ERC1820 注冊(cè)ERC20Token接口。

ERC777 信息說明函數(shù)

name(),symbol(),totalSupply(),balanceOf(address) 和含義和在ERC20 中完全一樣。

granularity() 用來定義代幣最小的劃分粒度(>=1), 要求必須在創(chuàng)建時(shí)設(shè)定,之后不可以更改,不管是在鑄幣、發(fā)送還是銷毀操作的代幣數(shù)量,必需是粒度的整數(shù)倍。

granularity 和 ERC20 的 decimals 不一樣,decimals用來定義小數(shù)位數(shù),decimals 是ERC20 可選函數(shù),為了兼容 ERC20 代幣, decimals 函數(shù)要求必須返回18。而 granularity 表示的是基于最小位數(shù)(內(nèi)部存儲(chǔ))的劃分粒度。例如:0.5個(gè)代幣存儲(chǔ)為 500,000,000,000,000,000 (0.5 X 10^18),如果粒度為2,則最小轉(zhuǎn)賬單位是2(相對(duì)于500,000,000,000,000,000)。

操作員

ERC777 定義了一個(gè)新的操作員角色,操作員被作為移動(dòng)代幣的地址。 每個(gè)地址直觀地移動(dòng)自己的代幣,將持有人和操作員的概念分開可以提供更大的靈活性。

與ERC20中的 approve 、 transferFrom 不同,其未明確定義批準(zhǔn)地址的角色。

此外,ERC777還可以定義默認(rèn)操作員(默認(rèn)操作員列表只能在代幣創(chuàng)建時(shí)定義的,并且不能更改),默認(rèn)操作員是被所有持有人授權(quán)的操作員,這可以為項(xiàng)目方管理代幣帶來方便,當(dāng)然認(rèn)何持有人仍然有權(quán)撤銷默認(rèn)操作員。

操作員相關(guān)的函數(shù)

  • defaultOperators(): 獲取代幣合約默認(rèn)的操作員列表.
  • authorizeOperator(address operator): 設(shè)置一個(gè)地址作為msg.sender 的操作員,需要觸發(fā)AuthorizedOperator事件。
  • revokeOperator(address operator): 移除 msg.sender 上 operator 操作員的權(quán)限, 需要觸發(fā)RevokedOperator事件。
  • isOperatorFor(address operator, address holder): 是否是某個(gè)持有者的操作員。

發(fā)送代幣

ERC777 發(fā)送代幣 使用以下兩個(gè)方法:

send(address to, uint256 amount, bytes calldata data) external

function operatorSend(
    address from,
    address to,
    uint256 amount,
    bytes calldata data,
    bytes calldata operatorData
) external

operatorSend 可以通過參數(shù)operatorData攜帶操作者的信息,發(fā)送代幣除了執(zhí)行對(duì)應(yīng)賬戶的余額加減和觸發(fā)事件之外,還有額外的規(guī)定

  1. 如果持有者有通過 ERC1820 注冊(cè) ERC777TokensSender 實(shí)現(xiàn)接口, 代幣合約必須調(diào)用其 tokensToSend 鉤子函數(shù)。
  2. 如果接收者有通過 ERC1820 注冊(cè) ERC777TokensRecipient 實(shí)現(xiàn)接口, 代幣合約必須調(diào)用其 tokensReceived 鉤子函數(shù)。
  3. 如果有 tokensToSend 鉤子函數(shù),必須在修改余額狀態(tài)之前調(diào)用。
  4. 如果有 tokensReceived 鉤子函數(shù),必須在修改余額狀態(tài)之后調(diào)用。
  5. 調(diào)用鉤子函數(shù)及觸發(fā)事件時(shí), dataoperatorData必須原樣傳遞,因?yàn)?tokensToSend 和 tokensReceived 函數(shù)可能根據(jù)這個(gè)數(shù)據(jù)取消轉(zhuǎn)賬(觸發(fā) revert)。

ERC777TokensSender 接口定義如下:

interface ERC777TokensSender {
    function tokensToSend(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata userData,
        bytes calldata operatorData
    ) external;
}

如果持有者希望在轉(zhuǎn)賬時(shí)收到代幣轉(zhuǎn)移通知,就需要在ERC1820合約上注冊(cè)及實(shí)現(xiàn) ERC777TokensSender 接口(稍后有案例介紹)。

有一個(gè)地方需要注意: 對(duì)于所有的 ERC777 合約, 一個(gè)持有者地址只能注冊(cè)一個(gè)ERC777TokensSender接口實(shí)現(xiàn)。因此 ERC777TokensSender 實(shí)現(xiàn)會(huì)被多個(gè)ERC777合約調(diào)用,在ERC777TokensSender接口的實(shí)現(xiàn)合約里, msg.sender 是ERC777合約地址,而不是操作者。

ERC777TokensRecipient 接口定義如下:

interface ERC777TokensRecipient {
    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) external;
}

如果接收者希望在轉(zhuǎn)賬時(shí)收到代幣轉(zhuǎn)移通知,就需要在ERC1820合約上注冊(cè)及實(shí)現(xiàn) ERC777TokensRecipient 接口。

如果接收者是一個(gè)合約地址, 則必須要注冊(cè)及實(shí)現(xiàn) ERC777TokensRecipient 接口(這樣可以防止代幣被鎖死),如果沒有實(shí)現(xiàn),ERC777代幣合約必須revert 回退交易狀態(tài)。

鑄幣與銷毀

鑄幣(挖礦)是產(chǎn)生新幣的過程,銷毀代幣則相反,在ERC20 中,沒有明確定義這兩個(gè)行為,通常會(huì)transfer方法和Transfer事件來表達(dá)。
ERC777 則定義了代幣從鑄幣、轉(zhuǎn)移到銷毀的整個(gè)生命周期。

ERC777 沒有定義鑄幣的方法名,只定義了 Minted事件,因?yàn)楹芏啻鷰?,是在?chuàng)建的時(shí)候就確定好代幣的數(shù)量。
如果有需要合約可以自己定義鑄幣函數(shù),鑄幣函數(shù)在實(shí)現(xiàn)時(shí)要求:

  1. 必須觸發(fā)Minted事件
  2. 發(fā)行量需要加上鑄幣量, 接收者是不為 0 ,且接收者余額加上鑄幣量。
  3. 如果接收者有通過 ERC1820 注冊(cè) ERC777TokensRecipient 實(shí)現(xiàn)接口, 代幣合約必須調(diào)用其 tokensReceived 鉤子函數(shù)。

ERC777 定義了兩個(gè)函數(shù)用于銷毀代幣 (burnoperatorBurn),可以方便錢包和dapps有統(tǒng)一的接口交互。burnoperatorBurn 的實(shí)現(xiàn)要求:

  1. 必須觸發(fā)Burned事件。
  2. 總供應(yīng)量必須減少代幣銷毀量, 持有者的余額必須減少代幣銷毀的數(shù)量。
  3. 如果持有者通過ERC1820注冊(cè)ERC777TokensSender 實(shí)現(xiàn),必須調(diào)用持有者的tokensToSend鉤子函數(shù)。

注意,零個(gè)代幣數(shù)量的交易(不管是轉(zhuǎn)移、鑄幣與銷毀)也是合法的,同樣滿足粒度(granularity) 的整數(shù)倍,因此需要正確處理。

ERC777 代幣實(shí)現(xiàn)

OpenZeppelin 實(shí)現(xiàn)了一個(gè) ERC777 基礎(chǔ)合約,要實(shí)現(xiàn)自己的ERC777代幣只需要繼承 OpenZeppelin ERC777。想了解 OpenZeppelin 的 ERC777 的實(shí)現(xiàn)可閱讀ERC777 源碼解析。

如果大家是Truffle開發(fā)(或者是Node工程),可以使用以下方式安裝 OpenZeppelin 合約庫(kù):

npm install @openzeppelin/contracts

發(fā)行一個(gè) 2100 個(gè)的 LBC7 代幣的代碼就很簡(jiǎn)單了:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC777/ERC777.sol";

contract MyERC777 is ERC777 {
    constructor(
        address[] memory defaultOperators
    )
        ERC777("MyERC777", "LBC7", defaultOperators)
        public
    {
        uint initialSupply = 2100 * 10 ** 18;
        _mint(msg.sender, msg.sender, initialSupply, "", "");
    }
}

實(shí)現(xiàn)主要是兩步:通過基類ERC777的構(gòu)造函數(shù)確認(rèn)代幣名稱、代號(hào)以及默認(rèn)操作員(可為空),然后調(diào)用 _mint 初始化發(fā)行量,注意發(fā)行量的小數(shù)位是固定的18位(和ether保持一致),在合約內(nèi)部是按小數(shù)位保存的,因此發(fā)行的幣數(shù)需要乘上1018。

監(jiān)聽代幣收款

我們假設(shè)有這樣一個(gè)需求:寺廟要實(shí)現(xiàn)了一個(gè)功德箱合約接收捐贈(zèng),功德箱合約需要記錄每位施主的善款金額。這時(shí)候就可以通過實(shí)現(xiàn) ERC777TokensRecipient接口來完成。代碼也很簡(jiǎn)單:

pragma solidity ^0.5.0;

import "@openzeppelin/contracts/token/ERC777/IERC777Recipient.sol";
import "@openzeppelin/contracts/token/ERC777/IERC777.sol";
import "@openzeppelin/contracts/introspection/IERC1820Registry.sol";

contract Merit is IERC777Recipient {

  mapping(address => uint) public givers;
  address _owner;
  IERC777 _token;

  IERC1820Registry private _erc1820 = IERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24);

  // keccak256("ERC777TokensRecipient")
  bytes32 constant private TOKENS_RECIPIENT_INTERFACE_HASH =
      0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b;

  constructor(IERC777 token) public {
    _erc1820.setInterfaceImplementer(address(this), TOKENS_RECIPIENT_INTERFACE_HASH, address(this));
    _owner = msg.sender;
    _token = token;
  }

// 收款時(shí)被回調(diào)
  function tokensReceived(
      address operator,
      address from,
      address to,
      uint amount,
      bytes calldata userData,
      bytes calldata operatorData
  ) external {
    givers[from] += amount;
  }

// 方丈取回功德箱token
  function withdraw () external {
    require(msg.sender == _owner, "no permision");
    uint balance = _token.balanceOf(address(this));
    _token.send(_owner, balance, "");
  }

}

功德箱合約在構(gòu)造時(shí),調(diào)用 ERC1820 注冊(cè)表合約的 setInterfaceImplementer函數(shù) 注冊(cè)ERC777TokensRecipient接口實(shí)現(xiàn)(接口的實(shí)現(xiàn)是自身),這樣在收到代幣時(shí),會(huì)回調(diào) tokensReceived函數(shù),tokensReceived函數(shù)通過givers映射來保存每個(gè)施主的善款金額。

注意: 如果是在本地的開發(fā)者網(wǎng)絡(luò)環(huán)境,可能會(huì)沒有ERC1820 注冊(cè)表合約,如果沒有需要先部署ERC1820注冊(cè)表合約,參考eip-1820 中文文檔。

功德箱這個(gè)實(shí)例僅僅是拋磚引玉,告訴大家如何實(shí)現(xiàn)收款時(shí)的回調(diào),之后有時(shí)間,我寫一個(gè)完整的存幣生息應(yīng)用。

普通賬戶地址監(jiān)聽代幣轉(zhuǎn)出

功德箱合約的例子,收款地址和收款監(jiān)聽是同一個(gè)合約, 現(xiàn)在來看看一個(gè)普通的用戶地址,如何委托一個(gè)合約來監(jiān)聽代幣的轉(zhuǎn)出。
監(jiān)聽代幣的轉(zhuǎn)出可以讓持有者對(duì)發(fā)出去的代幣有更多的控制,例如持有者可以設(shè)置一些黑名單,禁止操作員對(duì)黑名單內(nèi)賬號(hào)轉(zhuǎn)賬,

本部分的內(nèi)容請(qǐng)訂閱我的小專欄查看。

通過實(shí)現(xiàn) ERC777TokensSender 和 ERC777TokensRecipient 可以延伸出很多有意思的玩法,各位讀者可以自行探索。


參考

  1. openzeppelin 項(xiàng)目文檔
  2. openzeppelin 合約代碼庫(kù)
  3. EIP 165 提案文檔
  4. EIP 1820 提案文檔
  5. EIP 20 代幣標(biāo)準(zhǔn)提案文檔
  6. EIP 777 代幣標(biāo)準(zhǔn)提案文檔
  7. EIP 721 非同質(zhì)代幣提案文檔
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容