Sparkn

CodeFox Inc.
DeFiFoundryProxy
15,000 USDC
View results
Submission Details
Severity: low
Valid

`distribute` function can face DOS issue if any winner's address will be blacklist by ERC20 token.

Summary

Some tokens (e.g. USDC, USDT) have a contract level admin controlled address blocklist. If an address is blocked, then transfers to and from that address are forbidden.

distribute function transfer the rewards to multiple address togather inside the for loop. If any of the address will be blacklist then contract will not be able to transfer funds to other winners as well.

Vulnerability Details

Open

This is the test written by protocol team inside test/integration/ProxyFactoryTest.t.sol file.

I am going to modify it to show the effect when any user will be blacklist.

here user1 is the winner and we are going to blacklist this address to see the effect.

function testSucceedsWhenConditionsAreMet()
public
setUpContestForJasonAndSentJpycv2Token(organizer)
{
// before
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes memory data = createData();
vm.warp(9 days); // 9 days later
vm.startPrank(organizer);
proxyFactory.deployProxyAndDistribute(
randomId_,
address(distributor),
data
);
vm.stopPrank();
// after
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 9500 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 500 ether);
}

Instead of creating the whole MockERC20 token with the blacklist feature, I have added the blacklisted (user1) address inside the require.

uint256 winnersLength = winners.length; // cache length
for (uint256 i; i < winnersLength; ) {
uint256 amount = (totalAmount * percentages[i]) / BASIS_POINTS;
// * @audit DOS - if any winner blocks the payment from receiving then others will not be able to get the reward.
require(winners[i] != 0x000000000000000000000000000000000000000E, "User Blocked");
erc20.safeTransfer(winners[i], amount);
unchecked {
++i;
}
}

run the test using command forge test --mt testSucceedsWhenConditionsAreMet -vvv

├─ [72143] ProxyFactory::deployProxyAndDistribute(0xad40762e8c031b7ef93b899573e7257f7221bc68688c4c73bbfb58ce9c2e102d, Distributor: [0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0], 0x3cc83ebe00000000000000000000000090193c961a926261b756d1e5bb255e67ff9498a1000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000251c0000000000000000000000000000000000000000000000000000000000000000)
│ ├─ [28306] → new <Unknown>@0x717a630f36795235Aa9Bd5bf6DF42c431d2875FD
│ │ └─ ← 140 bytes of code
│ ├─ [9507] 0x717a630f36795235Aa9Bd5bf6DF42c431d2875FD::distribute(MockERC20: [0x90193C961A926261B756D1E5bb255e67ff9498A1], [0x000000000000000000000000000000000000000E], [9500], 0x)
│ │ ├─ [6708] Distributor::distribute(MockERC20: [0x90193C961A926261B756D1E5bb255e67ff9498A1], [0x000000000000000000000000000000000000000E], [9500], 0x) [delegatecall]
│ │ │ ├─ [2575] ProxyFactory::whitelistedTokens(MockERC20: [0x90193C961A926261B756D1E5bb255e67ff9498A1]) [staticcall]
│ │ │ │ └─ ← true
│ │ │ ├─ [563] MockERC20::balanceOf(0x717a630f36795235Aa9Bd5bf6DF42c431d2875FD) [staticcall]
│ │ │ │ └─ ← 10000000000000000000000 [1e22]
│ │ │ └─ ← "User Blocked"
│ │ └─ ← "User Blocked"
│ └─ ← "ProxyFactory__DelegateCallFailed()"
└─ ← "ProxyFactory__DelegateCallFailed()"

Impact

Due to few blacklisted winners' addresses other winners will not be able to receive the rewards.

Tools Used

Manual Review

Recommendations

Use Pull Over Push principle

View Mitigation

Distributor.sol

Add two state variables, book slot 0 for implementation to prevent storage collision, and use second mapping which tracks users token rewards.

+ address _implementation; // 0 slot booked for proxy implementation address
+ mapping(address token => mapping(address user => uint256 reward)) public usersTokenReward;
uint256 winnersLength = winners.length; // cache length
+ // add the local variable to calculate reward
+ uint256 rewardAmount = 0;
for (uint256 i; i < winnersLength; ) {
uint256 amount = (totalAmount * percentages[i]) / BASIS_POINTS;
// * @audit DOS - if any winner blocks the payment from receiving then others will not be able to get the reward.
+ // instead of transfering, store the data in a mapping
- erc20.safeTransfer(winners[i], amount);
+ usersTokenReward[token][winners[i]] = amount;
+ rewardAmount += amount;
unchecked {
++i;
}
}
// send commission fee as well as all the remaining tokens to STADIUM_ADDRESS to avoid dust remaining
+ uint256 commisionFee = erc20.balanceOf(address(this)) - rewardAmount;
- _commissionTransfer(erc20);
+ _commissionTransfer(erc20, commisionFee);
- function _commissionTransfer(IERC20 token) internal {
+ function _commissionTransfer(IERC20 token, uint256 amount) internal {
- token.safeTransfer(STADIUM_ADDRESS, token.balanceOf(address(this)));
+ token.safeTransfer(STADIUM_ADDRESS, amount);
}

Add the external getReward function which winners should call from the Proxy Factory for a contest (Make sure to validate there using salt).

function getReward(IERC20 token) external {
uint256 reward = usersTokenReward[address(token)][msg.sender];
if (reward == 0) {
revert Distributor__DidNotWinReward();
}
token.safeTransfer(msg.sender, reward);
}

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.