diff --git a/src/pages/docs/protocol/tips/tip-1028.mdx b/src/pages/docs/protocol/tips/tip-1028.mdx new file mode 100644 index 00000000..a80f64e8 --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1028.mdx @@ -0,0 +1,189 @@ +--- +id: TIP-1028 +title: Address-Level Receive Policies +description: Lets any account configure token filters and sender allowlists that silently redirect blocked transfers to a claimable receipt instead of reverting. +authors: Tempo Team +status: Implemented +related: TIP-403, TIP-20, TIP-1049 +protocolVersion: T6 +--- + +# TIP-1028: Address-Level Receive Policies + +## Abstract + +Allows any account to register a **receive policy** — a combination of token filter and sender policy — that governs what transfers it accepts. When a transfer is blocked by the policy it is not reverted; instead the tokens are held by the `ReceivePolicyGuard` precompile and a receipt is stored on-chain. The originator (or a nominated recovery authority) can later claim the receipt by rerouting the transfer to a permitted destination or resuming it once the policy changes. + +--- + +# Specification + +## Activation + +Receive policies activate at the T6 hardfork. Before T6, `setReceivePolicy` reverts and no blocking logic runs. + +## Setting a receive policy + +An account calls `setReceivePolicy` on the TIP-403 precompile to configure its policy: + +```solidity +function setReceivePolicy( + uint64 senderPolicyId, + uint64 tokenFilterId, + address recoveryAuthority +) external; +``` + +| Parameter | Description | +|---|---| +| `senderPolicyId` | ID of a TIP-403 sender policy. `0` disables sender filtering. | +| `tokenFilterId` | ID of a TIP-403 token filter. `0` disables token filtering. | +| `recoveryAuthority` | Address that may claim receipts. `address(0)` means the originator may claim without a designated authority. | + +Emits: + +```solidity +event ReceivePolicyUpdated( + address indexed account, + uint64 senderPolicyId, + uint64 tokenFilterId, + address recoveryAuthority +); +``` + +## Transfers subject to receive policy checks + +Receive policies are evaluated on the following TIP-20 operations: + +| Operation | Checked | +|---|---| +| `transfer` | Yes | +| `transferFrom` | Yes (allowance is consumed even when the transfer is blocked) | +| `transferWithMemo` | Yes | +| `transferFromWithMemo` | Yes | +| `systemTransferFrom` | Yes | +| `mint` | Yes | +| `mintWithMemo` | Yes | +| `approve` | No | +| `permit` | No | +| `burn` | No | +| Fee flows | No | + +## Evaluation order + +For every eligible transfer targeting an account that has a non-zero `tokenFilterId` or `senderPolicyId`: + +1. **Token filter** — if the token is not in the filter's allowlist, the transfer is blocked with reason `TOKEN_FILTER`. +2. **Sender policy** — if the sender is not authorised under the sender policy, the transfer is blocked with reason `RECEIVE_POLICY`. +3. **Pass** — if both checks pass (or neither is configured), the transfer completes normally with reason `NONE`. + +[TIP-1022](https://tips.sh/1022) virtual addresses are resolved to their master account before the policy check. + +## ReceivePolicyGuard precompile + +Blocked tokens are held by the `ReceivePolicyGuard` precompile: + +``` +0xB10C000000000000000000000000000000000000 +``` + +This address cannot be used as a `recoveryAuthority` or reroute destination. + +## Receipts + +When a transfer is blocked, `TransferBlocked` is emitted and a receipt is stored: + +```solidity +event TransferBlocked( + address indexed token, + address indexed receiver, + uint64 indexed blockedNonce, + uint256 amount, + uint8 receiptVersion, + bytes receipt +); +``` + +The receipt encodes: + +| Field | Description | +|---|---| +| `version` | Receipt schema version | +| `token` | TIP-20 token address | +| `recoveryAuthority` | Copied from the receiver's policy at block time | +| `originator` | Original `msg.sender` of the transfer | +| `recipient` | Intended receiver | +| `blockedTimestamp` | Block timestamp when the transfer was blocked | +| `blockedNonce` | Per-receiver monotonic counter | +| `blockedReason` | `TOKEN_FILTER` or `RECEIVE_POLICY` | +| `inboundKind` | Which TIP-20 operation was blocked | +| `memo` | Memo field if present, otherwise empty | + +Receipts are keyed by `keccak256(abi.encode(receipt))`. + +## Claiming a receipt + +Receipts are claimed via the `ReceivePolicyGuard` precompile. There are two claim types: + +### Resume + +A **resume** sends the tokens to the original receiver (`to == receiver`). It is available only when `recoveryAuthority != address(0)` and must be called by the `recoveryAuthority`. The receiver's current policy is **not** re-evaluated on resume. + +### Reroute + +A **reroute** sends the tokens to a different address (`to != receiver`, or any claim when `recoveryAuthority == address(0)`). Rules: + +- `to` cannot be `ReceivePolicyGuard`. +- If `to` is a TIP-1022 virtual address, it is resolved to its master before the transfer. +- `to` must be authorised as a sender under the token's TIP-403 policy. + +Emits on success: + +```solidity +event ReceiptClaimed( + address indexed token, + address indexed receiver, + uint64 indexed blockedNonce, + uint64 blockedAt, + uint8 receiptVersion, + address originator, + address recipient, + address recoveryAuthority, + address caller, + address to, + uint256 amount +); +``` + +If the receipt is burned without transferring (e.g. tokens are absorbed): + +```solidity +event ReceiptBurned( + address indexed token, + address indexed receiver, + uint64 indexed blockedNonce, + uint64 blockedAt, + uint8 receiptVersion, + address originator, + address recipient, + address recoveryAuthority, + address caller, + uint256 amount +); +``` + +--- + +# Invariants + +1. **No revert on block** — a transfer blocked by a receive policy never reverts. The transaction succeeds and a receipt is stored. + +2. **Allowance consumed on block** — for `transferFrom` and `transferFromWithMemo`, the caller's allowance is decremented even when the transfer is blocked. + +3. **Evaluation order is deterministic** — token filter is always checked before sender policy. A transfer blocked by both reasons is recorded as `TOKEN_FILTER`. + +4. **ReceivePolicyGuard is not a valid destination** — neither `recoveryAuthority` nor any reroute `to` may be the `ReceivePolicyGuard` address. + +5. **Virtual addresses resolved before check** — TIP-1022 virtual addresses in the sender position are resolved to their master account before the sender policy is evaluated. + +6. **Policy snapshot at block time** — the `recoveryAuthority` written into a receipt reflects the receiver's policy at the moment of blocking, not at the time of claiming. diff --git a/src/pages/docs/protocol/tips/tip-1049.mdx b/src/pages/docs/protocol/tips/tip-1049.mdx new file mode 100644 index 00000000..32f6316b --- /dev/null +++ b/src/pages/docs/protocol/tips/tip-1049.mdx @@ -0,0 +1,164 @@ +--- +id: TIP-1049 +title: Admin Access Keys +description: Introduces a privileged admin key tier that can manage other keys on an account but cannot carry spending limits, call scopes, or expiry. +authors: Tempo Team +status: Implemented +related: TIP-1011, TIP-1020 +protocolVersion: T6 +--- + +# TIP-1049: Admin Access Keys + +## Abstract + +Adds an **admin key** tier to the AccountKeychain. Admin keys share the key-management privileges of the root key — they can authorize, revoke, and promote other keys — but they cannot carry spending limits, call scopes, or expiry. This makes it practical to delegate account administration (e.g. to a guardian device or a recovery service) without granting unrestricted signing authority over arbitrary calls. + +--- + +# Specification + +## Activation + +Admin key support activates at the T6 hardfork. + +## Authorizing an admin key + +```solidity +function authorizeAdminKey( + address keyId, + SignatureType signatureType, + bytes32 witness +) external; +``` + +Constraints: + +- `keyId` cannot be the account address itself. +- `keyId` cannot already be authorized (active or revoked) on the account. +- `signatureType` must be `0` (secp256k1), `1` (P256), or `2` (WebAuthn). +- The `witness` is burned for replay protection. `bytes32(0)` is a valid witness. + +On success, two events are emitted: + +```solidity +/// Existing event — unchanged +event KeyAuthorized( + address indexed account, + address indexed publicKey, + SignatureType signatureType, + uint64 expiry, + bytes32 spendingLimitsBitmask, + bytes32 allowedCallsBitmask +); + +/// New event added by TIP-1049 +event AdminKeyAuthorized( + address indexed account, + address indexed publicKey +); +``` + +## Checking admin status + +```solidity +function isAdminKey(address account, address keyId) external view returns (bool); +``` + +Returns `true` for: + +- The account's **root key** (always admin). +- Any active, non-revoked key that was authorized as an admin key. + +Returns `false` for revoked keys and ordinary (non-admin) active keys. + +## Admin key capabilities + +| Action | Admin key | +|---|---| +| `authorizeKey` | Yes | +| `authorizeAdminKey` | Yes | +| `revokeKey` | Yes | +| Carry `expiry` | No | +| Carry spending limits | No | +| Carry call scopes | No | +| Deploy contracts | No | + +Admin keys always have the widest call and spending authority (equivalent to root key) and cannot be narrowed. + +## Storage layout + +The `AuthorizedKey` slot is a 32-byte value with the following byte layout: + +| Byte(s) | Field | +|---|---| +| 0 | `signature_type` | +| 1–8 | `expiry` (always `0` for admin keys) | +| 9 | `enforce_limits` (always `0` for admin keys) | +| 10 | `is_revoked` | +| **11** | **`is_admin`** | +| 12–31 | Reserved | + +## RLP encoding + +The keychain transaction RLP field list is: + +``` +rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, witness?, is_admin?, account?]) +``` + +`is_admin` and `account` are trailing optional fields introduced by TIP-1049. Existing decoders that treat the list as fixed-length will ignore them. + +## Keychain signature verification (T6) + +TIP-1049 adds two new entry points to the [TIP-1020](https://tips.sh/1020) `SignatureVerifier` precompile at `0x5165300000000000000000000000000000000000`: + +```solidity +/// Accepts a signature from any active key (root, admin, or limited). +function verifyKeychain( + address account, + bytes32 digest, + bytes calldata signature +) external view returns (bool); + +/// Accepts a signature only from the root key or an active admin key. +function verifyKeychainAdmin( + address account, + bytes32 digest, + bytes calldata signature +) external view returns (bool); +``` + +Both functions revert on malformed signatures. Callers **must** include chain ID, contract address, and account address in the digest to prevent cross-domain replay. + +Example digest construction: + +```solidity +bytes32 digest = keccak256( + abi.encode( + block.chainid, + address(this), // contract address + account, // key owner + nonces[account]++, + payload + ) +); +require( + StdPrecompiles.SIGNATURE_VERIFIER.verifyKeychainAdmin(account, digest, sig), + "not an admin key" +); +``` + +--- + +# Invariants + +1. **Admin keys cannot be narrowed** — an admin key always acts with root-equivalent call and spending authority. The `enforce_limits`, `expiry`, and scope fields in its storage slot are always zero and are ignored. + +2. **`isAdminKey` includes root** — the root key is always considered an admin key. Callers do not need to special-case root vs. admin. + +3. **Witness is burned** — re-submitting the same `(keyId, witness)` pair reverts even if the key was later revoked. Use a fresh witness for each authorization. + +4. **Revocation applies to admin keys** — `revokeKey` works on admin keys. After revocation, `isAdminKey` returns `false` and `verifyKeychainAdmin` rejects signatures from the revoked key. + +5. **`KeyAuthorized` fields for admin keys** — `expiry`, `spendingLimitsBitmask`, and `allowedCallsBitmask` are all zero in the `KeyAuthorized` event for admin keys. Indexers should use the co-emitted `AdminKeyAuthorized` event to distinguish admin authorizations.