90102 sc medium post slash deposit accounting in firelightvault leads to theft of recovered staker principal
Submitted on Aug 23rd 2026 at 01:59:33 UTC by @calbebop for Audit Comp | Firelight
Report ID: #90102
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:
Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield
Description
Brief/Intro
After a severe validated incident, FirelightVault can have zero underlying assets while slashed stakers still hold their original shares. Settlement closes the incident and immediately reopens public deposits. In that state, the vault’s share-conversion formula issues a new 1-wei depositor approximately the same number of shares as the entire pre-slash supply. When recovered principal is later returned to the vault, the new depositor can withdraw approximately half of it. In the PoC, a 1-wei post-settlement deposit diverts 5,000 FXRP out of a 10,000 FXRP recovery intended to restore the pre-slash staker.
Vulnerability Details
FirelightVault prices deposits using:
function _convertToSharesTotals(
uint256 assets,
uint256 totSupply,
uint256 totAssets,
Math.Rounding rounding
) private view returns (uint256) {
return assets.mulDiv(totSupply + 10 ** _decimalsOffset(), totAssets + 1, rounding);
}For this vault, _decimalsOffset() returns zero. A full slash can therefore leave:
A public 1-wei deposit is then minted as:
The new post-slash depositor consequently owns approximately half of all outstanding shares before recovered value is credited to the vault.
This state is reachable through ordinary incident settlement. IncidentManager.approveCurrentAssessment() executes payout, marks the incident closed, and decrements the active incident count:
When the count reaches zero, _decreaseIncident() clears the vault’s incident flag, immediately allowing deposits again.
Firelight’s documentation is consistent with this sequence:
slashed stakers retain their original share balances;
the vault’s redemption value falls after the slash;
deposits reopen when the slash settles;
recovered assets flow back through the loss waterfall in reverse order, with slashed stakers made whole first;
liquidation overperformance is returned to the vault rather than increasing payouts.
The existing zero-share deposit guard does not prevent this case. A 1-wei deposit in the zero-asset/nonzero-supply state does not round to zero shares; it receives S + 1 shares.
The issue is not limited to an exact zero-asset state. After a partial slash leaving residual assets A, an attacker who deposits A receives approximately S additional shares and captures approximately half of a later recovery. This was reproduced for slash severities from 25% through 100%.
A direct mitigation is to reject deposits while totalAssets() == 0 && totalSupply() > 0. A complete fix must also prevent post-slash deposits from sharing value earmarked to restore pre-slash shareholders—for example, by retiring or loss-adjusting slashed shares, migrating to a new share epoch, or distributing recovery against a pre-incident shareholder snapshot before deposits reopen.
Impact Details
The selected impact is direct theft of user funds other than unclaimed yield.
In the full-slash PoC:
Original staker principal
10,000 FXRP
Vault assets after legitimate payout
0
Outstanding shares after payout
10,000e6
Attacker deposit
1 wei
Shares received by attacker
10,000e6 + 1
Recovered principal returned to vault
10,000 FXRP
Attacker withdrawable value
5,000.000001 FXRP
Attacker net profit
5,000 FXRP
Pre-slash staker recovery without attacker
10,000 FXRP
Pre-slash staker recovery with attacker
5,000 FXRP
Without the attacker, the returned 10,000 FXRP restores the original staker’s full redeemable value. With the attacker, half of that recovery is transferred to an account that contributed only 1 wei after the loss.
The diverted value is recovered principal, not unclaimed yield: it is the FXRP previously slashed from the staker and returned through the documented recovery path.
The attacker is not the cover buyer and holds no privileged role. After settlement, the attack uses only the public vault functions deposit(), withdraw(), and claimWithdraw(). The incident and payout are handled through normal, non-malicious protocol operation.
References
Proof of Concept
Proof of Concept
The following was reproduced on a local Hardhat network using the real FirelightVault, CoverOrderAllocator, CoverNFT, IncidentManager, price-feed adapter, premium token, first-loss token, and vault asset. No public network or production service was tested.
1. Deploy the real Phase 2 integration
Deploy and connect the real contracts with their intended roles:
FirelightVaultCoverOrderAllocatorCoverNFTIncidentManagervault asset
premium token
first-loss token
price-feed adapter
Grant the incident manager its required vault roles and authorize the configured payout receiver.
2. Fund the documented first-loss layer
Fund the first-loss wallet with 1,000 first-loss tokens and authorize IncidentManager to spend them.
3. Deposit the original staker principal
An original staker deposits 10,000 FXRP:
4. Settle genuine cover
A cover buyer separate from the attacker purchases 11,000 USD of cover.
The curator creates the order, the allocator commits and settles it, and the cover buyer receives CoverNFT token 0.
5. Approve a legitimate incident
Create, confirm, assess, submit, and approve a genuine incident for the full 11,000 USD covered loss.
The payout consumes the funded 1,000 first-loss tokens first, then pays the remaining 10,000 FXRP from the vault.
6. Verify the post-slash state
Immediately after approval:
The incident has closed normally and public deposits are open again.
7. Verify that the attacker is unprivileged
The PoC checks that the attacker holds none of the relevant roles on the vault, allocator, or incident manager, including:
vault
DEFAULT_ADMIN_ROLEvault
PAYOUT_ROLEvault
INCIDENT_ROLEvault
CHECKPOINT_ROLEallocator
DEFAULT_ADMIN_ROLEallocator
CURATOR_ROLEallocator
ALLOCATOR_ROLEincident manager
DEFAULT_ADMIN_ROLEincident manager
CURATOR_ROLEincident manager
ASSESSMENT_APPROVER_ROLE
The attacker also has no ownership or beneficiary role in the settled cover.
8. Make the 1-wei post-slash deposit
The attacker approves and deposits 1 wei:
9. Return the exact recovered principal
The payout receiver transfers the exact 10,000 FXRP previously slashed from the vault back to the vault.
This represents the documented return of recovered vault-asset value to the vault layer. No new value is minted, and the recovered amount is the same principal previously removed from the pre-slash staker.
10. Verify the resulting ownership split
After the recovery transfer:
11. Complete the normal delayed withdrawal
The attacker requests:
The vault records a withdrawal request for the next period. After that withdrawal bucket becomes claimable, the attacker calls:
The vault transfers 5,000e6 + 1 FXRP to the attacker.
12. Verify the final state
After the claim:
FXRP conservation is also checked:
Negative control
Repeating the same recovery without the attacker’s 1-wei deposit restores the original staker’s full claim:
Thus, the attacker’s post-settlement 1-wei deposit is the sole change that converts a full 10,000 FXRP recovery into a 5,000 FXRP recovery for the slashed staker.
Partial-slash control
The same behavior was tested after ordinary partial slashes:
25%
50%
75%
90%
99%
100%
When the vault has residual assets A, an attacker depositing A after settlement receives approximately the old share supply and captures approximately half of the later recovery.
For example, after a 90% slash:
Routing control
The same result was also reproduced when the recovered FXRP was routed through the real VaultRewardDistributor instead of being transferred directly to the vault. The distributor was used only as an alternate asset-forwarding path; the source of the funds remained the previously slashed principal.
Was this helpful?