干货 | 代币支付的以太坊智能服务
我在之前那篇文章(中译本见文末)中解释了以太坊,尤其是ERC-20,代币合约是如何工作的。这篇文章将探讨如何使用代币支付那些智能合约提供的服务。
首先值得探讨的就是使用以太币(以太坊的基础数字货币)来支付服务的过程。举个例子,在区块链上储存一个数字的需要1以太币的服务费,当一个用户想要存东西时,他会把想存的信息和存储这个信息的花费一起发送到一个交易中:
上面的流程图表示了发送者给服务合约发送交易,交易调用函数 storeData(4)
,来存储值4,交易还包含花费的一个以太币。Solidity中执行相同功能的服务合约函数如下所示:
function storeData(uint256 payload) public payable {
require(msg.value == 1 ether);
info[msg.sender] = payload;
}
这个函数非常简单:第一行是要确定发送者是否把1以太币包含到交易中,第二行存储发送者包含在交易中信息(在上面的例子中这个信息是“4”)。
现在考虑一个相似的服务:服务要求花费代币而不是以太币。假设有一种STORE币,而且每存储一次信息都需要花费1STORE币。我们可以这样假设那个与上面等价的交易如下所示:由代币去支付
服务合约函数如下:
function storeData(uint256 payload) public payable {
require(msg.value == 1 STORE);
info[msg.sender] = payload;
}
但是这个函数是不可能实现的,因为在以太坊中“value”总是以太币,不可能是其他代币。那么,发送者和/或服务合约必须与STORE代币合约进行交互,来把代币从发送者那里转移到服务合约中。有很多方法可以做到这一点,本文将提供最常见的细节。
approve() 和 transferFrom()函数
approve()
和 transferFrom()
函数是 ECR-20 标准的一部分,应该出现在每一个兼容的代币合约中。 approve()
的操作是授权第三方(在这个例子中是服务合约)从发送者账户转移代币,然后通过 transferFrom()
函数来执行第三方的转移操作。
过程如下:
发送者通知代币合约:1STORE币已经授权给了服务合约(通过调用代币合约的
approve()
函数)发送者请求服务合约执行(通过调用服务合约的
storeData()
函数)。服务合约指示代币合约将代币从发送者的账户转移到服务合约的账户(通过调用服务合约的
transferFrom()
函数)并存储信息。
这个过程的流程如图所示:
这里 transferFrom()
函数的虚线表明它作为 storeData()
交易的一部分被调用,不需要代表服务合约的任何人工干预来触发它(这就意味着它不会在区块链上作为一个单独的交易出现,而是作为 storeData()
交易的一部分)。
以这种方式使用代币会让 Solidity 中的 storeData()
服务合约函数改成以下内容:
function storeData(uint256 payload) public {
require(tokenContract.transferFrom(msg.sender, address(this), 1));
info[msg.sender] = payload;
}
第一行已经变成执行从消息的发送者到服务合约预先认可的1STORE币的转移。不管任何原因如果执行失败,那么函数将会终止,以确保合约承担的工作会获得报酬。反之(第一行执行成功)它则会根据请求来存储信息。
approveAndCall()函数
尽管上述过程能够执行,但是它需要发送者进行两次交易,一次执行 approve()
,另一次执行 storeData()
。这是不可取的,因为它把额外的工作交给了发送者。使用 approveAndCall()
函数,发送者就能用一个交易来做相同的工作。
过程如下:
发送者通知代币合约:1STORE币授权给了服务合约(通过调用代币合约的
approveAndCall()
函数)代币合约通知服务合约:1STORE币已经授权给了服务合约(通过调用服务合约的
receiveApproval()
函数)服务合约指示代币合约将代币从发送者的账户转移到服务合约的账户(通过调用服务合约的
transferFrom()
函数)并且存储信息
这个过程的流程如图所示:
approveAndCall()
函数用代币来支付服务合约的功能-
这需要把approveAndCall()函数编写到代币合约中,代码如下:
function approveAndCall(address _recipient,
uint256 _value,
bytes _extraData) {
approve(_recipient, _value);
TokenRecipient(_recipient).receiveApproval(msg.sender,
_value,
address(this),
_extraData);
}
第一行执行标准 approve()
函数调用,第二行调用服务合约中的 receiveApproval()
函数。服务合约中 receiveApproval()
函数代码如下:
function receiveApproval(address _sender,
uint256 _value,
TokenContract _tokenContract,
bytes _extraData) {
require(_tokenContract == tokenContract);
require(tokenContract.transferFrom(_sender, address(this), 1));
uint256 payloadSize;
uint256 payload;
assembly {
payloadSize := mload(_extraData)
payload := mload(add(_extraData, 0x20))
}
payload = payload >> 8*(32 - payloadSize);
info[sender] = payload;
}
第一行确保调用此函数的是代币合约,第二行获取应得代币,3-9行获取需要存储的信息,第10行存储信息。
很快我们就能发现,这里的主要问题就是 receiveApproval()
函数代码比迄今为止所看到的代码都复杂的多。这是由于数据是用 bytes
形式传递的,所以需要附加的结构来存储和解析数据。数据发送之前还需要编码,这就增加了发送者的复杂度。
另外,还需要注意的是 approvalAndCall()
函数的定义并不在 ECR-20 标准中,这就意味着这个函数的实现并不能保证。尤其是:
一些代币合约根本不支持
approvalAndCall()
函数一些代币合约仅支持一个硬编码的
receiveApproval()
函数作为approvalAndCall()
函数的目标(就像上面图中显示的那样)一些代币合约通过把相关函数签名添加到发送者的
approvalAndCall()
函数中,支持任意的服务合约函数,就像下图所示(把storeData()
添加到发送者的调用中):
approveAndCall()
函数可以通过在 approveAndCall()
函数中添加一个函数签名来调用服务合约的任何函数-
这对代币合约的创建者(他们需要选择一种实现方式)和代币的使用者(他们在使用代币合约之前需要找出代币合约支持的实现方式)有影响。
transferAndCall()函数
ERC-677提供了一个 approveAndCall()
函数的变体—— transferAndCall()
函数。就像从函数名中看到那样, transferAndCall()
函数先执行一个 treansfer()
函数接着调用服务合约的函数。
过程如下:
发送者给服务合约发1STORE币(通过调用代币合约的
transferAndCall()
函数)代币合约调用通知服务合约,1STORE币的费用已转出给服务合约(通过调用代币合约的
transferAndCall()
函数)服务合约执行(通过调用代币合约的
tokenFallback()
函数)
这个过程的流程如图所示:
这要求非标准化的 transferAndCall()
函数编码到代币合约中:
function transferAndCall(address _recipient,
uint256 _value,
bytes _extraData) {
transfer(_recipient, _value);
require(TokenRecipient(_recipient).tokenFallback(msg.sender,
_value,
_extraData));
}
第一行执行标准的 transfer()
函数调用,第二行执行服务合约中的 tokenFallback()
函数。服务合约中的 tokenFallback()
函数代码如下:
function tokenFallback(address _sender,
uint256 _value,
bytes _extraData) returns (bool) {
require(msg.sender == tokenContract);
require(_value == 1);
uint256 payloadSize;
uint256 payload;
assembly {
payloadSize := mload(_extraData)
payload := mload(add(_extraData, 0x20))
}
payload = payload >> 8*(32 - payloadSize);
info[sender] = payload;
return true;
}
第一行确保是代币合约在调用这个函数,第二行确保发送者已经把代币传到服务合约中,3-9行获取要存储的信息,第10行存储信息。
最初 transferAndCall()
函数看上去似乎是 approveAndCall()
函数的直接升级。它确实提供了类似的功能,减少了gas消耗,降低了复杂度。然而,它会在高级的场景中失去一些灵活性,这将稍后讨论。
与 approveAndCall()
函数一样,这是一个非标准函数,并且有许多可用的变体。还应该注意的是 ERC-223 的 transfer()函数
包含了与 transferAndCall()
函数相似的功能,并且很可能这两种方式最终被合并到同一个代币标准中,从而替代 ERC-20 (可能对函数名称和签名上有所改变)。
高级场景
上述所有的功能都是在一种价格固定的交易基础上考虑的,但是还应该考虑两种更高级的情况,一个是变价交易一个是重复交易。
变价交易是指服务的价格在一定范围内,但是在发交易时,发送者不知道具体价格。变价交易的一个例子就是服务市场,其中发送者希望为一个变价服务支付最多4个代币,这个变价服务的价格最终由服务的负载所决定。
重复交易是一个服务合约多次被同一个发送者所调用。一个重复交易的例子就是赌博合约:一个人在不同游戏中分别投入不同的赌资,此时赌博合约将被同一个发送者多次调用。
在以上两种场景下,发送者可以直接调用一次 approve()
函数然后直接调用一次或多次服务合约,而不是多次调用 approveAndCall()
函数或 transferAndCall()
函数。这将使得发送者能够控制一批交易的总成本(通过在 approve()
函数中一次性设置交易花费上限,而不是单独管理每次交易的花费)。
总结
当使用代币来支付服务时,一共有三种选择: approve()
、 approveAndCall()
、 transferAndCall()
:
approve()
函数是标准 ERC-20 方法,通常都有很好的支持,但要求发送者进行多次交易approveAndCall()
函数功能更强大,但是更加复杂,不是 ERC-20 方法,所以并不是所有的代币合约都可以使用这个方法transferAndCall()
函数在服务成本固定的情况下比approveAndCall()
函数要好一点,但是复杂度和approveAndCall()
函数差不多
每一种方法都有利弊,每一个都不是完美的解决方案,不是万能普适的。因此,当建立一个代币支付的服务合约时,明确定义需求是非常重要的,这样才能选出最符合合约需求的方法。
原文链接: https://medium.com/@jgm.orinoco/ethereum-smart-service-payment-with-tokens-60894a79f75c
作者: Jim McDonald
翻译&校对: 刘艳安 & Elisa
本文经作者授权翻译及编发。
作者的上一篇文章的中译:
干货 | 理解ERC-20 token合约
你可能还会喜欢:
教程 | 在区块链上建立可更新的智慧合约(一) :
http://ethfans.org/ajian1984/articles/793
Solidity的映射类型深入详解(十二)|入门系列:
http://ethfans.org/hh3755/articles/433