diff --git a/tips/tip-1028.md b/tips/tip-1028.md
index ec5a8db790..1d2e4aa378 100644
--- a/tips/tip-1028.md
+++ b/tips/tip-1028.md
@@ -12,22 +12,135 @@ protocolVersion: TBD
## Abstract
-This TIP extends TIP-403 with token sets for TIP-20 token addresses, address-level receive controls, and non-reverting escrow for blocked TIP-20 inbounds. A blocked inbound credits `ESCROW_ADDRESS` and creates one fine-grained receipt bucket.
+TIP-1028 extends TIP-403 with address-level receive policies and token sets, allowing a receiver to control which originators and which TIP-20 tokens may credit it. When a receive policy blocks an inbound transfer or mint, the operation still succeeds at the protocol level, but the funds are credited to `ESCROW_ADDRESS` instead of the receiver and a corresponding escrow receipt is recorded.
-The escrow precompile stores only one keyed amount for each open blocked receipt. The rest of the receipt identity — receipt version, requested recipient, block reason, transfer-vs-mint kind, memo, timestamp, and nonce — is authenticated by the receipt witness and emitted in the blocked event. Claims may unwind to the receiver or reroute elsewhere; claim-to-receiver is an unwind, while reroutes are new spends. This TIP applies only to TIP-20 precompile flows; ordinary ERC-20 contracts remain out of scope.
+The receiver or a designated recovery contract may later claim these escrowed funds. Claims back to the receiver act as an unwind of the original inbound, while claims to other addresses are treated as new transfers.
+
+Token-level authorization remains unchanged and continues to revert on failure. This behavior applies only to TIP-20 precompile flows; ordinary contracts and other precompiles are unaffected.
## Motivation
-TIP-403 lets token issuers decide who may use a token, but some receivers also need inbound controls. A revert-based receiver policy creates a liveness problem: once a relationship exists, the receiver can later change policy and cause future transfers or mints to revert.
+TIP-403 allows token issuers to control who may use a token, but it does not give receivers control over which inbounds they accept. Some applications require receivers to restrict incoming transfers or mints based on the originator or token.
+
+A revert-based receiver policy introduces a liveness problem: once a sender relationship exists, a receiver can later change its policy and cause future transfers or mints to revert, breaking integrations and making delivery unreliable.
+
+TIP-1028 introduces receiver-side controls while preserving liveness. Instead of reverting when a receiver blocks an inbound, the transfer or mint succeeds at the protocol level and the funds are escrowed.
+
+Blocked inbounds remain recoverable and attributable, allowing receivers (or their recovery contracts) to claim funds while preserving context needed for offchain handling.
-This TIP keeps issuer-side semantics unchanged and changes only receiver-side failure handling. Token-level failures still revert; receiver-side failures are escrowed instead. The escrow representation must preserve more than an aggregate amount because TIP-1022 virtual addresses carry attribution in the literal `to` address, memo-bearing transfers carry memo data, recovery contracts may want time-based or memo-based rules, and offchain recovery flows need the original sender identity.
+## Assumptions
+
+- **TIP-403 registry exists and is extensible.** Token sets and address-level receive policies are added as new state and new entrypoints on the existing TIP-403 precompile registry, alongside its current address-policy surface. This TIP does not modify the existing TIP-403 address-policy ABI.
+- **TIP-1022 resolution is available at TIP-20 inbound time.** Virtual-address resolution runs before TIP-1028 receiver-side checks; failed resolution still reverts.
+- **TIP-1016 gas model.** Cost analysis uses the storage-cost model from TIP-1016 (fresh slot ≈ 250k gas, hot slot update ≈ 2.9k gas, baseline TIP-20 transfer ≈ 50k gas).
# Specification
-## 1. Scope and Model
+## 1. System Architecture
+
+This TIP is additive and does not alter TIP-403's existing address policy ABI, change issuer-side TIP-20 semantics, or introduce new surfaces in TIP-20 callers' interfaces. The new functionality is split across three protocol components, summarized below.
+
+1. **TIP-403 is extended** with new state and entrypoints:
+ - Token sets for filtering inbound tokens
+ - Per-address receive configuration (`receivePolicyId`, `tokenSetId`, `recoveryContract`)
+ - A new view `verifyAddressInbound(...)` used during TIP-20 flows
+
+2. **A new escrow precompile is introduced**:
+ - Holds blocked funds at `ESCROW_ADDRESS`
+ - Records one receipt per blocked inbound
+ - Exposes `recordBlockedInbound(...)` and `claimBlockedReceipt(...)`
+
+3. **TIP-20 transfer and mint flows add a receiver-side check**:
+ - Resolve TIP-1022 addresses
+ - Run existing issuer-side checks
+ - Call `verifyAddressInbound(...)`
+ - If authorized: credit the receiver
+ - If blocked: credit `ESCROW_ADDRESS` and record a receipt
+
+### 1.2 End-to-End Flow Overview
+
+With these components in place, TIP-20 transfers and mints execute as follows:
+
+1. Resolve the recipient (including TIP-1022 virtual address resolution if applicable).
+2. Run issuer-side checks (TIP-403 / TIP-1015).
+3. Call `verifyAddressInbound(...)`.
+4. If authorized, credit the receiver.
+5. If blocked, credit `ESCROW_ADDRESS` and record a receipt.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant Caller
+ participant Token as TIP-20 Token
+ participant TIP403 as TIP-403 Registry
+ participant Escrow
+ participant Recipient
+
+ Caller->>Token: transfer(to, amount)
+
+ Note over Token: TIP-20 checks:
pause, recipient, amount
(ok, else revert)
+
+ Token->>TIP403: TIP-403 + TIP-1015 token-level policies
+ TIP403-->>Token: ok, else revert
+
+ Token->>TIP403: TIP-403 (inbound) address-level policies
+ TIP403-->>Token: (authorized, blockedReason)
+
+ alt authorized (NONE)
+ Note over Token: TIP20 transfer:
debits sender,
credits recipient,
accrues rewards
+ Token->>Recipient: emit Transfer(caller, recipient, amount)
+ Token-->>Caller: success
+ else blocked (TOKEN_SET / RECEIVE_POLICY / both)
+ Note over Token: TIP20 transfer:
debits sender,
credits ESCROW,
reward-exempt
+ Token->>Escrow: emit Transfer(caller, ESCROW, amount)
+ Note over Escrow: store blocked-receipt
digest(receiver,
requestedRecipient,
originator,
recoveryContract,
reason, kind, memo
blockedAt, blockedNonce)
+ Escrow-->>Token: (blockedNonce, blockedAt)
+ Token-->>Caller: success
(funds escrowed)
+ end
+```
+
+Escrowed funds are released through `claimBlockedReceipt(...)` with the following flow:
+
+1. Validate the caller against the receipt’s `receiver` or snapshotted `recoveryContract`.
+2. Recompute the receipt key from the supplied witness.
+3. Load and consume the blocked receipt amount.
+4. If releasing to the original receiver, credit the receiver directly.
+5. If rerouting to another address, enforce destination checks before release.
+
+```mermaid
+sequenceDiagram
+ autonumber
+ participant Claimer
+ participant Escrow
+ participant TIP403 as TIP-403 Registry
+ participant Token as TIP-20 Token
+ participant Recipient
+
+ Claimer->>Escrow: claimBlockedReceipt(token, receiver,
recoveryContract, receipt, to)
+
+ Note over Escrow: caller check:
msg.sender == receiver if recoveryContract == 0,
else msg.sender == recoveryContract
(ok, else revert)
+
+ Note over Escrow: receiptKey = keccak256(witness)
load and delete blockedReceiptAmount[receiptKey]
(ok, else revert)
+
+ alt recipient == receiver (unwind)
+ Note over Token: escrow release:
debits ESCROW,
credits recipient,
reward-exempt
+ Token->>Recipient: emit Transfer(ESCROW, recipient, amount)
+ else recipient != receiver (reroute)
+ Escrow->>TIP403: TIP-403 + TIP-1015
token-level policies
for recipient
+ TIP403-->>Escrow: ok, else revert
+ Escrow->>TIP403: TIP-403 (inbound)
address-level policies
for receipt.originator
+ TIP403-->>Escrow: ok, else revert
+ Note over Escrow: if recoveryContract == 0 and via access key:
meter amount against receiver's spending limit
+ Note over Token: escrow release:
debits ESCROW,
credits recipient,
reward-exempt
+ Token->>Recipient: emit Transfer(ESCROW, recipient, amount)
+ end
+
+ Escrow-->>Claimer: success
(emit BlockedReceiptClaimed)
+```
-TIP-1028 applies to the following TIP-20 recipient-bearing paths:
+### 1.3 TIP-20 Affected Paths
+This TIP adds receiver-side checks to the following TIP-20 operations:
- `transfer`
- `transferFrom`
- `transferWithMemo`
@@ -46,39 +159,31 @@ For all such paths, TIP-1028 adds a receiver-side authorization layer:
If a token-level TIP-403 or TIP-1015 policy rejects the operation, the operation MUST revert exactly as it does today. If the receiver's address-level controls reject the inbound, the operation MUST be escrowed instead.
-If `to` is a TIP-1022 virtual address, TIP-1022 recipient resolution MUST occur before TIP-1028 receiver-side authorization. In that case:
+TIP-1028 does **NOT** alter:
-- TIP-1028 applies to the resolved master address, not the literal virtual address;
-- if virtual-address resolution fails, the operation MUST revert rather than escrow;
-- the blocked receipt's `receiver` MUST be the resolved master address;
-- the blocked receipt's `requestedRecipient` MUST be the literal `to` address so offchain systems can recover the TIP-1022 `userTag`; and
-- if the inbound is authorized, the success path MUST then follow TIP-1022 forwarding semantics.
+- `approve` / `permit` / `burn`
+- fee refunds via `transfer_fee_post_tx`
+- the reward subsystem's escrow-ability. Rewards are never escrowed; see [Section 6.3](#63-reward-subsystem)
-TIP-1028 does **not** alter `approve`, `permit`, `burn`, fee refunds via `transfer_fee_post_tx`, non-TIP-20 tokens deployed as ordinary contracts, or future recipient-bearing system-credit paths that do not identify a concrete originator.
+### 1.4 TIP-1022 Interaction
-DEX internal balances are not TIP-20 wallet balances and are not subject to address-level receive checks until withdrawn back onto the TIP-20 ledger.
+If the `to` address is a TIP-1022 virtual address, TIP-1022 recipient resolution MUST occur **before** TIP-1028 receiver-side authorization. Specifically:
-The reward subsystem is not receiver-side escrowable, but it is not exempt from recipient consent. `distributeReward` remains token-internal accounting; `setRewardRecipient` MUST reject a nonzero recipient whose address-level receive controls would block the holder as originator; and `claimRewards` MUST revalidate the actual payout recipient's address-level receive controls against the token contract as originator and MUST revert on failure rather than escrow.
+- TIP-1028 authorization applies to the resolved master address, never to the literal virtual address.
+- If virtual-address resolution itself fails, the operation MUST revert rather than escrow.
+- The blocked receipt's `receiver` MUST be the resolved master address.
+- The blocked receipt's `requestedRecipient` MUST be the literal `to`, so offchain systems can recover the TIP-1022 `userTag`.
+- If the inbound is authorized, the success path MUST follow TIP-1022 forwarding event semantics.
+- TIP-1022 virtual addresses MUST NOT configure their own receive policy (see [Section 3.1](#31-constraints)). The master address's policy governs all virtual addresses derived from it.
-For reward accounting, `ESCROW_ADDRESS` is a reward-exempt always-opted-out synthetic sink/source: blocked transfers, blocked mints, and claim releases MUST preserve the same opted-in-supply effects as a movement into or out of an always-opted-out address, and implementations MUST NOT create, update, or consult per-user reward state for `ESCROW_ADDRESS`.
+## 2. Token Sets (TIP-403 Extension)
-High-level flow:
+Token sets are a new TIP-403 primitive *for token addresses*. They answer a different question from address policies:
-1. If `to` is a TIP-1022 virtual address, resolve it first to `effectiveReceiver`; otherwise `effectiveReceiver = to`.
-2. Run the existing TIP-20 sender-side or issuer-side checks and the token's TIP-403 and TIP-1015 checks using `effectiveReceiver` wherever the current path is recipient-sensitive.
-3. Run the receiver's token-set and receive-policy checks against `effectiveReceiver`.
-4. If the inbound is authorized, credit the receiver normally.
-5. If the inbound is blocked by the receiver, credit `ESCROW_ADDRESS`, create one blocked receipt, and emit a blocked-inbound event naming both `receiver` and `requestedRecipient`.
-6. Later, the receiver or the receipt's recovery contract may claim the funds.
+- **address policy**: filters by *counterparty* — *"is this counterparty allowed to interact with this token?"*
+- **token set**: filters by *token* — *"is this token allowed to interact with this address?"*
-## 2. Token Sets
-
-Token sets are a dedicated TIP-403 primitive for TIP-20 token addresses. They answer a different question from address policies:
-
-- address policy: "is this originator authorized?"
-- token set: "is this token authorized?"
-
-Token sets use a separate ID space from policy IDs. They are not aliases for ordinary TIP-403 policy lists and do not reuse the compound-policy surface, but they mirror the ordinary TIP-403 list ergonomics, including create-with-members and batched membership updates.
+Token sets use a separate ID space from policy IDs. They are not aliases for ordinary TIP-403 policy lists and do not reuse the compound-policy surface, but they mirror ordinary TIP-403 list ergonomics, including create-with-members and batched membership updates.
### 2.1 Storage and Constraints
@@ -96,7 +201,12 @@ mapping(uint64 => mapping(address => bool)) internal tokenSetMembers;
Built-in meanings: `0` = always reject; `1` = always allow.
-Token sets MUST satisfy: `setType` is `WHITELIST` or `BLACKLIST`; `COMPOUND` token sets are forbidden; token-set type is immutable after creation; and membership is mutable by the token-set admin.
+Token sets MUST satisfy:
+
+- `setType` is `WHITELIST` or `BLACKLIST`.
+- `COMPOUND` token sets are forbidden.
+- Token-set type is immutable after creation.
+- Membership is mutable by the token-set admin.
### 2.2 Interface
@@ -142,11 +252,21 @@ interface ITIP403TokenSets {
### 2.3 Authorization Logic
-`isTokenAuthorized(tokenSetId, token)` MUST return reject for `tokenSetId == 0`, allow for `tokenSetId == 1`, and otherwise read the token set's immutable `setType`, returning the stored membership bit for `token` in a `WHITELIST` and the negation of that bit in a `BLACKLIST`.
+`isTokenAuthorized(tokenSetId, token)` MUST:
+
+- return `false` for `tokenSetId == 0`,
+- return `true` for `tokenSetId == 1`,
+- otherwise read the token set's immutable `setType` and return the stored membership bit for `token` in a `WHITELIST`, or its negation in a `BLACKLIST`.
### 2.4 Batch Update Semantics
-For `modifyTokenSetWhitelistBatch(...)` and `modifyTokenSetBlacklistBatch(...)`, `tokens.length` and the corresponding boolean array length MUST match; caller authorization and policy-type checks are identical to the single-entry mutation functions; updates MUST apply in order; the call MUST be atomic; and the implementation MUST emit the ordinary per-token update event once for each touched token, rather than a separate batch-only event. If TIP-403 standardizes a different canonical batch-list ABI before TIP-1028 is finalized, token sets SHOULD adopt that same ABI shape mutatis mutandis while preserving the semantics above.
+For `modifyTokenSetWhitelistBatch(...)` and `modifyTokenSetBlacklistBatch(...)`:
+
+- `tokens.length` and the corresponding boolean array length MUST match.
+- Caller authorization and policy-type checks are identical to the single-entry mutation functions.
+- Updates MUST apply in order.
+- The call MUST be atomic.
+- The implementation MUST emit the ordinary per-token update event once for each touched token, not a separate batch-only event.
### 2.5 Events and Errors
@@ -161,19 +281,25 @@ error InvalidTokenSetType();
error TokenSetBatchLengthMismatch();
```
-## 3. Address-Level Receive Controls
+## 3. Address-Level Receive Controls (TIP-403 Extension)
-Any address MAY configure three fields: `receivePolicyId` for which originators may credit the address, `tokenSetId` for which TIP-20 token addresses may credit the address, and `recoveryContract` for an optional claimer authorized to recover blocked receipts one receipt at a time; `address(0)` means the receiver claims directly. If an address has no configured receive controls, address-level authorization defaults to allow.
+Address-level receive controls are a new per-address configuration on the TIP-403 registry. They expose three fields:
-TIP-1022 virtual addresses are forwarding aliases, not canonical TIP-20 holders. `setAddressReceivePolicy()` MUST reject TIP-1022 virtual addresses and require configuration on the resolved master address instead.
+- `receivePolicyId` — which originators may credit the address.
+- `tokenSetId` — which TIP-20 token addresses may credit the address.
+- `recoveryContract` — an optional dedicated claimer for blocked receipts; `address(0)` means the receiver claims directly.
+
+If an address has no configured receive controls, address-level authorization defaults to allow (i.e., `verifyAddressInbound(...)` returns `(true, NONE)`).
### 3.1 Constraints
`receivePolicyId` MUST reference a simple `WHITELIST` policy, a simple `BLACKLIST` policy, or built-in policy `0` or `1`. It MUST NOT reference a `COMPOUND` policy. Address-level receive controls evaluate only one axis — whether a given inbound originator may credit the receiver — while TIP-1015 `COMPOUND` policies split authorization across sender, transfer-recipient, and mint-recipient roles.
-`recoveryContract` MAY be `address(0)`. If nonzero, it designates the sole direct claimer for future blocked receipts for this receiver and MUST NOT equal `ESCROW_ADDRESS` or be a TIP-1022 virtual address.
+`recoveryContract` MAY be `address(0)`. If nonzero, it designates the sole direct claimer for **future** blocked receipts for this receiver and MUST NOT equal `ESCROW_ADDRESS` or be a TIP-1022 virtual address.
-### 3.2 Blocked Reason and Recovery Contract
+TIP-1022 virtual addresses are forwarding aliases, not canonical TIP-20 holders. `setAddressReceivePolicy()` MUST reject TIP-1022 virtual addresses and require configuration on the resolved master address instead.
+
+### 3.2 Blocked Reasons
```solidity
enum BlockedReason {
@@ -184,9 +310,15 @@ enum BlockedReason {
}
```
-`BlockedReason` classifies why an inbound was escrowed; `NONE` is used only when the inbound is authorized. Each blocked inbound snapshots the receiver's current `recoveryContract`. That snapshot becomes part of the blocked receipt and its storage key and governs later claims for that receipt. Changing `recoveryContract` affects only future receipts.
+`BlockedReason` classifies why an inbound was escrowed. `NONE` is used only when the inbound is authorized; blocked events MUST NOT set `blockedReason == NONE`.
+
+### 3.3 Recovery Contract Semantics
+
+Each blocked inbound snapshots the receiver's *current* `recoveryContract` at block time. That snapshot becomes part of the blocked receipt and its storage key, and governs later claims for that receipt. **Changing `recoveryContract` affects only future receipts.** Existing receipt buckets remain governed by the recovery contract captured in their key.
-### 3.3 Packed Storage
+This makes recovery-contract authority explicit and non-retroactive. Receivers that rotate `recoveryContract` SHOULD keep the previous contract callable until receipts keyed to it are drained.
+
+### 3.4 Packed Storage
```solidity
mapping(address => uint256) public addressReceiveConfig;
@@ -206,7 +338,7 @@ When `hasAddressPolicy == 0`, the address is always authorized at the address le
`recoveryContract` is stored separately because a 160-bit address does not fit in the packed config slot.
-### 3.4 Interface
+### 3.5 Interface
```solidity
interface IAddressReceivePolicies {
@@ -237,18 +369,21 @@ interface IAddressReceivePolicies {
Implementations SHOULD read `addressRecoveryContract[to]` only after `verifyAddressInbound(...)` returns `authorized = false`.
-### 3.5 Authorization Logic
+### 3.6 Authorization Logic
-`verifyAddressInbound(token, originator, to)` MUST read the packed config for `to`. If `hasAddressPolicy == 0`, it MUST return `(true, NONE)`. Otherwise it MUST decode `receivePolicyId`, `receivePolicyType`, `tokenSetId`, and `tokenSetType`, evaluate whether `token` is allowed by the token set and whether `originator` is allowed by the receive policy, and return:
+`verifyAddressInbound(token, originator, to)` MUST:
-- `(true, NONE)` if both checks pass
-- `(false, TOKEN_SET_AND_RECEIVE_POLICY)` if both checks fail
-- `(false, TOKEN_SET)` if only the token-set check fails
-- `(false, RECEIVE_POLICY)` if only the receive-policy check fails
+1. Read the packed config for `to`.
+2. If `hasAddressPolicy == 0`, return `(true, NONE)`.
+3. Otherwise, decode `receivePolicyId`, `receivePolicyType`, `tokenSetId`, and `tokenSetType`. Evaluate whether `token` is allowed by the token set, and whether `originator` is allowed by the receive policy. Return:
+ - `(true, NONE)` if both checks pass.
+ - `(false, TOKEN_SET_AND_RECEIVE_POLICY)` if both fail.
+ - `(false, TOKEN_SET)` if only the token-set check fails.
+ - `(false, RECEIVE_POLICY)` if only the receive-policy check fails.
An address that wants to functionally disable filtering SHOULD set `receivePolicyId = 1` and `tokenSetId = 1`. The slot remains allocated.
-### 3.6 Events and Errors
+### 3.7 Events and Errors
```solidity
event AddressReceivePolicyUpdated(
@@ -264,15 +399,26 @@ error InvalidRecoveryContract();
## 4. Escrow Precompile
-Blocked inbounds are recorded in a dedicated escrow precompile. The raw TIP-20 balance is held at `ESCROW_ADDRESS` inside each TIP-20 token; the precompile stores only one keyed amount for each open blocked receipt.
+The escrow precompile is a new system precompile dedicated to blocked-inbound bookkeeping. It lives at `ESCROW_ADDRESS`: the blocked balance for each TIP-20 token sits in that token's `balances[ESCROW_ADDRESS]` slot — i.e., held by this precompile — while the precompile's own storage records **only one keyed amount per open blocked receipt**. The rest of the receipt identity is authenticated by the witness fields supplied at claim time and is emitted in the blocked-inbound event when the receipt is created.
+
+### 4.1 Precompile Address
```solidity
-ESCROW_ADDRESS = 0xE5C0000000000000000000000000000000000000
+address constant ESCROW_ADDRESS = 0xE5C0000000000000000000000000000000000000;
```
-### 4.1 Storage
+`ESCROW_ADDRESS` is the address of this precompile. Calls to it execute precompile code, and `balances[ESCROW_ADDRESS]` inside each TIP-20 token represents tokens held by this precompile. It is a system precompile, not a userland account, and the TIP-20 layer never enumerates blocked receipts at this address — receipt accounting lives in the precompile's own storage ([Section 4.2](#42-storage)).
+
+Reservation rules:
+
+- Userland TIP-20 calls with `to == ESCROW_ADDRESS` (transfer-like or mint-like) MUST revert with `EscrowAddressReserved()`.
+- `setAddressReceivePolicy(...)` MUST reject `ESCROW_ADDRESS` and any `recoveryContract == ESCROW_ADDRESS`.
+- Reroute claims with `to == ESCROW_ADDRESS` MUST revert.
+- Any TIP-20 logic that protects DEX or FeeManager balances as system balances MUST extend the same protection to `ESCROW_ADDRESS`.
-Each blocked inbound creates exactly one fine-grained receipt bucket.
+For reward accounting purposes (see [Section 6.3](#63-reward-subsystem)), `ESCROW_ADDRESS` is a reward-exempt always-opted-out synthetic sink/source: blocked transfers, blocked mints, and claim releases MUST preserve the same opted-in-supply effects as a movement into or out of an always-opted-out address, and implementations MUST NOT create, update, or consult per-user reward state for `ESCROW_ADDRESS`.
+
+### 4.2 Storage
```solidity
uint8 public constant BLOCKED_RECEIPT_VERSION = 1;
@@ -302,18 +448,17 @@ receiptKey = keccak256(
where:
-- `receiptVersion` is a one-byte bucketing version tag and MUST currently be `1`
-- `receiver` is the canonical TIP-20 holder that owns the blocked receipt
-- `originator` is `from` for transfers and mint caller for mints
-- `requestedRecipient` is the literal `to` address and therefore preserves TIP-1022 attribution
-- `recoveryContract` is the receiver's snapshotted recovery contract, or `address(0)`
-- `blockedReason` records whether the receiver blocked the inbound because of its token set, receive policy, or both
-- `kind` distinguishes transfer-blocked from mint-blocked receipts
-- `memo` preserves the original memo for memo-bearing paths and is `bytes32(0)` otherwise
-- `blockedAt` is the block timestamp captured when the receipt is recorded
-- `blockedNonce` is a monotonically increasing global disambiguator assigned at receipt creation time
-
-Future bucketing or receipt-key formats MUST use a different `receiptVersion` value.
+- `receiptVersion` — one-byte bucketing tag; MUST be `1` for receipts created under this TIP. Future bucketing or receipt-key formats MUST use a different value.
+- `token` — the TIP-20 token whose balance ledger holds the blocked amount at `ESCROW_ADDRESS`
+- `receiver` — the canonical TIP-20 holder that owns the receipt (resolved master address for TIP-1022 inbounds)
+- `originator` — `from` for transfers, mint caller for mints
+- `requestedRecipient` — the literal `to` supplied at the TIP-20 entrypoint; preserves TIP-1022 attribution. For non-virtual inbounds, `requestedRecipient == receiver`
+- `recoveryContract` — the receiver's snapshotted recovery contract, or `address(0)`
+- `blockedReason` — receiver's token set, receive policy, or both
+- `kind` — `TRANSFER` or `MINT` (see [Section 4.3](#43-interface))
+- `memo` — original memo for memo-bearing paths, `bytes32(0)` otherwise
+- `blockedAt` — block timestamp captured at receipt creation
+- `blockedNonce` — monotonically increasing global disambiguator assigned at receipt creation
`blockedReceiptAmount[receiptKey]` stores the full amount for that open receipt.
@@ -321,9 +466,9 @@ The escrow precompile does not need to store the rest of the receipt field-by-fi
For TIP-1022 virtual-address inbounds, `receiver` is the resolved master address while `requestedRecipient` preserves the literal virtual address.
-This is the same storage pattern as ordinary bucketing, but with a much finer key: one keyed amount per blocked inbound rather than one keyed amount per originator-wide aggregate. The precompile does **not** store receiver-wide aggregate balances, signer lists or multisig state, any global singleton recovery-policy state, or a field-by-field receipt struct in persistent storage.
+It also does not enumerate receiver-owned receipts onchain. Claimers MUST supply the receipt witness for the receipt they want to consume, typically using logs or offchain indexing. The protocol claim interface is intentionally single-receipt; recovery contracts or callers that want batching MAY loop or use multicall outside the protocol surface.
-### 4.2 Interface
+### 4.3 Interface
```solidity
interface IBlockedInboundEscrow {
@@ -375,52 +520,76 @@ interface IBlockedInboundEscrow {
}
```
-`recordBlockedInbound(...)` MUST be callable only by TIP-20 precompiles or protocol-internal system code.
+`recordBlockedInbound(...)` MUST be callable only by TIP-20 precompiles or protocol-internal system code. User callers MUST NOT be able to fabricate receipts.
-`requestedRecipient` is the literal `to` supplied to the TIP-20 entrypoint. For non-virtual inbounds, `requestedRecipient == receiver`. For TIP-1022 virtual-address inbounds, `receiver` is the resolved master address while `requestedRecipient` is the literal virtual address. `receipt.receiptVersion` MUST be `1` for receipts created under this TIP. The precompile does not enumerate receiver-owned receipts onchain. Claimers MUST supply the blocked receipt they want to consume, typically using logs or offchain indexing. The protocol claim interface is intentionally single-receipt; recovery contracts or callers that want batching MAY loop or use multicall outside the protocol surface.
-
-### 4.3 Claim Authorization
+### 4.4 Claim Authorization
Each blocked receipt is governed by the `recoveryContract` captured for that receipt at block time.
-`claimBlockedReceipt(...)` consumes only the explicitly supplied receipt bucket and releases only to `to`. It MUST require `msg.sender == receiver` when `recoveryContract == address(0)` and `msg.sender == recoveryContract` when `recoveryContract != address(0)`. It MUST interpret `receipt` as the tuple `(receipt.receiptVersion, token, receiver, receipt.originator, receipt.requestedRecipient, recoveryContract, receipt.blockedReason, receipt.kind, receipt.memo, receipt.blockedAt, receipt.blockedNonce)`, require `blockedReceiptAmount[receiptKey] > 0`, and consume the entire stored amount for that receipt or revert. Partial claims are not allowed.
+`claimBlockedReceipt(...)` consumes only the explicitly supplied receipt bucket and releases only to `to`. It MUST:
+
+- require `msg.sender == receiver` when `recoveryContract == address(0)`,
+- require `msg.sender == recoveryContract` when `recoveryContract != address(0)`,
+- interpret `receipt` as the witness tuple `(receipt.receiptVersion, token, receiver, receipt.originator, receipt.requestedRecipient, recoveryContract, receipt.blockedReason, receipt.kind, receipt.memo, receipt.blockedAt, receipt.blockedNonce)`,
+- recompute `receiptKey`,
+- require `blockedReceiptAmount[receiptKey] > 0`,
+- consume the entire stored amount for that receipt and delete the slot, or revert with `InvalidReceiptClaim()`.
-The supplied receipt witness is a selector, not an authority. Claim rights flow only from `receiver` or the snapshotted `recoveryContract`.
+**Partial claims are not allowed.** Claims MUST consume whole receipts.
-If a receiver wants delegate whitelists, originator self-claim, multisig approval, timelocks, or any other richer recovery policy, it SHOULD set `recoveryContract` to a userland contract or smart wallet that enforces that policy. See Section 4.6 and Appendix A for a non-normative standard pattern.
+The supplied receipt witness is a *selector*, not an authority. Claim rights flow only from `receiver` or the snapshotted `recoveryContract`. Receivers who want delegate whitelists, originator self-claim, multisig approval, timelocks, batching, or any richer recovery policy SHOULD set `recoveryContract` to a userland contract or smart wallet that enforces that policy. See [Section 4.6](#46-standard-recovery-contract-pattern-non-normative) and Appendix A for a non-normative reference pattern.
-### 4.4 Release Semantics
+### 4.5 Release Semantics
-All claims release only to the specified `to`.
+All claims release only to the caller-specified `to`. The escrow precompile MUST call an internal TIP-20 escrow-release path that:
-The escrow precompile MUST call an internal TIP-20 escrow-release path that:
+1. debits `balances[ESCROW_ADDRESS]`,
+2. credits the beneficiary,
+3. emits `Transfer(ESCROW_ADDRESS, beneficiary, amount)`,
+4. bypasses the token-level TIP-403 sender check for `ESCROW_ADDRESS`,
+5. treats `ESCROW_ADDRESS` as a reward-exempt always-opted-out synthetic sink/source.
-1. debits `balances[ESCROW_ADDRESS]`
-2. credits the beneficiary
-3. emits `Transfer(ESCROW_ADDRESS, beneficiary, amount)`
-4. bypasses the token-level TIP-403 sender check for `ESCROW_ADDRESS`
-5. treats `ESCROW_ADDRESS` as a reward-exempt always-opted-out synthetic sink/source
+The two release modes differ only in what they enforce on the destination side.
-If `to == receiver`, the claim is an unwind of a previously authorized inbound to that receiver. It MUST:
+#### 4.5.1 Claim to Receiver (Unwind)
-- bypass the receiver's address-level receive controls
-- bypass token-level recipient authorization for the receiver
-- bypass AccountKeychain spending-limit metering
+If `to == receiver`, the claim is an **unwind** of a previously authorized inbound. It MUST:
-This is intentional for TIP-1015 compound policies. A blocked mint has already satisfied the token's original `mint_recipient` authorization on the inbound path. Releasing that escrow back to the same `receiver` is an unwind of that previously authorized mint-like inbound, not a new transfer, so it MUST NOT be rechecked against transfer-recipient authorization.
+- bypass the receiver's address-level receive controls,
+- bypass token-level recipient authorization for the receiver,
+- bypass AccountKeychain spending-limit metering.
-If `to != receiver`, the claim is a rerouted release. It MUST:
+This is intentional for TIP-1015 compound policies. A blocked mint, for example, has already satisfied the token's original `mint_recipient` authorization on the inbound path. Releasing that escrow back to the same receiver is an unwind of a previously authorized mint-like inbound, not a new transfer, and MUST NOT be rechecked against transfer-recipient authorization.
+
+#### 4.5.2 Reroute (`to != receiver`)
+
+If `to != receiver`, the claim is a **reroute**, treated as a new spend by the receiver. It MUST:
- reject `to == ESCROW_ADDRESS`
- reject TIP-1022 virtual addresses as `to`
- enforce token-level transfer-recipient authorization for `to`
-- enforce `to`'s address-level receive controls against the receipt's `originator`
-- revert if that receipt fails the destination's address-level checks
+- enforce `to`'s address-level receive controls against the consumed receipt's `originator`
+- revert with `ClaimDestinationUnauthorized()` if the destination's checks fail
- if `recoveryContract == address(0)` and the reroute is initiated through an access key, meter the total claimed amount against the receiver's AccountKeychain spending limit exactly as an ordinary TIP-20 spend by the receiver
-If a receiver installs a custom `recoveryContract`, any equivalent delegation, timelock, multisig, or key policy is a userland concern.
+If a receiver installs a custom `recoveryContract`, any equivalent delegation, timelock, multisig, or key-policy enforcement is a userland concern of that contract.
+
+### 4.6 Standard Recovery Contract Pattern (Non-Normative)
+
+The protocol does not mandate any particular recovery-contract design. The standard pattern is a reusable receiver-owned implementation, not a global singleton:
+
+- the receiver deploys its own instance, proxy, or smart-wallet module;
+- sets `recoveryContract` to that instance via `setAddressReceivePolicy(...)`;
+- stores one canonical `receiver`;
+- uses `claimToReceiver(...)` as the default unwind path;
+- optionally permits reroutes to `to != receiver`;
+- if originator self-claim is supported, restricts it to receipts whose supplied `originator` equals the caller and only releases to the caller itself.
+
+If the receiver rotates to a new recovery contract, the old contract SHOULD remain callable until receipts keyed to it are drained (see [Section 3.3](#33-recovery-contract-semantics)).
+
+Appendix A gives a non-normative Solidity reference design for this pattern.
-### 4.5 Events and Errors
+### 4.7 Events and Errors
```solidity
event TransferBlocked(
@@ -474,14 +643,10 @@ error InvalidReceiptClaim();
A successful claim MUST emit exactly one `BlockedReceiptClaimed` event for the consumed receipt.
-### 4.6 Standard Recovery Contract Pattern (Non-Normative)
-
-The protocol does not mandate any particular recovery-contract design. The standard pattern is a reusable receiver-owned implementation, not a global singleton: the receiver deploys its own instance, proxy, or smart-wallet module; sets `recoveryContract` to that instance in `setAddressReceivePolicy(...)`; stores one canonical `receiver`; uses `claimToReceiver(...)` as the default unwind path; optionally permits reroutes to `to != receiver`; and, if originator self-claim is supported, restricts it to receipts whose supplied `originator` equals the caller and only to the caller itself. If the receiver rotates to a new recovery contract, the old contract SHOULD remain callable until receipts keyed to it are drained.
-
-Appendix A gives a non-normative Solidity reference design for this pattern.
-
## 5. TIP-20 Inbound Path Changes
+This section specifies the exact integration each TIP-20 path takes. All new logic lives in the TIP-20 token precompile; the token consults TIP-403 ([Section 3](#3-address-level-receive-controls-tip-403-extension)) and the escrow precompile ([Section 4](#4-blocked-inbound-escrow-precompile)) but owns the inbound dispatch.
+
Userland TIP-20 transfers or mints directly to `ESCROW_ADDRESS` MUST revert:
```solidity
@@ -490,111 +655,129 @@ error EscrowAddressReserved();
### 5.1 Transfer-like Paths
-For a transfer-like path:
+For `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, and `systemTransferFrom`:
- if `to == ESCROW_ADDRESS`, revert with `EscrowAddressReserved()`
- set `memo = bytes32(0)` for non-memo variants, or the supplied memo for memo-bearing variants
- compute `effectiveReceiver`:
- - `resolveRecipient(to)` if `to` is a TIP-1022 virtual address
+ - `resolveRecipient(to)` if `to` is a TIP-1022 virtual address (revert if resolution fails),
- otherwise `to`
- set `requestedRecipient = to`
-- run the existing TIP-20 pause, balance, allowance, and token-level TIP-403 and TIP-1015 checks, using `effectiveReceiver` wherever the current path is recipient-sensitive
-- call `verifyAddressInbound(token, from, effectiveReceiver)` and capture `blockedReason`
-- if the inbound is authorized:
- - follow the normal transfer path using `effectiveReceiver`
- - if `to` is virtual, use TIP-1022 forwarding event semantics
- - update rewards exactly as on a normal transfer
- - debit `from`
- - credit `effectiveReceiver`
- - return success
-- if the inbound is blocked by the receiver:
- - read the current `addressRecoveryContract[effectiveReceiver]` and capture `recoveryContract`
- - update rewards as if the raw recipient were a reward-exempt always-opted-out `ESCROW_ADDRESS`
- - debit `from`
- - credit `ESCROW_ADDRESS`
- - call `recordBlockedInbound(token, from, effectiveReceiver, requestedRecipient, recoveryContract, amount, blockedReason, TRANSFER, memo)` and capture `(blockedNonce, blockedAt)`
- - emit `Transfer(from, ESCROW_ADDRESS, amount)`
- - emit `TransferBlocked(token, from, effectiveReceiver, BLOCKED_RECEIPT_VERSION, blockedNonce, blockedAt, requestedRecipient, amount, blockedReason, recoveryContract, memo)`
- - return success
+- run the existing TIP-20 pause, balance, allowance, and token-level TIP-403 / TIP-1015 checks, using `effectiveReceiver` wherever the current path is recipient-sensitive (failure here MUST revert)
+- call `verifyAddressInbound(token, from, effectiveReceiver)` and capture `(authorized, blockedReason)`
+
+If the inbound is authorized:
+
+- follow the normal transfer path using `effectiveReceiver`
+- if `to` is virtual, use TIP-1022 forwarding event semantics
+- update rewards exactly as on a normal transfer
+- debit `from`, credit `effectiveReceiver`, return success
+
+If the inbound is blocked by the receiver:
+
+- read the current `addressRecoveryContract[effectiveReceiver]` and capture `recoveryContract`
+- update rewards as if the raw recipient were a reward-exempt always-opted-out `ESCROW_ADDRESS`
+- debit `from`, credit `ESCROW_ADDRESS`
+- call `recordBlockedInbound(token, from, effectiveReceiver, requestedRecipient, recoveryContract, amount, blockedReason, TRANSFER, memo)` and capture `(blockedNonce, blockedAt)`
+- emit `Transfer(from, ESCROW_ADDRESS, amount)`
+- emit `TransferBlocked(token, from, effectiveReceiver, BLOCKED_RECEIPT_VERSION, blockedNonce, blockedAt, requestedRecipient, amount, blockedReason, recoveryContract, memo)`
+- return success
Memo-bearing transfer variants MUST preserve the original memo in the blocked event. Their raw memo-bearing TIP-20 event MUST still name `ESCROW_ADDRESS` as the raw recipient when blocked.
### 5.2 Mint-like Paths
-For a mint-like path:
+For `mint` and `mintWithMemo`:
- if `to == ESCROW_ADDRESS`, revert with `EscrowAddressReserved()`
- set `memo = bytes32(0)` for non-memo variants, or the supplied memo for memo-bearing variants
-- compute `effectiveReceiver`:
- - `resolveRecipient(to)` if `to` is a TIP-1022 virtual address
- - otherwise `to`
+- compute `effectiveReceiver` as in [Section 5.1](#51-transfer-like-paths)
- set `requestedRecipient = to`
- run the existing issuer-role, mint-recipient, and supply-cap checks, using `effectiveReceiver` wherever the current path is recipient-sensitive
-- call `verifyAddressInbound(token, originator, effectiveReceiver)` and capture `blockedReason`, where `originator` is the mint caller
-- if the inbound is authorized:
- - follow the normal mint path using `effectiveReceiver`
- - if `to` is virtual, use TIP-1022 forwarding event semantics
- - update rewards exactly as on a normal mint
- - increase total supply
- - credit `effectiveReceiver`
- - return success
-- if the inbound is blocked by the receiver:
- - read the current `addressRecoveryContract[effectiveReceiver]` and capture `recoveryContract`
- - update rewards as if the raw recipient were a reward-exempt always-opted-out `ESCROW_ADDRESS`
- - increase total supply
- - credit `ESCROW_ADDRESS`
- - call `recordBlockedInbound(token, originator, effectiveReceiver, requestedRecipient, recoveryContract, amount, blockedReason, MINT, memo)` and capture `(blockedNonce, blockedAt)`
- - emit `Transfer(address(0), ESCROW_ADDRESS, amount)` and `Mint(ESCROW_ADDRESS, amount)`
- - emit `MintBlocked(token, originator, effectiveReceiver, BLOCKED_RECEIPT_VERSION, blockedNonce, blockedAt, requestedRecipient, amount, blockedReason, recoveryContract, memo)`
- - return success
+- call `verifyAddressInbound(token, originator, effectiveReceiver)` and capture `(authorized, blockedReason)`, where `originator` is the mint caller
-`mintWithMemo` MUST preserve the original memo in the blocked event. Its raw mint-related events MUST still name `ESCROW_ADDRESS` as the raw recipient when blocked.
+If the inbound is authorized:
-### 5.3 Reward Delegation and Claims
+- follow the normal mint path using `effectiveReceiver`
+- if `to` is virtual, use TIP-1022 forwarding event semantics
+- update rewards exactly as on a normal mint
+- increase total supply, credit `effectiveReceiver`, return success
-Reward flows are never escrowed, but they MUST respect recipient consent. For `setRewardRecipient(holder, recipient)`, `recipient == address(0)` remains the opt-out path and bypasses receive-policy checks; otherwise the token MUST call `verifyAddressInbound(token, holder, recipient)` and revert if unauthorized. For `claimRewards(...)`, the token MUST determine the actual payout recipient under its existing reward rules, revalidate that recipient with `verifyAddressInbound(token, address(token), recipient)` before crediting rewards, and revert rather than escrow if unauthorized.
+If the inbound is blocked by the receiver:
-### 5.4 Reward and Event Semantics
+- read the current `addressRecoveryContract[effectiveReceiver]` and capture `recoveryContract`
+- update rewards as if the raw recipient were a reward-exempt always-opted-out `ESCROW_ADDRESS`
+- increase total supply, credit `ESCROW_ADDRESS`
+- call `recordBlockedInbound(token, originator, effectiveReceiver, requestedRecipient, recoveryContract, amount, blockedReason, MINT, memo)` and capture `(blockedNonce, blockedAt)`
+- emit `Transfer(address(0), ESCROW_ADDRESS, amount)` and `Mint(ESCROW_ADDRESS, amount)`
+- emit `MintBlocked(token, originator, effectiveReceiver, BLOCKED_RECEIPT_VERSION, blockedNonce, blockedAt, requestedRecipient, amount, blockedReason, recoveryContract, memo)`
+- return success
+
+`mintWithMemo` MUST preserve the original memo in the blocked event. Its raw mint-related events MUST still name `ESCROW_ADDRESS` as the raw recipient when blocked.
-Blocked transfers, blocked mints, and claim releases MUST treat `ESCROW_ADDRESS` as a reward-exempt always-opted-out synthetic sink/source.
+### 5.3 Raw Event Truthfulness
Blocked inbounds MUST use truthful raw TIP-20 events:
- blocked transfer: `Transfer(from, ESCROW_ADDRESS, amount)`
- blocked mint: `Transfer(address(0), ESCROW_ADDRESS, amount)` and `Mint(ESCROW_ADDRESS, amount)`
+- claim release: `Transfer(ESCROW_ADDRESS, beneficiary, amount)`
-In addition, every blocked inbound MUST emit exactly one attribution event:
+In addition, every blocked inbound MUST emit exactly one attribution event (`TransferBlocked` or `MintBlocked`) whose `blockedReason != NONE`. For blocked TIP-1022 deposits, `requestedRecipient` preserves the literal virtual address while `receiver` names the resolved master address that owns the receipt:
- `TransferBlocked(token, from, receiver, receiptVersion, blockedNonce, blockedAt, requestedRecipient, amount, blockedReason, recoveryContract, memo)`
- `MintBlocked(token, operator, receiver, receiptVersion, blockedNonce, blockedAt, requestedRecipient, amount, blockedReason, recoveryContract, memo)`
-`blockedReason` MUST distinguish whether the inbound was blocked by the receiver's token set, the receiver's receive policy, or both. It MUST NOT be `NONE` in a blocked event.
+## 6. Adjacent Subsystems
-For blocked TIP-1022 deposits, `requestedRecipient` preserves the literal virtual address while `receiver` names the resolved master address that owns the receipt.
+The general rule is: every TIP-20 outbound onto the canonical balance ledger goes through the inbound dispatch in [Section 5](#5-tip-20-inbound-path-changes) and is therefore subject to receiver-side checks. This section calls out each adjacent subsystem and its specific handling.
-### 5.5 Tempo-Specific Protocol Interactions
+### 6.1 DEX, FeeManager, and TIPFeeAMM Wallet Payouts
-- **Stablecoin DEX internal balances are out of scope.** Internal DEX balances are not TIP-20 wallet balances and are not gated until withdrawn back onto the TIP-20 ledger.
+- **DEX internal balances are out of scope.** Internal DEX balances are not TIP-20 wallet balances and are not gated until withdrawn back onto the TIP-20 ledger.
- **DEX wallet payouts remain ordinary TIP-20 transfers.** DEX `withdraw` calls and swap outputs that transfer from the DEX address to a wallet remain subject to address-level receive controls and may therefore be escrowed.
- **FeeManager and TIPFeeAMM payouts remain ordinary TIP-20 transfers.** Validator fee distributions, AMM burns, and TIPFeeAMM outputs such as `rebalanceSwap` payouts remain subject to address-level receive controls and may therefore be escrowed.
-- **These protocol entrypoints become processed-vs-credited operations.** A DEX, FeeManager, or AMM call may complete successfully even if the final TIP-20 outbound was escrowed rather than credited to the intended wallet. Integrations that require guaranteed wallet credit MUST inspect `TransferBlocked` or `MintBlocked` or wrap these calls with additional logic.
-- **Fee refunds are exempt.** `transfer_fee_post_tx` is a refund of the current transaction's unused fee deposit, not a new third-party inbound. It MUST bypass address-level receive controls and MUST NOT be escrowed.
-- **Blocked memo-bearing inbounds keep their raw memo event.** The raw memo-bearing event still names `ESCROW_ADDRESS`; receivers that care about memo-based routing MUST correlate it with the blocked-receipt event in the same transaction.
-- **`ESCROW_ADDRESS` is a protected system address.** Any TIP-20 logic that protects DEX or FeeManager balances as system balances MUST extend the same protection to `ESCROW_ADDRESS`.
+- **Higher-level entrypoints become processed-vs-credited operations.** A DEX, FeeManager, or AMM call may complete successfully even if the final TIP-20 outbound was escrowed rather than credited to the intended wallet. Integrations that require guaranteed wallet credit MUST inspect `TransferBlocked` / `MintBlocked` or wrap these calls with additional logic. This applies to higher-level Tempo precompile events too; they are not rewritten to distinguish direct credit from escrow.
+
+### 6.2 Fee Refunds
+
+`transfer_fee_post_tx` is a refund of the current transaction's unused fee deposit, not a new third-party inbound. It MUST bypass address-level receive controls and MUST NOT be escrowed.
+
+### 6.3 Reward Subsystem
+
+Reward flows are **never escrowed**, but they are **not exempt from recipient consent**.
-### 5.6 Integration Consequence
+- `distributeReward` remains token-internal accounting.
+- `setRewardRecipient(holder, recipient)`:
+ - `recipient == address(0)` is the opt-out path and bypasses receive-policy checks.
+ - Otherwise the token MUST call `verifyAddressInbound(token, holder, recipient)` and revert if unauthorized.
+- `claimRewards(...)`:
+ - The token determines the actual payout recipient under its existing reward rules.
+ - It MUST revalidate that recipient with `verifyAddressInbound(token, address(token), recipient)` and revert (rather than escrow) if unauthorized.
-After TIP-1028, `transfer`, `transferFrom`, `mint`, `mintWithMemo`, DEX wallet payouts, and FeeManager or TIPFeeAMM wallet payouts may succeed either because the intended receiver was credited or because the inbound was escrowed. Contracts and offchain systems that must distinguish those outcomes MUST inspect `TransferBlocked` or `MintBlocked` or use wrapper logic. This includes higher-level Tempo precompile events, which are not rewritten to distinguish direct credit from escrow.
+For reward-state purposes, blocked transfers, blocked mints, and claim releases MUST treat `ESCROW_ADDRESS` as a reward-exempt always-opted-out synthetic sink/source. Implementations MUST NOT create, update, or consult per-user reward state for `ESCROW_ADDRESS`.
-## 6. Gas and Storage Analysis
+### 6.4 Memo-Bearing Paths
+
+Blocked memo-bearing inbounds keep their raw memo event. The raw memo-bearing event still names `ESCROW_ADDRESS`; receivers that care about memo-based routing MUST correlate it with the blocked-receipt event in the same transaction.
+
+### 6.5 Out-of-Scope Surfaces
+
+`approve`, `permit`, `burn`, non-TIP-20 tokens deployed as ordinary contracts, and future recipient-bearing system-credit paths that do not identify a concrete originator are unchanged by this TIP.
+
+### 6.6 Integration Consequence
+
+After TIP-1028, `transfer`, `transferFrom`, `mint`, `mintWithMemo`, DEX wallet payouts, and FeeManager / TIPFeeAMM wallet payouts may succeed either because the intended receiver was credited or because the inbound was escrowed. Contracts and offchain systems that must distinguish those outcomes MUST inspect `TransferBlocked` / `MintBlocked` events or use wrapper logic.
+
+## 7. Gas and Storage Analysis
This section uses the gas model from TIP-1016:
-- fresh storage slot: **250,000 gas**
+- fresh storage slot: **~250,000 gas**
- existing nonzero slot update: **~2,900 gas**
- typical TIP-20 transfer to an existing address: **~50,000 gas**
-### 6.1 Main Cases
+### 7.1 Main Cases
| Case | Rough cost | Notes |
|------|------------|-------|
@@ -606,34 +789,44 @@ This section uses the gas model from TIP-1016:
| blocked inbound without escrow-slot preinitialization | previous row + `~250k` | first live zero-to-nonzero write to `balances[ESCROW_ADDRESS]` |
| claim one receipt | one transfer from escrow + one receipt-slot delete + auth reads | batching belongs above the protocol layer |
-### 6.2 Storage Choices
+### 7.2 Storage Choices
+
+The design intentionally stores **one fine-grained receipt bucket per blocked inbound**. That is more expensive than an aggregate bucket, but it preserves:
+
+- the literal `requestedRecipient` needed for TIP-1022 attribution,
+- the original `originator`,
+- the block timestamp `blockedAt`,
+- the distinction between transfer-blocked and mint-blocked funds,
+- memo and block-reason data for programmable recovery rules.
-The design intentionally stores one fine-grained receipt bucket per blocked inbound. That is more expensive than an aggregate bucket, but it preserves the literal `requestedRecipient` needed for TIP-1022 attribution, the original `originator`, the block timestamp `blockedAt`, the distinction between transfer-blocked and mint-blocked funds, and memo and block-reason data for programmable recovery rules. The persistent state is still just one keyed amount per blocked inbound, while the richer receipt metadata is authenticated through the receipt witness and surfaced in the blocked events rather than stored as a multi-slot onchain struct.
+The persistent state is still just one keyed amount per blocked inbound. The richer receipt metadata is authenticated through the receipt witness and surfaced in the blocked events rather than stored as a multi-slot onchain struct.
-### 6.3 Escrow Slot Strategy
+### 7.3 Escrow Slot Strategy
The first zero-to-nonzero write to `balances[ESCROW_ADDRESS]` for a token can add roughly `250,000` gas to the first blocked live transfer.
-TIP-20 implementations SHOULD move that cost to deployment by initializing the `balances[ESCROW_ADDRESS]` slot when the token is created, before any blocked inbound occurs. One acceptable pattern is to do this with a non-user-claimable implementation-private escrow reserve. Implementations MAY use any equivalent deployment-time mechanism instead.
+TIP-20 implementations SHOULD move that cost to deployment by initializing the `balances[ESCROW_ADDRESS]` slot when the token is created, before any blocked inbound occurs. One acceptable pattern is to do this with a non-user-claimable implementation-private escrow reserve; implementations MAY use any equivalent deployment-time mechanism instead.
If an implementation uses such a reserve:
-- it MUST be created at token deployment time
-- it MUST exist only to initialize and keep live the `balances[ESCROW_ADDRESS]` slot
-- it MUST NOT correspond to any blocked receipt
-- it MUST NOT be claimable by users or recovery contracts
-- release and burn logic MUST preserve it as implementation-private state
+- it MUST be created at token deployment time;
+- it MUST exist only to initialize and keep live the `balances[ESCROW_ADDRESS]` slot;
+- it MUST NOT correspond to any blocked receipt;
+- it MUST NOT be claimable by users or recovery contracts;
+- release and burn logic MUST preserve it as implementation-private state.
-## 7. Security and Integration Considerations
+## 8. Security and Integration Considerations
-- **Success no longer implies receiver credit.** A successful transfer or mint means the inbound was processed, not necessarily that the intended receiver's balance increased.
-- **Ordinary contracts should usually not opt in.** A contract address that enables receive controls can cause callers to observe a successful `transfer`, `transferFrom`, or mint-like payout even though the asset was escrowed instead of credited to the contract. Contracts that are not explicitly built to inspect blocked-receipt events and claim from escrow SHOULD NOT opt in.
-- **Claim-to-receiver is an unwind, while reroutes are new transfers.** Claim-to-receiver bypasses the receiver's token-level and address-level receive checks. A reroute to `to != receiver` must satisfy token-level recipient authorization for `to`, that destination's address-level receive controls, and, for direct receiver reroutes through an access key, ordinary AccountKeychain spending-limit metering.
-- **Recovery-contract authority is explicit and not retroactive.** If a receiver sets `recoveryContract`, that address is the sole direct claimer for future blocked receipts. Any delegate whitelist, originator self-claim, multisig approval, timelock policy, key-spend policy, or batching policy for that path becomes a userland concern of the recovery contract. Older receipts remain keyed to the recovery contract captured when they were blocked, so rotation can strand funds if the old contract later becomes unusable.
+- **Success no longer implies receiver credit.** A successful transfer or mint means the inbound was processed, not necessarily that the intended receiver's balance increased. This is the most important behavioral change for integrators.
+- **Ordinary contracts should usually not opt in.** A contract address that enables receive controls can cause callers to observe a successful `transfer` / `transferFrom` / mint-like payout even though the asset was escrowed instead of credited to the contract. Contracts that are not explicitly built to inspect blocked-receipt events and claim from escrow SHOULD NOT opt in.
+- **Claim-to-receiver is an unwind; reroutes are new transfers.** Claim-to-receiver bypasses the receiver's token-level and address-level receive checks. Reroutes to `to != receiver` must satisfy token-level recipient authorization for `to`, that destination's address-level receive controls, and (for direct receiver reroutes through an access key) ordinary AccountKeychain spending-limit metering.
+- **Recovery-contract authority is explicit and not retroactive.** If a receiver sets `recoveryContract`, that address is the sole direct claimer for future blocked receipts. Any delegate whitelist, originator self-claim, multisig approval, timelock, key-spend policy, or batching is a userland concern of the recovery contract. Older receipts remain keyed to the recovery contract captured when they were blocked, so rotation can strand funds if the old contract later becomes unusable.
- **Reward delegation requires consent.** `setRewardRecipient` and `claimRewards` now fail if the target recipient's address-level receive controls reject the reward flow.
- **Policy configuration is permanent state.** An address can functionally disable filtering by setting allow-all values, but the storage slot remains allocated.
+- **Receipt witnesses are public.** Blocked-inbound events emit the full witness; anyone can construct a `ClaimReceipt`. The witness selects a receipt but does not authorize a claim — only `receiver` (when `recoveryContract == 0`) or `recoveryContract` may call `claimBlockedReceipt(...)`.
+- **`recordBlockedInbound(...)` is privileged.** Only TIP-20 precompiles or protocol-internal system code may call it. Otherwise an attacker could mint synthetic receipts by replaying or fabricating witnesses without backing escrow balance.
-## 8. Invariants
+## 9. Invariants
The following invariants MUST always hold:
@@ -689,7 +882,7 @@ The following invariants MUST always hold:
## Appendix A. Solidity Reference Recovery Contract (Non-Normative)
-The following contract is illustrative only. It is not part of the protocol, and receivers may use any recovery contract or smart wallet that obeys the rules in Sections 4.3 and 4.4.
+The following contract is illustrative only. It is not part of the protocol, and receivers may use any recovery contract or smart wallet that obeys the rules in [Section 4.4](#44-claim-authorization) and [Section 4.5](#45-release-semantics).
```solidity
pragma solidity ^0.8.24;