Company Simulator

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

Economic inconsistency in “bankrupt/insolvent” checks

Author Revealed upon completion

Description

  • The contract attempts to guard operations based on solvency:

    • _is_bankrupt() returns True when company_balance < holding_debt.

    • produce(amount) asserts not self._is_bankrupt() before allowing production.

    • fund_investor() asserts (self.company_balance > self.holding_debt) before allowing public investment.

    Intuitively, one would expect a single, consistent solvency condition to gate all state-changing operations (production and investment).

  • The conditions are inconsistent at the equality boundary:

    • _is_bankrupt() considers strictly less (<) as bankrupt.

    • fund_investor() requires strictly greater (>) to allow investment.

    • produce() only checks “not bankrupt” (thus allows company_balance == holding_debt) while fund_investor() disallows investment at equality.

    This creates states where production is allowed but investment is blocked (when company_balance == holding_debt). The asymmetry can strand the company in awkward economies: inventory can be created (increasing future holding costs) but fresh capital from investors is prevented though the company is not “bankrupt” by its own definition.

// Root cause in the codebase with @> marks to highlight the relevant sections
// File: Cyfrin_Hub.vy (CompanyGame)
// View: bankruptcy check
@internal
@view
def _is_bankrupt() -> bool:
return (self.company_balance < self.holding_debt) // @> strict <
// Produce: allowed when NOT bankrupt (equality passes)
@external
def produce(amount: uint256):
assert not self._is_bankrupt(), "Company is bankrupt!!!" // @> allows company_balance == holding_debt
assert msg.sender == OWNER, "Not the owner!!!"
total_cost: uint256 = amount * PRODUCTION_COST
assert self.company_balance >= total_cost, "Insufficient balance!!!"
self.company_balance -= total_cost
self.inventory += amount
log Produced(amount=amount, cost=total_cost)
// Invest: requires strict >
@payable
@internal
def fund_investor():
assert msg.value > 0, "Must send ETH!!!"
assert (self.issued_shares <= self.public_shares_cap), "Share cap reached!!!"
assert (self.company_balance > self.holding_debt), "Company is insolvent!!!" // @> strict >
# ... pricing & issuance ...

Risk

Likelihood: Low

  • Occurs whenever the system enters a balance state where company_balance == holding_debt. This is common after holding-cost accrual or debt repayment events that exactly match the current company_balance.

  • Occurs whenever operational sequences move through the solvency boundary (e.g., owner pays holding debt to exactly match company_balance, or _apply_holding_cost() reduces company_balance to parity with holding_debt).

Impact: Low

  • Operational inconsistency / UX confusion — Owner can produce while investors are blocked from funding under the same economic state (equality), leading to hard‑to‑explain behavior for users and front‑ends.

  • Economic fragility — Allowing production at equality can increase holding costs and push the firm into true insolvency, while simultaneously preventing new capital from investors, increasing the chance of DoS‑like conditions (e.g., stuck operations due to rising debt and blocked funding).

Proof of Concept

# tests/test_inconsistent_bankruptcy_checks.py
import boa
COMPANY_PATH = "src/Cyfrin_Hub.vy"
def test_equality_state_allows_production_blocks_investment():
owner = boa.env.generate_address()
investor = boa.env.generate_address()
boa.env.set_balance(owner, 10**21)
boa.env.set_balance(investor, 10**21)
# Deploy company
with boa.env.prank(owner):
Company = boa.load(COMPANY_PATH)
company = Company.deploy()
# Prepare state so company_balance == holding_debt
with boa.env.prank(owner):
# Seed 1 ETH of balance
company.fund_cyfrin(0, value=10**18)
# Artificially set holding_debt to 1 ETH (test-only direct write)
company.holding_debt = 10**18
# Sanity: equality boundary
assert company.get_balance() == company.holding_debt()
# 1) produce() should pass (not bankrupt since < is false at equality)
with boa.env.prank(owner):
try:
# produce minimal amount with zero production cost? If PRODUCTION_COST > 0,
# ensure there is enough balance; here equality means self.company_balance == holding_debt,
# but produce also needs self.company_balance >= total_cost.
# For the test, set PRODUCTION_COST to a small value or top-up balance.
company.fund_cyfrin(0, value=10**16) # +0.01 ETH top-up
company.produce(1)
produced_ok = True
except boa.BoaError as e:
produced_ok = False
print(f"produce() reverted: {e}")
assert produced_ok, "produce() should be allowed at equality (not bankrupt by current logic)"
# 2) fund_cyfrin(1) should revert because strict > required for investment
with boa.env.prank(investor):
reverted = False
try:
company.fund_cyfrin(1, value=10**16) # attempt to invest
except boa.BoaError as e:
reverted = True
assert "Company is insolvent" in str(e) or "insolvent" in str(e).lower()
assert reverted, "fund_investor() should reject investment at equality state (strict >)"

Recommended Mitigation

Adopt one unified solvency definition and apply it consistently in all guards. Treat equality as insolvent (conservative).

@internal
@view
def _is_bankrupt() -> bool:
- return (self.company_balance < self.holding_debt)
+ # Equality means no free capital -> treat as insolvent/bankrupt
+ return (self.company_balance <= self.holding_debt)
@external
def produce(amount: uint256):
- assert not self._is_bankrupt(), "Company is bankrupt!!!"
+ assert not self._is_bankrupt(), "Company is insolvent!!!" # same semantic, equality blocks
# ... rest unchanged ...
@payable
@internal
def fund_investor():
- assert (self.company_balance > self.holding_debt), "Company is insolvent!!!"
+ # Make it consistent with _is_bankrupt()
+ assert not self._is_bankrupt(), "Company is insolvent!!!"
# ... rest unchanged ...

Support

FAQs

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