90313 sc low firelightvault can return positive erc 4626 entry maxima that cannot be executed
Submitted on Aug 24th 2026 at 11:30:41 UTC by @go4ko for Audit Comp | Firelight
Report ID: #90313
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
After ordinary rewards raise the FXRP-per-share exchange rate near the vault's deposit cap, FirelightVault.maxDeposit and maxMint can return positive ERC-4626 entry limits that their corresponding deposit and mint functions reject in the same unchanged state. maxDeposit can report positive asset capacity even though every permitted positive amount rounds to zero shares, while maxMint can report one share unit more than the remaining asset headroom can fund. A user or integration trusting either quote spends gas and receives no stFXRP, and a composed router or strategy transaction can revert entirely; the caller's FXRP principal remains safe because execution reverts atomically.
Vulnerability Details
The vault enforces its deposit cap in asset units, but its entry functions convert between FXRP assets and stFXRP shares using virtual units and asymmetric rounding.
maxDeposit ignores the share-side constraint. It returns depositLimit - totalAssets() even when that complete remainder rounds down to zero shares. The current deposit implementation specifically rejects such an amount with InvalidAmount.
maxMint performs the asset/share calculation in the wrong order for a remaining-capacity query. It converts the entire deposit limit into shares and subtracts the existing supply afterward. The executable maximum must instead be computed by converting only the remaining asset headroom. Moving the subtraction across a floor-rounded conversion can add one share base unit. Because mint rounds the asset cost upward, that extra share can cost one asset unit more than the vault has room to accept.
Both failures are reproduced against the repository's normal configuration:
The PoC uses a minimal integration contract that reads each maximum and calls its corresponding entry function inside the same transaction. There is no opportunity for a deposit, reward, limit update, or other state change between quotation and execution.
ERC-4626 requires these maximum functions not to return more than the amount their corresponding entry operation would accept for the receiver. Underestimation is permitted when a vault needs to be conservative. EIP-4626
Firelight documents its ERC-4626 design as a composability feature and describes deposit/mint as the normal path by which an asset holder enters the vault and receives shares. Firelight vault documentation, Firelight common workflows
Root Cause
maxDeposit checks global entry restrictions but otherwise exposes all unused asset capacity:
The actual deposit path applies an additional restriction after converting the assets with floor rounding:
For this six-decimal vault, _decimalsOffset() == 0, so the conversion is:
where S is the live total share supply and A is the live total asset balance. If the remaining asset headroom H = L - A satisfies:
then even H, the largest amount returned by maxDeposit, produces zero shares. Every smaller positive amount does as well. The correct maximum is therefore zero.
maxMint converts the whole cap and then subtracts supply:
That produces:
The maximum shares whose rounded-up cost fits the asset headroom is instead:
These expressions are not equivalent after rounding. The current quote may be one share unit larger. Execution then calculates the assets using ceiling rounding:
and _depositFunds rejects the resulting asset total:
Failure A: maxDeposit Reports Capacity When the Executable Maximum Is Zero
The first test begins with the repository's 50,000-FXRP cap. A normal user deposits 49,998.999999 FXRP, and the real reward distributor forwards a 1-FXRP incentive:
The view reports:
But its corresponding conversion is:
The integration contract obtains the value from maxDeposit and immediately calls deposit(1, receiver). The real vault reverts InvalidAmount because the advertised maximum cannot mint a share.
There is no smaller positive value to try. Therefore:
Failure B: maxMint Is One Share Unit Above the Executable Boundary
The second test again uses the unmodified 50,000-FXRP cap. This time a user deposits 49,998.95 FXRP, followed by an ordinary 1-FXRP reward:
The asset-side maximum remains usable:
However, the share-side maximum is overstated:
The quoted mint would make total assets equal depositLimit + 1, so an atomic maxMint → mint call reverts DepositLimitExceeded.
The PoC then attempts one share unit less:
This proves that a positive executable maximum exists and that the failure is specifically the extra share unit introduced by maxMint's rounding order.
End-to-End Execution Sequence
Deploy the real upgradeable
FirelightVaultusing the repository's default six-decimal asset and 50,000-FXRP deposit limit.Deposit normally until the vault is close to its configured cap. The cap is not changed after the state is established.
Deploy and authorize the real upgradeable
VaultRewardDistributorwithCHECKPOINT_ROLE.Forward a normal 1-FXRP incentive. This increases
totalAssetswithout minting shares, creating a legitimate exchange rate above one.Fund a minimal ERC-4626 integration caller with standard vault assets.
In the first fixture, the caller reads
maxDeposit(receiver)and supplies that exact value todepositbefore returning control. The call revertsInvalidAmount.Confirm that neither the caller's underlying balance nor the vault's balance changed and the receiver received no shares.
In a separate fixture, the caller reads
maxMint(receiver)and supplies the result directly tomint. The call revertsDepositLimitExceeded.Confirm again that balances remain unchanged after the revert.
Mint one share base unit less and observe a successful real
Depositevent, establishing the precise executable limit.
Known-Issue Distinction
Macro identified an earlier deposit behavior in which a dust asset amount could be transferred even though it minted zero shares. The audited implementation remediates that loss by reverting when shares == 0.
This report does not claim that the old direct-loss bug remains. Instead, it shows that maxDeposit was not synchronized with the remediation: it still returns the zero-share amount as executable even though the corrected deposit path rejects it.
The maxMint case does not depend on zero shares at all. The true maximum is a positive 49,998 share units, while maxMint returns 49,999. Macro described mint as unaffected by its dust-deposit finding and did not report this limit-conversion ordering issue.
The competition's supplied known issues do not disclose either maximum-view mismatch. The PoC additionally avoids the assumptions that commonly invalidate token-accounting reports:
it uses a standard FAsset without transfer fees;
total supply is large and nonzero;
it does not require an empty V2 deployment;
it does not require a donation from an attacker;
the configured limit is the repository default;
it does not rely on an active incident or a privileged mistake; and
quotation and execution happen in the same transaction.
Recommended Remediation
Calculate both maximums from the unused asset capacity and ensure maxDeposit reflects the zero-share guard.
For example:
This makes maxMint equal to:
which is the greatest share amount whose ceiling-rounded mint cost can fit the remaining assets.
Regression properties should assert:
Test these properties after direct deposits, reward forwarding, payouts, and every small remainder below the deposit limit.
Impact Details
The closest listed impact is Low - Contract fails to deliver promised returns, but doesn't lose value.
Affected Parties and Losses
The depositing user is the immediate affected party. A user or wallet that relies on the vault's maximum quote submits an operation that is guaranteed to fail in that state. They pay the failed transaction's gas and receive no stFXRP. Their approved FXRP principal is not lost.
ERC-4626 routers and aggregators may use the standardized maximum views to size vault entry. A revert at the Firelight call can unwind a complete atomic route, batch, rebalance, or multicall, including unrelated operations earlier in the same transaction.
Automated strategy operators cannot safely treat the reported limits as executable. They need Firelight-specific simulation, decrement-and-retry logic, or a conservative local calculation, weakening the interoperability the maximum functions are designed to provide.
Firelight and its existing stakers may miss a small final capital inflow and the prospective rewards associated with it until the caller works around the quote. The demonstrated boundary error is small and does not threaten vault solvency or existing balances.
No attacker needs to act and no attacker profits. The state arises from an ordinary deposit, an ordinary reward, and natural use of the configured cap. The PoC has no transaction-ordering dependency.
The direct realized loss is limited to the gas consumed by the reverted call. Any earlier operations in the same atomic transaction are reverted rather than economically lost, although the user or integration also loses the immediate opportunity to acquire the quoted stFXRP position.
Why the Selected Low Impact Applies
The Low category expressly contemplates a contract failing to deliver promised behavior without losing user value. Both elements are demonstrated here. The maximum functions return an explicit protocol result intended for machine consumption and state that a positive amount can enter the vault, but the corresponding operations cannot deliver the represented stFXRP position. At the same time, the caller's FXRP principal remains safe because execution reverts.
This is not merely a best-practice recommendation or documentation improvement. ERC-4626 normatively requires maxDeposit and maxMint not to overestimate the amounts their corresponding entry functions accept, and Firelight advertises ERC-4626 compatibility as the basis for integration with the wider DeFi ecosystem. The defect changes observable execution: a standards-based integration receives a positive limit and then reverts while consuming that limit in the unchanged state. Correcting it changes contract behavior rather than optimizing gas or improving explanatory documentation.
The issue also does not concern architectural decentralization or code optimization. No role distribution, governance model, or performance characteristic is implicated. The inconsistency is between public state-returning functions and the state-changing operations those values are defined to bound. It therefore does not fit any of the program's four Insight categories: Security best practices, Code Optimizations and Enhancements, Architectural decentralization, or Documentation Improvements.
The maxDeposit manifestation is stronger than a quote that is merely slightly too high. Its returned value is positive while every positive amount at or below it converts to zero shares and reverts. The actual executable maximum remains zero for as long as the same state persists; it becomes positive only if vault assets decrease, the deposit limit increases, or another state transition materially changes the exchange-rate/cap relationship. A caller cannot recover by subtracting one unit because that produces the prohibited zero-asset call.
Value is nevertheless preserved: the revert prevents any asset transfer or share minting from surviving. The user loses transaction gas and the immediate opportunity to enter at the quoted amount, but not their FXRP principal. This limited consequence is why the report selects Low rather than Medium griefing.
Likelihood and Severity Boundary
The likelihood is limited to states near the asset cap after assets have accrued without proportional share issuance. That is nevertheless a normal vault lifecycle: rewards are intentionally forwarded directly into the vault, and users can progressively fill the remaining capacity.
The issue should not be classified above Low because both state-changing calls revert atomically, no FXRP is retained, no shares are burned or frozen, and no attacker receives value. The maxMint caller can succeed by reducing the request to the true maximum; the maxDeposit caller must wait for a relevant state change because its actual executable maximum is zero.
The claimed impact is therefore restricted to failure to deliver the entry represented by the standardized return value, failed gas expenditure, delayed share acquisition, and composability failures. No theft, permanent freezing, or protocol insolvency is claimed.
References
Proof of Concept
Proof of Concept
Create:
The PoC is end-to-end for the vulnerable ERC-4626 entry path. It uses the real upgradeable FirelightVault and VaultRewardDistributor. The helper contract only represents a standard integration that consumes a maximum quote atomically. The local FAsset is mocked because it is an external dependency.
Run:
Expected output:
contracts/test/VaultMaxEntryCaller.sol:
test/poc_max_entry_quote_consistency.js:
Was this helpful?