漏洞合约函数
function receive(uint256 fromChainId, address to, uint256 nonce, uint256 volume, Signature[] memory signatures) virtual external payable {
_chargeFee();
require(received[fromChainId][to][nonce] == 0, 'withdrawn already');
uint N = signatures.length;
require(N >= MappingTokenFactory(factory).getConfig(_minSignatures_), 'too few signatures');
for(uint i=0; i<N; i++) {
for(uint j=0; j<i; j++)
require(signatures[i].signatory != signatures[j].signatory, 'repetitive signatory');
bytes32 structHash = keccak256(abi.encode(RECEIVE_TYPEHASH, fromChainId, to, nonce, volume, signatures[i].signatory));
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", _DOMAIN_SEPARATOR, structHash));
address signatory = ecrecover(digest, signatures[i].v, signatures[i].r, signatures[i].s);
require(signatory != address(0), "invalid signature");
require(signatory == signatures[i].signatory, "unauthorized");
_decreaseAuthQuota(signatures[i].signatory, volume);
emit Authorize(fromChainId, to, nonce, volume, signatory);
}
received[fromChainId][to][nonce] = volume;
_receive(to, volume);
emit Receive(fromChainId, to, nonce, volume);
}
function _decreaseAuthQuota(address signatory, uint decrement) virtual internal updateAutoQuota(signatory) returns (uint quota) {
quota = _authQuotas[signatory].sub(decrement);
_authQuotas[signatory] = quota;
emit DecreaseAuthQuota(signatory, decrement, quota);
}
modifier updateAutoQuota(address signatory) virtual {
uint quota = authQuotaOf(signatory);
if(_authQuotas[signatory] != quota) {
_authQuotas[signatory] = quota;
lasttimeUpdateQuotaOf[signatory] = now;
}
_;
}
function authQuotaOf(address signatory) virtual public view returns (uint quota) {
quota = _authQuotas[signatory];
uint ratio = autoQuotaRatio != 0 ? autoQuotaRatio : Factory(factory).getConfig(_autoQuotaRatio_);
uint period = autoQuotaPeriod != 0 ? autoQuotaPeriod : Factory(factory).getConfig(_autoQuotaPeriod_);
if(ratio == 0 || period == 0 || period == uint(-1))
return quota;
uint quotaCap = cap().mul(ratio).div(1e18);
uint delta = quotaCap.mul(now.sub(lasttimeUpdateQuotaOf[signatory])).div(period);
return Math.max(quota, Math.min(quotaCap, quota.add(delta)));
}
相关交易与地址
攻击者地址:0xeda5066780de29d00dfb54581a707ef6f52d8113
漏洞合约地址:0x089165ac9a7Bf61833Da86268F34A01652543466
执行的恶意交易:
具体细节分析
在receive函数里,仅仅对signature做了完整性判断,而没有判断它的正确性,是通过_decreaseAuthQuota函数里的sub函数,根据减后的quota值是否小于0来判断该签名是否正确,并决定是否revert。不过,_decreaseAuthQuota包含updateAutoQuota修饰器,该修饰器会先调用authQuotaOf函数对签名的quota进行更新。在更新过程中,忽略了solidity对于key不存在的情况,并不会报错,而是返回一个空值,所以对于quotaCap.mul(now.sub(lasttimeUpdateQuotaOf[signatory]))语句,攻击者传入一个并不存在于lasttimeUpdateQuotaOf数组的signatory,令该数组返回零,会让quotaCap乘上等同于当前时间戳的倍数,使得delta的值巨大,最后莫名令不存在于数组里的signatory拥有了一笔quota,从而绕过了验证。攻击者即可调用receive函数,获得任意多的代币。
攻击产生的事件:
参考链接
https://etherscan.io/
https://zhuanlan.zhihu.com/p/389738041