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

## Assumptions

- The FeeManager address (`0xfeec000000000000000000000000000000000000`) is a protocol-level singleton that cannot be controlled by any external account.
- Token issuers understand that whitelisting the FeeManager address opts their token into AMM participation (pool creation, rebalancing, LP operations).

---

# 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-sender + FeeManager-as-recipient + `to`-as-recipient on both tokens) and `burn` (`msg.sender`-as-sender on both tokens).

## Fee Collection Path — Exempt FeeManager

The following operations skip TIP-403 authorization checks for the FeeManager address. The counterparty (user, validator) is still checked.

### `collectFeePreTx`

Currently checks both the user (as sender) and the FeeManager (as recipient):

```solidity
uint64 policyId = ITIP20(userToken).transferPolicyId();
if (
    !TIP403_REGISTRY.isAuthorizedSender(policyId, user)
        || !TIP403_REGISTRY.isAuthorizedRecipient(policyId, address(this))
) {
    revert ITIP20.PolicyForbids();
}
```

**Change**: Remove the `isAuthorizedRecipient(policyId, address(this))` check. Only check the user:

```solidity
uint64 policyId = ITIP20(userToken).transferPolicyId();
if (!TIP403_REGISTRY.isAuthorizedSender(policyId, user)) {
    revert ITIP20.PolicyForbids();
}
```

### `transferFeePreTx`

No change. This function already does not perform TIP-403 checks (it relies on the caller having checked).

### `executeFeeSwap`

No change. This is an internal function that only updates pool reserves.

### `transferFeePostTx`

No change. This function already does not perform TIP-403 checks.

## Public AMM Operations — Full TIP-403 Enforcement

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

### `distributeFees`

No change. Calls `ITIP20(token).transfer(validator, amount)`, which enforces TIP-403 on both sender (FeeManager) and recipient (validator).

### `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);
}
```

### `rebalanceSwap`

No change. Calls `systemTransferFrom(msg.sender, address(this), amountIn)` and `transfer(to, amountOut)`, which enforce TIP-403 on all participants.

### `mint` — Enhanced Authorization

In addition to the existing TIP-403 enforcement on the `validatorToken` transfer (via `systemTransferFrom`, which checks the caller as sender and the FeeManager as recipient on `validatorToken`), `mint` adds two new authorization checks: the caller must be authorized as a sender on the `userToken`, and `to` must be authorized as a recipient on both tokens.

**Rationale**:
- Caller as sender on `userToken`: the caller is providing liquidity to a pool that facilitates swaps for `userToken`. By minting, they become an economic participant in that token's ecosystem — pool reserves shift during fee swaps and rebalancing, and on `burn` the LP receives pro-rata shares of both tokens. The caller should be authorized to interact with the `userToken` even though no `userToken` moves during `mint`.
- `to` as recipient on both tokens: LP shares are a claim on the underlying pool reserves. Without this check, a recipient-denied account could be made an LP-share holder by a whitelisted caller and later redeem the shares through `burn`, bypassing the recipient policy. This mirrors how the DEX authorizes the recipient of escrowed value.

**Change**: Before processing the mint, treat the operation as if the caller were transferring `userToken` into the pool *and* the LP shares represented a future claim that `to` must be eligible to receive:

```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: treat mint as if caller were transferring userToken to the pool,
    // and pin LP-share authorization to `to` on both tokens.
    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);
}
```

Note: There is an inherent TOCTOU (time-of-check-time-of-use) gap — a policy may change between when the LP mints and when the pool is used for subsequent `rebalanceSwap`, `burn`, or `distributeFees`. This is a best-effort check and is consistent with how TIP-403 operates elsewhere (e.g., a token holder may be blacklisted after receiving tokens).

Additionally, since `rebalanceSwap` checks the relevant policies, a token admin can prevent the liquidity on the FeeAMM from being refreshed, which means it will eventually be exhausted. This should give token admins an adequate tool for disabling the FeeAMM for their token even if someone manages to create liquidity on the pool before the admin sets that policy.

## Behavioral Summary

| Operation | FeeManager TIP-403 check | Counterparty TIP-403 check | New explicit auth checks |
|-----------|-------------------------|---------------------------|--------------------------|
| `collectFeePreTx` | **Removed** | ✅ User checked as sender | — |
| `transferFeePreTx` | N/A (no check today) | N/A | — |
| `executeFeeSwap` | N/A (internal) | N/A | — |
| `transferFeePostTx` | N/A (no check today) | N/A | — |
| `distributeFees` | ✅ FeeManager checked as sender | ✅ Validator checked as recipient | — |
| `mint` | ✅ FeeManager checked as recipient (both tokens, `validatorToken` via `transferFrom`, `userToken` via new gate) | ✅ Caller checked as sender (both tokens, `validatorToken` via `transferFrom`, `userToken` via new gate) | ✅ `to` checked as recipient on both tokens |
| `burn` | ✅ FeeManager checked as sender | ✅ `to` checked as recipient | ✅ `msg.sender` checked as sender on both tokens |
| `rebalanceSwap` | ✅ FeeManager checked as recipient + sender | ✅ Caller + `to` checked | — |

## 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 | ✅ | ✅ Pools can be created, 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. **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.

3. **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`.

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

5. **Policy 0 blocks all paths**: A token with `transferPolicyId = 0` (always-reject) blocks all operations — fee collection (user fails sender check), AMM operations (all participants fail), and fee distribution (all participants fail).

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

7. **No new bypass paths**: A user who is blocked by TIP-403 as a sender cannot pay fees in that token. A user who is blocked as a recipient cannot receive tokens via `burn`, `rebalanceSwap`, or `distributeFees`. A recipient-denied account cannot acquire LP-share value via `mint`-then-`burn` routed through whitelisted helpers, because the `to`-as-recipient check on `mint` and the `msg.sender`-as-sender check on `burn` pin authorization to the LP itself on both ends of the lifecycle.

## 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 FeeManager-as-recipient leg of `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 even while permitting LP/fee withdrawals.
