The Fee AMM (Automated Market Maker) enables automatic conversion between different USD-denominated stablecoins during fee payment. When a user pays fees in one stablecoin and the validator prefers a different one, the Fee AMM converts between them seamlessly.
Overview
The Fee AMM is a specialized AMM deployed as a precompile at:
0xfeeC000000000000000000000000000000000000 (FeeManager contract)
It provides:
Automatic conversion between USD stablecoins during fee payment
Directional pools optimized for one-way flow (user token → validator token)
Simple linear pricing with a 0.3% conversion spread
Single-sided liquidity provision (validator token only)
Rebalancing mechanism for arbitrage and liquidity management
Pool Design
Directional Pools
Each (userToken, validatorToken) pair has its own directional pool :
poolId = keccak256 ( abi . encode (userToken, validatorToken))
Key properties:
Ordered pairs : (USDC → pathUSD) is a different pool than (pathUSD → USDC)
Independent reserves : Each direction maintains separate reserves
Optimized for one-way flow : User → validator conversions during fee payment
Example : If users pay in USDC and validators want pathUSD:
Pool: (USDC → pathUSD)
User Token Reserve: 1,000,000 USDC (grows as users pay fees)
Validator Token Reserve: 500,000 pathUSD (shrinks as fees are converted)
The reverse pool (pathUSD → USDC) would have different reserves and serve a different purpose.
Pool Structure
struct Pool {
uint128 reserveUserToken; // Accumulated user token (from fee payments)
uint128 reserveValidatorToken; // Validator token (for conversion)
}
See FeeAMM.sol:10.
Conversion Pricing
The Fee AMM uses a simple linear pricing model for fee conversions:
M = 0.9970 (scaled as 9970 / 10000 )
N = 0.9985 (scaled as 9985 / 10000 )
See FeeAMM.sol:12-13.
Fee Swap (User → Validator)
During fee payment, the protocol calls executeFeeSwap:
amountOut = amountIn × M
= amountIn × 0.9970
This provides a 0.3% conversion spread (1 - 0.9970 = 0.003).
Example :
User pays: 100,000 USDC
AmountOut: 100,000 × 0.9970 = 99,700 pathUSD
Validator receives: 99,700 pathUSD
Spread: 300 USDC (0.3%)
The spread compensates liquidity providers for the directional flow risk.
See FeeAMM.sol:65-85.
Rebalance Swap (Validator → User)
Liquidity providers or arbitrageurs can call rebalanceSwap to buy accumulated user tokens:
amountIn = (amountOut × N / SCALE) + 1
= (amountOut × 0.9985 / 1.0000 ) + 1
Rounding up ensures the pool doesn’t lose value.
Example :
Arbitrageur wants: 100,000 USDC (from accumulated user tokens)
AmountIn required: (100,000 × 9985 / 10000) + 1 = 99,851 pathUSD
Spread: 149 USDC (0.15%)
The tighter spread (0.15% vs 0.3%) incentivizes rebalancing activity.
See FeeAMM.sol:87-125.
Liquidity Provision
The Fee AMM supports single-sided liquidity provision with validator tokens only.
Adding Liquidity
Liquidity providers call mint to add validator tokens:
function mint (
address userToken ,
address validatorToken ,
uint256 amountValidatorToken ,
address to
) external returns ( uint256 liquidity )
See FeeAMM.sol:127-182.
First liquidity provider :
liquidity = amountValidatorToken / 2 - MIN_LIQUIDITY
where :
MIN_LIQUIDITY = 1 , 000 (permanently locked)
The minimum liquidity lock prevents certain edge cases and ensures pools always have reserves.
Example (initial deposit) :
Deposit: 1,000,000 pathUSD
Liquidity minted: (1,000,000 / 2) - 1,000 = 499,000 LP tokens
Locked: 1,000 LP tokens (permanently)
Subsequent providers :
The formula accounts for accumulated user tokens from fee payments:
liquidity = amountValidatorToken × totalSupply / (V + n × U)
where :
V = current validator token reserve
U = current user token reserve
n = N / SCALE = 0.9985 (rebalance conversion rate)
totalSupply = total LP tokens outstanding
This ensures LPs receive shares proportional to the pool’s total value, including both validator tokens and accumulated user tokens (valued at the rebalance rate).
See FeeAMM.sol:156-162.
Removing Liquidity
LPs call burn to redeem their share:
function burn (
address userToken ,
address validatorToken ,
uint256 liquidity ,
address to
) external returns (
uint256 amountUserToken ,
uint256 amountValidatorToken
)
LPs receive a pro-rata share of both reserves :
amountUserToken = liquidity × pool.reserveUserToken / totalSupply
amountValidatorToken = liquidity × pool.reserveValidatorToken / totalSupply
This means LPs receive:
Their share of validator tokens (what they deposited)
Their share of user tokens (accumulated from fee swaps)
See FeeAMM.sol:184-232.
Pool Lifecycle Example
Consider a (USDC → pathUSD) pool:
1. Initial Liquidity
LP deposits: 1,000,000 pathUSD
Pool state:
reserveUserToken: 0 USDC
reserveValidatorToken: 1,000,000 pathUSD
totalSupply: 499,000 LP tokens
locked: 1,000 LP tokens
2. Fee Conversions
Users pay fees in USDC, protocol converts to pathUSD:
User pays: 10,000 USDC
Protocol calls: executeFeeSwap(USDC, pathUSD, 10,000)
AmountOut: 10,000 × 0.9970 = 9,970 pathUSD
Pool state:
reserveUserToken: 10,000 USDC (accumulated)
reserveValidatorToken: 990,030 pathUSD (depleted)
After many fee payments:
Pool state:
reserveUserToken: 1,000,000 USDC (highly accumulated)
reserveValidatorToken: 30,000 pathUSD (nearly depleted)
If reserveValidatorToken falls below the required conversion amount, executeFeeSwap reverts with InsufficientLiquidity. Users must either:
Switch to a fee token with sufficient liquidity
Wait for rebalancing
Provide liquidity themselves
3. Rebalancing
An arbitrageur notices the imbalance and calls rebalanceSwap:
Arbitrageur calls: rebalanceSwap(USDC, pathUSD, 500,000, recipient)
AmountIn required: (500,000 × 0.9985) + 1 = 499,251 pathUSD
Pool state:
reserveUserToken: 500,000 USDC (rebalanced)
reserveValidatorToken: 529,251 pathUSD (replenished)
The arbitrageur receives 500,000 USDC for 499,251 pathUSD, earning approximately 749 USDC (0.15% spread) by helping rebalance the pool.
4. LP Withdrawal
The original LP decides to withdraw:
LP burns: 499,000 LP tokens (all their shares)
Amounts returned:
USDC: 500,000 × 499,000 / 500,000 = 499,000 USDC
pathUSD: 529,251 × 499,000 / 500,000 = 528,195 pathUSD
Total value: ~1,027,195 USD (deposited 1,000,000)
Profit: ~27,195 USD (2.7% gain from fee conversion spreads)
The LP profits from the 0.3% spread on all fee conversions that flowed through their pool.
Liquidity Incentives
Liquidity providers earn returns from:
Conversion spreads (0.3% on all user → validator swaps)
Accumulated user tokens (captured when LPs withdraw)
Potential stablecoin basis (if user token trades at premium)
Key risks:
Directional flow : Pools naturally accumulate user tokens and deplete validator tokens
Stablecoin depeg : If user token depegs below $1, LPs hold devalued assets
Liquidity depletion : If validator token reserve empties, no fees can be converted
Validators have a natural incentive to provide liquidity to their own preferred tokens, as this enables users to pay fees in any stablecoin while the validator receives their preferred one.
Rebalancing Mechanisms
Manual Rebalancing
Anyone can call rebalanceSwap to trade validator tokens for accumulated user tokens:
function rebalanceSwap (
address userToken ,
address validatorToken ,
uint256 amountOut ,
address to
) external returns ( uint256 amountIn )
Use cases :
Arbitrage : Buy user tokens at 0.15% discount, sell on external market
Validator management : Validators convert their accumulated user tokens
LP rebalancing : LPs manage their pool positions
See FeeAMM.sol:87-125.
Market-Driven Rebalancing
The pricing spread incentivizes natural rebalancing:
Fee swap spread: 0.3% (user pays premium)
Rebalance spread: 0.15% (rebalancer gets discount)
Net LP profit: 0.15% per round-trip
This creates a profitable arbitrage opportunity that naturally balances pools.
Required Token Checks
All Fee AMM operations require tokens to be:
Valid TIP-20 tokens (deployed via TIP20Factory)
USD-denominated (currency() == "USD")
The AMM validates this using:
function _requireUSDTIP20 ( address token ) internal view {
if ( ! TempoUtilities. isTIP20 (token)) revert InvalidToken ();
if ( keccak256 ( bytes ( ITIP20 (token). currency ())) != keccak256 ( bytes ( "USD" ))) {
revert InvalidCurrency ();
}
}
See FeeAMM.sol:30-36.
This ensures only USD stablecoins participate in fee conversion, maintaining the dollar-denominated fee model.
Pool Discovery
Get Pool ID
function getPoolId (
address userToken ,
address validatorToken
) public pure returns ( bytes32 )
See FeeAMM.sol:38-41.
Get Pool State
function getPool (
address userToken ,
address validatorToken
) external view returns ( Pool memory )
Returns the current reserves for the directional pool.
See FeeAMM.sol:43-50.
Check Liquidity
Before attempting a fee conversion, the protocol checks liquidity:
function checkSufficientLiquidity (
address userToken ,
address validatorToken ,
uint256 maxAmount
) internal view
This is called during transaction validation to ensure the pool can handle the maximum possible fee.
See FeeAMM.sol:52-63.
Integration with FeeManager
The Fee AMM is tightly integrated with the FeeManager precompile:
Pre-Transaction Check
// In collectFeePreTx
if (userToken != validatorToken) {
checkSufficientLiquidity (userToken, validatorToken, maxAmount);
}
See FeeManager.sol:57-59.
Post-Transaction Conversion
// In collectFeePostTx
if (userToken != validatorToken && actualUsed > 0 ) {
uint256 amountOut = executeFeeSwap (userToken, validatorToken, actualUsed);
collectedFees[feeRecipient][validatorToken] += amountOut;
}
See FeeManager.sol:81-83.
Gas Costs
Fee AMM operations have the following approximate gas costs:
Operation Gas Cost executeFeeSwap ~15,000 rebalanceSwap ~45,000 mint (first LP) ~180,000 mint (subsequent) ~90,000 burn ~65,000
These costs are automatically included in the transaction gas when fee conversion is required.
Usage Examples
Providing Liquidity
import { IFeeAMM } from "./interfaces/IFeeAMM.sol" ;
import { IERC20 } from "./interfaces/IERC20.sol" ;
contract LiquidityProvider {
IFeeAMM constant FEE_AMM = IFeeAMM ( 0xfeeC000000000000000000000000000000000000 );
function provideLiquidity (
address userToken , // e.g., USDC
address validatorToken , // e.g., pathUSD
uint256 amount
) external {
// Approve the FeeAMM to spend validator tokens
IERC20 (validatorToken). approve ( address (FEE_AMM), amount);
// Add liquidity (single-sided, validator token only)
uint256 liquidity = FEE_AMM. mint (
userToken,
validatorToken,
amount,
msg.sender // LP tokens sent to caller
);
}
}
Rebalancing
contract Arbitrageur {
IFeeAMM constant FEE_AMM = IFeeAMM ( 0xfeeC000000000000000000000000000000000000 );
function rebalancePool (
address userToken ,
address validatorToken ,
uint256 desiredUserTokens
) external {
// Calculate required validator tokens
uint256 amountIn = (desiredUserTokens * 9985 ) / 10000 + 1 ;
// Approve validator token
IERC20 (validatorToken). approve ( address (FEE_AMM), amountIn);
// Execute rebalance swap
uint256 actualAmountIn = FEE_AMM. rebalanceSwap (
userToken,
validatorToken,
desiredUserTokens,
msg.sender // User tokens sent to caller
);
}
}
Security Considerations
Integer Overflow Protection
All reserve amounts are stored as uint128, with explicit checks:
function _requireU128 ( uint256 x ) internal pure {
if (x > type ( uint128 ).max) revert InvalidAmount ();
}
See FeeAMM.sol:24-27.
Reentrancy Protection
The Fee AMM uses the checks-effects-interactions pattern and updates reserves before external calls:
// Update reserves first
pool.reserveValidatorToken += uint128 (amountIn);
pool.reserveUserToken -= uint128 (amountOut);
// Then transfer tokens
ITIP20 (validatorToken). systemTransferFrom ( msg.sender , address ( this ), amountIn);
IERC20 (userToken). transfer (to, amountOut);
See FeeAMM.sol:117-122.
Permanent Liquidity Lock
The first LP permanently locks MIN_LIQUIDITY = 1,000 LP tokens, preventing:
Complete drainage of the pool
Precision loss attacks
Division by zero in share calculations
See FeeAMM.sol:15 and FeeAMM.sol:153-154.
Next Steps
Stablecoin Fees Learn how to pay gas fees in USD stablecoins
Gas Pricing Understand attodollar denomination and cost calculations
Fee System Overview High-level overview of Tempo’s fee system