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

89876 sc low rounding inconsistent maxdeposit and maxmint values can advertise vault entry that necessarily reverts

Submitted on Aug 21st 2026 at 16:09:07 UTC by @Nescient for Audit Comp | Firelight

  • Report ID: #89876

  • 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

Description

Brief/Intro

FirelightVault.maxDeposit reports the raw asset headroom below depositLimit, without checking whether that amount converts to a nonzero number of shares. After ordinary rewards increase the vault's asset-per-share exchange rate, the remaining headroom can be too small to mint even one share base unit. maxDeposit nevertheless reports that headroom as available, while deposit deliberately rejects it with InvalidAmount.

FirelightVault.maxMint takes a different inconsistent route: it converts the total deposit limit to shares and only then subtracts the existing share supply. Executable mint capacity must instead be derived from the remaining asset headroom. Because conversion rounds down while mint rounds its asset cost up, the current calculation can advertise one share base unit more than the remaining headroom can purchase. Calling mint(maxMint(receiver), receiver) then reverts with DepositLimitExceeded.

The defect is reachable in a nonempty V2 vault through normal protocol operations. The fully end-to-end PoC:

  1. deposits 100 FXRP through the real upgradeable FirelightVault;

  2. forwards 1 FXRP through the real upgradeable VaultRewardDistributor, increasing assets without minting shares;

  3. queries and executes each advertised maximum in the same transaction; and

  4. proves that the quote is rejected without any intervening state change.

This conflicts with ERC-4626, which requires maxDeposit and maxMint not to overestimate the amount accepted by their corresponding entry function. The standard expressly permits these functions to underestimate when necessary. EIP-4626 — https://eips.ethereum.org/EIPS/eip-4626

Firelight also documents that its vault is ERC-4626-based for DeFi compatibility and composability, and that a normal deposit or mint transfers the underlying asset and returns vault shares. Firelight vault documentation — https://docs.firelight.finance/introducing-firelight/core-features/vaults, Firelight common workflows — https://docs.firelight.finance/technical-documents/common-workflows

Vulnerability Details

The maximum asset quote subtracts live assets from the limit:

This checks only the asset limit. It omits the execution constraint added to deposit: the asset amount must mint at least one share unit.

For the six-decimal FXRP vault, the conversion is:

Let:

maxDeposit returns H. If:

then previewDeposit(H) == 0. Because conversion is monotonic, every positive amount at or below the advertised maximum also converts to zero. The actual executable maximum is therefore zero, not H.

The maximum share quote is calculated as follows:

The returned value is:

However, mint rounds the asset requirement upward and _depositFunds rejects any result above L:

Accordingly, the actual maximum number of mintable shares is:

When rewards have made A > S, converting L before subtracting S can preserve a fractional component from the existing assets. It can round reportedMaxMint one share base unit above actualMaxMint. previewMint(reportedMaxMint) then exceeds H, so the advertised maximum necessarily reverts.

Concrete maxDeposit Failure

The first PoC case uses six-decimal FXRP units:

The vault reports:

The PoC's integration caller invokes maxDeposit and passes its result directly to deposit within the same transaction. deposit(1, receiver) reverts InvalidAmount. The vault balance and caller balance remain unchanged and the receiver obtains no shares.

Because the advertised maximum is one unit, no smaller positive amount exists. maxDeposit therefore advertises positive capacity while the actual maximum executable deposit is zero.

Concrete maxMint Failure

The second case starts from the same ordinary deposit and incentive state but uses this configured limit:

The current calculation returns:

The same-transaction quote-and-mint call reverts DepositLimitExceeded. The PoC then proves the precise executable boundary:

Thus the failure is not caused by pause, blocklisting, insufficient approval, an active incident, a changing price, or a concurrent deposit. It is the rounding order in maxMint itself.

Fully End-to-End Failure Sequence

  1. The vault is initialized with its deposit limit. The limit is not changed after the test state is created.

  2. A normal staker deposits 100 FXRP and receives 100 stFXRP.

  3. An authorized distributor forwards an ordinary 1-FXRP incentive through the real VaultRewardDistributor. This increases totalAssets to 101 FXRP while totalSupply remains 100 stFXRP, as intended for share-accruing rewards.

  4. There is no active incident, the vault is not paused, and neither the integration caller nor receiver is blocklisted.

  5. For the maxDeposit configuration, the remaining headroom is one asset base unit. maxDeposit returns one even though that amount produces zero shares.

  6. A test-only integration caller queries maxDeposit and immediately calls deposit with the returned amount in one transaction. The real vault reverts InvalidAmount.

  7. In an independent fixture for maxMint, the remaining headroom is 101,001 asset units. maxMint returns 100,001 shares, whose rounded-up cost is 101,002 assets.

  8. The integration caller queries maxMint and immediately calls mint with the returned amount in one transaction. The real vault reverts DepositLimitExceeded.

  9. Minting one share base unit less succeeds, confirming the true boundary and isolating the faulty maximum quote.

Affected Parties and Practical Impact

  • Direct staker: pays gas for a transaction that the vault's own maximum view represented as executable and receives none of the expected stFXRP. The FXRP principal remains safe because the revert rolls back the transfer.

  • ERC-4626 router or vault aggregator: a standard max*-bounded entry flow can revert atomically at the Firelight leg. If Firelight is one operation in a larger multicall, strategy deployment, or rebalance, the whole transaction can revert and the other operations are rolled back.

  • Automated strategy/user interface: a client using maxDeposit or maxMint to enable its final-capacity action can repeatedly propose a transaction that can never succeed in the unchanged state. It must implement Firelight-specific retry logic or reduce the quote heuristically.

  • Firelight/stakers: the last quoted capacity cannot be consumed through the affected maximum-based path. The protocol may miss a small amount of capital deployment and the associated prospective fee participation. The demonstrated amount is dust; no material protocol insolvency or liveness impact is claimed.

  • Attacker: none is required and nobody receives the caller's lost gas. The inconsistent state arises naturally after reward distribution as the vault approaches its configured cap. Front-running is not part of the PoC.

This report deliberately does not claim theft, freezing, or principal loss. Both operations fail before assets are transferred. The proposed Low impact is limited to failure to deliver the standardized, currently executable vault entry returned by maxDeposit/maxMint, the resulting failed-transaction gas, and integration disruption.

Known-Issue Distinction

Macro's audit reported that an earlier deposit implementation could accept dust assets while minting zero shares, causing direct asset loss. That issue is fixed in the audited code by:

The maxDeposit manifestation is a residual inconsistency after that fix. The guard correctly prevents the old loss, but maxDeposit was not updated to return zero when all remaining deposit headroom is rejected by the new guard. This report does not claim the fixed asset-loss impact; it demonstrates that the maximum view still promises an entry the fixed execution path rejects.

Macro expressly stated that mint was unaffected by its zero-share deposit issue. The maxMint manifestation here is different: a positive share quote has a rounded-up asset cost above the deposit limit because conversion occurs before existing supply is subtracted.

Neither manifestation relies on:

  • zero total supply or a fresh empty V2 deployment;

  • fee-on-transfer, rebasing, or otherwise nonstandard tokens;

  • donation by an attacker;

  • V1-to-V2 storage state;

  • a malicious or incorrect privileged action;

  • incident processing; or

  • a state change between quotation and execution.

The supplied competition known issues do not disclose either maximum-quote inconsistency. The supplied Macro audit does not disclose maxMint, and its dust-deposit finding is distinguished above.

Impact Details

The best available in-scope classification is Low — Contract fails to deliver promised returns, but doesn't lose value.

The relevant promise is concrete and machine-consumed: the ERC-4626 maximum functions return the greatest amount the corresponding entry function should accept in the current state. Firelight advertises ERC-4626 compatibility specifically for broader DeFi integration. An immediate call using either returned maximum fails, so the receiver does not obtain the quoted vault position.

The category's qualifier is what makes it the correct fit. "But doesn't lose value" is not incidental — a failure that loses value belongs in a higher tier, and this tier exists for functional failures where nothing is lost. Both halves are satisfied here: an ERC-4626 entry function rejects the exact amount the contract's own maximum view advertised, and the revert is atomic so no principal moves.

The Insight categories do not fit. EIP-4626 states that maxDeposit and maxMint MUST NOT overestimate the amount their corresponding entry function accepts; a normative requirement in a standard the project advertises compliance with is not a "best practice". Nothing here is a code optimization, an architectural decentralization concern, or a documentation gap — the documentation is correct and the implementation contradicts it.

In the maxDeposit case the executable maximum is not merely lower than advertised, it is zero: no positive deposit within the reported headroom mints a nonzero share, so the vault advertises available capacity while accepting none, and remains in that state until assets leave or the limit is raised.

The value-preservation qualifier is also satisfied: the vault reverts before transferring the caller's FXRP or minting shares. The report therefore does not elevate the issue to Medium griefing. There is no attacker profit, no third-party principal damage, and no sustained protocol outage. The practical losses are transaction gas, failure of a composed transaction, delayed vault entry, and potentially missed prospective participation while the client diagnoses or works around the inaccurate quote.

Derive both maximums from the remaining asset headroom and the same conversion constraints used by execution.

Conceptually:

previewDeposit(headroom) rounds down to the greatest share amount whose rounded-up previewMint cost remains within the headroom. Returning zero for zero-share asset dust also keeps maxDeposit aligned with the explicit deposit guard.

Add invariants covering reward-accrued exchange rates and every deposit-limit remainder:

References

  • FirelightVault.maxDeposit — https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L371-L378

  • FirelightVault.maxMint — https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L385-L393

  • FirelightVault.deposit — https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L646-L674

  • FirelightVault.mint — https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L682-L706

  • FirelightVault._depositFunds — https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L976-L992

  • FirelightVault conversion helpers — https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/FirelightVault.sol#L1058-L1074

  • VaultRewardDistributor.distributeIncentive — https://github.com/immunefi-team/audit-comp-firelight/blob/42f9ea5e43d88b35197b901acc2a8c24b314fa62/contracts/core/VaultRewardDistributor.sol#L149-L162

  • EIP-4626 — https://eips.ethereum.org/EIPS/eip-4626

  • Firelight vault documentation — https://docs.firelight.finance/introducing-firelight/core-features/vaults

  • Firelight common workflows — https://docs.firelight.finance/technical-documents/common-workflows

  • Supplied Macro audit — https://library-pr-440.onrender.com/library/audits/firelight-1

Proof of Concept

Proof of Concept

Create contracts/test/MaxQuoteExecutor.sol and test/max-quote-rounding-poc.js with the contents below.

This is a fully end-to-end V2 quotation/execution PoC. It deploys the real upgradeable FirelightVault and the real upgradeable VaultRewardDistributor. The vault state is established through an ordinary deposit and ordinary incentive distribution. The only helper is a minimal ERC-4626 integration caller that queries a maximum and calls the corresponding function in the same transaction. The external FAsset dependency remains mocked locally.

Run:

Expected result:

contracts/test/MaxQuoteExecutor.sol:

test/max-quote-rounding-poc.js:

Was this helpful?