Company Simulator

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

Unbounded Holding Cost Calculation Allows Instant Bankruptcy After Long Inactivity

[H-05] Unbounded Holding Cost Calculation Allows Instant Bankruptcy After Long Inactivity

Description

The _apply_holding_cost() internal function deducts per-second storage cost from company_balance based on elapsed time since last_hold_time.
If the company remains inactive for a long period (e.g., months), the computed cost can exceed total balance.
When this happens, company_balance is set to 0 and the remainder is added to holding_debt, instantly bankrupting the company on the next call to any external function that applies holding cost.

@internal
def _apply_holding_cost():
seconds_passed: uint256 = block.timestamp - self.last_hold_time # @audit Unbounded elapsed time
cost_per_second: uint256 = HOLDING_COST_PER_ITEM // 3600
cost: uint256 = (seconds_passed * self.inventory * cost_per_second) # @audit Unbounded multiplication
if self.company_balance >= cost:
self.company_balance -= cost
else:
self.holding_debt += cost - self.company_balance
self.company_balance = 0 # @audit Forced bankruptcy

Root Cause

  • The function uses unbounded linear growth based on elapsed time.

  • It applies the full accumulated cost in a single call after inactivity.

  • There is no cap or dampening mechanism, allowing instant depletion of company_balance.

Risk

Likelihood: High

  • Triggered automatically after long inactivity (e.g., downtime or low user activity).

  • No malicious input required.

Impact: Critical

  • Instantly drains all ETH in company_balance.

  • Adds unbounded holding_debt, marking the company insolvent.

  • Blocks future withdrawals and investment due to failed balance checks.

  • Can cause full platform lock.

Proof of Concept

# tests/test_holding_bankruptcy.py
import boa
from eth_utils import to_wei
from pathlib import Path
CONTRACT_PATH = Path("src/Cyfrin_Hub.vy")
def test_holding_cost_inactivity_bankrupts_company():
"""
PoC for [H-05] Holding Cost Function Can Trigger Instant Bankruptcy After Inactivity
Demonstrates how long inactivity causes _apply_holding_cost()
(indirectly via produce()) to drain company_balance entirely and create massive debt.
"""
# --- Setup accounts ---
owner = boa.env.generate_address("owner")
boa.env.set_balance(owner, to_wei(20, "ether"))
# --- Deploy the contract ---
with boa.env.prank(owner):
company = boa.load_partial(str(CONTRACT_PATH))()
# --- Fund company and initialize inventory ---
with boa.env.prank(owner):
company.fund_cyfrin(0, value=to_wei(10, "ether"))
company.inventory = 100 # assume 100 stored items
company_balance_before = int(company.company_balance())
holding_debt_before = int(company.holding_debt())
last_hold_time_before = int(company.last_hold_time())
print(f"Initial company_balance: {company_balance_before}")
print(f"Initial holding_debt: {holding_debt_before}")
# --- Simulate long inactivity period (e.g., 1 year) ---
one_year = 60 * 60 * 24 * 365
boa.env.time_travel(seconds=one_year)
# --- Trigger any function that applies holding cost internally ---
with boa.env.prank(owner):
try:
company.produce(1) # small call triggers _apply_holding_cost()
except Exception:
# even if production reverts, the holding cost will still be applied before revert
pass
# --- Observe post-state ---
company_balance_after = int(company.company_balance())
holding_debt_after = int(company.holding_debt())
last_hold_time_after = int(company.last_hold_time())
print(f"After inactivity -> company_balance: {company_balance_after}")
print(f"After inactivity -> holding_debt: {holding_debt_after}")
print(f"Time advanced from {last_hold_time_before} to {last_hold_time_after}")
# --- Assertions ---
assert company_balance_after == 0, "Company should go bankrupt after long inactivity"
assert holding_debt_after > holding_debt_before, "Holding debt should increase significantly after inactivity"
print("Vulnerability confirmed — long inactivity triggers auto-bankruptcy via unbounded cost accumulation.")

Run with:

mox test tests/test_withdraw_lock.py -vv

Recommended Mitigation

Implement a maximum cap on cost accumulation and gracefully decay the rate over time.

@internal
def _apply_holding_cost():
seconds_passed: uint256 = block.timestamp - self.last_hold_time
+ if seconds_passed > 30 * 24 * 3600: # 30 days
+ seconds_passed = 30 * 24 * 3600
cost_per_second: uint256 = HOLDING_COST_PER_ITEM // 3600
cost: uint256 = (seconds_passed * self.inventory * cost_per_second)
if self.company_balance >= cost:
self.company_balance -= cost
else:
self.holding_debt += cost - self.company_balance
self.company_balance = 0
self.last_hold_time = block.timestamp
Updates

Lead Judging Commences

0xshaedyw Lead Judge
7 days ago
0xshaedyw Lead Judge 5 days ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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