89486 sc low incorrect rounding in maxmint causes mint maxmint to revert with depositlimitexceeded violating erc 4626 spec
Submitted on Aug 19th 2026 at 15:45:58 UTC by @omsanto for Audit Comp | Firelight
Report ID: #89486
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
FirelightVault.maxMint() uses Floor rounding to compute the maximum mintable shares, but mint() uses Ceil rounding to convert those shares back to assets. After yield distribution via VaultRewardDistributor (normal protocol operation), this rounding mismatch causes mint(maxMint()) to revert with DepositLimitExceeded, violating the ERC-4626 guarantee that mint() MUST NOT revert when called with the value returned by maxMint().
Vulnerability Details
maxMint() calls convertToShares(depositLimit) which uses Math.Rounding.Floor:
function convertToShares(uint256 assets) public view virtual returns (uint256) {
return _convertToShares(assets, Math.Rounding.Floor);
}
function _convertToShares(uint256 assets, Math.Rounding rounding) internal view virtual returns (uint256) {
return assets.mulDiv(totalSupply() + 10 ** _decimalsOffset(), totalAssets() + 1, rounding);
}
mint() converts those shares back to assets using Math.Rounding.Ceil inside _previewTotals():
(uint256 assets, ...) = _previewTotals(shares, false, Math.Rounding.Ceil);
_depositFunds() then checks:
if (_totalAssets > depositLimit) revert DepositLimitExceeded();
The problem is the rounding mismatch. maxMint() computes the share limit using Floor. mint() converts those same shares back to assets using Ceil. Ceil always produces a result equal to or greater than Floor. This means mint() can require 1 wei more assets than maxMint() assumed was safe, pushing totalAssets 1 wei above depositLimit and causing the revert.
This bug triggers whenever two normal ongoing protocol conditions exist simultaneously:
Condition 1 — totalAssets > totalSupply (share price above 1:1)
This happens every time VaultRewardDistributor.distributeRewards() is called, which is the normal mechanism for distributing yield to stakers. After every yield distribution, share price rises above 1:1. This is expected and intended protocol behavior.
Condition 2 — Vault is near its depositLimit
Admins routinely set depositLimit close to current TVL as a risk management measure. As the vault grows, the admin periodically raises the limit. At any point where the limit is set close to current totalAssets, this condition is met.
Both conditions are normal ongoing states of the protocol. They are not edge cases or attack setups, they describe the vault during routine operation. Whenever both conditions exist at the same time, any user calling the standard ERC-4626 pattern of mint(vault.maxMint(receiver)) will receive an unexpected revert.
Concrete example from passing Hardhat test:
- totalAssets = 1100 FXRP (1000 deposited by stakers + 100 yield distributed by VaultRewardDistributor)
- totalSupply = 1000 shares
- depositLimit = 1101 FXRP (only 1 FXRP of capacity remaining)
At this point the share price is 1.1 FXRP per share (1100/1000).
So with only 1 FXRP of remaining capacity, a user can mint approximately
0.909 shares (1 / 1.1 = 0.909). With 6 decimal precision this is 909091 raw share units.
maxMint() computes this using Floor rounding and returns 909091 shares advertising this as the safe maximum.
mint(909091) converts those shares back to FXRP using Ceil rounding:
assets needed = ceil(909091 * 1100000001 / 1000000001) = 1000001 raw units (1.000001 FXRP)
_depositFunds then checks:
totalAssets + assets = 1100000000 + 1000001 = 1101000001
depositLimit = 1101000000
1101000001 > 1101000000 → REVERT DepositLimitExceeded
The Floor rounding in maxMint() assumed the user needed 1000000 assets (1 FXRP exactly).
The Ceil rounding in mint() actually charged 1000001 assets (1.000001 FXRP).
That 1 wei difference causes the revert.Impact Details
No funds are lost, the transaction reverts. However the vault breaks the ERC-4626 guarantee that mint() MUST NOT revert when called with maxMint() value. Any protocol or user using the standard ERC-4626 integration pattern of mint(vault.maxMint(receiver)) will get an unexpected revert during normal protocol operation after yield distribution. The impact matches "Contract fails to deliver promised returns, but doesn't lose value."
References
ERC-4626 specification: https://eips.ethereum.org/EIPS/eip-4626
Relevant section : maxMint(): "MUST return the maximum amount of shares mint would allow to be deposited to receiver and not cause a revert, which MUST NOT be higher than the actual maximum that would be accepted."
FirelightVault intentionally deviates from ERC-4626 withdrawal semantics (documented), but the maxMint() violation is not listed as intentional.
Proof of Concept
Proof of Concept
Step-by-step reproduction:
Was this helpful?