跳转到正文
Lishude's Web Note
返回

分享:我是如何为公司追回价值 8 百万人民币的虚拟货币的

几个月之前,钱包部门的同事和我说有一个 ERC20 Token 无法进行转账,转账交易发出去后是交易成功了但是余额没有变化,然后给我发了交易的 txid 让我来看看有没有解决办法。在 etherscan 上查看这个交易状态是成功的,但是并没有发出 ERC20 Transfer 事件,而是一个 RejectedPaymentFromLockedUpWallet 事件。然后我查看了合约代码,不得不说实在太长了,还有一些非英文的注释,发现了 transfer 调用时存在锁定验证,如下所示,transfer 调用时会检查限制地址收付款。

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 合约状态,根据名称可以看出来和锁定有一定关系,其可见性是公开的,然后我根据根据被锁定的地址来获取这个状态,返回的结果是收款没有被锁定而转账被锁定了了几千年。之后只能看看锁定调用有没有其它漏洞,代码如下所示,锁定和解锁调用只能被管理员调用。这个情况下技术上几乎是没有解决方式了,只能通过商务谈判。

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 代码是有漏洞的,如下所示可以看到合约程序员已经偷懒了,估计是复制粘贴了其它合约的代码,这两个地方没有进行任何权限限制。

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 拿回来了,按照当时市价差不多接近八百万人民币。



上一篇
开始使用 Go Module
下一篇
node.js Buffer 与 BigInt 类型互转