Root + Impact
Two critical vulnerabilities were identified in the ContestManager contract: (1) No validation that players and rewards arrays have matching lengths, and (2) No upper bound on array sizes, allowing gas exhaustion attacks. Both issues lead to logical errors and potential Denial of Service.
Description
-
Normally, when creating a contest, the contract should ensure that each player receives a corresponding reward. This requires that the players and rewards arrays have identical lengths and that the number of participants is reasonable to avoid excessive gas consumption.
-
The createContest function accepts two arrays (players and rewards) without verifying that their lengths are equal. Additionally, there is no maximum limit on the array size. This allows the owner (or any caller with onlyOwner privileges) to:
Create a contest with mismatched array lengths (e.g., 10 players but 9 rewards), causing the underlying Pot contract to fail due to out-of-bounds access or arithmetic errors.
Pass an excessively large array (e.g., 10,000 players), consuming gas far beyond the block gas limit, making the transaction impossible to execute.
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}
Risk
Likelihood:High
The owner (or anyone with owner privileges) may unintentionally create a contest with mismatched arrays due to human error.
A malicious owner could intentionally supply large arrays to halt contract functionality.
Since the function is restricted to onlyOwner, the immediate risk is limited to owner actions, but in multi-signature or governance contexts, a compromised owner account could exploit this.
Impact:High
Mismatched Arrays: Causes the Pot contract to behave unexpectedly—either reverting with out-of-bounds errors or corrupting internal state. This can lock funds or prevent reward distribution entirely.
Unbounded Arrays: The transaction consumes excessive gas (tested: 352 million gas for 5000 players), far exceeding the Ethereum block gas limit (~30 million). This makes contest creation impossible, effectively causing a permanent DoS for that functionality. Funds sent to the contract may become stuck if contests cannot be created.
Proof of Concept
The following Foundry tests demonstrate both vulnerabilities:
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/ContestManager.sol";
import "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";
contract SimpleERC20 is ERC20 {
constructor(string memory name, string memory symbol) ERC20(name, symbol) {
_mint(msg.sender, 1_000_000 * 10**18);
}
}
contract ContestManagerTest is Test {
ContestManager manager;
IERC20 token;
address owner = address(0x123);
function setUp() public {
vm.startPrank(owner);
token = new SimpleERC20("Test", "TST");
manager = new ContestManager();
vm.stopPrank();
}
function testMismatchedArrays() public {
address[] memory players = new address[](3);
players[0] = address(0x1);
players[1] = address(0x2);
players[2] = address(0x3);
uint256[] memory rewards = new uint256[](2);
rewards[0] = 100;
rewards[1] = 200;
uint256 totalRewards = 300;
vm.prank(owner);
vm.expectRevert();
manager.createContest(players, rewards, token, totalRewards);
}
function testLargeArrayDoS() public {
uint256 size = 5000;
address[] memory players = new address[](size);
uint256[] memory rewards = new uint256[](size);
for (uint256 i = 0; i < size; i++) {
players[i] = address(uint160(i + 1));
rewards[i] = 1;
}
uint256 totalRewards = size;
vm.prank(owner);
uint256 gasBefore = gasleft();
address potAddress = manager.createContest(players, rewards, token, totalRewards);
uint256 gasUsed = gasBefore - gasleft();
console.log("Gas used for", size, "players:", gasUsed);
assertGe(gasUsed, 30_000_000, "Gas consumption should exceed block gas limit");
console.log("Pot address:", potAddress);
}
}
Test Results:
testMismatchedArrays fails with panic: array out-of-bounds access (0x32)
testLargeArrayGasExceedsLimit consumes 354,311,425 gas, well above the 30M block limit
[⠆] Compiling...
[⠘] Compiling 1 files with Solc 0.8.33
[⠃] Solc 0.8.33 finished in 1.43s
Compiler run successful!
Ran 2 tests for test/ContestManager3.t.sol:ContestManagerTest
[PASS] testLargeArrayDoS() (gas: 354311425)
[PASS] testMismatchedArrays() (gas: 310544)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 305.84ms (293.15ms CPU time)
Ran 1 test suite in 320.98ms (305.84ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
Recommended Mitigation
Add a require statement to enforce equal array lengths, and introduce a maximum array size to prevent gas exhaustion
+ error ContestManager__ArrayLengthMismatch();
+ error ContestManager__ExceededMaxPlayers();
+ uint256 public constant MAX_PLAYERS = 500;
function createContest(address[] memory players, uint256[] memory rewards, IERC20 token, uint256 totalRewards)
public
onlyOwner
returns (address)
{
+ if (players.length != rewards.length) {
+ revert ContestManager__ArrayLengthMismatch();
+ }
+ if (players.length > MAX_PLAYERS) {
+ revert ContestManager__ExceededMaxPlayers();
+ }
Pot pot = new Pot(players, rewards, token, totalRewards);
contests.push(address(pot));
contestToTotalRewards[address(pot)] = totalRewards;
return address(pot);
}