FPMM
The FPMM contract is the core pool of Mento V3. Each instance holds two ERC-20 tokens (reserves), executes swaps at the oracle rate (minus fees), and allows rebalancing only by allowlisted liquidity strategies when the reserve-implied price deviates from the oracle by at least a configured threshold.
Contract: mento-protocol/mento-core — contracts/swap/FPMM.sol
Interacting with the pool (code examples)
The snippets below show how to use the FPMM from Solidity. You need the pool address (from the factory or deployments) and the token addresses (pool uses sorted order: token0 < token1). For most app flows, the Router or the Mento SDK simplify quoting and execution.
Finding the pool
Get the pool for a token pair from the factory (tokens are sorted by address):
IFPMMFactory factory = IFPMMFactory(factoryAddress);
(address token0, address token1) = factory.sortTokens(usdc, usdm);
address pool = factory.getPool(token0, token1);
// pool == address(0) if not deployed yetQuoting a swap
Get the expected amount of output token for a given amount in. The quote uses the oracle rate minus fees. Reverts if the oracle is invalid (e.g. market closed, breaker tripped, stale).
IFPMM pool = IFPMM(poolAddress);
uint256 amountIn = 1_000_000; // 1 USDC (6 decimals)
uint256 amountOut = pool.getAmountOut(amountIn, usdc);
// amountOut is in output token decimals (e.g. USDm)Executing a swap (via Router)
The Router handles token transfers and calls the pool’s swap with the correct output amount. Approve the router to spend amountIn of the input token, then:
Executing a swap (direct pool call)
The pool’s swap(amount0Out, amount1Out, to, data) is output-specified. The caller must ensure the pool receives the input tokens (e.g. by implementing IFPMMCallee and passing data so the pool calls back into your contract, which then transfers the input tokens). For simple EOA flows, use the Router instead.
Checking rebalance state
Useful for keepers or UIs to see if the pool is eligible for rebalance and the current deviation:
Adding and removing liquidity
Mint: transfer both tokens to the pool in the current reserve ratio, then call mint(to). Burn: transfer LP tokens to the pool, then call burn(to).
Invariants (design)
The contract enforces:
Swap does not decrease the total value of the pool (value at oracle, after crediting fees).
Rebalance reduces the price difference and keeps the same direction (above oracle stays above, below stays below).
Rebalance keeps the post-rebalance price difference at or above the configured threshold (no overshoot past the band).
Rebalance does not decrease reserve value more than the rebalance incentive (minimum repayment / incentive cap).
State (FPMMStorage)
token0, token1
Pool tokens, ordered by address (token0 < token1).
decimals0, decimals1
Scaling: 10^decimals for each token (capped at 18).
reserve0, reserve1
Last known reserve balances (updated after swap/mint/burn/rebalance).
oracleAdapter
Contract used for the oracle rate; pool calls getFXRateIfValid(referenceRateFeedID).
referenceRateFeedID
Rate feed ID passed to the oracle adapter (e.g. address derived from pair identifier).
invertRateFeed
If true, the adapter rate is inverted (denominator/numerator) for quoting.
lpFee, protocolFee
Swap fees in basis points (BPS, denominator 10,000). Combined max 200 (2%).
protocolFeeRecipient
Receives protocol fee.
feeSetter
Address (with owner) allowed to set fees.
rebalanceIncentive
Max incentive in BPS (max 100 = 1%) for rebalance; enforces minimum token-in.
rebalanceThresholdAbove, rebalanceThresholdBelow
Min price difference (BPS) to allow rebalance when reserve price > oracle or < oracle. Above max 10,000 bps; below max 5,000 bps.
liquidityStrategy[addr]
If true, addr may call rebalance.
tradingLimits[token]
Per-token TradingLimitsV2 config and state (5-min and 1-day net-flow caps).
Initialization
initialize(token0, token1, oracleAdapter, referenceRateFeedID, invertRateFeed, initialOwner, params) sets tokens, oracle adapter, rate feed ID, invert flag, owner, and FPMMParams (fees, fee recipient, fee setter, rebalance incentive, rebalance thresholds). Tokens are not re-sorted in initialize; the factory passes them already sorted. Token decimals must be ≤ 18.
Oracle rate
The pool uses oracle rate for all swap quotes and value checks. It does not use a reserve-based curve.
Source:
_getRateFeed()callsoracleAdapter.getFXRateIfValid(referenceRateFeedID). If the adapter reverts (e.g.FXMarketClosed,TradingSuspended,NoRecentRate), the pool reverts.Invert: If
invertRateFeedis true, the rate is swapped to (denominator, numerator) so that the pool’s quote direction matches the intended token pair (e.g. token1/token0).
Swap
Quote:
getAmountOut(amountIn, tokenIn)returns the amount of the other token at the oracle rate minus combined fee:rate × amountIn × (BPS - lpFee - protocolFee) / BPS, with decimal scaling. Reverts if the adapter’sgetFXRateIfValidfails.Execution:
swap(amount0Out, amount1Out, to, data)is output-specified: exactly one ofamount0Outoramount1Outmust be > 0. The pool transfers the output toto, then, ifdata.length > 0, callsIFPMMCallee(to).hook(...)so the caller can supply the input tokens. The pool infersamount0Inandamount1Infrom balance deltas.Checks:
Value protection:
_swapCheck: reserve value at oracle (in token1, 1e18-scaled) after the swap must be ≥ initial value + fee value; otherwiseReserveValueDecreased.TradingLimitsV2: After the swap,
_applyTradingLimitsis called for each token (fee is deducted from inflow before net flow). If a limit is exceeded, the swap reverts.
Fees: Protocol fee is transferred to
protocolFeeRecipient; LP fee remains in the pool (increases reserve value). Reserves are synced with_update().
Mint and burn
Mint:
mint(to)— Caller must have transferred token0 and token1 to the pool in the current reserve ratio. Liquidity is computed: first mintsqrt(amount0*amount1) - MINIMUM_LIQUIDITY(withMINIMUM_LIQUIDITYlocked ataddress(1)); subsequent mints proportional to reserves. Shares are minted toto.Burn:
burn(to)— Caller must have transferred LP tokens to the pool. Burn is pro-rata: amounts out = (liquidity / totalSupply) × reserves; tokens sent toto, reserves updated.
Rebalance
Entrypoint:
rebalance(amount0Out, amount1Out, data). Only callable by an address withliquidityStrategy[msg.sender] == true.Preconditions:
Exactly one of
amount0Out,amount1Out> 0.Reserve price deviation from oracle ≥ threshold (above or below depending on direction).
Pool has sufficient liquidity for the requested output.
Flow:
Pool transfers the requested token(s) to
msg.sender(the strategy).Pool calls
ILiquidityStrategy(msg.sender).onRebalance(msg.sender, amount0Out, amount1Out, data).Strategy must send back the other token (and optionally keep an incentive within the cap).
Pool infers
amount0In,amount1Infrom balance deltas._rebalanceCheck(swapData)enforces: price difference improves, direction preserved, no overshoot (new difference ≥ threshold), and minimum repayment (incentive cap).
Rebalancing state:
getRebalancingState()returns oracle price, reserve price, whether reserve price is above oracle, the applicable threshold, and current price difference in BPS. Off-chain or strategy logic can use this to decide rebalance size.
TradingLimitsV2
Config:
configureTradingLimit(token, config)(owner) sets the TradingLimitsV2 config for that token (e.g. 5-min and 1-day limits).Application: After every swap,
_applyTradingLimits(token0, ...)and_applyTradingLimits(token1, ...)update state and revert if a limit is exceeded. Fee is deducted from inflow before net flow.View:
getTradingLimits(token)returns config and state for the token.
Admin
setLPFee, setProtocolFee
onlyFeeSetter
Combined with the other fee ≤ 200 bps.
setProtocolFeeRecipient
owner
setFeeSetter
owner
setRebalanceIncentive
onlyFeeSetter
Max 100 bps.
setRebalanceThresholds(above, below)
owner
Above ≤ 10,000 bps; below ≤ 5,000 bps.
setOracleAdapter, setReferenceRateFeedID, setInvertRateFeed
owner
setLiquidityStrategy(addr, allowed)
owner
Allowlist for rebalance.
configureTradingLimit(token, config)
owner
TradingLimitsV2 per token.
Key errors
ReserveValueDecreased— Swap would decrease pool value at oracle after fees.NotLiquidityStrategy— Caller ofrebalancenot allowlisted.PriceDifferenceTooSmall— Rebalance called but deviation below threshold.PriceDifferenceNotImproved,PriceDifferenceMovedInWrongDirection,PriceDifferenceMovedTooFarFromThresholds— Rebalance acceptance failed.InsufficientAmount0In/InsufficientAmount1In— Strategy did not return enough of the other token (incentive cap).TradingSuspended,NoRecentRate,FXMarketClosed— From oracle adapter when rate is invalid.
See also
FPMMFactory — Deployment and default params.
OracleAdapter — How the pool gets the rate.
TradingLimitsV2 — Net-flow caps.
Liquidity strategies — Who can call rebalance and how.
Dive Deeper: FPMMs — Design and invariant.
Last updated