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

88676 sc low firelightvault redeem burns a staker s shares for zero assets records no withdrawal credit and returns success

Submitted on Aug 14th 2026 at 13:35:30 UTC by @whitehorse for Audit Comp | Firelight

  • Report ID: #88676

  • Report Type: Smart Contract

  • Report severity: Low

  • Target: https://github.com/immunefi-team/audit-comp-firelight/blob/v1_audit_ready/contracts/core/FirelightVault.sol

  • Impacts:

    • Contract fails to deliver promised returns, but doesn't lose value

Description

Severity

Low. In scope impact: "Contract fails to deliver promised returns, but doesn't lose value."

Brief/Intro

redeem validates the caller supplied shares, converts it to assets with floor rounding, and never checks what the conversion returned. Once the vault has paid a claim and the share price falls below 1, redeeming a small share count burns the shares, adds zero to every withdrawal ledger entry, and returns success. The holder's balance is destroyed and nothing is recorded in exchange for it. The project already ruled this a bug on the entry side: 0xMacro's Firelight A-1 audit reported L-1, that deposit could mint zero shares while still transferring assets, because the deposit amount was guarded and the resulting share count was not. It was fixed in commit 6806f35, "fix: [L-1] prevent zero-share deposits", whose diff to FirelightVault.sol is the three lines now at :667 to :669 and touches no other function. The exit path kept the same shape and is still unguarded at the in-scope commit 42f9ea5e.

Vulnerability Details

The conversion floors, at _convertToAssetsTotals:

_decimalsOffset() is not overridden anywhere in the tree and has no setter, so the cushion is fixed at 10 ** 0 = 1 and the zero output condition is exact, with A the total assets and S the total supply:

While A >= S the right hand side is at most 1, so only shares == 0 qualifies and :731 already covers it. Once A < S the band widens to every share count strictly below (S + 1) / (A + 1). Nothing rechecks the value:

In a later period claimWithdraw reads withdrawSharesOf[period][account], finds zero, and reverts NoWithdrawalAmount(period) at :803, with the shares already gone.

A < S is the state the vault is built to enter. payout() at :892 transfers the approved claim out at :962 and pushes the reduced total at :952 without touching totalSupply, and contains no _mint, _burn or _update. Absorbing claim losses into the share price is the product's core function, so a vault that has paid a claim sits at A < S until fresh yield restores the gap. payout is new in this phase. The legacy predeposit vault has no such function, and in FirelightVaultPredeposits.sol totalAssets() at :382 nets out pendingWithdrawAssets, so the only site that lowers it is the withdrawal request at :811, which burns the matching shares at :817; the claimWithdraw transfer at :708 is neutral, since :705 decrements pendingWithdrawAssets by the same amount. Assets never fell without shares, so the price could not drop below 1 and the band this defect needs did not exist.

redeem is the only entry point that rounds a derived quantity down and does not check it. deposit also floors, at :665, and rejects the zero at :669. withdraw and mint round up, at :771 and :701, so their derived quantity cannot reach zero. The two sides are the same trade reversed: in deposit assets enter and no shares come back, in redeem shares are destroyed and no assets come back, and either way the value accrues to the other holders. redeem additionally splits the failure across two transactions, since the burn settles immediately while the empty claim reverts only a period later. redeem at :717 carries whenNotPaused, three notBlocklisted modifiers and nonReentrant, and no role, allowlist, price test or timing gate sits between the external call and the burn at :1025.

Impact Details

After a claim payout, any staker who calls redeem with a share count inside the band loses those shares outright and receives nothing. The call does not revert. It emits WithdrawRequest with assets zero and leaves a withdrawal request carrying no claim.

On the live vault, totalAssets() 57,360,574.888550 FXRP against totalSupply() 57,356,448.484049 stXRP, the band opens once a claim exceeds the current surplus of 4,126.404501 FXRP and widens with the claim: 1 share unit at a claim of 10 percent of the vault, 99 share units at 99 percent. The assets the burned shares were entitled to stay in the vault and accrue pro rata to the remaining holders, and that entitlement is strictly below one raw asset unit per call, below 0.000001 FXRP, because the floor identity that zeroes the output also bounds it. The vault's asset balance is unchanged by the burn.

Give the exit the guard the entry already has, immediately after the conversion at :737:

Proof of Concept

poc_zero_asset_redeem.js runs on the repository's own Hardhat suite at commit 42f9ea5e with no contract modified. Three tests pass at live mainnet parameters: test 1 is the negative control, showing the defect does not fire at the live price, test 2 is the defect after a claim of 10 percent of the vault, test 3 pins the band boundary and the value ceiling.

Adding the guard above after :737 turns tests 2 and 3 into InvalidAmount reverts and both zero credit assertions fail.

References

  • contracts/core/FirelightVault.sol:717 to :739, redeem, and :731, the input only check

  • contracts/core/FirelightVault.sol:1064, :1067, :1073, the floor conversion and the never overridden _decimalsOffset

  • contracts/core/FirelightVault.sol:1015 to :1019, :1025, the zero ledger writes and the unconditional burn

  • contracts/core/FirelightVault.sol:803, the later NoWithdrawalAmount revert

  • contracts/core/FirelightVault.sol:665, :667 to :669, the guarded floor conversion in deposit

  • contracts/core/FirelightVault.sol:892, :952, :962, payout lowering assets without touching supply

  • 0xMacro Firelight A-1, finding L-1, "prevent zero-share deposits", commit 6806f35

Proof of Concept

Was this helpful?