Company Simulator

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

Owner can fund the company by choosing route intended only for investors

Author Revealed upon completion

Description

  • The funding entry point fund_cyfrin(action) is designed to route ETH either to owner capital injection (action = 0, no shares minted) or to public investor funding (action = 1, shares minted to the caller). The intent is that the owner uses fund_owner() (no shares), while non‑owner users use fund_investor() (receive shares).

  • The internal function fund_investor() does not restrict the caller. Because the external router fund_cyfrin(1) also lacks a “non‑owner” check, the owner can call the investor route and mint shares to themselves, contrary to the intended separation of roles. This lets the owner acquire public shares, dilute other investors, and later redeem value via withdraw_shares().

// Root cause in the codebase with @> marks to highlight the relevant section
// File: Cyfrin_Hub.vy (CompanyGame)
@external
@payable
def fund_cyfrin(action: uint256):
if action == 0:
self.fund_owner()
elif action == 1:
// @> No check to prevent OWNER from selecting the investor route
self.fund_investor()
else:
raise "Input MUST be between 0 and 1!!!"
@payable
@internal
def fund_investor():
// @> No restriction on msg.sender; OWNER can reach this via fund_cyfrin(1)
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!!!"
# ... calculates share_price and mints shares to msg.sender

Risk

Likelihood: High

  • Occurs whenever the owner chooses fund_cyfrin(1) (the investor route) instead of fund_cyfrin(0).

  • Occurs whenever the owner account is compromised or behaves adversarially, using privileged knowledge to front‑run public investment.

Impact: High

  • Owner self‑minting/dilution — The owner can mint public shares to themselves, diluting other investors and capturing future payouts.

  • Economic manipulation — The owner controls production, share cap increases, and can steer share_price and timing to accumulate shares cheaply, then withdraw with minimal penalty after the lockup period.

Proof of Concept

  • The owner can call fund_cyfrin(1) and receive shares (via fund_investor()), even though the owner is supposed to fund without shares (fund_owner()).

# tests/test_owner_can_invest.py
import boa
COMPANY_PATH = "src/Cyfrin_Hub.vy"
def test_owner_can_invest():
# Prepare owner EOA
owner = boa.env.generate_address()
boa.env.set_balance(owner, 10**21)
# Deploy company
with boa.env.prank(owner):
company = boa.load(COMPANY_PATH)
# Seed company so it's not insolvent (required by fund_investor)
with boa.env.prank(owner):
company.fund_cyfrin(0, value=10**19) # owner route; no shares
# Owner chooses investor route (action = 1) and sends 1 ETH
initial_owner_shares = 0
with boa.env.prank(owner):
company.fund_cyfrin(1, value=10**18) # investor route
# Check that shares were minted to OWNER
with boa.env.prank(owner):
owner_shares = company.get_my_shares()
assert owner_shares > initial_owner_shares, (
"Owner should have received investor shares via fund_cyfrin(1)"
)
print(f"[✓] Owner received {owner_shares} shares via investor route (bug confirmed)")

Recommended Mitigation

  • Block owner from investor route.

@external
@payable
def fund_cyfrin(action: uint256):
if action == 0:
self.fund_owner()
elif action == 1:
+ # Prevent OWNER from using the investor path
+ assert msg.sender != OWNER, "Owner must use fund_owner()"
self.fund_investor()
else:
raise "Input MUST be between 0 and 1!!!"
Updates

Lead Judging Commences

0xshaedyw Lead Judge
4 days ago
0xshaedyw Lead Judge 2 days ago
Submission Judgement Published
Invalidated
Reason: Out of scope
Assigned finding tags:

Not Valid - Owner is Trusted

Support

FAQs

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