Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions tips/tip-1042.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
---
id: TIP-1042
title: FeeAMM TIP-403 Policy Exemptions
description: Exempts the FeeAMM address from TIP-403 transfer policy checks during fee collection, while preserving full enforcement on public AMM operations.
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, `mint` is extended with a new authorization check that treats the operation as if the caller were transferring both tokens to the pool: the caller must be authorized as a sender on the `userToken` and the FeeManager must be an authorized recipient, even when only `validatorToken` is deposited.

## 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 a new authorization gate on `mint`.

## 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 `IERC20(token).transfer(validator, amount)`, which enforces TIP-403 on both sender (FeeManager) and recipient (validator).

### `burn`

No change. Calls `IERC20(userToken).transfer(to, amountUserToken)` and `IERC20(validatorToken).transfer(to, amountValidatorToken)`, which enforce TIP-403 on both sender (FeeManager) and recipient (`to`).

### `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, `mint` adds an authorization check on the `userToken` for the caller.

**Rationale**: 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`.

**Change**: Before processing the mint, check that the caller is authorized as a sender on the `userToken` and that the FeeManager is an authorized recipient — as if the caller were transferring `userToken` into the pool:

```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();
_requireUSDTIP20(userToken);
_requireUSDTIP20(validatorToken);

// New: treat mint as if caller were transferring userToken to the pool.
// Caller must be authorized as sender, FeeManager as recipient.
uint64 userTokenPolicyId = ITIP20(userToken).transferPolicyId();
if (
!TIP403_REGISTRY.isAuthorizedSender(userTokenPolicyId, msg.sender)
|| !TIP403_REGISTRY.isAuthorizedRecipient(userTokenPolicyId, address(this))
) {
revert ITIP20.PolicyForbids();
}

// ... rest of mint logic (unchanged)
}
```

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 fee swaps or rebalancing. 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 `userToken` auth check |
|-----------|-------------------------|---------------------------|--------------------------|
| `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) | ✅ Caller checked as sender (both tokens) | ✅ (see above) |
| `burn` | ✅ FeeManager checked as sender | ✅ `to` checked as recipient | — |
| `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 authorization**: A call to `mint(userToken, validatorToken, ...)` must revert with `PolicyForbids` if the caller is not authorized as a sender on `userToken`, or if the FeeManager is not an authorized recipient on `userToken`, under its current transfer policy.

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

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

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

## 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 unauthorized caller**: Caller not authorized on `userToken` attempts `mint(userToken, validatorToken, ...)`. Reverts with `PolicyForbids`.

7. **Mint with authorized caller**: Caller authorized on both `userToken` and `validatorToken`. Mint succeeds.

8. **Policy change after mint**: LP mints when authorized. Policy changes to block LP. Subsequent `burn` reverts for LP (checked as recipient). Fee swaps through the pool continue to work (fee path is exempt).
Loading