88330 sc medium post slash zero assets state inverts the erc4626 virtual offset a 1 wei deposit mints half of all shares and captures 50 of all future vault yield
Submitted on Aug 12th 2026 at 19:10:53 UTC by @pawelschubert for Audit Comp | Firelight
Report ID: #88330
Report Type: Smart Contract
Report severity: Medium
Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/FirelightVault.sol
Impacts:
Theft of unclaimed yield
Description
Bug Description
FirelightVault uses the OpenZeppelin ERC4626 virtual-offset defense in its conversion math:
// FirelightVault.sol:1058-1065
function _convertToSharesTotals(
uint256 assets,
uint256 totSupply,
uint256 totAssets,
Math.Rounding rounding
) private view returns (uint256) {
return assets.mulDiv(totSupply + 10 ** _decimalsOffset(), totAssets + 1, rounding);
}The virtual offset assumes the invariant totalSupply() == 0 whenever totalAssets() == 0. OpenZeppelin's defense holds under that assumption; when it is broken (supply > 0, assets == 0), the defense inverts and becomes the vulnerability:
A deposit of D wei mints D * (totalSupply + 1) shares, i.e. a fraction D / (D + 1) of post-deposit supply. 1 wei captures ~50% of all shares; ~200 wei captures ~99%.
FirelightVault.payout() breaks the invariant by design: it slashes vault assets for incident claims without burning any shares:
A full-slash payout therefore leaves the vault in the state totalAssets() == 0 && totalSupply() > 0 (all pre-slash shares remain outstanding, now worthless). This state is reachable through the protocol's intended operation:
the per-payout exposure cap is
totalAssetsAt(capturePeriodStart)(FirelightVault.sol:911) - the full period-start balance;covered losses can legitimately reach or exceed the vault balance because cover capacity is leveraged up to 5x minCAR by design.
After the incident resolves, deposits reopen (the incident-gating machinery is explicitly designed to resume deposits), and the very next deposit - by anyone, no privileges needed - mints a supply-majority share position for dust.
The Macro L-1 fix does not cover this. L-1 was the opposite direction (a dust deposit minting zero shares while assets are pulled); the fix added a shares == 0 revert in deposit() (FirelightVault.sol:667-669). That check does nothing for the hyper-inflation direction, which only became reachable with Phase 2's payout() slash.
Step-by-step attack path (PoC-verified numbers, 6-decimal asset)
Victim deposits 20,000 tokens →
totalSupply = 20,000,000,000shares, 1:1.An incident captured next period is approved for a loss ≥ TVL (legitimate per the exposure cap and 5x leverage) →
payout()drains everything:totalAssets() = 0; the victim's 20e9 shares remain.Incident resolves; deposits reopen. Attacker calls
deposit(1, attacker)and receives 20,000,000,001 shares = victim's shares + 1 (>50% of supply).The VaultRewardDistributor forwards 10,000 tokens of cover premiums →
totalAssets() = 10,000.000001.Attacker requests withdrawal and claims after the standard delay: receives 4,999.99999 tokens for a 1-wei investment, and permanently owns ~50% of every future reward cycle. Depositing ~200 wei instead captures ~99.5%.
The attacker's exploitation step (deposit(1)) is fully unprivileged and permissionless; the precondition (a near-total slash) is a state the protocol is explicitly designed to reach and continue operating after.
Impact
Theft of unclaimed yield (Medium). An attacker captures ~50% (or ~99% for trivially more dust) of all future yield forwarded to the vault - cover premiums and incentives distributed to stakers - for ~1 wei, immediately after any full-slash incident. The loss falls on legitimate stakers whose post-slash recapitalization and all subsequent rewards are siphoned. Additionally, the share price is permanently distorted (supply-majority holder at zero cost basis), so every later depositor is diluted - "contract fails to deliver promised returns".
Why this is not a known issue:
Not Macro L-1: opposite rounding direction, different precondition (
totalAssets == 0 && totalSupply > 0, only reachable via the Phase-2 payout slash), different impact (theft vs. lost dust). The L-1 fix provably does not touch this path.Not the accepted "new deposits capture pending reward distribution" risk: that accepted dilution assumes deposits buy shares at a fair price (pay X, capture ∝ X). Here the price function itself is broken - the capture fraction is
D/(D+1)of all supply, unbounded relative to the deposit.The exploitation step requires no privileged role.
Recommendation
When totalAssets() == 0 && totalSupply() > 0, either:
revert
deposit/mint(require a governance re-seed / re-initialization of the vault after a full slash), ormint new deposits 1:1 (the surviving pre-slash shares are already worthless; new money must not receive the supply for free), or
burn/anticipate the surviving shares as part of
payout()when the slash reaches ~100%.
Link to Proof of Concept
https://gist.github.com/schvbertt/c215f1d51fadaa760458b54f8a551584
Proof of Concept
Runnable Hardhat test against the real FirelightVault deployed via the repo's own fixture.
Setup:
Expected output (1 passing):
Was this helpful?