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

88960 sc low redeem can burn shares while crediting zero assets

Submitted on Aug 16th 2026 at 00:36:54 UTC by @Web3Vinay for Audit Comp | Firelight

  • Report ID: #88960

  • 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

Brief/Intro

deposit() rejects a dust input that would round to zero shares, and says why in a comment. redeem() has no matching guard on the computed assets. Once the share price is below 1 (any payout() does this), a small enough shares floors assets to 0 — the full shares still get burned, WithdrawRequest still emits as if the request worked, and the later claimWithdraw for that period reverts NoWithdrawalAmount.

Vulnerability Details

deposit() guards the mirror case:

// contracts/core/FirelightVault.sol:662-669
(uint256 shares, uint256 _totalSupply, uint256 _totalAssets) = _previewTotals(assets, true, Math.Rounding.Floor);
// A dust deposit can floor to zero shares once the share price exceeds 1;
// reject it instead of pulling assets in exchange for nothing.
@> if (shares == 0) revert InvalidAmount();

redeem() only checks the input, never the computed output:

_requestWithdraw then burns shares unconditionally, whatever assets came out to:

The flooring condition (FirelightVault.sol:1067-1074, _decimalsOffset() == 0): assets == 0 for every shares < (totalSupply()+1)/(totalAssets()+1), i.e. whenever totalAssets() < totalSupply() — the state any incident payout() produces.

Impact Details

  • The exact case deposit()'s comment says the code refuses to produce ("shares in exchange for nothing") happens anyway on the redeem side.

  • redeem()'s NatSpec (:715) promises the return is the "Amount of assets that will be received" — it silently returns 0 instead.

  • WithdrawRequest fires on a request that credits nothing; any indexer/dashboard treating the event as "a withdrawal is pending" is wrong until claimWithdraw reverts NoWithdrawalAmount at claim time.

Any account that can move the shares (owner or an approved spender) can hit this once the share price has dropped — no privileged role needed.

Mitigation

References

https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L662-L669

https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L731-L741

https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L1015-L1027

Proof of Concept

One deposit, one payout() to drop the share price, one redeem() at the dust boundary. Save as test/RedeemDustBurn.t.sol and run forge test --match-contract INSIGHT_RedeemDustBurn -vv.

Run

Result

redeem() returned 0, the shares were still burned, and WithdrawRequest fired with assets = 0 (asserted exactly via vm.expectEmit) — the request looks successful on-chain and off-chain, but there is nothing to claim.

Was this helpful?