Company Simulator

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

Reputation Gating Bypass Allows Demand Regardless of Company Reputation

Author Revealed upon completion

Root + Impact

The reputation check in CustomerEngine.trigger_demand() is completely ineffective,
allowing customers to trigger demand regardless of the company's reputation score.
This defeats the entire reputation system designed to incentivize good company
behavior.

Description

  • Normal Behavior: Customers should only be able to trigger demand when company
    reputation ≥ 60, creating incentive for companies to maintain good standing

  • Specific Issue: The reputation is only updated in sell_to_customer(), but this
    function is only called AFTER the reputation check passes, creating a logical
    circular dependency

// CustomerEngine.vy - Shows the reputation check happening BEFORE updates
@external
@payable
def trigger_demand():
// ... cooldown checks ...
rep: uint256 = staticcall CompanyGame(self.company).reputation() // @> Gets current reputation
assert rep >= MIN_REPUTATION, "Reputation too low for demand!!!" // @> Check happens here
// ... demand calculation ...
// Call CompanyGame - this is where reputation gets updated
raw_call(self.company, data, value=total_cost, revert_on_failure=True) // @> But reputation update happens AFTER the check
// Cyfrin_Hub.vy - Shows where reputation updates actually happen
@external
@payable
def sell_to_customer(requested: uint256):
assert msg.sender == self.CUSTOMER_ENGINE, "Not the customer engine!!!"
assert not self._is_bankrupt(), "Company is bankrupt!!!"
self._apply_holding_cost()
if self.inventory >= requested:
// ... successful sale logic ...
if self.reputation < 100:
self.reputation = min(self.reputation + REPUTATION_REWARD, 100) // @> Reputation updated here
else:
self.reputation = min(max(self.reputation - REPUTATION_PENALTY, 0), 100) // @> Or here

Risk

Likelihood:

  • This occurs EVERY TIME a customer attempts to trigger demand, regardless of
    company performance

  • The circular dependency ensures reputation changes can never affect the gating
    mechanism

  • No reputation-based restrictions are ever enforced

Impact:

  • Complete bypass of the reputation system

  • No incentive for companies to maintain good reputation

  • System fairness and economic model completely compromised

  • Reputation becomes a meaningless metric

Proof of Concept

// Test demonstrating the circular dependency
def test_reputation_gating_bypass():
// Setup: Company with inventory
owner.fund_cyfrin(0, value=10 ether)
owner.produce(5)
// The vulnerability: Reputation check happens in CustomerEngine.vy
// but reputation updates happen in Cyfrin_Hub.vy
// This creates a circular dependency where reputation can never affect gating
// Step 1: CustomerEngine.trigger_demand() checks reputation (100)
// Step 2: Check passes, calls Cyfrin_Hub.sell_to_customer()
// Step 3: sell_to_customer() updates reputation based on sale result
// Step 4: But the check already happened in Step 1!
customer.trigger_demand(value=0.1 ether) // Always succeeds regardless of reputation

EXPLANATION OF POC:

This proof of concept demonstrates the circular dependency by showing:

  1. CustomerEngine.vy: The reputation check happens at the beginning of trigger_demand()

  2. Cyfrin_Hub.vy: The reputation updates happen in sell_to_customer()

  3. Circular Dependency: The check in CustomerEngine happens BEFORE the update in Cyfrin_Hub

  4. Vulnerability: This makes the reputation check completely ineffective because it can
    never see updated reputation values

The key insight is that the reputation system is split across two contracts, with the
check happening in one contract before the updates can occur in the other contract.


Recommended Mitigation

Explanation of Mitigation

This mitigation fixes the circular dependency by:

  1. Moving reputation check to Cyfrin_Hub.vy: The reputation check now happens in the
    same contract where reputation updates occur

  2. Proper ordering in Cyfrin_Hub.vy: Holding costs are applied first, then bankruptcy
    check, then reputation check

  3. Removing check from CustomerEngine.vy: Eliminates the circular dependency by
    removing the early reputation check

  4. Centralized validation: All validation logic is now in one place, making the
    system more maintainable

The key insight is that the reputation check was happening in the wrong contract
(CustomerEngine.vy) before any reputation updates could occur. By moving it to
Cyfrin_Hub.vy where the reputation updates happen, we ensure the check operates
on the current state rather than a stale state.

// Changes needed in Cyfrin_Hub.vy
@external
@payable
def sell_to_customer(requested: uint256):
assert msg.sender == self.CUSTOMER_ENGINE, "Not the customer engine!!!"
+ // Apply holding costs first to get accurate financial state
+ self._apply_holding_cost()
+ // Check bankruptcy after applying costs
+ assert not self._is_bankrupt(), "Company is bankrupt!!!"
+ // Check reputation before processing sale
+ assert self.reputation >= 60, "Reputation too low for sale!!!"
- assert not self._is_bankrupt(), "Company is bankrupt!!!"
- self._apply_holding_cost()
if self.inventory >= requested:
// ... successful sale logic with reputation updates
else:
// ... failed sale logic with reputation updates
// Changes needed in CustomerEngine.vy
def trigger_demand():
// ... cooldown checks ...
- rep: uint256 = staticcall CompanyGame(self.company).reputation()
- assert rep >= MIN_REPUTATION, "Reputation too low for demand!!!"
+ // All validation (reputation, bankruptcy) now handled in sell_to_customer()
// ... demand calculation ...
// Call CompanyGame - all validation happens inside sell_to_customer
raw_call(self.company, data, value=total_cost, revert_on_failure=True)

Support

FAQs

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