Fee AMM Specification
Abstract
This specification defines a system of one-way Automated Market Makers (AMMs) designed to facilitate gas fee payments from a user using one stablecoin (the userToken) to a validator who prefers a different stablecoin (the validatorToken). Each AMM handles fee swaps from a userToken to a validatorToken at one price (0.9970 validatorToken per userToken), and allows rebalancing in the other direction at another fixed price (0.9985 validatorToken per userToken).
Motivation
Current blockchain fee systems typically require users to hold native tokens for gas payments. This creates friction for users who prefer to transact in stablecoins.
The Fee AMM is a dedicated AMM for trading between stablecoins, which can only be used by the protocol (and by arbitrageurs rebalancing it to keep it balanced). The protocol automatically collects fees in many different coins during the block, and then sells them all at the end of the block (paying a constant price) into the token preferred by the validator.
The system is designed to minimize several forms of MEV:
- No Probabilistic MEV: The fixed fee swap rate and batch settlement prevent profitable backrunning of fee swaps. There is no way to profitably spam the chain with transactions hoping an opportunity might arise.
- No Sandwich Attacks: Fee swaps execute at a fixed rate and settle atomically at block end, eliminating sandwich attack vectors.
- Top-of-Block Auction: The main MEV in the AMM (from rebalancing) occurs as a single race at the top of the next block rather than creating probabilistic spam throughout.
Specification
Overview
The Fee AMM implements two distinct swap mechanisms:
- Fee Swaps: Fixed-rate swaps at a price of
0.9970(validator token per user token) fromuserTokentovalidatorToken - Rebalancing Swaps: Fixed-rate swaps at a price of
0.9985(validator token per user token) fromvalidatorTokentouserToken
Core Components
1. FeeAMM Contract
The primary AMM contract managing liquidity pools and swap operations.
Pool Structure
struct Pool {
uint128 reserveUserToken; // Reserve of userToken
uint128 reserveValidatorToken; // Reserve of validatorToken
}Each pool is directional: userToken → validatorToken. For a pair of tokens A and B, there are two separate pools:
- Pool(A, B): for swapping A to B at fixed rate of 0.997 (fee swaps) and B to A at fixed rate of 0.9985 (rebalancing)
- Pool(B, A): for swapping B to A at fixed rate of 0.997 (fee swaps) and A to B at fixed rate of 0.9985 (rebalancing)
Constants
M = 9970(scaled by 10000, representing 0.9970)N = 9985(scaled by 10000, representing 0.9985)SCALE = 10000MIN_LIQUIDITY = 1000
Key Functions
function getPool(
address userToken,
address validatorToken
) external view returns (Pool memory)Returns the pool structure for a given token pair.
function getPoolId(
address userToken,
address validatorToken
) external pure returns (bytes32)Returns the pool ID for a given token pair (used internally for pool lookup).
function rebalanceSwap(
address userToken,
address validatorToken,
uint256 amountOut,
address to
) external returns (uint256 amountIn)Executes rebalancing swaps from validatorToken to userToken at fixed rate of 0.9985 (validator token per user token). Can be executed by anyone. Calculates amountIn = (amountOut * N) / SCALE + 1 (rounds up). Updates reserves immediately. Emits RebalanceSwap event.
function mint(
address userToken,
address validatorToken,
uint256 amountUserToken,
uint256 amountValidatorToken,
address to
) external returns (uint256 liquidity)Adds liquidity to a pool with both tokens. First provider sets initial reserves and must burn MIN_LIQUIDITY tokens. Subsequent providers must provide proportional amounts. Receives fungible LP tokens representing pro-rata share of pool reserves.
function mintWithValidatorToken(
address userToken,
address validatorToken,
uint256 amountValidatorToken,
address to
) external returns (uint256 liquidity)Single-sided liquidity provision with validator token only. Treats the deposit as equivalent to performing a hypothetical rebalanceSwap first at rate n = 0.9985 until the ratio of reserves match, then minting liquidity by depositing both. Formula: liquidity = amountValidatorToken * _totalSupply / (V + n * U), where n = N / SCALE. Rounds down to avoid over-issuing LP tokens. Updates reserves by increasing only validatorToken by amountValidatorToken. Emits Mint event with amountUserToken = 0.
function burn(
address userToken,
address validatorToken,
uint256 liquidity,
address to
) external returns (uint256 amountUserToken, uint256 amountValidatorToken)Burns LP tokens and receives pro-rata share of reserves. Reverts if withdrawal would prevent pending swaps at the end of the block. Emits Burn event.
function executePendingFeeSwaps(
address userToken,
address validatorToken
) internal returns (uint256 amountOut)Settles all pending fee swaps by updating reserves. Calculates amountOut = (amountIn * M) / SCALE. Only executed by the protocol, at the end of each block. Emits FeeSwap event.
function reserveLiquidity(
address userToken,
address validatorToken,
uint256 maxAmount
) internal returns (bool)Reserves liquidity for a pending fee swap. Calculates maxAmountOut = (maxAmount * M) / SCALE. Verifies sufficient validator token reserves (accounting for pending swaps). Tracks pending swap input.
function releaseLiquidityPostTx(
address userToken,
address validatorToken,
uint256 refundAmount
) internalReleases reserved liquidity when fees are refunded. Decreases pending swap input by refund amount.
2. FeeManager Contract
Tempo introduces a precompiled contract, the FeeManager, at the address 0xfeec000000000000000000000000000000000000.
The FeeManager is a singleton contract that implements all the functions of the Fee AMM for every pool. It also handles the collection and refunding of fees during each transaction, stores fee token preferences for users and validators, and implements the executeBlock() function that is called by a system transaction at the end of each block.
Key Functions
function setUserToken(address token) externalSets the default fee token preference for the caller (user). Requires token to be a USD TIP-20 token. Emits UserTokenSet event. Access: Direct calls only (not via delegatecall).
function setValidatorToken(address token) externalSets the fee token preference for the caller (validator). Requires token to be a USD TIP-20 token. Cannot be called during a block built by that validator. Emits ValidatorTokenSet event. Access: Direct calls only (not via delegatecall).
function collectFeePreTx(
address user,
address userToken,
uint256 maxAmount
) externalCalled by the protocol before transaction execution. The fee token (userToken) is determined by the protocol before calling using logic that considers: explicit tx fee token, setUserToken calls, stored user preference, tx.to if TIP-20. Reserves AMM liquidity if user token differs from validator token. Collects maximum possible fee from user. Access: Protocol only (msg.sender == address(0)).
function collectFeePostTx(
address user,
uint256 maxAmount,
uint256 actualUsed,
address userToken
) externalCalled by the protocol after transaction execution. The validator token and fee recipient are inferred from block.coinbase. Calculates refund amount: refundAmount = maxAmount - actualUsed. Refunds unused tokens to user. Releases reserved liquidity for refunded amount. Tracks collected fees for block-end settlement. Access: Protocol only (msg.sender == address(0)).
function executeBlock() externalCalled once in a system transaction at the end of each block. Processes all collected fees and executes pending swaps. For each token with collected fees: if token differs from validator token, executes pending fee swaps via AMM and updates reserves and calculates output amount. Transfers all validator tokens to the validator (block.coinbase). Clears fee tracking arrays. Access: Protocol only (msg.sender == address(0)).
Swap Mechanisms
Fee Swaps
- Rate: Fixed at m=0.9970 (validator receives 0.9970 of their preferred token per 1 user token that user pays)
- Direction: User token to validator token
- Purpose: Convert tokens paid by users as fees to tokens preferred by validators
- Settlement: Batched at block end via
executePendingFeeSwaps - Access: Protocol only
Rebalancing Swaps
- Rate: Fixed at n=0.9985 (swapper receives 1 of the user token for every 0.9985 that they put in of the validator's preferred token)
- Direction: Validator token to user token
- Purpose: Refill reserves of validator token in the pool
- Settlement: Immediate
- Access: Anyone
Fee Collection Flow
-
Pre-Transaction:
- Protocol determines user's fee token using logic that considers: explicit tx fee token, setUserToken calls, stored user preference, tx.to if TIP-20
- Protocol calculates maximum gas needed (
maxAmount = gasLimit * maxFeePerGas) FeeManager.collectFeePreTx(user, userToken, maxAmount)is called:- If user token differs from validator token, reserves AMM liquidity via
reserveLiquidity() - Collects maximum fee from user using
transferFeePreTx()
- If user token differs from validator token, reserves AMM liquidity via
- If any check fails (insufficient balance, insufficient liquidity), transaction is invalid
-
Post-Transaction:
- Calculate actual gas used (
actualUsed = gasUsed * gasPrice) FeeManager.collectFeePostTx(user, maxAmount, actualUsed, userToken)is called:- Validator token and fee recipient are inferred from
block.coinbase - Calculates refund:
refundAmount = maxAmount - actualUsed - Refunds unused tokens to user via
transferFeePostTx() - Releases reserved liquidity for refunded amount via
releaseLiquidityPostTx() - Tracks collected fees (actual used amount) for block-end settlement
- Validator token and fee recipient are inferred from
- Calculate actual gas used (
-
Block End:
- System transaction calls
FeeManager.executeBlock():- For each token with collected fees:
- If token differs from validator token, executes pending fee swaps via
executePendingFeeSwaps() - Updates pool reserves (adds userToken, subtracts validatorToken)
- If token differs from validator token, executes pending fee swaps via
- Transfers all validator tokens to validator (
block.coinbase) - Clears fee tracking arrays
- For each token with collected fees:
- System transaction calls
Events
event RebalanceSwap(
address indexed userToken,
address indexed validatorToken,
address indexed swapper,
uint256 amountIn,
uint256 amountOut
)
event FeeSwap(
address indexed userToken,
address indexed validatorToken,
uint256 amountIn,
uint256 amountOut
)
event Mint(
address indexed sender,
address indexed userToken,
address indexed validatorToken,
uint256 amountUserToken,
uint256 amountValidatorToken,
uint256 liquidity
)
event Burn(
address indexed sender,
address indexed userToken,
address indexed validatorToken,
uint256 amountUserToken,
uint256 amountValidatorToken,
uint256 liquidity,
address to
)
event UserTokenSet(address indexed user, address indexed token)
event ValidatorTokenSet(address indexed validator, address indexed token)Transfer events are emitted as usual for transactions, with the exception of paying gas fees via TIP20 tokens. For fee payments, a single Transfer event is emitted post execution to represent the actual fee amount consumed (i.e. gasUsed * gasPrice).
System transactions
This specification introduces system transactions, with the first being the executeBlock() call to the FeeManager contract at the end of each block. A system transaction is a legacy transaction with an empty signature (r = 0, s = 0, yParity = false) and with the sender as the 0 address (0x0000000000000000000000000000000000000000).
System transactions are only allowed when there is a specific consensus rule allowing them. A block is invalid if any required system transaction is missing or if any extra system transaction is present.
System transactions do not consume block gas, do not increment a sender nonce, do not contribute to block gas limit, and do not pay fees. They may set any gas price and gas limit (as specified by a specific rule), regardless of their execution gas or the block base fee. System transactions must not revert.
Execution transaction
Under this specification, exactly one system transaction must appear at the end of every block. It must have the following parameters:
| Field | Value / Requirement | Notes / Validation |
|---|---|---|
| Type | Legacy transaction | |
| Position in Block | Last transaction | Block is invalid if absent or not last. |
| From (sender) | 0x0000000000000000000000000000000000000000 | Zero address |
| To (recipient) | 0xfeec000000000000000000000000000000000000 | FeeManager precompile. |
| Calldata | 0xb306cc70 | ABI-encoded executeBlock(), no arguments. |
| Value | 0 | No native token transfer. |
| Nonce | 0 | |
| Gas Limit | 0 | Does not contribute to block gas accounting. |
| Gas Price | 0 | Independent of block base fee; does not pay fees. |
| Signature | r = 0, s = 0, yParity = false | Empty signature designates system transaction. |
The proposer must construct and include this transaction when building the block. A block is invalid if the transaction is absent or not in the final position.
Gas
Fee swaps are designed to be gas-free from the user perspective. The pre-tx and post-tx steps in each transaction do not cost any gas; nor does the system transaction at the end of each block.