02.Sparkn-2023-08@codehawks
2023-09-18 11:11:36
# 21.audit
Sparkn-2023-08@codehawks
项目意图
创造一个比赛平台,采用代理模式
人员:制造比赛的人,参加比赛的人,还有赞助比赛的人和owner。
进行比赛:每一个参赛者一个proxy存储合约,然后delegatecall逻辑合约
比赛结束之后分发奖金。
漏洞 1.未校验的输入参数
严重性:高
漏洞:owner可能会恶意将参赛员应得的奖金窃取
例子:参赛员A完成了题目,然后owner拿到它比赛的salt,领取奖金。而owner拿自己的账户作为proxy接收奖金,这是允许的,因为没有检测proxy。
修复:对Proxy进行校验,require(getProxyAddress(salt, implementation) == proxy)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function distributeByOwner( address proxy, // 每一个参赛员对应一个proxy,这里作为接收奖金的地址 address organizer, bytes32 contestId, address implementation, bytes calldata data ) public onlyOwner { if (proxy == address(0)) revert ProxyFactory__ProxyAddressCannotBeZero(); bytes32 salt = _calculateSalt(organizer, contestId, implementation); if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered(); // distribute only when it exists and expired if (saltToCloseTime[salt] + EXPIRATION_TIME > block.timestamp) revert ProxyFactory__ContestIsNotExpired(); _distribute(proxy, data); }
2.未校验的输入参数
严重性:高
漏洞:任何人拿到奖金签名之后,都可以将奖金发放到任意已经注册过比赛的用户地址
例子:参赛员A完成了比赛并且比赛已经结束,我拿到了A的奖金签名帮助他发放奖金,通过了ECDSA.recover验证之后,我设置implementation为参赛员B的地址,那么B就窃取了A的奖金
修复:对implementation进行校验,require(getProxyAddress(salt, implementation) == proxy)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function deployProxyAndDistributeBySignature( address organizer, bytes32 contestId, address implementation, bytes calldata signature, bytes calldata data ) public returns (address) { bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(contestId, data))); if (ECDSA.recover(digest, signature) != organizer) revert ProxyFactory__InvalidSignature(); bytes32 salt = _calculateSalt(organizer, contestId, implementation); // 漏洞 if (saltToCloseTime[salt] == 0) revert ProxyFactory__ContestIsNotRegistered(); if (saltToCloseTime[salt] > block.timestamp) revert ProxyFactory__ContestIsNotClosed(); address proxy = _deployProxy(organizer, contestId, implementation); _distribute(proxy, data); return proxy; }
3.未校验的输入参数
严重性:高
漏洞:如果winner设置为0地址,那么分配的奖金将会丢失,错误的发送到0地址
例子:address[] memory winners
中的一个地址设置为0地址,那么此次奖金分配也是成功的,因为没有任何地方对winners的元素进行了0地址检验
修复:在_distribute()
遍历数组元素的时候,增加require(winners[i] != address(0));
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 30 31 32 33 34 35 36 37 38 39 40 41 function _distribute(address token, address[] memory winners, uint256[] memory percentages, bytes memory data) internal { // token address input check if (token == address(0)) revert Distributor__NoZeroAddress(); if (!_isWhiteListed(token)) { revert Distributor__InvalidTokenAddress(); } // winners and percentages input check if (winners.length == 0 || winners.length != percentages.length) revert Distributor__MismatchedArrays(); uint256 percentagesLength = percentages.length; uint256 totalPercentage; for (uint256 i; i < percentagesLength;) { totalPercentage += percentages[i]; unchecked { ++i; } } // check if totalPercentage is correct if (totalPercentage != (10000 - COMMISSION_FEE)) { revert Distributor__MismatchedPercentages(); } IERC20 erc20 = IERC20(token); uint256 totalAmount = erc20.balanceOf(address(this)); // if there is no token to distribute, then revert if (totalAmount == 0) revert Distributor__NoTokenToDistribute(); uint256 winnersLength = winners.length; // cache length for (uint256 i; i < winnersLength;) { uint256 amount = totalAmount * percentages[i] / BASIS_POINTS; erc20.safeTransfer(winners[i], amount); unchecked { ++i; } } // send commission fee as well as all the remaining tokens to STADIUM_ADDRESS to avoid dust remaining _commissionTransfer(erc20); emit Distributed(token, winners, percentages, data); }
4.未校验的返回值
严重性:低
漏洞:CREATE2创建合约的时候,并没有对返回值proxy进行校验。项目在审计时的版本,由于proxy合约较小,失败的概率很小,但也是有可能的,后续的升级后,proxy合约也许会变大则更容易失败,然后proxy就会是address(0)。造成的后果是本次分发奖金失败,并且交易不会revert,消耗了更多的gas,暂时不会有其他损失。用户只需再次调用_distribute()
直到成功即可。
例子:
第一步:发送奖金到计算好的proxy地址,然后第二步调用分发奖金的时候,proxy失败得到一个address(0),那么执行分发_deployProxy()
的是时候,.call()
就会失败
不过没关系啊,奖金并没有留在address(0),它是存在于计算好的正确的proxy地址(尚未成功部署)
第二步:我们提高gas,再次调用distribute()
分发,合约就会被部署出来,奖金一样会被分发,并不会永远丢失
修复:可以增加一个返回值检验:if (proxy == address(0)) revert ProxyFactory_ProxyDeploymentFailed(proxy);
1 2 3 4 5 function _deployProxy(address organizer, bytes32 contestId, address implementation) internal returns (address) { bytes32 salt = _calculateSalt(organizer, contestId, implementation); address proxy = address(new Proxy{salt: salt}(implementation)); return proxy; }
5.验证不严
严重性:中等
漏洞:_distribute()
分发奖金的时候,并没有让其他人对winners和percentages进行校验。造成的问题是如果winners和percentage写错了或者存在恶意的嫌疑,这么没人能阻止。
例子:我在分发奖金的时候winners和percentage写的不对,或者我想作恶,导致了winners的奖金受到影响
修复:增加一个第三方对这次的调用进行校验,校验winners和percentage。有点像转移owner权限的时候,设置ownerPending,而不是直接转移,让新的owner自己去接收。
1 2 3 4 5 6 7 8 9 10 11 function _distribute(address token, address[] memory winners, uint256[] memory percentages, bytes memory data) internal{ ... for (uint256 i; i < winnersLength;) { uint256 amount = totalAmount * percentages[i] / BASIS_POINTS; erc20.safeTransfer(winners[i], amount); unchecked { ++i; } } ... }
6.未校验的输入参数
严重性:低
漏洞:未对implementation进行校验,此项目中implementation应该是一个合约
例子:如果部署的时候,不小心将用一个EOA账户赋给implementation,那么就无法达到预期目的
修复:增加implementation是否是合约的检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fallback() external { address implementation = _implementation; assembly { let ptr := mload(0x40) calldatacopy(ptr, 0, calldatasize()) let result := delegatecall(gas(), implementation, ptr, calldatasize(), 0, 0) let size := returndatasize() returndatacopy(ptr, 0, size) switch result case 0 { revert(ptr, size) } default { return(ptr, size) } } }
7.未校验的参数
严重性:低
漏洞1:未对amount和percentages进行校验,那么存在分发0元奖金的情况。有些奇怪的ERC20代币并不支持发送0元,他会revert,从而导致整个交易失败
例子:这个奇怪的代币是LEND,不支持发送0元
修复:增加对amount数额的判断,发送0元则revert
漏洞2:当获奖者是位于黑名单中的,那么所有的获奖者都会受到影响,无法获取奖金,DoS
例子:我是USDT黑名单的用户,我为了搞破坏,也参加了这个比赛,然后我获奖了,当发送奖金给我的时候,无法成功,因为USDT把我拉进黑名单,任何USDT都无法转入我的地址
修复:此项目增加一个添加黑名单的功能,一旦出现因为黑名单转账失败的问题,也把它拉进黑名单
1 2 3 4 5 6 7 8 uint256 winnersLength = winners.length; // cache length for (uint256 i; i < winnersLength;) { uint256 amount = totalAmount * percentages[i] / BASIS_POINTS; erc20.safeTransfer(winners[i], amount); unchecked { ++i; } }
8.owner权限转移
严重性:低
漏洞:引入了openzeppelin的owner库,它的转移机制是直接转移,如果转错了地址就寄了。恰巧,此项目的owner拥有绝对的权限,一旦丢失,项目崩溃
例子:略
修复:换用openzeppelin的Ownable2Step 。
1 import {Ownable} from "openzeppelin/access/Ownable.sol";