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

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 enforces totalAssets() + 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() = 4

  • totalSupply() = 3

  • depositLimit = 10

Then:

  • maxMint() computes sharesLimit = floor(10 × (3+1) / (4+1)) = floor(40/5) = 8, so it returns 5

  • mint(5) computes assets = ceil(5 × (4+1) / (3+1)) = ceil(25/4) = 7

  • _depositFunds() sees 4 + 7 = 11 > 10 and reverts with DepositLimitExceeded()

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(...)) pattern

  • wasted 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.sol maxDeposit() and maxMint() implementation

  • FirelightVault.sol mint() and _depositFunds() implementation

  • FirelightVault.sol deposit() zero-share guard

  • scope.md in-scope categories for Low / Medium griefing / theft of gas

  • ERC-4626 standard requirements for maxDeposit / maxMint not 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

  1. 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.

  2. Call maxMint(receiver).

  3. Submit mint(maxMint(receiver), receiver).

  4. The call reverts with DepositLimitExceeded() even though maxMint() returned that share amount as safe.

Scenario 2: dust edge case on maxDeposit()

  1. Bring the vault into a high share-price state where the remaining asset headroom is 1 wei or similarly tiny.

  2. Call maxDeposit(receiver).

  3. Submit deposit(maxDeposit(receiver), receiver).

  4. 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?