想必很多同學(xué)都已經(jīng)使用過(guò)ERC20 創(chuàng)建過(guò)代幣[1],或許已經(jīng)被老板要求在ERC20代幣上實(shí)現(xiàn)一些附加功能搞的焦頭爛額,如果還有選擇,一定要選擇
想必很多同學(xué)都已經(jīng)使用過(guò)ERC20 創(chuàng)建過(guò)代幣[1],或許已經(jīng)被老板要求在ERC20代幣上實(shí)現(xiàn)一些附加功能搞的焦頭爛額,如果還有選擇,一定要選擇 ERC777 。
ERC20 的問(wèn)題
以下是一個(gè)遇到很多次的場(chǎng)景:有一天老板過(guò)來(lái)找你(開發(fā)者),最近存幣生息很火,我們也做一個(gè)合約吧, 用戶打幣過(guò)來(lái)給他計(jì)算利息, 看起來(lái)是一個(gè)很簡(jiǎn)單的需求,你滿口答應(yīng)說(shuō)好,結(jié)果自己一研究發(fā)現(xiàn),使用 ERC20 標(biāo)準(zhǔn)沒辦法在合約里記錄是誰(shuí)發(fā)過(guò)來(lái)多少幣,從而沒法計(jì)算利息(因?yàn)榻邮照吆霞s并不知道自己接收到ERC20代幣)。
ERC20 標(biāo)準(zhǔn)下,可以通過(guò)一個(gè)變通的辦法,采用兩個(gè)交易組合完成,方法是:第1步:先讓用戶把要轉(zhuǎn)移的金額用 ERC20 的approve 授權(quán)的存幣生息合約(這步通常稱為解鎖),第2步:再次讓用戶調(diào)用存幣生息合約的計(jì)息函數(shù),計(jì)息函數(shù)中通過(guò) transferFrom 把代幣從用戶手里轉(zhuǎn)移的合約內(nèi),并開始計(jì)息。
同樣由于ERC20 標(biāo)準(zhǔn)沒有一個(gè)轉(zhuǎn)賬通知機(jī)制,很多ERC20代幣誤轉(zhuǎn)到合約之后,再也沒有辦法把幣轉(zhuǎn)移出來(lái),已經(jīng)有大量的ERC20 因?yàn)檫@個(gè)原因被鎖死,如鎖死的QTUM[2],鎖死的EOS[3] 。
另外一個(gè)問(wèn)題是ERC20 轉(zhuǎn)賬時(shí),無(wú)法攜帶額外的信息,例如:我們有一些客戶希望讓用戶使用 ERC20 代幣購(gòu)買商品,因?yàn)檗D(zhuǎn)賬沒法攜帶額外的信息, 用戶的代幣轉(zhuǎn)移過(guò)來(lái),不知道用戶具體要購(gòu)買哪件商品,從而展加了線下額外的溝通成本。
ERC777很好的解決了這些問(wèn)題,同時(shí)ERC777 也兼容 ERC20 標(biāo)準(zhǔn)。因此強(qiáng)烈建議新開發(fā)的代幣使用ERC777標(biāo)準(zhǔn)。
ERC777 在 ERC20的基礎(chǔ)上定義了 send(dest, value, data) 來(lái)轉(zhuǎn)移代幣, send函數(shù)額外的參數(shù)用來(lái)攜帶其他的信息,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)聽, 聽起來(lái)有點(diǎn)神奇,其實(shí)這是通過(guò) ERC1820 接口注冊(cè)表合約來(lái)是實(shí)現(xiàn)的。
ERC1820 如此的重要,以至于ERC777單獨(dú)把它拆出來(lái)作為一個(gè)EIP。
ERC1820 是一個(gè)全局的合約,有一個(gè)唯一在以太坊鏈上都相同的合約地址,它總是 0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24 ,這個(gè)合約是通過(guò)非常巧妙的方式進(jìn)行部署的,有興趣的同學(xué)可以閱讀EIP1820文檔[4]。
ERC 1820 合約的官方實(shí)現(xiàn)代碼在ERC1820文檔[5]可以查閱,這里說(shuō)明合約實(shí)現(xiàn)的主要內(nèi)容。
ERC1820合約提過(guò)了兩個(gè)主要接口:
•setInterfaceImplementer(address _addr, bytes32 _interfaceHash, address _implementer) 用來(lái)設(shè)置地址(_addr)的接口(_interfaceHash 接口名稱的 keccak256 )由哪個(gè)合約實(shí)現(xiàn)(_implementer)。
•getInterfaceImplementer(address _addr, bytes32 _interfaceHash) external view returns (address) 這個(gè)函數(shù)用來(lái)查詢地址(_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() 通過(guò) interfaces 這個(gè)mapping 來(lái)獲得接口的實(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ú)立的命名(避免相同的命令無(wú)法分辨是哪個(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ù)[6] 里找到,路徑為:contracts/token/ERC777/IERC777.sol 。
接口說(shuō)明與實(shí)現(xiàn)約定
所有的ERC777 合約除了必須實(shí)現(xiàn)上述接口,還有一些其他的必須遵守的約定(直接導(dǎo)致了ERC777官方文檔又長(zhǎng)又臭...哭~)。
ERC777 合約必須要通過(guò) 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),還必須通過(guò)ERC1820 注冊(cè)ERC20Token接口。
ERC777 信息說(shuō)明函數(shù)
name(),symbol(),totalSupply(),balanceOf(address) 和含義和在ERC20 中完全一樣。
granularity() 用來(lái)定義代幣最小的劃分粒度(>=1), 要求必須在創(chuàng)建時(shí)設(shè)定,之后不可以更改,不管是在鑄幣、發(fā)送還是銷毀操作的代幣數(shù)量,必需是粒度的整數(shù)倍。
granularity 和 ERC20 的 decimals 不一樣,decimals用來(lái)定義小數(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)目方管理代幣帶來(lái)方便,當(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 可以通過(guò)參數(shù)operatorData攜帶操作者的信息,發(fā)送代幣除了執(zhí)行對(duì)應(yīng)賬戶的余額加減和觸發(fā)事件之外,還有額外的規(guī)定:
1.如果持有者有通過(guò) ERC1820 注冊(cè) ERC777TokensSender 實(shí)現(xiàn)接口, 代幣合約必須調(diào)用其 tokensToSend 鉤子函數(shù)。
2.如果接收者有通過(guò) 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í), data 和 operatorData必須原樣傳遞,因?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)生新幣的過(guò)程,銷毀代幣則相反,在ERC20 中,沒有明確定義這兩個(gè)行為,通常會(huì)transfer方法和Transfer事件來(lái)表達(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.如果接收者有通過(guò) ERC1820 注冊(cè) ERC777TokensRecipient 實(shí)現(xiàn)接口, 代幣合約必須調(diào)用其 tokensReceived 鉤子函數(shù)。
ERC777 定義了兩個(gè)函數(shù)用于銷毀代幣 (burn 和 operatorBurn),可以方便錢包和dapps有統(tǒng)一的接口交互。burn 和 operatorBurn 的實(shí)現(xiàn)要求:
1.必須觸發(fā)Burned事件。
2.總供應(yīng)量必須減少代幣銷毀量, 持有者的余額必須減少代幣銷毀的數(shù)量。
3.如果持有者通過(guò)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 源碼解析[7]。
如果大家是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)主要是兩步:通過(guò)基類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í)候就可以通過(guò)實(shí)現(xiàn) ERC777TokensRecipient接口來(lái)完成。代碼也很簡(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ù)通過(guò)givers映射來(lái)保存每個(gè)施主的善款金額。
注意:如果是在本地的開發(fā)者網(wǎng)絡(luò)環(huán)境,可能會(huì)沒有ERC1820 注冊(cè)表合約,如果沒有需要先部署ERC1820注冊(cè)表合約,參考eip-1820 中文文檔[8]。
功德箱這個(gè)實(shí)例僅僅是拋磚引玉,告訴大家如何實(shí)現(xiàn)收款時(shí)的回調(diào),之后有時(shí)間,我寫一個(gè)完整的存幣生息應(yīng)用。
普通賬戶地址監(jiān)聽代幣轉(zhuǎn)出
功德箱合約的例子,收款地址和收款監(jiān)聽是同一個(gè)合約, 現(xiàn)在來(lái)看看一個(gè)普通的用戶地址,如何委托一個(gè)合約來(lái)監(jiān)聽代幣的轉(zhuǎn)出。監(jiān)聽代幣的轉(zhuǎn)出可以讓持有者對(duì)發(fā)出去的代幣有更多的控制,例如持有者可以設(shè)置一些黑名單,禁止操作員對(duì)黑名單內(nèi)賬號(hào)轉(zhuǎn)賬。(Tiny 熊)