你好,请选择
语言
关闭

Decentraland 希望对 2017 年第一次拍卖时(即创世纪城生日)剩下的 9,300 块无主的 LAND 地块进行再次分发。 我们在仔细分析后认为分发所有的这些 LAND 的最佳方式是通过荷兰式拍卖,因为链上交易更为适合使用荷兰式拍卖。

荷兰式拍卖意即所有土地价格会以一个较高的相同价格开始,并且价格会随着时间逐渐降低。无论是谁,只要是无主土地的第一“投标人”,就能以当前的价格拍得土地。

我们的另一个目标是通过邀请其它基于 ERC20 项目的参与来扩展我们的社区,如 BNB,ZIL,DAI,MAKER 等。 通过让其它更广泛的社区参与拍卖,我们将能保证在拍卖期间绝大部分 LAND 都能卖出。

作为合作的一部分,除了 DAI 是捐赠给 NeedsList 的慈善基金会外,我们决定销毁🔥所有用于购买 LAND 的通证。

在对合约进行第一次审计后,我们发现大多数 ERC20 通证的核心函数并没有我们原来认为的那样标准化。在研究了各通证的不同实现代码后,我们注意到 transfer, transferFromapprove 在部署的不同合约间有很大区别。让我们看看……

transfer

标准实现

Zeppelin ERC20 的实现方式 :

function transfer(address to, uint256 value) public returns (bool) {
 _transfer(msg.sender, to, value);
 return true;
}

没有回滚

RCN 在不符合前置条件时返回 false

function transfer(address _to, uint256 _value) returns (bool success) {
 if (balances[msg.sender] >= _value) {
   balances[msg.sender] = balances[msg.sender].sub(_value);
   balances[_to] = balances[_to].add(_value);
   Transfer(msg.sender, _to, _value);
   return true;
 } else {
   return false;
 }
}

没有返回值

BNB 在执行 transfer 时没有返回值:

function transfer(address _to, uint256 _value) {
 if (_to == 0x0) throw; // Prevent transfer to 0x0 address. Use burn() instead
 if (_value <= 0) throw;
 if (balanceOf[msg.sender] < _value) throw; // Check if the sender has enough
 if (balanceOf[_to] + _value < balanceOf[_to]) throw; // Check for overflows
 balanceOf[msg.sender] = SafeMath.safeSub(balanceOf[msg.sender], _value); // Subtract from the sender
 balanceOf[_to] = SafeMath.safeAdd(balanceOf[_to], _value); // Add the same to the recipient
 Transfer(msg.sender, _to, _value); // Notify anyone listening that this transfer took place
}

transferFrom

标准实现

Zeppelin ERC20 的实现:

function transferFrom(
   address from,
   address to,
   uint256 value
 )
   public
   returns (bool)
 {
   require(value <= _allowed[from][msg.sender]);

   _allowed[from][msg.sender] = _allowed[from][msg.sender].sub(value);
   _transfer(from, to, value);
   return true;
}

没有回滚

RCN 在不符合前置条件时返回 false

function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
 if (balances[_from] >= _value && allowed[_from][msg.sender] >= _value) {
   balances[_to] = balances[_to].add(_value);
   balances[_from] = balances[_from].sub(_value);
   allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
   Transfer(_from, _to, _value);
   return true;
 } else {
   return false;
 }
}

approve

标准实现

ERC20 的实现方式:

function approve(address spender, uint256 value) public returns (bool) {
 require(spender != address(0));

 _allowed[msg.sender][spender] = value;
 emit Approval(msg.sender, spender, value);
 return true;
}

先清除许可的方式

MANA 在设置之前检查 allowed 值是否为 0 或在设置前是否为 0:

function approve(address _spender, uint256 _value) returns (bool) {
   // To change the approve amount you first have to reduce the addresses`
   //  allowance to zero by calling `approve(_spender, 0)` if it is not
   //  already 0 to mitigate the race condition described here:
   //  https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
   require((_value == 0) || (allowed[msg.sender][_spender] == 0));

   allowed[msg.sender][_spender] = _value;
   Approval(msg.sender, _spender, _value);
   return true;
 }

不用先清除许可

BNB 在使用时不必先清零:

function approve(address _spender, uint256 _value) returns (bool success) {
 if (_value <= 0) throw;
 allowance[msg.sender][_spender] = _value;
 return true;
}

这样,我们看到了有需要创建 SafeERC20.sol :一个建立在ERC20 标准接口上的抽象库,可以通过检查前置和后置条件来安全地调用其方法。

另外,每个方法都会返回 bool 值,它还可以被 revert 调用,以防止在交易失败时损失所有的 gas 矿工费。 这对于在 Solidity 包含 revert 函数前开发的依然使用 throw 和/或 assert 的通证非常有用。

SafeERC20 接口

safeTransfer

执行 ERC20transfer 方法。

  • 检查要转移的金额是否低于或等于账户余额。
  • 检查转账后的账户余额是否等于先前的余额减去转移的金额。
function safeTransfer(IERC20 _token, address _to, uint256 _value) internal returns (bool) {
 uint256 prevBalance = _token.balanceOf(address(this));

 if (prevBalance < _value) {
     // Insufficient funds
     return false;
 }

  address(_token).call(
     abi.encodeWithSignature("transfer(address,uint256)", _to, _value)
 );

 if (prevBalance - _value != _token.balanceOf(address(this))) {
     // Transfer failed
     return false;
 }

 return true;
}

safeTransferFrom

执行 ERC20transferFrom 方法。

  • 检查要转移的金额是否低于或等于账户余额。
  • 检查要转移的金额是否低于或等于将要执行转移的帐户的允许值。
  • 检查转账后的账户余额是否等于先前的余额减去转移的金额
function safeTransferFrom(
       IERC20 _token,
       address _from,
       address _to,
       uint256 _value
   ) internal returns (bool)
{
 uint256 prevBalance = _token.balanceOf(_from);

 if (prevBalance < _value) {
     // Insufficient funds
     return false;
 }

 if (_token.allowance(_from, address(this)) < _value) {
     // Insufficient allowance
     return false;
 }

 address(_token).call(
     abi.encodeWithSignature("transferFrom(address,address,uint256)", _from, _to, _value)
 );

 if (prevBalance - _value != _token.balanceOf(_from)) {
     // Transfer failed
     return false;
 }

 return true;
}

safeApprove

执行 ERC20approve 方法。

  • 检查 allowance 设置是否等于要批准的值。
function safeApprove(IERC20 _token, address _spender, uint256 _value) internal returns (bool) {
 address(_token).call(
     abi.encodeWithSignature("approve(address,uint256)",_spender, _value)
 );

 if (_token.allowance(address(this), _spender) != _value) {
     // Approve failed
     return false;
 }

 return true;
}

clearApprove

清除许可的方法。

BNB 通证不接受 0 作为 approve 的有效值。 因此,如果使用 0 调用 safeApprove 会失败,则库会尝试使用 1 WEI

function clearApprove(IERC20 _token, address _spender) internal returns (bool) {
 bool success = safeApprove(_token, _spender, 0);

 if (!success) {
     return safeApprove(_token, _spender, 1);
 }

 return true;
}

完整的源代码在此

注意事项

  • 使用诸如 transfer 之类的接口方法对于不返回值的通证(如BNB)将失败。 这是因为自拜占庭硬分叉以来,EVM 有一个名为RETURNDATASIZE 的新操作码。 此操作码存储外部调用的返回数据的大小。 然后,代码会在外部调用后检查返回值的大小,并在返回数据短于预期的情况下回退交易。 您可以在[此处](https://medium.com/coinmonks/missing-return-value-bug-at-least-130-tokens-affected-d67bf08521ca)找到有关此问题的更多信息,以及所有存在这个问题的通证列表(以及如何用 assembly 解决这个问题)。

  • 一些通证会在 transfer 时检查值是否 <= 0,如果是的话会抛出一个错误。 我们决定在库中这样处理,不检查 transfer 调用是否成功,而是在调用后检查余额。 因此,如果转移值为 0transfer 调用将失败,但检查帐户余额的后置条件则会成功。

  • 我们避免使用 assembly,虽然它消耗的 gas 矿工费较少,因为它是标准 Solidity 开发人员的黑盒子,也容易出错。这样更容易阅读和理解。

最后,Solidity 0.5.x 中 .call(), .delegatecall().staticcall() 方法都会返回 (bool, bytes memory) 来提供访问返回数据权限。 我们正在开发更新的版本库,来支持这些更改。

我们希望这将有助于进一步实现大多数 ERC20, ERC721 及社区大量使用的其他通证的标准化。

特别感谢Agustin Aguilar,是他在对LANDAuction 合约的审核中发现了 ERC20 通证间的首个不同,另外还要感谢 Patricio Palladino 提供了有关 Solidity 编译器的一些信息。

开始建设!
我们的 SDK 提供了开发游戏和应用所需的一切
让我们开始吧

相关文章