Company Simulator

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

M03. Irrecoverable Investor Loss During Bankruptcy in withdraw_shares()

Root + Impact

Description

  • Under normal conditions, withdraw_shares() allows investors to redeem their shares for ETH based on the company’s current net worth per share. The function ensures fairness by applying penalties and payout caps, and then transfers the appropriate value to the investor.

  • The issue occurs when the company is bankrupt (company_balance < holding_debt). In this case, the calculated share price becomes zero, resulting in a payout of zero. The function still resets the investor’s shares and decreases issued_shares, permanently erasing investor holdings without any compensation. This occurs silently without reverting, leading to a total and irreversible loss of investor funds.

# Root cause: irreversible share deletion despite zero-value payout
@external
def withdraw_shares():
shares_owned: uint256 = self.shares[msg.sender]
assert shares_owned > 0, "Not an investor!!!"
share_price: uint256 = self.get_share_price()
payout: uint256 = shares_owned * share_price
# @> share_price == 0 when company_balance < holding_debt (bankruptcy)
# @> shares are still burned and payout remains 0
self.shares[msg.sender] = 0
self.issued_shares -= shares_owned
assert self.company_balance >= payout, "Insufficient company funds!!!"
raw_call(msg.sender, b"", value=payout, revert_on_failure=True)

Risk

Likelihood:

  • This occurs whenever the company becomes insolvent (its debt exceeds its liquid balance).

  • Investors attempting to redeem shares during this period automatically trigger the condition since get_share_price() returns zero.

Impact:

  • Investors permanently lose all of their shares without receiving any compensation.

  • The company’s share registry and total supply are altered, enabling potential manipulation of future equity or recovery events.

Proof of Concept

import boa
from eth_utils import to_wei
SET_OWNER_BALANCE = to_wei(1000, "ether")
FUND_VALUE = to_wei(20, "ether")
def test_Investor_Loss_During_Bankruptcy(industry_contract, OWNER, INVESTOR):
# Arrange: Fund the company just enough to create shares, then force bankruptcy
boa.env.set_balance(OWNER, SET_OWNER_BALANCE)
# Owner funds company
with boa.env.prank(OWNER):
industry_contract.fund_cyfrin(0, value=FUND_VALUE)
# Investor funds company to receive shares
with boa.env.prank(INVESTOR):
industry_contract.fund_cyfrin(1, value=FUND_VALUE)
# Force bankruptcy by artificially inflating holding debt
industry_contract.holding_debt = industry_contract.get_balance() + to_wei(10, "ether") # company_balance < holding_debt
shares_before = industry_contract.get_my_shares(caller=INVESTOR)
assert shares_before > 0, "Investor should have shares"
# Act: Investor tries to withdraw shares
with boa.env.prank(INVESTOR):
industry_contract.withdraw_shares()
shares_after = industry_contract.get_my_shares(caller=INVESTOR)
# Assert: Investor loses all shares, receives 0 ETH
assert shares_after == 0, "Investor lost all shares"
assert industry_contract.get_balance() == 0, "Company balance cannot cover payout"

Explanation of PoC:

  1. The owner funds the company to provide initial liquidity.

  2. The investor purchases shares via fund_cyfrin(1).

  3. We artificially inflate holding_debt to exceed company_balance to simulate bankruptcy.

  4. The investor calls withdraw_shares().

  5. Because the company is insolvent:

    • get_share_price() returns 0

    • payout = shares * share_price = 0

    • Investor shares are reset to 0 anyway

  6. Result: The investor permanently loses their shares and receives no ETH, demonstrating the vulnerability.


Recommended Mitigation

Add a solvency check before allowing share withdrawals to prevent irreversible loss.

- assert self.company_balance >= payout, "Insufficient company funds!!!"
+ assert self.company_balance > self.holding_debt, "Company is insolvent, withdrawals disabled!!!"
+ assert payout > 0, "Cannot withdraw during insolvency!!!"
Updates

Lead Judging Commences

0xshaedyw Lead Judge 3 days ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

Not Valid

Description: Insolvency is not permanent; owner can always restore solvency via capital injection or debt repayment. Investor loss at zero net worth is normal investment risk, not a bug.

Support

FAQs

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