---
id: TIP-1092
title: TIP-20 Policy IDs in TIP-403
description: Adds TIP-403 transfer policy bindings for TIP-20 tokens so token policy selection can be provable when needed.
authors: Tanishk Goyal
status: Draft
related: TIP-20, TIP-403, TIP-1015
protocolVersion: T9
---

# TIP-1092: TIP-20 Policy IDs in TIP-403

## Abstract

This TIP lets TIP-403 store the active transfer policy ID for a TIP-20 token. New TIP-20 tokens write this binding during creation. Existing TIP-20 tokens receive it when they are migrated for zone/provable-contract use or when an admin first changes their policy after activation.

Unmigrated existing tokens keep working by falling back to the token-local policy ID. Zone and provable-contract flows must use the TIP-403 binding and cannot rely on that fallback.

## Motivation

TIP-403 already owns policy contents, but TIP-20 currently owns the selected `transferPolicyId`. Zones therefore need to inspect TIP-20 state to know which policy applies to a token.

Storing the binding in TIP-403 gives zones a direct registry lookup for a token's policy ID. It also lets a verifier prove both parts of the transfer-policy decision from TIP-403 state: which policy a token uses, and what that policy permits.

## Assumptions

- TIP-403 is available to zones and provable-contract flows.
- Existing TIP-20 tokens can remain unmigrated until a zone/provable-contract flow needs the TIP-403 binding or an admin changes their policy after activation.

---

# Specification

## Registry State

TIP-403 stores one registry-owned transfer-policy binding per migrated or newly created TIP-20 token:

```text
token_transfer_policy: mapping(address token => uint256 packed)

bits 0..63: policy_id
bit 64: is_set
bits 65..255: reserved
```

The `is_set` bit is required because policy ID `0` is a valid built-in reject-all policy. A packed value of zero means unset. A packed value with `is_set == 1` and `policy_id == 0` means the token is explicitly registered to policy `0`.

For a registered token, `policy_id` is the token's active transfer policy ID and must refer to an existing TIP-403 policy.

## TIP-403 Interface

TIP-403 exposes token-keyed policy lookup:

```solidity
function tokenTransferPolicyId(address token) external view returns (bool isSet, uint64 policyId);
function migrateTransferPolicyIds(address[] calldata tokens) external returns (uint256 migrated);
```

`tokenTransferPolicyId(token)` returns `isSet == true` and the TIP-403 binding if one exists.
Otherwise it returns `isSet == false` and falls back to the legacy token-local policy ID. If
`token` is not a deployed TIP-20 token, the lookup reverts.

Zone and provable-contract flows must require `isSet == true`. `ZonePortal` checks the binding
before enabling a token. If it is unset, the portal calls `migrateTransferPolicyIds`, checks again,
and rejects the token if `isSet` is still false. ZonePortal deployments using this flow assume T9
is active.

## TIP-20 Behavior

After activation:

- `transferPolicyId()` returns the effective policy ID: the TIP-403 registry binding if one exists, otherwise the token-local policy ID.
- `changeTransferPolicyId(uint64 newPolicyId)` MUST set the TIP-403 binding to `newPolicyId`. It MUST NOT migrate or delete the local TIP-20 policy slot.
- TIP-20 authorization paths use the effective policy ID.

## Token Creation

The TIP-20 creation path initializes the TIP-403 token binding atomically with token initialization.

## Existing TIP-20 Migration

TIP-403 exposes a batch migration path:

```solidity
function migrateTransferPolicyIds(address[] calldata tokens) external returns (uint256 migrated);
```

`migrateTransferPolicyIds(tokens)` MUST, for each valid TIP-20 token without a TIP-403 binding, copy the local policy ID into TIP-403, then delete the local TIP-20 policy slot. It MUST skip an existing TIP-403 binding without reading local state. Invalid token addresses are skipped. The function is callable by any account.

Operators manually migrate existing tokens that are already enabled on a running zone. Zone token enablement migrates an unset binding before accepting the token. An admin policy update also creates the binding for an otherwise unmigrated token, but does not copy or delete the prior local policy ID because `newPolicyId` replaces it.

As of July 10, 2026, chain scans found:

| Network | Block scanned | TIP-20 tokens |
|---------|--------------:|--------------:|
| Presto mainnet | 29,189,230 | 4,355 |
| Moderato testnet | 25,830,853 | 15,291,992 |

The testnet count makes full automatic migration impractical because each migrated token creates a new TIP-403 registry slot. This TIP therefore does not require a hardfork-time batch migration, a system transaction, or migration of every historical token before activation; bindings are created only by targeted migration, token creation, or a post-activation policy update.

## Threat Model

- This TIP does not change token-admin authority. The migration path is permissionless because it can only copy the token's current local policy ID into TIP-403 and delete that local value; it cannot choose a different policy ID. Only the existing token admin can create a binding with a different value through `changeTransferPolicyId`.
- The actor or script calling `migrateTransferPolicyIds()` cannot change a policy ID, move it back from TIP-403 to a TIP-20 contract, or block any TIP-20 operation.

# Tooling

Operators need a small migration script that accepts a list of TIP-20 tokens, migrates them in batches, and verifies that TIP-403 has a binding for each one.

SDKs and indexers that read `transferPolicyId()` from TIP-20 can keep using that API. Low-level tools that need provable token-to-policy bindings must use TIP-403 and must check that `tokenTransferPolicyId(token)` returns `isSet == true`.

# Observability

This TIP does not change policy-update events. The existing TIP-20 `TransferPolicyUpdate` event remains the event for active policy changes. It is emitted from the TIP-20 token address, so the token identity is already part of the log.

# Invariants

- Unset registry state remains distinguishable from policy ID `0`.
- Migration copies the token's current local policy ID into TIP-403 and deletes the local policy ID; a post-activation policy update writes `newPolicyId` directly to TIP-403 without migrating or deleting the local policy ID.
- Once a TIP-403 binding exists, the token-local policy slot is not read or written.
