88234 sc low maxmint and maxdeposit advertise amounts that revert eip 4626 violation
Submitted on Aug 12th 2026 at 10:22:49 UTC by @shadowHunter for Audit Comp | Firelight
Report ID: #88234
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
EIP-4626 is explicit that the maximum functions must return values the corresponding action will accept:
maxMint— "MUST return the maximum amount of sharesmintwould allow to be deposited toreceiverand not cause a revert."maxDeposit— "MUST return the maximum amount of underlying assets that can be deposited into the Vault for thereceiver, through adepositcall, and not cause a revert."
FirelightVault violates both. The vault is marketed as ERC-4626 compatible in README.md and its interface inherits IERC4626 (contracts/core/interfaces/IFirelightVault.sol:22), so the full ERC-4626 selector set is published in the ABI that integrators compile against.
Defect 1 — maxMint derives a share ceiling from the whole deposit limit
contracts/core/FirelightVault.sol:385-393
function maxMint(address receiver) public view override returns (uint256 amount) {
uint256 shares = totalSupply();
uint256 sharesLimit = convertToShares(depositLimit); // whole limit, floored
if (isBlocklisted[receiver] || paused() || _hasActiveIncident() || shares > sharesLimit) {
return 0;
} else {
return sharesLimit - shares;
}
}mint, however, enforces an asset ceiling on the remaining headroom:
contracts/core/FirelightVault.sol:976-991 (_depositFunds)
convertToShares floors while previewMint ceils, so once the share price exceeds 1 the two disagree and maxMint over-reports.
Worked example with totalSupply S = 100, totalAssets A = 101, depositLimit D = 102 (_decimalsOffset() is not overridden, so the virtual offset is 1):
The condition A > S is the steady state after the first reward distribution, because VaultRewardDistributor.distributeRewards forwards assets into the vault without minting shares (contracts/core/VaultRewardDistributor.sol:146). The share price is therefore permanently above 1 in normal operation.
Defect 2 — maxDeposit ignores the zero-share guard
contracts/core/FirelightVault.sol:371-378
It returns the remaining asset headroom without checking that the headroom is large enough to mint at least one share. deposit rejects exactly that case:
contracts/core/FirelightVault.sol:667-669
With an empty vault, a 10-asset donation and depositLimit = 11: maxDeposit returns 1, but previewDeposit(1) is 0 and deposit(1) reverts.
Note on prior review
This code was introduced by commit 29cfa99 ("Fix 4626 max methods implementation"), which was the remediation for the OpenZeppelin client-reported issue CR-01 ("Limit Functions Not Overridden to Enforce Deposit/Withdraw Restrictions"). CR-01 asked only that the maximum functions reflect deposit limits, blocklist status and pause state — it did not concern rounding consistency. The defect is therefore a genuine unintended bug, though it sits in previously reviewed code.
Steps to reproduce
Case 1 — maxMint
1. A user deposits 100 assets into an empty vault, receiving 100 shares at the 1:1 bootstrap rate. depositLimit is 102.
2. One asset reaches the vault without minting shares. In production this is a reward distribution (contracts/core/VaultRewardDistributor.sol:146); in the PoC it is a direct transfer, which is equivalent from the vault's point of view because totalAssets() is balance-derived.
3. The integrator reads the advertised maximum.
4. The integrator performs the canonical ERC-4626 call and it reverts.
Case 2 — maxDeposit
1. An empty vault has depositLimit = 11, and 10 assets are donated to it.
2. The integrator reads the advertised maximum.
3. The canonical call reverts.
Impact
No principal is lost — both transactions revert with no state change. The impact is purely that the contract does not deliver the API behaviour it advertises.
Any router, zapper, vault aggregator, or front-end "Max" button that follows the canonical ERC-4626 pattern
vault.mint(vault.maxMint(user), user)orvault.deposit(vault.maxDeposit(user), user)will submit a guaranteed-to-fail transaction, wasting the user's gas.The failures look non-deterministic from the integrator's side, because whether they occur depends on the flooring of
depositLimit * (S + 1) / (A + 1)— so they are hard to diagnose and will present as intermittent bugs in the integrating protocol.The
maxMintcase is not an edge condition that resolves itself: the share price sits permanently above 1 after the first reward distribution, so the divergence is the normal operating state rather than a transient one.Integrations that automate deposits (yield routers, auto-compounders) may retry indefinitely against a maximum that can never be executed.
The repository's own tests assert the intended invariant maxMint == previewDeposit(maxDeposit) (test/deposit_and_withdraw.js:41-47, :61-67, :84-90), but only at a 1:1 share price, where the two formulas coincide — which is why the divergence is untested.
Recommendation
Derive maxMint from the remaining asset headroom rather than from the whole limit, so it is consistent with the ceiling mint actually enforces:
And return zero from maxDeposit when the remaining headroom cannot mint at least one share:
Extend the existing limit tests to run at a share price above 1 (for example after a simulated reward distribution), so the two formulas are exercised where they diverge.
Proof of Concept
Proof of Concept
Environment
Create hardhat.poc.config.js in the repository root
Save the PoC as test/poc_low_erc4626_limits.js
Run
Output
What the output proves
Both cases use the real FirelightVault behind a transparent proxy.
Case 1 — maxMint over-reports. The state assertions pin the exact scenario: totalSupply() == 100, totalAssets() == 101 (share price above 1), depositLimit == 102.
maxMint(user) == 1— the vault advertises that one share can be minted.previewMint(1) == 2— but minting that one share costs two assets. The vault's own two views already contradict each other.mint(1, user)reverts — the advertised maximum is not executable, because101 + 2 = 103exceeds the limit of102atFirelightVault.sol:987.
This is a direct violation of the EIP-4626 maxMint MUST clause.
Case 2 — maxDeposit over-reports.
maxDeposit(user) == 1— one asset of headroom is advertised.previewDeposit(1) == 0— depositing it would mint zero shares.deposit(1, user)reverts on theshares == 0guard atFirelightVault.sol:669.
In both cases the transaction reverts with no state change and no principal at risk, which is why this is reported as Low: the harm is a guaranteed-failing transaction and wasted gas for any integrator that trusts the standard interface.
Was this helpful?