几个月之前,钱包部门的同事和我说有一个 ERC20 Token 无法进行转账,转账交易发出去后是交易成功了但是余额没有变化,然后给我发了交易的 txid 让我来看看有没有解决办法。在 etherscan 上查看这个交易状态是成功的,但是并没有发出 ERC20 Transfer 事件,而是一个 RejectedPaymentFromLockedUpWallet 事件。然后我查看了合约代码,不得不说实在太长了,还有一些非英文的注释,发现了 transfer 调用时存在锁定验证,如下所示,transfer 调用时会检查限制地址收付款。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| mapping (address => LockedInfo) public lockedWalletInfo;
struct LockedInfo { uint timeLockUpEnd; bool sendLock; bool receiveLock; }
event RejectedPaymentToLockedUpWallet (address indexed from, address indexed to, uint256 value); event RejectedPaymentFromLockedUpWallet (address indexed from, address indexed to, uint256 value);
// token transfer function transfer(address _to, uint256 _value) public returns (bool) { require(_to != address(this)); // 这里验证转出是否已经被锁定 if(lockedWalletInfo[msg.sender].timeLockUpEnd > now && lockedWalletInfo[msg.sender].sendLock == true) { emit RejectedPaymentFromLockedUpWallet(msg.sender, _to, _value); return false; } // 这里验证收款是否已经被锁定 else if(lockedWalletInfo[_to].timeLockUpEnd > now && lockedWalletInfo[_to].receiveLock == true) { emit RejectedPaymentToLockedUpWallet(msg.sender, _to, _value); return false; } else { // 验证通过后可直接调用标准 ERC20 转账 return super.transfer(_to, _value); } }
|
按照 ERC20 的规范,转账失败是不能返回 true 而且不能没有报错的。而这个合约验证锁定失败后直接发出一个调用失败的事件没有报错,这导致同事发现问题时候账户都已经被锁定快一个月了。之前这个合约是被专人审核过的,当时的 checklist 基本都是看其是否有溢出漏洞、权限漏洞等,这样的锁定漏洞没有被重视。
根据上述代码,存在一个 lockedWalletInfo 合约状态,根据名称可以看出来和锁定有一定关系,其可见性是公开的,然后我根据根据被锁定的地址来获取这个状态,返回的结果是收款没有被锁定而转账被锁定了了几千年。之后只能看看锁定调用有没有其它漏洞,代码如下所示,锁定和解锁调用只能被管理员调用。这个情况下技术上几乎是没有解决方式了,只能通过商务谈判。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| event Locked (address indexed target, uint timeLockUpEnd, bool sendLock, bool receiveLock); event Unlocked (address indexed target);
function walletLock(address _targetWallet, uint _timeLockEnd, bool _sendLock, bool _receiveLock) onlyOwner public { require(_targetWallet != 0x0);
// 如果设置 _sendLock 和 _receiveLock 则代表解锁 if(_sendLock == false && _receiveLock == false) { _timeLockEnd = 0; }
lockedWalletInfo[_targetWallet].timeLockUpEnd = _timeLockEnd; lockedWalletInfo[_targetWallet].sendLock = _sendLock; lockedWalletInfo[_targetWallet].receiveLock = _receiveLock;
if(_timeLockEnd > 0) { emit Locked(_targetWallet, _timeLockEnd, _sendLock, _receiveLock); } else { emit Unlocked(_targetWallet); } }
|
不过过了一个晚上,我发现其代码的 transferFrom 和 approve 代码是有漏洞的,如下所示可以看到合约程序员已经偷懒了,估计是复制粘贴了其它合约的代码,这两个地方没有进行任何权限限制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| mapping (address => mapping (address => uint256)) internal allowed;
// ERC20 标准 transferFrom function transferFrom(address _from, address _to, uint256 _value) public returns (bool) { require(_to != address(0)); require(_value <= balances[_from]); require(_value <= allowed[_from][msg.sender]);
balances[_from] = balances[_from].sub(_value); balances[_to] = balances[_to].add(_value); allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value); emmit Transfer(_from, _to, _value); return true; }
// ERC20 标准 approve function approve(address _spender, uint256 _value) public returns (bool) { allowed[msg.sender][_spender] = _value; emit Approval(msg.sender, _spender, _value); return true; }
|
那么这个时候就很简单了,让被锁定的地址调用 approve 给一个新地址所有的 Token 余额,然后新地址调用 transferFrom 就可以进行转账了,就这样被锁定的几亿个 Token 拿回来了,按照当时市价差不多接近八百万人民币。