几个月之前,钱包部门的同事和我说有一个 ERC20 Token 无法进行转账,转账交易发出去后是交易成功了但是余额没有变化,然后给我发了交易的 txid 让我来看看有没有解决办法。在 etherscan 上查看这个交易状态是成功的,但是并没有发出 ERC20 Transfer 事件,而是一个 RejectedPaymentFromLockedUpWallet 事件。然后我查看了合约代码,不得不说实在太长了,还有一些非英文的注释,发现了 transfer 调用时存在锁定验证,如下所示,transfer 调用时会检查限制地址收付款。
1 | mapping (address => LockedInfo) public lockedWalletInfo; |
按照 ERC20 的规范,转账失败是不能返回 true 而且不能没有报错的。而这个合约验证锁定失败后直接发出一个调用失败的事件没有报错,这导致同事发现问题时候账户都已经被锁定快一个月了。之前这个合约是被专人审核过的,当时的 checklist 基本都是看其是否有溢出漏洞、权限漏洞等,这样的锁定漏洞没有被重视。
根据上述代码,存在一个 lockedWalletInfo 合约状态,根据名称可以看出来和锁定有一定关系,其可见性是公开的,然后我根据根据被锁定的地址来获取这个状态,返回的结果是收款没有被锁定而转账被锁定了了几千年。之后只能看看锁定调用有没有其它漏洞,代码如下所示,锁定和解锁调用只能被管理员调用。这个情况下技术上几乎是没有解决方式了,只能通过商务谈判。
1 | event Locked (address indexed target, uint timeLockUpEnd, bool sendLock, bool receiveLock); |
不过过了一个晚上,我发现其代码的 transferFrom 和 approve 代码是有漏洞的,如下所示可以看到合约程序员已经偷懒了,估计是复制粘贴了其它合约的代码,这两个地方没有进行任何权限限制。
1 | mapping (address => mapping (address => uint256)) internal allowed; |
那么这个时候就很简单了,让被锁定的地址调用 approve 给一个新地址所有的 Token 余额,然后新地址调用 transferFrom 就可以进行转账了,就这样被锁定的几亿个 Token 拿回来了,按照当时市价差不多接近八百万人民币。