88295 sc low maxmint overstates the mintable amount and mint maxmint x x can revert erc 4626 conformance break near the deposit cap
Submitted on Aug 12th 2026 at 16:43:51 UTC by @Oxb4b for Audit Comp | Firelight
Report ID: #88295
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:
Griefing (e.g. no profit motive for an attacker, but damage to the users or the protocol)
Description
Brief/Intro
FirelightVault.sol exposes ERC-4626-style maxDeposit() and maxMint() bounds, but maxMint() is not round-trip safe with mint(). In certain valid vault states, maxMint(receiver) returns a share amount that mint(maxMint(receiver), receiver) rejects with DepositLimitExceeded(). This is a spec conformance issue and an integration nuisance, not a fund-loss bug.
Vulnerability Details
maxMint() is implemented by converting the asset deposit limit to shares with floor rounding and subtracting current supply:
sharesLimit = convertToShares(depositLimit)return
sharesLimit - totalSupply()
mint() uses a different path:
it converts requested shares back to assets with ceil rounding via
_previewTotals(..., Math.Rounding.Ceil)_depositFunds()then enforcestotalAssets() + assets <= depositLimit
Because the bound is computed with floor rounding in one direction and mint execution uses ceil rounding in the opposite direction, the result can be off by 1 asset unit. That is enough to make mint(maxMint(x), x) revert even though maxMint() claimed the call was valid.
A concrete counterexample exists with _decimalsOffset() == 0:
totalAssets() = 4totalSupply() = 3depositLimit = 10
Then:
maxMint()computessharesLimit = floor(10 × (3+1) / (4+1)) = floor(40/5) = 8, so it returns5mint(5)computesassets = ceil(5 × (4+1) / (3+1)) = ceil(25/4) = 7_depositFunds()sees4 + 7 = 11 > 10and reverts withDepositLimitExceeded()
A related dust-edge case exists for maxDeposit(): it returns the remaining asset headroom, but if that headroom floors to zero shares, deposit(maxDeposit(...)) reverts on the intentional zero-share guard with InvalidAmount().
ERC‑4626 requires that maxMint/maxDeposit “MUST NOT be higher than the actual maximum that would be accepted (it should underestimate if necessary).” In the current implementation, mint(maxMint(receiver), receiver) and deposit(maxDeposit(receiver), receiver) can revert due to rounding‑induced overstatement, violating this requirement.
Impact and Likelihood Details
This does not steal funds or corrupt accounting. The vault fails closed before taking assets, so the impact is limited to:
broken ERC-4626 conformance
failed integrations for routers, zappers, aggregators, and vault-of-vaults that rely on the canonical
mint(maxMint(...))/deposit(maxDeposit(...))patternwasted gas on reverted transactions
This creates a griefing surface: repeated reverts and wasted gas can occur for integrators and users that rely on mint(maxMint(...)) / deposit(maxDeposit(...)) when the vault is near its cap, even without any attacker activity.
Likelihood is medium as a nuisance. The state is realistic because the vault’s asset/share ratio changes through ordinary deposits, rewards, and payout-related asset movements. It is not a governance bug and does not require malicious use of privileged roles.
References
FirelightVault.solmaxDeposit()andmaxMint()implementationFirelightVault.solmint()and_depositFunds()implementationFirelightVault.soldeposit()zero-share guardscope.mdin-scope categories for Low / Medium griefing / theft of gasERC-4626 standard requirements for
maxDeposit/maxMintnot to overstate the safe amount
Proof of Concept
Proof of Concept
add the below testcase to the existing test file test/deposit_and_withdraw.js and run
Scenario 1: mint(maxMint(...)) reverts near the cap
Bring the vault to a state where total assets and total supply are not in a clean ratio and the vault is close to
depositLimit.Call
maxMint(receiver).Submit
mint(maxMint(receiver), receiver).The call reverts with
DepositLimitExceeded()even thoughmaxMint()returned that share amount as safe.
Scenario 2: dust edge case on maxDeposit()
Bring the vault into a high share-price state where the remaining asset headroom is 1 wei or similarly tiny.
Call
maxDeposit(receiver).Submit
deposit(maxDeposit(receiver), receiver).The call reverts with
InvalidAmount()because the deposit floors to zero shares.
Reproduction note
The included deposit_and_withdraw.js testcase demonstrates both behaviors with concrete numbers and a deterministic state progression. It does not invent the bug; it reproduces a reachable edge state that causes the mismatch.
Was this helpful?