Skip to content
LogoLogo

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 (1.0015 userToken per validatorToken).

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 and immediately swaps them (paying a constant price) into the token preferred by the validator. Fees accumulate in the FeeManager, and validators can claim them on-demand.

The system is designed to minimize several forms of MEV:

  • No Probabilistic MEV: The fixed fee swap rate prevents 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, 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:

  1. Fee Swaps: Fixed-rate swaps at a price of 0.9970 (validator token per user token) from userToken to validatorToken
  2. Rebalancing Swaps: Fixed-rate swaps at a price of 1.0015 (user token per validator token) from validatorToken to userToken

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: userTokenvalidatorToken. 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 = 10000
  • MIN_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 1.0015 (user token per validator 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 mint(
    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. Emits Burn event.

function executeFeeSwap(
    address userToken,
    address validatorToken,
    uint256 amountIn
) internal returns (uint256 amountOut)

Executes a fee swap immediately. Calculates amountOut = (amountIn * M) / SCALE. Only executed by the protocol during transaction execution. Emits FeeSwap event. Note: FeeSwap events are not emitted for immediate swaps.

function checkSufficientLiquidity(
    address userToken,
    address validatorToken,
    uint256 maxAmount
) internal view

Verifies sufficient validator token reserves for the fee swap. Calculates maxAmountOut = (maxAmount * M) / SCALE. Reverts if insufficient liquidity.

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 handles the collection and refunding of fees during each transaction, executes fee swaps immediately, stores fee token preferences for users and validators, and accumulates fees for validators to claim via distributeFees().

Key Functions
function setUserToken(address token) external

Sets 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) external

Sets 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
) external

Called 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
) external

Called 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. If user token differs from validator token, executes the fee swap immediately and accumulates the output for the validator. Access: Protocol only (msg.sender == address(0)).

function distributeFees(address validator, address token) external

Allows anyone to trigger distribution of accumulated fees for a specific token to a validator. Transfers all accumulated fees in the specified token to the validator address. If no fees have accumulated for that token, this is a no-op.

function collectedFees(address validator, address token) external view returns (uint256)

Returns the amount of accumulated fees for a validator and specific token that can be claimed via distributeFees().

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: Immediate during transaction execution
  • 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

  1. 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, checks AMM has sufficient liquidity via checkSufficientLiquidity()
      • Collects maximum fee from user using transferFeePreTx()
    • If any check fails (insufficient balance, insufficient liquidity), transaction is invalid
  2. 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()
      • If user token differs from validator token and actualUsed > 0, executes fee swap immediately via executeFeeSwap()
      • Accumulates swapped fees for the validator
  3. Fee Distribution:

    • Validators (or anyone) can call distributeFees(validator) at any time to transfer accumulated fees to the validator

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).

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.