Company Simulator

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

Bankruptcy Check Bypass Allows Sales When Company Is Insolvent

Author Revealed upon completion

Root + Impact

The bankruptcy check in the sell_to_customer() function is fundamentally broken, allowing customers to purchase items even when the company is insolvent. This creates a critical vulnerability where the company can be drained of funds while accumulating massive debt.

Description

  • Normal Behavior: The system should prevent sales when company_balance < holding_debt to protect against insolvency

  • Specific Issue: The bankruptcy check occurs AFTER holding costs are applied, creating a circular dependency where bankruptcy can only be detected after a sale attempt

// Root cause in the codebase with @> marks to highlight the relevant section
@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!!!" // @> This check happens AFTER holding costs
self._apply_holding_cost() // @> Holding costs applied here, but bankruptcy check was already done
if self.inventory >= requested:
// ... sale logic

Risk

Likelihood:

  • This occurs EVERY TIME a customer attempts to purchase when the company has accumulated holding debt

  • The circular dependency ensures bankruptcy can never be properly detected before sales

Impact:

  • Customers can drain company funds even when insolvent

  • Holding debt can accumulate indefinitely without any protection

  • Complete system integrity compromise

Proof of Concept


Explanation :

This proof of concept demonstrates the critical flaw in the bankruptcy detection system. The test sets up a scenario where:

  1. Setup Phase: Company is funded with minimal ETH (1 ETH) and produces 100 items

  1. Time Accumulation: We fast-forward 1 hour, which should create 0.1 ETH in holding costs (100 items × 0.001 ETH/hour)

  1. Bankruptcy State: The company now has 0 ETH balance but 0.1 ETH in holding debt, making it technically bankrupt

  1. Vulnerability Demonstration: The _is_bankrupt() function returns False because it checks the state BEFORE holding costs are applied

  1. Exploitation: A customer can successfully trigger demand and purchase items, even though the company is insolvent

The core issue is that _is_bankrupt() checks company_balance < holding_debt, but holding_debt is only updated when _apply_holding_cost() is called, which happens AFTER the bankruptcy check in sell_to_customer().

// Test demonstrating the vulnerability
def test_bankruptcy_bypass():
// Setup: Company with minimal funds and large inventory
owner.fund_cyfrin(0, value=1 ether)
owner.produce(100) // 100 items = 0.1 ETH holding cost per hour
// Fast forward time to accumulate holding debt
time_travel(3600) // 1 hour
// Company should be bankrupt but check fails
assert company.get_balance() < company.holding_debt() // True
assert company._is_bankrupt() // False - VULNERABILITY!
// Customer can still purchase despite bankruptcy
customer.trigger_demand(value=0.1 ether) // Succeeds - should fail!


Explaination of Mitigation

The fix addresses the circular dependency by reordering the operations:

  1. Apply Holding Costs First: _apply_holding_cost() is called before the bankruptcy check, ensuring that holding_debt is updated with the current time's accumulated costs

  1. Check Bankruptcy After: The _is_bankrupt() check now operates on the updated state where holding_debt reflects the current accumulated costs

  1. Proper State Validation: This ensures that the bankruptcy check accurately reflects the company's current financial state



Recommended Mitigation

@external
@payable
def sell_to_customer(requested: uint256):
assert msg.sender == self.CUSTOMER_ENGINE, "Not the customer engine!!!"
+ self._apply_holding_cost() // Apply holding costs FIRST
- assert not self._is_bankrupt(), "Company is bankrupt!!!"
+ assert not self._is_bankrupt(), "Company is bankrupt!!!" // Check AFTER applying costs
- self._apply_holding_cost()
if self.inventory >= requested:
// ... rest of sale logic

Support

FAQs

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