想必很多同學都已經(jīng)使用過ERC20 創(chuàng)建過代幣[1],或許已經(jīng)被老板要求在ERC20代幣上實現(xiàn)一些附加功能搞的焦頭爛額,如果還有選擇,一定要選擇
想必很多同學都已經(jīng)使用過ERC20 創(chuàng)建過代幣[1],或許已經(jīng)被老板要求在ERC20代幣上實現(xiàn)一些附加功能搞的焦頭爛額,如果還有選擇,一定要選擇 ERC777 。
ERC20 的問題
以下是一個遇到很多次的場景:有一天老板過來找你(開發(fā)者),最近存幣生息很火,我們也做一個合約吧, 用戶打幣過來給他計算利息, 看起來是一個很簡單的需求,你滿口答應說好,結果自己一研究發(fā)現(xiàn),使用 ERC20 標準沒辦法在合約里記錄是誰發(fā)過來多少幣,從而沒法計算利息(因為接收者合約并不知道自己接收到ERC20代幣)。
ERC20 標準下,可以通過一個變通的辦法,采用兩個交易組合完成,方法是:第1步:先讓用戶把要轉(zhuǎn)移的金額用 ERC20 的approve 授權的存幣生息合約(這步通常稱為解鎖),第2步:再次讓用戶調(diào)用存幣生息合約的計息函數(shù),計息函數(shù)中通過 transferFrom 把代幣從用戶手里轉(zhuǎn)移的合約內(nèi),并開始計息。
同樣由于ERC20 標準沒有一個轉(zhuǎn)賬通知機制,很多ERC20代幣誤轉(zhuǎn)到合約之后,再也沒有辦法把幣轉(zhuǎn)移出來,已經(jīng)有大量的ERC20 因為這個原因被鎖死,如鎖死的QTUM[2],鎖死的EOS[3] 。
另外一個問題是ERC20 轉(zhuǎn)賬時,無法攜帶額外的信息,例如:我們有一些客戶希望讓用戶使用 ERC20 代幣購買商品,因為轉(zhuǎn)賬沒法攜帶額外的信息, 用戶的代幣轉(zhuǎn)移過來,不知道用戶具體要購買哪件商品,從而展加了線下額外的溝通成本。
ERC777很好的解決了這些問題,同時ERC777 也兼容 ERC20 標準。因此強烈建議新開發(fā)的代幣使用ERC777標準。
ERC777 在 ERC20的基礎上定義了 send(dest, value, data) 來轉(zhuǎn)移代幣, send函數(shù)額外的參數(shù)用來攜帶其他的信息,send函數(shù)會檢查持有者和接收者是否實現(xiàn)了相應的鉤子函數(shù),如果有實現(xiàn)(不管是普通用戶地址還是合約地址都可以實現(xiàn)鉤子函數(shù)),則調(diào)用相應的鉤子函數(shù)。
ERC1820 接口注冊表合約
即便是一個普通用戶地址,同樣可以實現(xiàn)對 ERC777 轉(zhuǎn)賬的監(jiān)聽, 聽起來有點神奇,其實這是通過 ERC1820 接口注冊表合約來是實現(xiàn)的。
ERC1820 如此的重要,以至于ERC777單獨把它拆出來作為一個EIP。
ERC1820 是一個全局的合約,有一個唯一在以太坊鏈上都相同的合約地址,它總是 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 ,這個合約是通過非常巧妙的方式進行部署的,有興趣的同學可以閱讀EIP1820文檔[4]。
ERC 1820 合約的官方實現(xiàn)代碼在ERC1820文檔[5]可以查閱,這里說明合約實現(xiàn)的主要內(nèi)容。
ERC1820合約提過了兩個主要接口:
•setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer) 用來設置地址(_addr)的接口(_interfaceHash 接口名稱的 keccak256 )由哪個合約實現(xiàn)(_implementer)。
•getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address) 這個函數(shù)用來查詢地址(_addr)的接口由哪個合約實現(xiàn)。
setInterfaceImplementer函數(shù)會參數(shù)信息記錄到下面這個interfaces映射里:
// 記錄 地址(第一個鍵) 的接口(第二個鍵)的實現(xiàn)地址(第二個值)
mapping(address => mapping(bytes32 => address)) interfaces;
相對應的 getInterfaceImplementer() 通過 interfaces 這個mapping 來獲得接口的實現(xiàn)。
ERC777 使用 send轉(zhuǎn)賬時會分別在持有者和接收者地址上使用ERC1820 的getInterfaceImplementer函數(shù)進行查詢,查看是否有對應的實現(xiàn)合約,ERC777 標準規(guī)范里預定了接口及函數(shù)名稱,如果有實現(xiàn)則進行相應的調(diào)用。
ERC777 標準規(guī)范
ERC777 接口
ERC777 為了在實現(xiàn)上可以兼容ERC20,除了查詢函數(shù)和ERC20一致外,操作接口均采用的獨立的命名(避免相同的命令無法分辨是哪個標準),ERC777的接口定義如下,要求所有的ERC777代幣合約都必須實現(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);
// 操作員 相關的操作(操作員是可以代表持有者發(fā)送和銷毀代幣的賬號地址)
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
);
// 授權操作員事件
event AuthorizedOperator(
address indexed operator,
address indexed holder
);
// 撤銷操作員事件
event RevokedOperator(address indexed operator, address indexed holder);
}
接口定義在 openzeppelin代碼庫[6] 里找到,路徑為:contracts/token/ERC777/IERC777.sol 。
接口說明與實現(xiàn)約定
所有的ERC777 合約除了必須實現(xiàn)上述接口,還有一些其他的必須遵守的約定(直接導致了ERC777官方文檔又長又臭...哭~)。
ERC777 合約必須要通過 ERC1820 注冊 ERC777Token 接口,這樣任何人都可以查詢合約是否是ERC777標準的合約,注冊方法是: 調(diào)用ERC1820 注冊合約的 setInterfaceImplementer 方法,參數(shù) _addr 及 _implementer 均是合約的地址,_interfaceHash 是 ERC777Token 的 keccak256 哈希值(0xac7fbab5...177054)
如果 ERC777 要實現(xiàn)ERC20標準,還必須通過ERC1820 注冊ERC20Token接口。
ERC777 信息說明函數(shù)
name(),symbol(),totalSupply(),balanceOf(address) 和含義和在ERC20 中完全一樣。
granularity() 用來定義代幣最小的劃分粒度(>=1), 要求必須在創(chuàng)建時設定,之后不可以更改,不管是在鑄幣、發(fā)送還是銷毀操作的代幣數(shù)量,必需是粒度的整數(shù)倍。
granularity 和 ERC20 的 decimals 不一樣,decimals用來定義小數(shù)位數(shù),decimals 是ERC20 可選函數(shù),為了兼容 ERC20 代幣, decimals 函數(shù)要求必須返回18。而 granularity 表示的是基于最小位數(shù)(內(nèi)部存儲)的劃分粒度。例如:0.5個代幣存儲為 500,000,000,000,000,000 (0.5 X 10^18),如果粒度為2,則最小轉(zhuǎn)賬單位是2(相對于500,000,000,000,000,000)。
操作員
ERC777 定義了一個新的操作員角色,操作員被作為移動代幣的地址。每個地址直觀地移動自己的代幣,將持有人和操作員的概念分開可以提供更大的靈活性。
與ERC20中的 approve 、 transferFrom 不同,其未明確定義批準地址的角色。
此外,ERC777還可以定義默認操作員(默認操作員列表只能在代幣創(chuàng)建時定義的,并且不能更改),默認操作員是被所有持有人授權的操作員,這可以為項目方管理代幣帶來方便,當然認何持有人仍然有權撤銷默認操作員。
操作員相關的函數(shù):
•defaultOperators(): 獲取代幣合約默認的操作員列表.
•authorizeOperator(address operator): 設置一個地址作為msg.sender 的操作員,需要觸發(fā)AuthorizedOperator事件。
•revokeOperator(address operator): 移除 msg.sender 上 operator 操作員的權限, 需要觸發(fā)RevokedOperator事件。
•isOperatorFor(address operator, address holder):是否是某個持有者的操作員。
發(fā)送代幣
ERC777 發(fā)送代幣 使用以下兩個方法:
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í)行對應賬戶的余額加減和觸發(fā)事件之外,還有額外的規(guī)定:
1.如果持有者有通過 ERC1820 注冊 ERC777TokensSender 實現(xiàn)接口, 代幣合約必須調(diào)用其 tokensToSend 鉤子函數(shù)。
2.如果接收者有通過 ERC1820 注冊 ERC777TokensRecipient 實現(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ā)事件時, data 和 operatorData必須原樣傳遞,因為 tokensToSend 和 tokensReceived 函數(shù)可能根據(jù)這個數(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)賬時收到代幣轉(zhuǎn)移通知,就需要在ERC1820合約上注冊及實現(xiàn) ERC777TokensSender 接口(稍后有案例介紹)。
有一個地方需要注意: 對于所有的 ERC777 合約, 一個持有者地址只能注冊一個ERC777TokensSender接口實現(xiàn)。因此 ERC777TokensSender 實現(xiàn)會被多個ERC777合約調(diào)用,在ERC777TokensSender接口的實現(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)賬時收到代幣轉(zhuǎn)移通知,就需要在ERC1820合約上注冊及實現(xiàn) ERC777TokensRecipient 接口。
如果接收者是一個合約地址, 則必須要注冊及實現(xiàn) ERC777TokensRecipient 接口(這樣可以防止代幣被鎖死),如果沒有實現(xiàn),ERC777代幣合約必須revert 回退交易狀態(tài)。
鑄幣與銷毀
鑄幣(挖礦)是產(chǎn)生新幣的過程,銷毀代幣則相反,在ERC20 中,沒有明確定義這兩個行為,通常會transfer方法和Transfer事件來表達。ERC777 則定義了代幣從鑄幣、轉(zhuǎn)移到銷毀的整個生命周期。
ERC777 沒有定義鑄幣的方法名,只定義了 Minted事件,因為很多代幣,是在創(chuàng)建的時候就確定好代幣的數(shù)量。如果有需要合約可以自己定義鑄幣函數(shù),鑄幣函數(shù)在實現(xiàn)時要求:
1.必須觸發(fā)Minted事件
2.發(fā)行量需要加上鑄幣量, 接收者是不為 0 ,且接收者余額加上鑄幣量。
3.如果接收者有通過 ERC1820 注冊 ERC777TokensRecipient 實現(xiàn)接口, 代幣合約必須調(diào)用其 tokensReceived 鉤子函數(shù)。
ERC777 定義了兩個函數(shù)用于銷毀代幣 (burn 和 operatorBurn),可以方便錢包和dapps有統(tǒng)一的接口交互。burn 和 operatorBurn 的實現(xiàn)要求:
1.必須觸發(fā)Burned事件。
2.總供應量必須減少代幣銷毀量, 持有者的余額必須減少代幣銷毀的數(shù)量。
3.如果持有者通過ERC1820注冊ERC777TokensSender 實現(xiàn),必須調(diào)用持有者的tokensToSend鉤子函數(shù)。
注意,零個代幣數(shù)量的交易(不管是轉(zhuǎn)移、鑄幣與銷毀)也是合法的,同樣滿足粒度(granularity) 的整數(shù)倍,因此需要正確處理。
ERC777 代幣實現(xiàn)
OpenZeppelin 實現(xiàn)了一個 ERC777 基礎合約,要實現(xiàn)自己的ERC777代幣只需要繼承 OpenZeppelin ERC777。想了解 OpenZeppelin 的 ERC777 的實現(xiàn)可閱讀ERC777 源碼解析[7]。
如果大家是Truffle開發(fā)(或者是Node工程),可以使用以下方式安裝 OpenZeppelin 合約庫:
npm install @openzeppelin/contracts
發(fā)行一個 2100 個的 LBC7 代幣的代碼就很簡單了:
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, "", "");
}
}
實現(xiàn)主要是兩步:通過基類ERC777的構造函數(shù)確認代幣名稱、代號以及默認操作員(可為空),然后調(diào)用 _mint 初始化發(fā)行量,注意發(fā)行量的小數(shù)位是固定的18位(和ether保持一致),在合約內(nèi)部是按小數(shù)位保存的,因此發(fā)行的幣數(shù)需要乘上1018。
監(jiān)聽代幣收款
我們假設有這樣一個需求:寺廟要實現(xiàn)了一個功德箱合約接收捐贈,功德箱合約需要記錄每位施主的善款金額。這時候就可以通過實現(xiàn) ERC777TokensRecipient接口來完成。代碼也很簡單:
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;
}
// 收款時被回調(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, "");
}
}
功德箱合約在構造時,調(diào)用 ERC1820 注冊表合約的 setInterfaceImplementer函數(shù) 注冊ERC777TokensRecipient接口實現(xiàn)(接口的實現(xiàn)是自身),這樣在收到代幣時,會回調(diào) tokensReceived函數(shù),tokensReceived函數(shù)通過givers映射來保存每個施主的善款金額。
注意:如果是在本地的開發(fā)者網(wǎng)絡環(huán)境,可能會沒有ERC1820 注冊表合約,如果沒有需要先部署ERC1820注冊表合約,參考eip-1820 中文文檔[8]。
功德箱這個實例僅僅是拋磚引玉,告訴大家如何實現(xiàn)收款時的回調(diào),之后有時間,我寫一個完整的存幣生息應用。
普通賬戶地址監(jiān)聽代幣轉(zhuǎn)出
功德箱合約的例子,收款地址和收款監(jiān)聽是同一個合約, 現(xiàn)在來看看一個普通的用戶地址,如何委托一個合約來監(jiān)聽代幣的轉(zhuǎn)出。監(jiān)聽代幣的轉(zhuǎn)出可以讓持有者對發(fā)出去的代幣有更多的控制,例如持有者可以設置一些黑名單,禁止操作員對黑名單內(nèi)賬號轉(zhuǎn)賬。(Tiny 熊)