90282 sc medium once payout drives totalassets to 0 with shares still outstanding share pricing degenerates any address can mint totalsupply 1 shares per wei of deposit capturing
Submitted on Aug 24th 2026 at 07:14:57 UTC by @Another for Audit Comp | Firelight
Report ID: #90282
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
Brief/Intro
_convertToSharesTotals prices a deposit as assets · (totalSupply + 1) / (totalAssets + 1). That is correct while the pool holds assets. It degenerates when totalAssets() reaches zero with totalSupply() still large — the denominator collapses to 1, so every wei deposited mints totalSupply + 1 shares.
Phase 2 makes that state reachable by design. A full-loss incident payout drains the active pool; _decreaseIncident then clears hasActiveIncident, deposits reopen in the same or the following period (same-period reopen requires the assessment to close while still in the capture period and the prior period to be incident-free, since _hasActiveIncident() also checks period - 1), and the vault continues operating on premium revenue forwarded by VaultRewardDistributor. The stakers who bore the loss are the ones that revenue is meant to make whole. A dust depositor arriving between the payout and the next distribution takes almost all of it.
Phase 1 could not reach this state: with no payout(), totalAssets() never fell.
Vulnerability Details
contracts/core/FirelightVault.sol:
_decimalsOffset() is not overridden, so it is 0 and the numerator term is totSupply + 1. With totAssets == 0 the expression is exactly assets · (totSupply + 1).
The only guards on _depositFunds are a zero-share check and the deposit cap:
Neither constrains the share count. A 10-wei deposit into a pool holding 3000e18 shares and zero assets mints 3.0e22 shares — ten times the entire existing supply.
Nothing blocks the deposit either. maxDeposit returns depositLimit - totalAssets(), which is the full limit when assets are zero, and _hasActiveIncident() is false as soon as the incident closes:
so the deposit window opens in the very transaction that finishes draining the vault.
NB: This is not the classic ERC4626 inflation attack and the usual mitigation does not apply. The standard attack inflates totalAssets by donation to round a victim's shares down to zero; raising _decimalsOffset() fixes that. Here the failure runs the other way — totalAssets collapses toward zero and the share count explodes. Increasing the offset makes the numerator larger and the problem slightly worse, not better.
The severity degrades gracefully rather than cliff-edging, which widens the window considerably. Cost to acquire half the vault is approximately the assets remaining:
100%
0
1 wei
99.9999%
0.001
0.001
99%
10
10
50%
500
500
At 50% impairment the pricing is fair — paying 500 to double a 500-asset pool earns half of it. Only as assets approach zero does the price of control detach from the value of what is being bought, because NAV does not include the vault's claim on future premium revenue.
Impact Details
Verified end-to-end:
90.9% of the vault acquired for 10 wei. Three LPs deposited 1000 each; a full-loss payout took all 3000; the attacker deposited 10 wei and received
3.0e22shares against an existing supply of3.0e21.545 of 600 assets of subsequent premium revenue captured. The three original LPs, who contributed 3000, were left with 54.5 between them.
It attacks the recovery mechanism specifically. The protocol is designed to survive a full loss and rebuild through premium flow. This converts that design property into the attack's payoff.
References
contracts/core/FirelightVault.sol: (_convertToSharesTotals), (deposit),_depositFunds,maxDeposit,_hasActiveIncidentcontracts/core/FirelightVault.sol: (payout, the sink that makestotalAssets == 0reachable)contracts/core/IncidentManager.sol_decreaseIncident(clears the deposit block on close)contracts/core/VaultRewardDistributor.soldistributeRewards/distributeIncentive(the captured revenue stream)
Proof of Concept
Proof of Concept
hardhat.config.js:
contracts/harness/Harness.sol:
scripts/stack.js — deploys vault + CoverNFT + allocator + IncidentManager behind proxies and wires the roles:
Add the test below to scripts/
Logs
Was this helpful?