---
id: TIP-1042
title: FeeAMM TIP-403 Policy Exemptions
description: Exempts the FeeAMM address from TIP-403 transfer policy checks during fee collection, preserves enforcement on `burn`/`rebalanceSwap`/`distributeFees`, and adds new authorization gates on `mint` and `burn` to pin authorization to the LP across the mint/burn lifecycle.
authors: Dan Robinson
status: Draft
related: TIP-403, TIP-1015, TIP-20
---

# TIP-1042: FeeAMM TIP-403 Policy Exemptions

## Abstract

This TIP modifies how TIP-403 transfer policies interact with the FeeAMM/FeeManager precompile. The FeeManager address is exempted from TIP-403 authorization checks during the protocol fee collection path (`collectFeePreTx`, `transferFeePreTx`, `executeFeeSwap`, `transferFeePostTx`), while full TIP-403 enforcement is preserved on all public AMM operations (`mint`, `burn`, `rebalanceSwap`, `distributeFees`). Additionally, two new authorization gates are added to pin authorization to the LP itself across the mint/burn lifecycle:
- `mint` requires the caller to be authorized as a sender on the `userToken`, the FeeManager to be authorized as a recipient on the `userToken`, and `to` to be authorized as a recipient on both tokens.
- `burn` requires `msg.sender` to be authorized as a sender on both tokens.

## Motivation

TIP-403 policy checks during the transaction fee collection flow (`collectFeePreTx` / `collectFeePostTx`) complicate block building and impose costs on every transaction without meaningful benefit. The check on whether the FeeManager is an allowed recipient can be done at the time liquidity is minted into the pool, rather than on every transaction. By moving this check to `mint` time, block builders no longer need to account for the possibility that a token's policy might block the FeeManager as a recipient, simplifying transaction validity logic.

Note that explicit whitelisting of the FeeAMM is still necessary after this change — without it, nobody can call `mint`, and therefore no pool liquidity can be created. Issuers retain full control over whether their token participates in AMM liquidity and trading through the existing requirement to whitelist FeeManager for `mint`, `burn`, `rebalanceSwap`, and `distributeFees`.

# Specification

## Overview

The changes are divided into two categories:

1. **Fee collection path**: Remove TIP-403 checks on the FeeManager address during protocol-initiated fee operations.
2. **Public AMM operations**: Preserve existing TIP-403 enforcement, plus add new authorization gates on `mint` (caller as `userToken` sender, FeeManager as `userToken` recipient, and `to` as recipient on both tokens) and `burn` (`msg.sender` as sender on both tokens).

## Fee Collection Path — Exempt FeeManager

The fee collection path skips TIP-403 authorization checks for the FeeManager address. The counterparty is still checked.

### `collectFeePreTx`

Currently checks both the user as sender and the FeeManager as recipient.

**Change**: Remove the FeeManager recipient check from fee collection. `collectFeePreTx` must only require the user to be authorized as a sender under the current `userToken` transfer policy.

The recipient check is moved to `mint` time, rather than re-checked on every transaction.

### `transferFeePostTx`

When fee pre-collection exceeds the transaction's actual spending, `transferFeePostTx` returns the
unused amount to the original fee payer. This refund does not perform a separate TIP-403 recipient
authorization check on the fee payer. The payer was authorized as a sender during
`collectFeePreTx`, and the refund only restores part of the balance collected from that same
account; it cannot be redirected to another recipient.

## Activation and Existing Pools

The authorization checks added to `mint` apply only when liquidity is added under T8. Activation
does not revalidate or migrate pools created before T8. Existing pools are grandfathered and remain
eligible for fee routing, even if the tokens' current policies would reject a new T8 `mint` call.

Policy checks on later public operations still use the policy in effect when each operation runs.
Consequently, an issuer can prevent new liquidity or block a subsequent `burn`, `rebalanceSwap`, or
`distributeFees`, but a policy change does not retroactively remove an existing pool from fee-route
selection.

## Public AMM Operations — Full TIP-403 Enforcement

The public AMM operations continue to enforce TIP-403 on all participants, including the FeeManager address, via the underlying `transfer()` and `systemTransferFrom()` calls on TIP-20 tokens.

| Operation | Current allowed path | Updated allowed path |
|-----------|----------------------|----------------------|
| `mint` | The caller must be authorized to send `validatorToken`, and the FeeManager must be authorized to receive `validatorToken`. | The current `validatorToken` path remains required. In addition, the caller must be authorized to send `userToken`, the FeeManager must be authorized to receive `userToken`, and `to` must be authorized to receive both pool tokens. |
| `burn` | The FeeManager must be authorized to send each pool token, and `to` must be authorized to receive each pool token. | The current transfer paths remain required. In addition, the LP burning shares must be authorized to send both pool tokens. |

### `mint` — Enhanced Authorization

The existing `validatorToken` transfer in `mint` already goes through `systemTransferFrom(msg.sender, address(this), amountValidatorToken)`, so TIP-403 checks that `msg.sender` may send `validatorToken` and that the FeeManager address may receive it. The additional checks cover the side of the pool that is not touched by that transfer: `msg.sender` must be authorized to send `userToken`, the FeeManager address must be authorized to receive `userToken`, and `to` must be authorized to receive both pool tokens.

Checking `userToken` at `mint` time matters even though no `userToken` moves in that call. Adding liquidity makes the caller an economic participant in the `userToken` pool: reserves can shift during fee swaps and rebalancing, and LP shares can later be burned for a pro-rata share of both tokens. This is also the point where the FeeManager receive check for `userToken` belongs after removing it from `collectFeePreTx`; the issuer opts the FeeManager into pool liquidity once, instead of forcing block builders to re-evaluate that permission on every fee-paying transaction.

The `to` checks bind the LP-share recipient to the underlying assets. Without them, an authorized caller could mint shares to an account that is not allowed to receive one of the pool tokens, and that account would hold a redeemable claim on reserves it could not receive directly.

Before processing the mint, add:

```solidity
function mint(
    address userToken,
    address validatorToken,
    uint256 amountValidatorToken,
    address to
) external returns (uint256 liquidity) {
    // Existing validations
    if (userToken == validatorToken) revert IdenticalAddresses();
    if (amountValidatorToken == 0) revert InvalidAmount();
    // (validates both tokens are USD-denominated TIP-20 tokens)

    // New: validate userToken participation and the LP recipient's future claim.
    uint64 userTokenPolicyId = ITIP20(userToken).transferPolicyId();
    uint64 validatorTokenPolicyId = ITIP20(validatorToken).transferPolicyId();
    if (
        !TIP403_REGISTRY.isAuthorizedSender(userTokenPolicyId, msg.sender)
            || !TIP403_REGISTRY.isAuthorizedRecipient(userTokenPolicyId, address(this))
            || !TIP403_REGISTRY.isAuthorizedRecipient(userTokenPolicyId, to)
            || !TIP403_REGISTRY.isAuthorizedRecipient(validatorTokenPolicyId, to)
    ) {
        revert ITIP20.PolicyForbids();
    }

    // ... rest of mint logic (unchanged):
    //   systemTransferFrom(msg.sender, address(this), amountValidatorToken);
}
```

These checks are still point-in-time checks. A policy may change after `mint`, and later calls to `rebalanceSwap`, `burn`, or `distributeFees` are evaluated under the policy in effect when those calls execute. That is consistent with TIP-403 elsewhere. Because `rebalanceSwap` remains fully policy-checked, an issuer can stop fresh pool activity by changing policy; existing liquidity can then only drain.

### `burn` — Enhanced Authorization

In addition to the existing TIP-403 enforcement on both transfers (`ITIP20(userToken).transfer(to, amountUserToken)` and `ITIP20(validatorToken).transfer(to, amountValidatorToken)`, which check the FeeManager as sender and `to` as recipient on each token), `burn` adds an authorization check on `msg.sender` for both tokens.

**Rationale**: LP shares represent a claim on both pool tokens, so the LP burning those shares is the originating party of both outflows. Without checking `msg.sender`, a recipient-denied account could acquire LP shares via an authorized depositor (`mint`) and redeem them through an authorized payout address (`to`), bypassing both gates. This mirrors the sender check that the DEX applies to limit-order makers.

**Change**: Before processing the burn, check that `msg.sender` is authorized as a sender on both `userToken` and `validatorToken`:

```solidity
function burn(
    address userToken,
    address validatorToken,
    uint256 liquidity,
    address to
) external returns (uint256 amountUserToken, uint256 amountValidatorToken) {
    // Existing validations
    if (userToken == validatorToken) revert IdenticalAddresses();
    if (liquidity == 0) revert InvalidAmount();

    // New: pin authorization to the LP themselves on both tokens.
    uint64 userTokenPolicyId = ITIP20(userToken).transferPolicyId();
    uint64 validatorTokenPolicyId = ITIP20(validatorToken).transferPolicyId();
    if (
        !TIP403_REGISTRY.isAuthorizedSender(userTokenPolicyId, msg.sender)
            || !TIP403_REGISTRY.isAuthorizedSender(validatorTokenPolicyId, msg.sender)
    ) {
        revert ITIP20.PolicyForbids();
    }

    // ... rest of burn logic (unchanged):
    //   ITIP20(userToken).transfer(to, amountUserToken);
    //   ITIP20(validatorToken).transfer(to, amountValidatorToken);
}
```

## Issuer Control Surface

The FeeManager address exemption does **not** remove issuer control. Instead, it shifts the control mechanism from "whitelist the FeeManager address" to "whitelist determines AMM participation":

| Issuer action | Fee payment | AMM pools | `distributeFees` |
|---------------|-------------|-----------|-------------------|
| Whitelist FeeManager | Same-token and cross-token fees work | Pools can be created and rebalanced | Fees distributed |
| Don't whitelist FeeManager | Same-token fees work. Cross-token fees require pool liquidity (see below). | `mint` blocked (FeeManager not authorized as recipient on either token). No pools; no cross-token fee swaps. | `transfer` from FeeManager blocked |

### Edge Case: Stranded Fees

If a token does not whitelist the FeeManager address, same-token fees (where `userToken == validatorToken`) will be collected successfully — the fee collection path is exempt. However, `distributeFees` calls `transfer()` which checks the FeeManager as sender. If the FeeManager is not whitelisted, the validator cannot claim these fees and they remain stranded in the FeeManager.

This is an expected consequence: the issuer has not opted their token into the fee distribution system. Validators should be aware that setting their preferred fee token to a token that has not whitelisted the FeeManager may result in uncollectable fee balances. Wallet and validator tooling should surface this information.

---

# Invariants

1. **Fee collection never checks FeeManager**: In `collectFeePreTx`, the FeeManager address is never checked against TIP-403 policies. Only the fee payer is checked as a sender.

2. **Refunds return to the original payer without a recipient check**: `transferFeePostTx` may only restore unused pre-collected tokens to the original fee payer; it does not perform a separate TIP-403 recipient-authorization check.

3. **Existing pools remain routable**: T8 does not revalidate pools created before activation. A legacy pool remains eligible for fee routing even if a new `mint` would fail under the current policy.

4. **Public AMM operations always check FeeManager**: `distributeFees`, `mint`, `burn`, and `rebalanceSwap` continue to enforce TIP-403 on the FeeManager address via the underlying TIP-20 `transfer()` and `systemTransferFrom()` calls.

5. **Mint requires `userToken` sender authorization and recipient authorization for both `address(this)` and `to`**: A call to `mint(userToken, validatorToken, ..., to)` must revert with `PolicyForbids` if any of the following are false under the tokens' current transfer policies: the caller is authorized as a sender on `userToken`; the FeeManager (`address(this)`) is authorized as a recipient on `userToken`; `to` is authorized as a recipient on `userToken`; `to` is authorized as a recipient on `validatorToken`.

6. **Burn requires LP-side authorization**: A call to `burn(userToken, validatorToken, ...)` must revert with `PolicyForbids` if `msg.sender` is not authorized as a sender on either `userToken` or `validatorToken`, under their current transfer policies. This is in addition to the existing TIP-403 checks performed by the underlying `transfer()` calls (FeeManager as sender, `to` as recipient).

7. **Policy 0 and existing pools**: Policy 0 on the user fee token blocks pre-collection because the fee payer fails the sender check. Policy changes do not remove an existing pool from fee-route selection, including when policy 0 applies to the validator token. Route eligibility does not guarantee that fee collection, public AMM operations, or fee distribution will succeed; those paths apply their own authorization checks.

8. **Policy 1 allows all paths**: A token with `transferPolicyId = 1` (always-allow) permits all operations without restriction (existing behavior, unchanged).

## Test Cases

1. **Whitelist token without FeeManager whitelisted — same-token fees**: Whitelisted user pays fees in token X where `userToken == validatorToken`. Fees are collected successfully. `distributeFees` reverts (FeeManager not authorized as sender).

2. **Whitelist token without FeeManager whitelisted — cross-token fees**: `mint` reverts when attempting to create pool liquidity (FeeManager not authorized as recipient on either token). Without liquidity, cross-token fee collection reverts with `InsufficientLiquidity`.

3. **Whitelist token with FeeManager whitelisted**: All operations succeed. Fees collected, swapped, distributed. AMM mint/burn/rebalance all work.

4. **Blacklist token — default behavior**: FeeManager is not blacklisted by default. All operations succeed (unchanged behavior).

5. **Blocked user pays fees**: User blacklisted on token X attempts to pay fees in X. `collectFeePreTx` reverts (`isAuthorizedSender` returns false for user).

6. **Mint with caller not authorized as sender on userToken**: Caller not authorized as sender on `userToken` attempts `mint(userToken, validatorToken, ..., to=caller)`. Reverts with `PolicyForbids`.

7. **Mint with FeeManager not authorized as recipient on userToken**: Caller authorized as sender on `userToken`, but FeeManager not authorized as recipient on `userToken`. Reverts with `PolicyForbids`.

8. **Mint with `to` not authorized as recipient on userToken**: Caller authorized as sender, FeeManager authorized as recipient, but `to` not authorized as recipient on `userToken`. Reverts with `PolicyForbids`.

9. **Mint with `to` not authorized as recipient on validatorToken**: All checks pass on `userToken`, but `to` not authorized as recipient on `validatorToken`. Reverts with `PolicyForbids`.

10. **Mint with authorized caller and `to`**: Caller authorized as sender on `userToken`, FeeManager authorized as recipient on `userToken`, and `to` authorized as recipient on both tokens. Mint succeeds.

11. **Burn with LP not authorized as sender on userToken**: LP holding shares attempts `burn(userToken, validatorToken, ...)` while not authorized as sender on `userToken`. Reverts with `PolicyForbids`.

12. **Burn with LP not authorized as sender on validatorToken**: LP authorized as sender on `userToken`, not on `validatorToken`. Reverts with `PolicyForbids`.

13. **Burn with authorized LP**: LP authorized as sender on both tokens; `to` authorized as recipient on both tokens. Burn succeeds.

14. **Bypass attempt — recipient-denied LP via `mint`/`burn` routing**: Whitelisted helper A calls `mint(..., to=X)` where X is recipient-denied on `userToken`. Mint reverts (Test Case 8 covers the same gate). If the gate were absent, X would acquire LP shares; subsequent `burn` called by X to `to=Y` (an authorized recipient) would also revert because X fails the new `msg.sender`-as-sender check (Test Case 11). Both gates together prevent the bypass.

15. **Policy change after mint**: LP mints when authorized. Policy changes to block LP as a sender on `userToken`. Subsequent `burn` reverts (LP fails the new `msg.sender`-as-sender check). Fee swaps through the pool continue to work (fee path is exempt).

---

# Rationale

### Why not also exempt the FeeManager from sender checks on `burn` and `distributeFees`?

An alternative considered was to additionally exempt the FeeManager from the sender-side TIP-403 check on outflow paths (`burn`, `distributeFees`), so that LP funds and accrued fees could not be frozen by a post-hoc policy change that revokes the FeeManager's sender authorization (e.g., LPs mint while the FeeManager is whitelisted, then the issuer later removes the FeeManager from the whitelist, stranding LP withdrawals and accrued fees in the contract).

This was rejected. Funds becoming stuck inside a contract when its address is sender-blocked is ordinary behavior for any TIP-20 holder under TIP-403, and the FeeManager is not a special case. Issuers that want to operate the FeeManager in **withdraw-only mode** — allowing existing LPs and validators to redeem while preventing new pool participation — already have an in-protocol tool: a TIP-1015 compound policy that authorizes the FeeManager as a sender (so `burn` and `distributeFees` succeed) while denying it as a recipient (so `mint` and the receive-side FeeManager check in `rebalanceSwap` fail).

### Why does `rebalanceSwap` remain fully policy-checked?

`rebalanceSwap` is intentionally checked on both sides even when an issuer puts a token into withdraw-only mode via a compound policy. The issuer should retain the ability to stop pool refresh activity while permitting LP/fee withdrawals.
