For the complete documentation index, see llms.txt. This page is also available as Markdown.

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:

Loss taken
Assets remaining
Cost of a 50% stake

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:

  1. 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.0e22 shares against an existing supply of 3.0e21.

  2. 545 of 600 assets of subsequent premium revenue captured. The three original LPs, who contributed 3000, were left with 54.5 between them.

  3. 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, _hasActiveIncident

  • contracts/core/FirelightVault.sol: (payout, the sink that makes totalAssets == 0 reachable)

  • contracts/core/IncidentManager.sol _decreaseIncident (clears the deposit block on close)

  • contracts/core/VaultRewardDistributor.sol distributeRewards / 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?