JavaScript 不可用。

我们检测到浏览器禁用了 JavaScript。请启用 JavaScript 或改用支持的浏览器来继续访问

假充值漏洞的合约规范描述【非常重要】

作者:Anban Chu

发表日期:2018年09月13日

所属目录:黑客攻击

描述

这是一个合约新手非常容易犯的失误,分为两种情况

  1. 转账的时候不判断返回值
  2. 转账的时候不判断余额是否真正的按照预期值增加/减少

在生产环境的合约中,如果你的合约涉及资金池的概念,而且可能上未知的任何 Token,那么这两个操作都是必须做的。核心是由于 Token 不规范导致的。

第一种情况的解决办法

第一个 转账的时候不判断返回值 的解决办法是使用 '@openzeppelin/contracts'SafeERC20 来解决。转账的时候使用 erc20Token.safeTransfererc20Token.safeTransferFrom 来进行。

因为 ERC20 的规范里并没有对返回值做要求。关于 ERC20 的 transfer 是否需要返回值。按照标准来说并不做要求,规范是 function transfer(address to, uint value) external; 没有 returns (bool success)

因为规范没有要求,所以主流分为两种操作方法

  1. transfer 函数默认表示交易成功。如果遇到失败,ERC20 合约内部做 revert ,直接回滚交易。
  2. transfer 函数通过返回值表示交易状态,而不是暴力的直接 revert 交易。由调用合约内部灵活处理该部分的逻辑。返回 false 值代表交易失败,返回 true 表示交易成功。

这些都是符合 ERC20 标准的,所以 @openzeppelin/contracts 使用 SafeERC20 进行兼容。核心代码如下

function safeTransfer(IERC20 token, address to, uint256 value) internal {
    _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
}

function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
    _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
}

function _callOptionalReturn(IERC20 token, bytes memory data) private {
    bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
    if (returndata.length > 0) { // Return data is optional
        // solhint-disable-next-line max-line-length
        require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
    }
}

上面代码是如果没有返回值,则直接完成,如果交易失败,Token 内部做 revert 会导致交易失败。但是如果有返回值,就使用 bool 解析返回值,返回值必须为 true,否则报错 SafeERC20: ERC20 operation did not succeed。这个兼容处理是很棒的。

第二种情况的解决办法

一定要判断余额是否按照预期进行,它可能是内部扣除 10% 手续费的垃圾币,也可能是只转了一半的金额,其他都销毁了。也可能是内部逻辑出错了,比如下面的代码

// 如果用户要转账的 amount 大于用户当前余额,则要转账的 amount 重写为用户当前的余额
if (amount > balanceOf(from)) {
    amount = balanceOf(from);
}

此时 Token 合约内不抛错,也不返回 false,而是直接执行了。

尤其是做 erc20Token.safeTransferFrom 的相关业务的时候,对应合约的方法可能是 deposit(amount) 这种方法。 用户在做 deposit 的时候,会先将 Token 授权给合约,授权后用户存钱,此时合约从用户地址将需要储存的 amount 用 safeTransferFrom 拿到指定地址。但是此时我们没办法知道该 Token 是是否为作恶的币

以上几种行为,可能都是该代币项目方的刻意那么做的,代码程序员也是按照需求做事。但是他和大家正常认为的 token 转账并不相同,所以一旦不做余额判断,而合约又是对外开放的,这将是一个非常危险的操作。

建议做余额判断,比如下面的演示

uint256 balBefore = erc20Token.balanceOf(address(this));
erc20Token.safeTransferFrom(depositer, address(this), amount);
uint256 balAfter = erc20Token.balanceOf(address(this));
require((balAfter - balBefore) == amount, 'depositer error'); // 这里使用 assert 也很推荐

反思

第一种操作是很多合约开发者都会做的,第二种操作可能是被遗漏的。如果你的合约代码没有做第二种判断,那么恰好你的资金池是需要 owner 权限才能创建的,创建不对外开放;那么每次上资金池的时候都需要合约开发者 review 代币代码后再上架,不要直接上架,否则可能会承受损失。

写代码的时候建议两种操作都做。





以上就是本篇文章的全部内容了,希望对你有帮助。

>> 本站不提供评论服务,技术交流请在 Twitter 上找我 @anbang_account