Sparkn

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

Distribution will fail and run out of gas in case of too many winners

Summary

If a contest involves a lot of winners, the Distributor.sol:_distribute function will likely fail to execute successfully due to running out of gas.

Vulnerability Details

The existing Distributor.sol:_distribute function lacks optimization to work with a large number of winners and varying percentages. The tests focus on scenarios with a maximum of 3 winners. However, real contests could involve a significantly larger number of winners. In such instances, the current implementation of _distribute is prone to out of gas error due to its structure involving two for loops iterating the entire winners array.

POC

Here's an example of a Proof of Concept for a contest featuring 9500 winners (the most extreme scenario). However, a similar issue can arise even with a lower number of winners.

Add the following test to the ProxyFactoryTest.t.sol file:

function createRealisticData() public view returns (bytes memory data) {
// Array of 9500 winners
address[] memory winners = new address[](9500);
address winner;
uint256[] memory percentages_ = new uint256[](9500);
for(uint256 i = 0; i < 9500; i++){
winner = address(uint160(uint256(keccak256(abi.encodePacked(block.timestamp, blockhash(block.number - 1), i)))));
winners[i] = winner;
percentages_[i] = 1;
}
address[] memory tokens_ = new address[](1);
tokens_[0] = jpycv2Address;
data = abi.encodeWithSelector(Distributor.distribute.selector, jpycv2Address, winners, percentages_, "");
}
function testOutOfGasMannyWinners() public setUpContestForJasonAndSentJpycv2Token(organizer) {
assertEq(MockERC20(jpycv2Address).balanceOf(user1), 0 ether);
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 0 ether);
bytes32 randomId_ = keccak256(abi.encode("Jason", "001"));
bytes memory data = createRealisticData();
vm.warp(9 days); // 9 days later
vm.startPrank(organizer);
proxyFactory.deployProxyAndDistribute(randomId_, address(distributor), data);
vm.stopPrank();
// after
assertEq(MockERC20(jpycv2Address).balanceOf(stadiumAddress), 500 ether);
}

Run the test with 30 million gas limit per block (Ethereum current block gas limit):
forge test --gas-limit 30000000

The result, the transaction with 9500 winners will fail, due to not enough gas:

Impact

Distributing prizes for contests with a large number of winners would be impossible.

Tools Used

Manual Review

Recommendations

  1. Optimize the Distributor.sol:_distribute function as I mentioned in the Discord Message.

No need for dual iterations through the arrays (as their lengths match). Merge the loops responsible for percentage calculation and money distribution to winners. Following the execution of this consolidated loop, perform the percentages check, and if an error exist, revert the entire transaction. Given the transaction's atomic nature, all transfers will be reversed, resulting in substantial gas savings.

  1. When dealing with many winners, consider enabling the division of the distribution process into multiple transactions. This approach overcomes the gas limit constraint that could prevent the prize distribution process for a contest. While implementing this may introduce added complexity to the code, it guarantees that regardless of the number of winners, prize distribution remains feasible.

Support

FAQs

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