02.Sparkn-2023-08@codehawks
2023-09-18 11:11:36 # 21.audit

Sparkn-2023-08@codehawks

项目意图

  1. 创造一个比赛平台,采用代理模式
  2. 人员:制造比赛的人,参加比赛的人,还有赞助比赛的人和owner。
  3. 进行比赛:每一个参赛者一个proxy存储合约,然后delegatecall逻辑合约
  4. 比赛结束之后分发奖金。

漏洞

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";
Prev
2023-09-18 11:11:36 # 21.audit
Next