Company Simulator

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

Debt payment can lock liquidity

Author Revealed upon completion

Root + Impact

Description

pay_holding_debt requires the owner to send ETH to extinguish storage liabilities. The routine reduces holding_debt, but it never credits company_balance with the transferred funds. Because the contract keeps the ETH on-chain, the internal ledger now understates available liquidity and reports net worth as zero.

@payable
@external
def pay_holding_debt():
assert msg.sender == OWNER, "Not the owner!!!"
assert self.holding_debt > 0, "No debt to pay"
if msg.value >= self.holding_debt:
excess: uint256 = msg.value - self.holding_debt
@> self.holding_debt = 0
@> self.company_balance += excess
else:
@> self.holding_debt -= msg.value

Risk

Likelihood: Holding debt appears under normal operations whenever _apply_holding_cost exceeds available cash. The owner is incentivized to repay it, so the faulty path is expected during routine maintenance.

Impact:

  • Investors cannot withdraw after debt service because company_balance stays at zero, triggering “Insufficient company funds!!!”

  • get_share_price continues to return zero, freezing new public investment.

  • Liquidity becomes trapped: the ETH sent to service debt remains on-chain but is unreachable through legitimate flows.

Proof of Concept

  1. Induce debt by running _apply_holding_cost once company_balance is exhausted (e.g., create inventory, fast-forward time).

  2. Pay the debt exactly: pay_holding_debt with msg.value == holding_debt.

  3. Observe post-call state: holding_debt == 0, company_balance == 0, yet the contract’s actual ETH balance increased by the payment.

  4. Attempt withdraw_shares; it reverts with “Insufficient company funds!!!” despite the contract holding sufficient ETH.

Illustrative pytest-style check:

def test_debt_payment_locks_funds(hub, owner, investor, chain):
hub.fund_cyfrin(action=1, value=10**18, sender=investor)
hub.produce(amount=10, sender=owner) # create inventory
chain.sleep(30 * 3600)
hub.sell_to_customer(requested=0, sender=hub.CUSTOMER_ENGINE()) # or call internal hook to accrue debt
debt = hub.holding_debt()
hub.pay_holding_debt(value=debt, sender=owner)
assert hub.holding_debt() == 0
assert hub.company_balance() == 0 # ledger empty
with reverts("Insufficient company funds!!!"):
hub.withdraw_shares(sender=investor)

Recommended Mitigation

Accurately mirror liquidity on the ledger:

@external
@payable
def pay_holding_debt():
assert msg.sender == OWNER, "Not the owner!!!"
assert self.holding_debt > 0, "No debt to pay"
+ self.company_balance += msg.value
if self.company_balance >= self.holding_debt:
- excess: uint256 = msg.value - self.holding_debt
- self.holding_debt = 0
- self.company_balance += excess
+ self.company_balance -= self.holding_debt
+ self.holding_debt = 0
else:
- self.holding_debt -= msg.value
+ self.holding_debt -= self.company_balance
+ self.company_balance = 0

This approach treats the owner’s payment as cash-in first, then offsets liabilities. The ledger now matches on-chain ETH, preserving withdrawals and accurate share pricing

Support

FAQs

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