使用 token 为服务付费

使用 token 为服务付费

最近参与的几个以太坊 dApp 项目,大都涉及到抵押 token 或通过 token 付费来获取某种服务的问题。本质上和 medium 上这篇文章 讲的是一回事。本文对其重新规整,得出使用 token 为服务付费的经验性做法。

0x01 使用以太币付费

首先我们来看看用以太币付费是怎么做的。假定我们要提供的服务是为付费用户在智能合约合约上记录一个数字。那么,如果用户想存点儿东西上来,可以在付费的同时,将要存储的东西一起发送给提供服务的智能合约。

使用以太币为提供服务的合约付费

上图展示了用户(sender)与提供服务的合约(service contract)进行交互的过程。 SenderService contract 发送了一笔交易,这笔交易通过调用 Service contract 上的一个 storeData 的方法将数字 4 存在了以太坊区块链上,并向 Service contract 支付了一个以太币作为费用。相应的 Solidity 实现代码如下所示:

function storeData(uint256 payload) public payable {
  require(msg.value == 1 ether);
  info[msg.sender] = payload;
}

这个实现代码比较简单易懂,将 storeData 这个方法定义为 payable 使其可以接受以太币付款。方法的第一行确保调用者付费不多不少正好是一个以太币,第二行将调用者所传的数字信息记录下来。

0x02 使用 ERC20 token 付费

作为项目方,大都希望用自己发行的应用型 token 而不是以太币在自己的生态里流通。 现在如果我们提供同样的数字信息存储服务,但是要求用户用 ERC20 token 付费,应该怎么做呢? 假定我们已经创建出了这么一个 token 并命名为 STORE。用户需要支付一个 STORE token 来存储数字信息。相应的支付流程应该如下图所示:

使用 STORE token 为提供服务的合约付费

相应的实现应该是下面这个样子:

function storeData(uint256 payload) public payable {
  require(msg.value == 1 STORE);
  info[msg.sender] = payload;
}

很遗憾, 这是不可能的 。因为我们在合约中通过 msg.value 获取到的只能是以太币数量,而不可能是 STORE token 的数量。 Sender 需要通过与实现 STORE token 的智能合约交互将一定数量的 STORE token 从 Sender 的账户转到 Service contract 对应的账户。

这也是我很想吐槽以太坊和当前若干主链的地方之一,发币机制实现的非常不彻底。发一个新的代币,使用时却脱离不了底层平台币,为 dApp 终端用户增加了不少使用门槛。理想中的公链平台,应该将代币发行,代币上市,代币交易统一起来,结合相应的治理机制,每一个发行的代币都能被矿工接受,并可以被用作平台 gas 费用才好嘛

吐槽归吐槽,我们还得要面对现实,所幸我们可以运用 ERC20 规范提供的接口来处理这种情况,尽管不甚完美,也不失解决问题的一个路径。

approve() 和 transferFrom()

ERC20 规范提供了 approve() 和 transferFrom() 这两个接口。每个 ERC20 token 的合约都必须提供这两个接口的实现。通过 approve(),交易发送者可以授权第三方从自己账户里转出一定数量的代币。通过 transferFrom(),被授权的第三方可以将被授权的代币转到自己的账户里面。 这样,用户就可以接合使用 approve() 和 transferFrom() 实现付费一个 STORE token 并存储数字信息到区块链。具体过程如下:

  1. 用户 (sender) 通过调用 STORE token 合约的 approve() 方法 授权提供服务的合约 (service contract)一个 STORE 的转账权。
  2. Sender 调用 Service contract 的 storeData 方法去存储数据信息。
  3. Service contract 通过调用 STORE token 合约的 transferFrom() 方法将一个 STORE token 转到自己帐上,然后执行将数据信息保存下来的操作。

整个过程可以图形化表示如下:

通过 approve() 为提供服务的合约付费

上图中 transferFrom() 下面的虚线表示这个方法调用交易是从 storeData() 交易内部发起的,不需要任何通过 service contract 的人工干预。

使用这种方式,我们 Service contract 中对 storeData() 方法的实现可以调整为下面这种方式:

function storeData(uint256 payload) public {
  require(tokenContract.transferFrom(msg.sender, address(this), 1));
  info[msg.sender] = payload;
}

这个实现代码的第一行将预授权的一个 STROE token 从 sender 账户里转出到 service contract 账户中,如果转账失败,交易会被回退。如果转账成功,第二行会将传进来的数据信息保存下来。

0x03 改进的空间

上面提供的这种方式可以实现我们的目标,但它需要来自用户的两次交易,一次交易是用来调用 token 合约的 approve() 方法,另一次交易是用来调用服务合约的 storeData() 方法。这就意味着我们把一个业务操作分成了两个,用户需要为了完成存储信息这一个事情,需要与合约交互两次,付两次 gas 费用。

有没有可能让用户发起一次合约调用就可以了呢?

approveAndCall()

让我们来尝试将 approve() 和 storeData() 相应的操作合并到一起,看看应该如何做:

  1. 用户通过调用 token 合约的 approveAndCall() 方法(approveAndCall 是我们在 token 合约新实现的一个方法)授权服务合约一个 STORE token 的转账交易。
  2. token 合约通过调用服务合约的 receiveApproval() 方法(receiveApproval 是我们在服务合约新实现的一个方法)通知服务合约 token 授权已经完成。
  3. 服务合约调用 token 合约的 transferFrom() 方法将 token 从用户账户转到服务合约账户,并将用户传进来的数据信息保存下来。

整个过程可以图像化保存如下:

通过 approveAndCall 为提供服务的合约实现 STORE token 付费

这需要我们在 token 合约里添加 approveAndCall() 方法的实现:

function approveAndCall(address _recipient,
                        uint256 _value,
                        bytes _extraData) {
  approve(_recipient, _value);
  TokenRecipient(_recipient).receiveApproval(msg.sender,
                                             _value,
                                             address(this),
                                             _extraData);
}

这个实现代码的第一行调用了 ERC20 标准的 approve() 方法,第二行调用了服务合约的 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;
}

这个实现的第一行确保该方法是从指定的 token 合约调用的,第二行从 token 合约中将预授权的 token 转出到服务合约账户中,第 3 ~ 9 行获取用户传进来的数据并将其保存下来。

值得注意的是,我们这里调用的 token 合约的 approveAndCall() 方法并不是 ERC20 标准的一部分。这意味着这种实现有可能会遇到下面的问题:

  1. 大部分的 token 合约并不会实现 approveAndCall() 方法,尽管我们自己写的 token 合约可以自行添加这个方法的实现。
  2. 有的 token 合约有类似实现,但只支持一个 receiveApproval() 方法的硬编码实现。
  3. 有些 token 合约通过往 approveAndCall() 方法传输方法签名来调用服务合约的任意方法。如下图所示:

由于没有统一的标准规范,合约实现者和用户都要关注具体采用哪种实现方式,如果每个 dApp 都用不同的实现方式,无疑对用户来说是个不小的负担。

0x04 ERC667

ERC-677 提供了一个类似于approveAndCall() 的方法叫 transferAndCall()。具体流程如下:

  1. 用户通过调用 token 合约的 transferAndCall() 将 token 转给服务合约。
  2. token 合约通过调用服务合约的 tokenFallback() 方法执行数据存储操作。 整个过程如下图所示:

    通过调用 transferAndCall() 向服务合约使用 token 付费

token 合约中的 transferAndCall() 方法实现如下:

function transferAndCall(address _recipient,
                        uint256 _value,
                        bytes _extraData) {
  transfer(_recipient, _value);
  require(TokenRecipient(_recipient).tokenFallback(msg.sender,
                                                   _value,
                                                   _extraData));
}

代码第一行通过调用 ERC20 标准的 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;
}

代码的第一行确保该方法是被指定的 token 合约调用的,第二行确保 token 数量是对的,第 3 ~ 10 行加载并存储数据。

初看起来,transferAndCall() 的实现比 approveAndCall() 的实现更加的简单直接,所花费的 gas 费用也会更少。 但是,在一些特定场景,比如说服务合约所提供服务的价格是浮动的,或者服务合约需要被多次调用等情况下,这种方式失去了我们所需要的灵活性。

目前 ERC-677 提案还处于 Open 状态,并没有被接纳为最终标准规范。另外还有不少类似的提案,比如 ERC-223 。这点儿也是需要我们注意的,意味着类似规范并没完全被整个行业接受,实现哪个规范是我们自己的行为,这个时候与 ERC20 的兼容性就显得比较重要,这也是 ERC677 相比与 ERC223 的一大优势。

0x05 总结

由于以太坊智能合约不能直接识别 ERC20 token,我们就需要通过合约间的交互来完成通过 token 为服务付费的需求。 本文提供了三种不同的方式:

  1. 使用 ERC20 标准规范提供的 approve() 和 transferFrom() 接口。
  2. 使用 approveAndCall() 的方式。
  3. 使用 transferAndCall() 的方式。

  • 发表于 2018-11-06 09:53
  • 阅读 ( 721 )
  • 学分 ( 5 )
  • 分类:以太坊

0 条评论

请先 登录 后评论
Ashton
Ashton

83 篇文章, 4746 学分