diff --git a/src/pages/guide/tempo-transaction/index.mdx b/src/pages/guide/tempo-transaction/index.mdx index 720ca368..383b3ecf 100644 --- a/src/pages/guide/tempo-transaction/index.mdx +++ b/src/pages/guide/tempo-transaction/index.mdx @@ -83,13 +83,4 @@ If you are an EVM smart contract developer, see the [Foundry guide for Tempo](/s ## Properties -:::info[T3 examples] -The examples below show the T3 Tempo Transaction shape. If you still support T2, the affected sections include a `T2 -> T3 changes` note with the migration details. - -- [Configurable Fee Tokens](#configurable-fee-tokens) -- [Access Keys](#access-keys) - -The examples assume you are using a T3-compatible network and the most recent SDK releases. -::: - diff --git a/src/pages/protocol/rpc/index.mdx b/src/pages/protocol/rpc/index.mdx index 168068fa..94cb671e 100644 --- a/src/pages/protocol/rpc/index.mdx +++ b/src/pages/protocol/rpc/index.mdx @@ -59,15 +59,15 @@ cast rpc tempo_forkSchedule --rpc-url https://rpc.tempo.xyz ```json { "schedule": [ - { "name": "T0", "activationTime": 0, "active": true, "forkId": "0xa88e90f8" }, - { "name": "T1", "activationTime": 1770908400, "active": true, "forkId": "0x5e3041a4" }, - { "name": "T1A", "activationTime": 1770908400, "active": true, "forkId": "0x5e3041a4" }, - { "name": "T1B", "activationTime": 1771858800, "active": true, "forkId": "0x92b1c4d7" }, - { "name": "T1C", "activationTime": 1773327600, "active": true, "forkId": "0xf3a901bc" }, + { "name": "T0", "activationTime": 0, "active": true, "forkId": "0xfde57c3e" }, + { "name": "T1", "activationTime": 1770908400, "active": true, "forkId": "0x9e6fe384" }, + { "name": "T1A", "activationTime": 1770908400, "active": true, "forkId": "0x9e6fe384" }, + { "name": "T1B", "activationTime": 1771858800, "active": true, "forkId": "0x73a4f670" }, + { "name": "T1C", "activationTime": 1773327600, "active": true, "forkId": "0x2a3ee80d" }, { "name": "T2", "activationTime": 1774965600, "active": true, "forkId": "0x471a451c" }, - { "name": "T3", "activationTime": 0, "active": false } + { "name": "T3", "activationTime": 1777298400, "active": true, "forkId": "0xd2087b77" } ], - "active": "T2" + "active": "T3" } ``` diff --git a/src/pages/protocol/tip20-rewards/spec.mdx b/src/pages/protocol/tip20-rewards/spec.mdx index 47cb4b63..2e37a8c0 100644 --- a/src/pages/protocol/tip20-rewards/spec.mdx +++ b/src/pages/protocol/tip20-rewards/spec.mdx @@ -4,22 +4,12 @@ description: Technical specification for the TIP-20 reward distribution system u # TIP-20 Rewards Distribution -:::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. -::: - ## Abstract An opt-in, scalable, pro-rata reward distribution mechanism built into TIP-20 tokens. The system uses a "reward-per-token" accumulator pattern to distribute rewards proportionally to opted-in holders without requiring staking or per-holder iteration. Rewards are distributed instantly; time-based streaming distributions are planned for a future upgrade. ## Motivation Many applications require pro-rata distribution of tokens to existing holders (incentive programs, deterministic inflation, staking rewards). Building this into TIP-20 allows efficient distribution without forcing users to stake tokens elsewhere or requiring distributors to loop over all holders. -## Upcoming changes - -T3 updates the TIP-20 rewards spec through [TIP-1022](/protocol/tips/tip-1022) in one place: - -- `setRewardRecipient(...)` will reject [TIP-1022](/protocol/tips/tip-1022) virtual addresses. Reward recipients must remain canonical accounts rather than forwarding aliases, because reward assignment is not a TIP-20 forwarding path. - ## Specification The rewards mechanism allows anyone to distribute token rewards to opted-in holders proportionally based on holdings. Users must opt in to receiving rewards and may delegate rewards to a recipient address. @@ -67,6 +57,8 @@ Users must call `setRewardRecipient(recipient)` to opt in. When opted in: Setting recipient to `address(0)` opts out. +`setRewardRecipient(...)` rejects [TIP-1022](/protocol/tips/tip-1022) virtual addresses: reward recipients must remain canonical accounts rather than forwarding aliases, because reward assignment is not a TIP-20 forwarding path. + ## TIP-403 Integration All token movements must pass TIP-403 policy checks: - `distributeReward`: Validates funder authorization @@ -77,3 +69,13 @@ All token movements must pass TIP-403 policy checks: - `globalRewardPerToken` must monotonically increase - `optedInSupply` must equal the sum of balances for all opted-in users - All token movements must comply with TIP-403 policies + +## T2 → T3 migration + +:::info[Migration appendix] +This section captures changes introduced by the [T3 network upgrade](/protocol/upgrades/t3) for integrators migrating from T2. The spec above is the canonical post-T3 specification; this appendix exists for reference and will be removed in a future docs revision. +::: + +T3 introduced [TIP-1022](/protocol/tips/tip-1022) virtual addresses, which affect TIP-20 rewards in one place: + +- `setRewardRecipient(...)` now rejects virtual addresses. Reward recipients must be canonical accounts, not forwarding aliases. diff --git a/src/pages/protocol/tip20/spec.mdx b/src/pages/protocol/tip20/spec.mdx index e346efc6..327724e4 100644 --- a/src/pages/protocol/tip20/spec.mdx +++ b/src/pages/protocol/tip20/spec.mdx @@ -4,10 +4,6 @@ description: Technical specification for TIP-20, the optimized token standard ex # TIP20 -:::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. -::: - ## Abstract TIP20 is a suite of precompiles that provide a built-in optimized token implementation in the core protocol. It extends the ERC-20 token standard with built-in functionality like memo fields and reward distribution. @@ -16,15 +12,6 @@ All major stablecoins today use the ERC-20 token standard. While ERC-20 provides TIP-20 extends ERC-20, building these features into precompiled contracts that anyone can permissionlessly deploy on Tempo. This makes token operations much more efficient, allows issuers to quickly set up on Tempo, and simplifies integrations since it ensures standardized behavior across tokens. It also enables deeper integration with token-specific Tempo features like paying gas in stablecoins and payment lanes. -## Upcoming changes - -T3 updates TIP-20 behavior through [TIP-1022](/protocol/tips/tip-1022). All changes below come from TIP-1022: - -- [TIP-1022](/protocol/tips/tip-1022) adds virtual-address recipient resolution for recipient-bearing TIP-20 paths: `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, and `mintWithMemo`. -- When a TIP-20 operation targets a registered virtual address, the effective recipient becomes the registered master wallet before recipient authorization and mint-recipient checks run. -- Forwarded virtual-address deposits appear as two-hop standard `Transfer` events in the same transaction. Indexers and explorers should collapse that pair into one logical deposit to the resolved master wallet. -- Virtual addresses are valid TIP-20 recipients on those paths, but they remain forwarding aliases rather than canonical TIP-20 holders. Non-TIP-20 tokens sent to a virtual address do not forward. - ## Specification TIP-20 tokens support standard fungible token operations such as transfers, mints, and burns. They also support transfers, mints, and burns with an attached 32-byte memo; a role-based access control system for token administrative operations; and a system for opt-in [reward distribution](/protocol/tip20-rewards/spec). @@ -460,6 +447,11 @@ Reward operations (`distributeReward`, `setRewardRecipient`, `claimRewards`) als TIP-20 tokens cannot be sent to other TIP-20 token contract addresses. The implementation uses a `validRecipient` guard that rejects recipients whose address is zero, or has the TIP-20 prefix (`0x20c000000000000000000000`). Any attempt to transfer to a TIP-20 token address must revert with `InvalidRecipient`. This prevents accidental token loss by sending funds to token contracts instead of user accounts. +## Virtual Address Recipients +Recipient-bearing TIP-20 paths — `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, and `mintWithMemo` — resolve [TIP-1022](/protocol/tips/tip-1022) virtual addresses before running recipient authorization and mint-recipient checks. When a recipient `to` is a registered virtual address, the effective recipient becomes the registered master wallet, and authorization, balance updates, and event emission target that master wallet. + +Virtual addresses are valid TIP-20 recipients on those paths but remain forwarding aliases rather than canonical TIP-20 holders. Non-TIP-20 tokens sent to a virtual address do not forward. Forwarded deposits appear as two-hop standard `Transfer` events in the same transaction; indexers and explorers should collapse that pair into one logical deposit to the resolved master wallet. + ## Currencies and Quote Tokens Each TIP-20 token declares a currency identifier and a corresponding `quoteToken` used for pricing and routing in the Stablecoin DEX. Stablecoin currency identifiers should be [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html) three-letter codes representing the underlying fiat currency (e.g., `"USD"`, `"EUR"`, `"GBP"`) — not the token's own symbol. The currency is set at token creation and **cannot be changed afterward**. **Only tokens with `currency == "USD"` are eligible for paying transaction fees.** Tokens with `currency == "USD"` must pair with a USD-denominated TIP-20 token. @@ -611,3 +603,16 @@ interface ITIP20Factory { - When `paused` is `true`, no functions that move tokens (`transfer`, `transferFrom`, memo variants, `systemTransferFrom`, `transferFeePreTx`, `distributeReward`, `setRewardRecipient`, `claimRewards`) can succeed. - TIP20 tokens cannot be transferred to another TIP20 token contract address. - `systemTransferFrom`, `transferFeePreTx`, and `transferFeePostTx` never change `totalSupply()`. + +## T2 → T3 migration + +:::info[Migration appendix] +This section captures changes introduced by the [T3 network upgrade](/protocol/upgrades/t3) for integrators migrating from T2. The spec above is the canonical post-T3 specification; this appendix exists for reference and will be removed in a future docs revision. +::: + +T3 introduced [TIP-1022](/protocol/tips/tip-1022) virtual addresses, which affect TIP-20 in the following ways: + +- Virtual-address recipient resolution applies to recipient-bearing TIP-20 paths: `transfer`, `transferFrom`, `transferWithMemo`, `transferFromWithMemo`, `mint`, and `mintWithMemo`. See [Virtual Address Recipients](#virtual-address-recipients). +- When an operation targets a registered virtual address, the effective recipient becomes the registered master wallet before recipient authorization and mint-recipient checks run. +- Forwarded virtual-address deposits appear as two-hop standard `Transfer` events in the same transaction. Indexers and explorers should collapse that pair into one logical deposit to the resolved master wallet. +- Virtual addresses are valid TIP-20 recipients on those paths but remain forwarding aliases rather than canonical TIP-20 holders. Non-TIP-20 tokens sent to a virtual address do not forward. diff --git a/src/pages/protocol/tip403/spec.mdx b/src/pages/protocol/tip403/spec.mdx index b5df3c6c..397dd567 100644 --- a/src/pages/protocol/tip403/spec.mdx +++ b/src/pages/protocol/tip403/spec.mdx @@ -4,10 +4,6 @@ description: Technical specification for TIP-403, the policy registry system ena # Overview -:::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. -::: - ## Abstract TIP-403 provides a policy registry system that allows TIP-20 tokens to inherit access control and compliance policies. The registry supports two types of policies (whitelist and blacklist) and includes special built-in policies for common use cases. Policies can be shared across multiple tokens, enabling consistent compliance enforcement. @@ -18,13 +14,6 @@ Token issuers often need to implement compliance policies such as KYC/AML requir TIP-403 addresses this by providing a centralized registry that tokens can reference for authorization decisions. This enables consistent policy enforcement across multiple tokens and reduces implementation complexity for token issuers. -## Upcoming changes - -T3 updates TIP-403 interactions with token recipients through [TIP-1022](/protocol/tips/tip-1022) as follows: - -- Policy-configuration functions that accept literal member addresses will reject [TIP-1022](/protocol/tips/tip-1022) virtual addresses. -- TIP-20 policy checks for virtual-address transfers and mints will run against the resolved master wallet, not the forwarding alias, so policy membership must be configured on the master address. - --- # Specification @@ -242,6 +231,8 @@ TIP-20 tokens store the current TIP403 registry policy ID they adhere to in thei **Policy Changes:** When a token's transfer policy is changed via `changeTransferPolicyId()`, all future transfers are immediately subject to the new policy. +**Virtual addresses:** Policy-configuration functions that accept literal member addresses (`createPolicyWithAccounts`, `modifyPolicyWhitelist`, `modifyPolicyBlacklist`) reject [TIP-1022](/protocol/tips/tip-1022) virtual addresses. TIP-20 policy checks for transfers and mints to a virtual address run against the resolved master wallet rather than the forwarding alias, so policy membership must be configured on the master address. + ### Example Usage Creating and setting a policy: @@ -277,3 +268,14 @@ return data.policyType == PolicyType.WHITELIST - When policyId = 0, all authorization checks must return false for every address. - When policyId = 1, all authorization checks must return true for every address. - Only the policy’s current admin may update the admin address for that policy. + +# T2 → T3 migration + +:::info[Migration appendix] +This section captures changes introduced by the [T3 network upgrade](/protocol/upgrades/t3) for integrators migrating from T2. The spec above is the canonical post-T3 specification; this appendix exists for reference and will be removed in a future docs revision. +::: + +T3 introduced [TIP-1022](/protocol/tips/tip-1022) virtual addresses, which affect TIP-403 in two places: + +- Policy-configuration functions that take literal member addresses (`createPolicyWithAccounts`, `modifyPolicyWhitelist`, `modifyPolicyBlacklist`) now reject virtual addresses. +- TIP-20 policy checks for virtual-address transfers and mints now run against the resolved master wallet, not the forwarding alias. Policy membership must be configured on the master address. diff --git a/src/pages/protocol/transactions/AccountKeychain.mdx b/src/pages/protocol/transactions/AccountKeychain.mdx index 0329075e..e96e78ee 100644 --- a/src/pages/protocol/transactions/AccountKeychain.mdx +++ b/src/pages/protocol/transactions/AccountKeychain.mdx @@ -1,55 +1,20 @@ --- -description: Technical specification for the Account Keychain precompile managing access keys with expiry timestamps, spending limits, and post-T3 call-scope restrictions. +description: Technical specification for the Account Keychain precompile managing access keys with expiry timestamps, spending limits, and call-scope restrictions. --- # Account Keychain Precompile **Address:** `0xAAAAAAAA00000000000000000000000000000000` -:::info[T3 will change this precompile] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the current T2 behavior and include `T2 -> T3 changes` notes for each section. If you are migrating now, start with [Account keychain post-T3](#account-keychain-post-t3). -::: - ## Overview -The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) to provision scoped "secondary" Access Keys with expiry timestamps and per-TIP20 token spending limits. +The Account Keychain precompile manages authorized Access Keys for accounts, enabling Root Keys (e.g., passkeys) to provision scoped "secondary" Access Keys with expiry timestamps, recurring or one-time per-TIP20 token spending limits, and explicit call scopes that restrict which targets, selectors, and recipients an Access Key may invoke. -At T3, `authorizeKey(...)` moves from top-level `expiry`, `enforceLimits`, and `limits` arguments to a `KeyRestrictions` tuple. `TokenLimit` gains `period`, so limits can be one-time or recurring, and access keys can optionally be restricted to specific targets, selectors, and recipients. Access-key-signed transactions also can no longer create contracts after T3 activation. +`authorizeKey(...)` takes a `KeyRestrictions` tuple that bundles expiry, spending limits, and call scopes. Access-key-signed transactions cannot create contracts; use a Root Key for deployment flows. ## Motivation -The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a (scoped) Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting. - -### T2 -> T3 changes - -At T2, the main focus is expiry and spending limits. T3 extends the same model to cover recurring budgets and explicit call scoping, which makes access keys more powerful for subscriptions, connected apps, and session-key-style flows. - -## Account keychain post-T3 - -T3 changes the Account Keychain authorization shape on any network where the upgrade is active. Existing authorized keys continue to work. The breaking change is in how new key authorizations are encoded and what restrictions can now be enforced. - -### Migration summary - -- The legacy `authorizeKey(address,uint8,uint64,bool,(address,uint256)[])` entrypoint is no longer accepted after T3 activation. Calls to selector `0x54063a55` revert with `LegacyAuthorizeKeySelectorChanged(0x980a6025)`. -- `authorizeKey(...)` now takes a `KeyRestrictions` tuple that carries expiry, spending limits, and call scopes. -- `TokenLimit` now includes `period`, so limits can be one-time (`period = 0`) or recurring. -- Access keys can now be scoped to specific targets, selectors, and recipients. -- The precompile adds `setAllowedCalls(...)`, `removeAllowedCalls(...)`, `getAllowedCalls(...)`, and `getRemainingLimitWithPeriod(...)`. -- Access-key-signed transactions can no longer create contracts after T3 activation. Use a Root Key for deployment flows. - -### Before and after `authorizeKey(...)` - -```text -T2 -authorizeKey(address,uint8,uint64,bool,(address,uint256)[]) -selector: 0x54063a55 - -T3 -authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])) -selector: 0x980a6025 -``` - -The T3 call must use the tuple-form signature above. A flattened seven-argument signature is not equivalent. In Foundry, that flattened form hashes to `0x203e2736`, which the precompile rejects as an unknown selector. +The Tempo Transaction type unlocks a number of new signature schemes, including WebAuthn (Passkeys). However, for an Account using a Passkey as its Root Key, the sender will subsequently be prompted with passkey prompts for every signature request. This can be a poor user experience for highly interactive or multi-step flows. Additionally, users would also see "Sign In" copy in prompts for signing transactions which is confusing. This proposal introduces the concept of the Root Key being able to provision a (scoped) Access Key that can be used for subsequent transactions, without the need for repetitive end-user prompting. Recurring spending budgets and explicit call scoping make these keys suitable for subscriptions, connected apps, and session-key-style flows. ## Concepts @@ -57,42 +22,40 @@ The T3 call must use the tuple-form signature above. A flattened seven-argument Access Keys are secondary signing keys authorized by an account's Root Key. They can sign transactions on behalf of the account with the following restrictions: -- **Expiry**: Unix timestamp when the key becomes invalid (0 = never expires, non-zero values must be > current timestamp) +- **Expiry**: Unix timestamp when the key becomes invalid. A non-expiring `key_authorization` omits `expiry` at the transaction RLP layer; the protocol translates that omission to `u64::MAX` before calling the precompile. Direct precompile callers should pass `u64::MAX` for a non-expiring key. Literal `0` is treated as past expiry and rejected. - **Spending Limits**: Per-TIP20 token limits that deplete as tokens are spent - - Limits deplete as tokens are spent and can be updated by the Root Key via `updateSpendingLimit()` + - Limits are one-time (`period = 0`) or recurring (`period > 0`, in seconds). Recurring limits roll over to `max` when `current_timestamp >= periodEnd`. + - Limits can be updated by the Root Key via `updateSpendingLimit()`. An update resets `remaining` and `max` to `newLimit` but preserves the configured `period` and current `periodEnd`. - Spending limits only apply to TIP20 `transfer()`, `transferWithMemo()`, and `approve()` calls - Spending limits only apply when `msg.sender == tx.origin` (direct EOA calls, not contract calls) - Native value transfers and `transferFrom()` are NOT limited -- **Privilege Restrictions**: Cannot authorize new keys or modify their own limits - -#### T2 -> T3 changes - -At T3, spending limits can recur through `TokenLimit.period`. A `period` of `0` keeps the limit one-time, while a non-zero value makes it recurring. Call scoping also becomes a first-class restriction type, and access-key-signed transactions can no longer create contracts after T3 activation. +- **Call Scopes**: An Access Key is either unrestricted (`allowAnyCalls = true`) or restricted to an explicit allowlist of `(target, selector, recipients)` tuples. An empty allowlist with `allowAnyCalls = false` means scoped deny-all. +- **No Contract Creation**: Access-key-signed transactions cannot perform `CREATE` or `CREATE2`, including via factory contracts. Use a Root Key for deployments. +- **Privilege Restrictions**: Cannot authorize new keys or modify their own limits or scopes. ### Authorization Hierarchy The protocol enforces a strict hierarchy at validation time: 1. **Root Key**: The account's main key (derived from the account address) - - Can call all precompile functions - - Has no spending limits + - Can call all precompile functions, including the Root-Key-only mutators (`authorizeKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) + - Has no spending limits or call-scope restrictions 2. **Access Keys**: Secondary authorized keys - Cannot call mutable precompile functions (only view functions are allowed) - Subject to per-TIP20 token spending limits + - Subject to call-scope checks during execution + - Cannot create contracts - Can have expiry timestamps -#### T2 -> T3 changes - -The hierarchy itself does not change. T3 adds new mutable call-scope management functions, and they remain Root-Key-only. Access Keys are also subject to call-scope checks during execution. - ## Storage The precompile uses a `keyId` (address) to uniquely identify each access key for an account. **Storage Mappings:** - `keys[account][keyId]` → Packed `AuthorizedKey` struct (signature type, expiry, enforce_limits, is_revoked) -- `spendingLimits[keccak256(account || keyId)][token]` → Remaining spending amount for a specific token (uint256) +- `spendingLimits[keccak256(account || keyId)][token]` → `SpendingLimitState { remaining, max, period, periodEnd }` +- `keyScopes[keccak256(account || keyId)]` → Tree of `(target, selector, recipients)` allowlists used during call-scope checks - `transactionKey` → Transient storage for the key ID that signed the current transaction (slot 0) **AuthorizedKey Storage Layout (packed into single slot):** @@ -101,169 +64,8 @@ The precompile uses a `keyId` (address) to uniquely identify each access key for - byte 9: enforce_limits (bool) - byte 10: is_revoked (bool) -### T2 -> T3 changes - -At T3, `spendingLimits[...]` expands from a single remaining amount into `SpendingLimitState { remaining, max, period, periodEnd }`, and a new `keyScopes[keccak256(account || keyId)]` tree stores target, selector, and recipient allowlists. `transactionKey` remains the same. - ## Interface -### T2 interface - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.13; - -interface IAccountKeychain { - /*////////////////////////////////////////////////////////////// - STRUCTS - //////////////////////////////////////////////////////////////*/ - - /// @notice Signature type - enum SignatureType { - Secp256k1, - P256, - WebAuthn, - } - - /// @notice Token spending limit structure - struct TokenLimit { - address token; // TIP20 token address - uint256 amount; // Spending limit amount - } - - /// @notice Key information structure - struct KeyInfo { - SignatureType signatureType; // Signature type of the key - address keyId; // The key identifier - uint64 expiry; // Unix timestamp when key expires - bool enforceLimits; // Whether spending limits are enforced for this key - bool isRevoked; // Whether this key has been revoked - } - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - /// @notice Emitted when a new key is authorized - event KeyAuthorized( - address indexed account, - address indexed publicKey, - uint8 signatureType, - uint64 expiry - ); - - /// @notice Emitted when a key is revoked - event KeyRevoked(address indexed account, address indexed publicKey); - - /// @notice Emitted when a spending limit is updated - event SpendingLimitUpdated( - address indexed account, - address indexed publicKey, - address indexed token, - uint256 newLimit - ); - - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - - error KeyAlreadyExists(); - error KeyNotFound(); - error KeyInactive(); - error KeyExpired(); - error KeyAlreadyRevoked(); - error SpendingLimitExceeded(); - error InvalidSignatureType(); - error ZeroPublicKey(); - error UnauthorizedCaller(); - - /*////////////////////////////////////////////////////////////// - MANAGEMENT FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Authorize a new key for the caller's account - * @dev MUST only be called in transactions signed by the Root Key - * The protocol enforces this restriction by checking transactionKey[msg.sender] - * @param keyId The key identifier (address) to authorize - * @param signatureType Signature type of the key (0: Secp256k1, 1: P256, 2: WebAuthn) - * @param expiry Unix timestamp when key expires (MUST be > current_timestamp) - * @param enforceLimits Whether to enforce spending limits for this key - * @param limits Initial spending limits for tokens (only used if enforceLimits is true) - */ - function authorizeKey( - address keyId, - SignatureType signatureType, - uint64 expiry, - bool enforceLimits, - TokenLimit[] calldata limits - ) external; - - /** - * @notice Revoke an authorized key - * @dev MUST only be called in transactions signed by the Root Key - * The protocol enforces this restriction by checking transactionKey[msg.sender] - * @param keyId The key ID to revoke - */ - function revokeKey(address keyId) external; - - /** - * @notice Update spending limit for a specific token on an authorized key - * @dev MUST only be called in transactions signed by the Root Key - * The protocol enforces this restriction by checking transactionKey[msg.sender] - * @param keyId The key ID to update - * @param token The token address - * @param newLimit The new spending limit - */ - function updateSpendingLimit( - address keyId, - address token, - uint256 newLimit - ) external; - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /** - * @notice Get key information - * @param account The account address - * @param keyId The key ID - * @return Key information (returns default values if key doesn't exist) - */ - function getKey( - address account, - address keyId - ) external view returns (KeyInfo memory); - - /** - * @notice Get remaining spending limit for a key-token pair - * @param account The account address - * @param keyId The key ID - * @param token The token address - * @return Remaining spending amount - */ - function getRemainingLimit( - address account, - address keyId, - address token - ) external view returns (uint256); - - /** - * @notice Get the transaction key used in the current transaction - * @dev Returns Address::ZERO if the Root Key is being used - * @return The key ID that signed the transaction - */ - function getTransactionKey() external view returns (address); -} -``` - -### T2 -> T3 changes - -At T3, `TokenLimit` gains `period`, and the interface adds `SelectorRule`, `CallScope`, and `KeyRestrictions`. `authorizeKey(...)` moves from top-level `expiry`, `enforceLimits`, and `limits` arguments to a `KeyRestrictions` tuple, while new Root-Key-only mutation methods (`setAllowedCalls(...)` and `removeAllowedCalls(...)`) and new views (`getRemainingLimitWithPeriod(...)` and `getAllowedCalls(...)`) are added around it. The legacy `getRemainingLimit(...)` selector is dropped at T3, and new T3-specific errors include `InvalidSpendingLimit()`, `ExpiryInPast()`, `SignatureTypeMismatch(uint8,uint8)`, `CallNotAllowed()`, `InvalidCallScope()`, and `LegacyAuthorizeKeySelectorChanged(bytes4)`. - -### T3 interface - ```solidity // SPDX-License-Identifier: MIT pragma solidity ^0.8.13; @@ -369,28 +171,30 @@ interface IAccountKeychain { ### Key Authorization -- Creates a new key entry with the specified `signatureType`, `expiry`, `enforceLimits`, and `isRevoked` set to `false` -- If `enforceLimits` is `true`, initializes spending limits for each specified token +- Creates a new key entry with the specified `signatureType`, `config.expiry`, `config.enforceLimits`, and `isRevoked` set to `false` +- If `enforceLimits` is `true`, initializes spending limits for each specified token. Each `TokenLimit` carries a `period` (0 = one-time, non-zero = recurring in seconds). +- If `allowAnyCalls` is `false`, stores the `allowedCalls` allowlist in `keyScopes`. `allowAnyCalls = false` with `allowedCalls = []` means scoped deny-all. +- Recipient-constrained selector rules are validated before any state is written. - Emits `KeyAuthorized` event **Requirements:** - MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) +- MUST be invoked via the canonical selector `0x980a6025` (the `(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))` shape). Other selectors revert. - `keyId` MUST NOT be `address(0)` (reverts with `ZeroPublicKey`) - `keyId` MUST NOT already be authorized with `expiry > 0` (reverts with `KeyAlreadyExists`) - `keyId` MUST NOT have been previously revoked (reverts with `KeyAlreadyRevoked` - prevents replay attacks) - `signatureType` MUST be `0` (Secp256k1), `1` (P256), or `2` (WebAuthn) (reverts with `InvalidSignatureType`) -- `expiry` CAN be any value (0 means never expires, stored as-is) +- `config.expiry` MUST be strictly greater than the current block timestamp (reverts with `ExpiryInPast`) +- To authorize a non-expiring key, omit `key_authorization.expiry` in the transaction RLP or pass `u64::MAX` when calling the precompile ABI directly. Do not pass `0`. - `enforceLimits` determines whether spending limits are enforced for this key -- `limits` are only processed if `enforceLimits` is `true` - -#### T2 -> T3 changes - -At T3, the legacy selector is no longer accepted and reverts with `LegacyAuthorizeKeySelectorChanged(newSelector: 0x980a6025)`. `authorizeKey(...)` now takes `KeyRestrictions` instead of top-level `expiry`, `enforceLimits`, and `limits` arguments, `config.expiry` must be greater than the current block timestamp, duplicate token entries are invalid, `allowAnyCalls = false` with `allowedCalls = []` means scoped deny-all, and recipient-constrained selector rules are validated before state is written. +- `limits` are only processed if `enforceLimits` is `true`. Duplicate token entries revert with `InvalidSpendingLimit`. +- Invalid call-scope shapes (zero targets, duplicate targets, duplicate selectors, duplicate recipients, malformed recipient-bound rules) revert with `InvalidCallScope`. ### Key Revocation - Marks the key as revoked by setting `isRevoked` to `true` and `expiry` to `0` - Once revoked, a `keyId` can NEVER be re-authorized for this account (prevents replay attacks) +- Any stored call-scope and periodic-limit state becomes inaccessible. `getAllowedCalls(...)` returns scoped deny-all (`isScoped = true, scopes = []`) for revoked keys. - Key can no longer be used for transactions - Emits `KeyRevoked` event @@ -398,16 +202,13 @@ At T3, the legacy selector is no longer accepted and reverts with `LegacyAuthori - MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) - `keyId` MUST exist (key with `expiry > 0`) (reverts with `KeyNotFound` if not found) -#### T2 -> T3 changes - -Revoked keys still behave as inactive for all legacy reads. T3 also treats any stored call-scope and periodic-limit state as inaccessible once the key is revoked, and `getAllowedCalls(...)` returns scoped deny-all for revoked keys. - ### Spending Limit Update - Updates the spending limit for a specific token on an authorized key - Allows Root Key to modify limits without revoking and re-authorizing the key - If the key had unlimited spending (`enforceLimits == false`), enables limits -- Sets the new remaining limit to `newLimit` +- Sets both `remaining` and `max` to `newLimit`. The configured `period` and current `periodEnd` are preserved. +- `newLimit` MUST fit within TIP20's `u128` supply range. - Emits `SpendingLimitUpdated` event **Requirements:** @@ -415,53 +216,43 @@ Revoked keys still behave as inactive for all legacy reads. T3 also treats any s - `keyId` MUST exist and not be revoked (reverts with `KeyNotFound` or `KeyAlreadyRevoked`) - `keyId` MUST not be expired (reverts with `KeyExpired`) -#### T2 -> T3 changes +### Allowed Call Updates + +- `setAllowedCalls(...)` creates or replaces one or more target scopes for an existing key. +- `removeAllowedCalls(...)` removes one stored target scope. +- An empty `selectorRules` array means any selector on that target is allowed. +- `setAllowedCalls(...)` rejects an empty scope batch, zero targets, duplicate targets, duplicate selectors, duplicate recipients, and invalid recipient-constrained rules (reverts with `InvalidCallScope`). -At T3, `newLimit` resets both `remaining` and `max` while preserving the existing `period` and current `periodEnd`. `newLimit` must also fit within TIP20's `u128` supply range. +**Requirements:** +- MUST be called by Root Key only (verified by checking `transactionKey[msg.sender] == 0`) +- `keyId` MUST exist and not be revoked ### View Behavior - `getKey(...)` returns key metadata. -- `getRemainingLimit(...)` returns the remaining amount for a key-token pair. +- `getRemainingLimitWithPeriod(...)` returns the effective `remaining` amount and current `periodEnd` for a key-token pair, accounting for periodic rollover. +- `getAllowedCalls(...)` returns `(isScoped, scopes)`. Unrestricted keys return `isScoped = false`. Scoped keys return `isScoped = true` with their `CallScope[]`. Missing, revoked, or expired access keys return `isScoped = true, scopes = []` (scoped deny-all). - `getTransactionKey()` returns the key used in the current transaction. `address(0)` means the Root Key. - -#### T2 -> T3 changes - -At T3, callers must switch from `getRemainingLimit(...)` to `getRemainingLimitWithPeriod(...)`. The legacy `getRemainingLimit(...)` selector is dropped, while `getRemainingLimitWithPeriod(...)` returns both effective `remaining` and current `periodEnd`. Missing, revoked, or expired keys return zeroed limit values, and `getAllowedCalls(...)` distinguishes unrestricted keys from scoped deny-all keys by returning `isScoped = true, scopes = []` for missing, revoked, or expired access keys. - -### Allowed Call Updates - -This behavior does not exist at T2. - -#### T2 -> T3 changes - -At T3, `setAllowedCalls(...)` creates or replaces one or more target scopes, and `removeAllowedCalls(...)` removes one stored target scope. Empty `selectorRules` means any selector on that target is allowed, while `setAllowedCalls(...)` rejects an empty scope batch, zero targets, duplicate targets, duplicate selectors, duplicate recipients, and invalid recipient-constrained rules. +- Missing, revoked, or expired keys return zeroed limit values. ## Security Considerations ### Access Key Storage -Access Keys should be securely stored to prevent unauthorized access: +Access Keys should be securely stored to prevent unauthorized access. Call scopes make per-app and per-device key isolation more important, because a mis-scoped key may have a broader allowlist than intended. - **Device and Application Scoping**: Access Keys SHOULD be scoped to a specific client device AND application combination. Access Keys SHOULD NOT be shared between devices or applications, even if they belong to the same user. - **Non-Extractable Keys**: Access Keys SHOULD be generated and stored in a non-extractable format to prevent theft. For example, use WebCrypto API with `extractable: false` when generating Keys in web browsers. - **Secure Storage**: Private Keys MUST never be stored in plaintext. Private Keys SHOULD be encrypted and stored in a secure manner. For web applications, use browser-native secure storage mechanisms like IndexedDB with non-extractable WebCrypto keys rather than storing raw key material. -#### T2 -> T3 changes - -- T3 call scopes make per-app and per-device key isolation more important, because a mis-scoped key may have a broader allowlist than intended. - ### Privilege Escalation Prevention Access Keys cannot escalate their own privileges because: -1. Management functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`) are restricted to Root Key transactions +1. Management functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) are restricted to Root Key transactions 2. The protocol sets `transactionKey[account]` during transaction validation to indicate which key signed the transaction 3. These management functions check that `transactionKey[msg.sender] == 0` (Root Key) before executing -4. Access Keys cannot bypass this check - transactions will revert with `UnauthorizedCaller` - -#### T2 -> T3 changes - -The same Root-Key-only restriction applies to `setAllowedCalls(...)` and `removeAllowedCalls(...)`. On T2+ networks, mutable precompile calls also require `msg.sender == tx.origin`, which prevents contract-mediated confused-deputy patterns. +4. Mutable precompile calls also require `msg.sender == tx.origin`, which prevents contract-mediated confused-deputy patterns +5. Access Keys cannot bypass these checks - transactions will revert with `UnauthorizedCaller` ### Spending Limit Enforcement @@ -469,23 +260,19 @@ The same Root-Key-only restriction applies to `setAllowedCalls(...)` and `remove - Keys with `enforceLimits == false` have unlimited spending (no limits checked) - Spending limits are enforced by the protocol internally calling `verify_and_update_spending()` during execution - Limits are per-TIP20 token and deplete as TIP20 tokens are spent +- Recurring limits (`period > 0`) roll over to `max` when `current_timestamp >= periodEnd`. Callers can observe rollover state via `getRemainingLimitWithPeriod(...)`. - Spending limits only track TIP20 token transfers (via `transfer` and `transferWithMemo`) and approvals (via `approve`) - For approvals: only increases in approval amount count against the spending limit. This means approvals indirectly control `transferFrom` spending, since `transferFrom` requires a prior approval - Non-TIP20 asset movements (ETH, NFTs) are not subject to spending limits - Root keys (`keyId == address(0)`) have no spending limits - the function returns immediately +- Missing, revoked, or expired keys have an effective remaining limit of zero - Failed limit checks revert the entire transaction with `SpendingLimitExceeded` -#### T2 -> T3 changes - -At T3, recurring limits roll over when `current_timestamp >= periodEnd`. Missing, revoked, or expired keys have an effective remaining limit of zero, and `getRemainingLimitWithPeriod(...)` lets callers observe rollover state directly. - ### Call Scope Enforcement -This behavior does not exist at T2. - -#### T2 -> T3 changes - -At T3, call-scope checks run on top-level calls signed by an Access Key. If a key is scoped and a call does not match the stored target, selector, and recipient rules, execution reverts with `CallNotAllowed`, and access-key-signed transactions cannot create contracts after T3 activation. +- Call-scope checks run on top-level calls signed by an Access Key. +- If a key is scoped and a call does not match the stored target, selector, and recipient rules, execution reverts with `CallNotAllowed`. +- Access-key-signed transactions cannot create contracts in any configuration — including direct `CREATE`, factory `CREATE`, and internal `CREATE2`. Only Root-Key-signed transactions may perform contract creation. ### Key Expiry @@ -493,41 +280,137 @@ At T3, call-scope checks run on top-level calls signed by an Access Key. If a ke - Expired keys cause transaction rejection with `KeyExpired` error (checked via `validate_keychain_authorization()`) - New authorizations require a future expiry timestamp - Expiry is checked as: `current_timestamp >= expiry` (key is expired when current time reaches or exceeds expiry) - -#### T2 -> T3 changes - -Expired keys return zeroed limit and call-scope reads at T3. +- Expired keys return zeroed limit and call-scope reads. ## Usage Patterns ### First-Time Access Key Authorization -1. User signs Passkey prompt → signs over `key_authorization` for a new Access Key (e.g., WebCrypto P256 key) +1. User signs Passkey prompt → signs over `key_authorization` for a new Access Key (e.g., WebCrypto P256 key). The signed authorization carries `KeyRestrictions`, allowing the same first-use flow to provision recurring limits and call scopes. 2. User's Access Key signs the transaction 3. Transaction includes the `key_authorization` AND the Access Key `signature` 4. Protocol validates Passkey signature on `key_authorization`, sets `transactionKey[account] = 0`, calls `AccountKeychain.authorizeKey()`, then validates Access Key signature -5. Transaction executes with Access Key's spending limits enforced via internal `verify_and_update_spending()` - -#### T2 -> T3 changes - -The same flow still applies, but the signed authorization now carries `KeyRestrictions` instead of top-level expiry and limit fields. That lets the same first-use flow provision recurring limits and call scopes. +5. Transaction executes with Access Key's spending limits enforced via internal `verify_and_update_spending()`, plus call-scope checks if the key is scoped ### Subsequent Access Key Usage 1. User's Access Key signs the transaction (no `key_authorization` needed) 2. Protocol validates the Access Key via `validate_keychain_authorization()`, sets `transactionKey[account] = keyId` -3. Transaction executes with spending limit enforcement via internal `verify_and_update_spending()` +3. Transaction executes with spending limit enforcement via internal `verify_and_update_spending()` and call-scope enforcement. Contract creation is rejected. + +### Root Key Revoking or Updating an Access Key + +1. User signs Passkey prompt → signs transaction calling `revokeKey(keyId)`, `updateSpendingLimit(...)`, `setAllowedCalls(...)`, or `removeAllowedCalls(...)` +2. Transaction executes, marking the Access Key as inactive or updating its restrictions +3. Future transactions signed by that Access Key are rejected (after revocation) or evaluated against the updated restrictions + +## T2 → T3 migration + +:::info[Migration appendix] +This section captures changes introduced by the [T3 network upgrade](/protocol/upgrades/t3) for integrators migrating from T2. The spec above is the canonical post-T3 specification; this appendix exists for reference and will be removed in a future docs revision. +::: + +### Migration summary + +- The legacy `authorizeKey(address,uint8,uint64,bool,(address,uint256)[])` entrypoint (selector `0x54063a55`) is no longer accepted. Calls revert with `LegacyAuthorizeKeySelectorChanged(0x980a6025)`. +- `authorizeKey(...)` now takes a `KeyRestrictions` tuple that bundles expiry, spending limits, and call scopes. The flattened seven-argument form (which hashes to `0x203e2736` in Foundry) is rejected as an unknown selector — the call must use the canonical tuple-form selector `0x980a6025`. +- A non-expiring key is no longer represented by `expiry = 0`. Omit `key_authorization.expiry` in the transaction RLP, or pass `u64::MAX` when calling `authorizeKey(...)` directly. Literal `0` reverts with `ExpiryInPast`. +- `TokenLimit` gains a `period` field, so limits can be one-time (`period = 0`) or recurring. +- Access keys can be scoped to specific targets, selectors, and recipients via `KeyRestrictions.allowAnyCalls` and `KeyRestrictions.allowedCalls`. +- The precompile adds `setAllowedCalls(...)`, `removeAllowedCalls(...)`, `getAllowedCalls(...)`, and `getRemainingLimitWithPeriod(...)`. +- The legacy `getRemainingLimit(...)` selector is dropped. Callers must switch to `getRemainingLimitWithPeriod(...)`, which returns both `remaining` and `periodEnd`. +- Access-key-signed transactions can no longer create contracts. Use a Root Key for deployment flows. +- New errors: `InvalidSpendingLimit()`, `ExpiryInPast()`, `SignatureTypeMismatch(uint8,uint8)`, `CallNotAllowed()`, `InvalidCallScope()`, `LegacyAuthorizeKeySelectorChanged(bytes4)`. + +### Before and after `authorizeKey(...)` + +```text +T2 +authorizeKey(address,uint8,uint64,bool,(address,uint256)[]) +selector: 0x54063a55 + +T3 +authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])) +selector: 0x980a6025 +``` + +### Legacy T2 interface (reference only) + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +interface IAccountKeychainT2 { + enum SignatureType { + Secp256k1, + P256, + WebAuthn, + } + + struct TokenLimit { + address token; + uint256 amount; + } + + struct KeyInfo { + SignatureType signatureType; + address keyId; + uint64 expiry; + bool enforceLimits; + bool isRevoked; + } + + event KeyAuthorized( + address indexed account, + address indexed publicKey, + uint8 signatureType, + uint64 expiry + ); + event KeyRevoked(address indexed account, address indexed publicKey); + event SpendingLimitUpdated( + address indexed account, + address indexed publicKey, + address indexed token, + uint256 newLimit + ); + + error KeyAlreadyExists(); + error KeyNotFound(); + error KeyInactive(); + error KeyExpired(); + error KeyAlreadyRevoked(); + error SpendingLimitExceeded(); + error InvalidSignatureType(); + error ZeroPublicKey(); + error UnauthorizedCaller(); -#### T2 -> T3 changes + function authorizeKey( + address keyId, + SignatureType signatureType, + uint64 expiry, + bool enforceLimits, + TokenLimit[] calldata limits + ) external; -The same flow still applies, but T3 also enforces call scopes during execution and disallows contract creation from access-key-signed transactions. + function revokeKey(address keyId) external; -### Root Key Revoking an Access Key + function updateSpendingLimit( + address keyId, + address token, + uint256 newLimit + ) external; -1. User signs Passkey prompt → signs transaction calling `revokeKey(keyId)` -2. Transaction executes, marking the Access Key as inactive -3. Future transactions signed by that Access Key will be rejected + function getKey( + address account, + address keyId + ) external view returns (KeyInfo memory); -#### T2 -> T3 changes + function getRemainingLimit( + address account, + address keyId, + address token + ) external view returns (uint256); -The Root Key can still call `revokeKey(...)`. It can additionally call `updateSpendingLimit(...)`, `setAllowedCalls(...)`, and `removeAllowedCalls(...)` to modify restrictions after authorization, and `updateSpendingLimit(...)` now preserves the token's configured `period` and current `periodEnd`. + function getTransactionKey() external view returns (address); +} +``` diff --git a/src/pages/protocol/transactions/index.mdx b/src/pages/protocol/transactions/index.mdx index dda381ac..a218f46b 100644 --- a/src/pages/protocol/transactions/index.mdx +++ b/src/pages/protocol/transactions/index.mdx @@ -67,15 +67,6 @@ If you are an EVM smart contract developer, see the [Foundry guide for Tempo](/s ## Properties -:::info[T3 examples] -The examples below show the T3 Tempo Transaction shape. If you still support T2, the affected sections include a `T2 -> T3 changes` note with the migration details. - -- [Configurable Fee Tokens](#configurable-fee-tokens) -- [Access Keys](#access-keys) - -The examples assume you are using a T3-compatible network and the most recent SDK releases. -::: - diff --git a/src/pages/protocol/transactions/spec-tempo-transaction.mdx b/src/pages/protocol/transactions/spec-tempo-transaction.mdx index 1d6e3476..9a7603c7 100644 --- a/src/pages/protocol/transactions/spec-tempo-transaction.mdx +++ b/src/pages/protocol/transactions/spec-tempo-transaction.mdx @@ -4,10 +4,6 @@ description: Technical specification for the Tempo transaction type (EIP-2718) w # Tempo Transaction -:::info[T3 will change this spec] -The [T3 network upgrade](/protocol/upgrades/t3) will update this specification. The sections below describe the currently active behavior. See [Upcoming changes](#upcoming-changes) for the upcoming deltas. -::: - ## Abstract This spec introduces native protocol support for the following features, using a new Tempo transaction type: @@ -24,18 +20,6 @@ Current accounts are limited to secp256k1 signatures and sequential nonces, crea Users cannot leverage modern authentication methods like passkeys, applications face throughput limitations due to sequential nonces. -## Upcoming changes - -T3 updates the Tempo Transaction spec through [TIP-1011](/protocol/tips/tip-1011) in the following ways: - -- `KeyAuthorization` expands with the [TIP-1011](/protocol/tips/tip-1011) fields `allowed_calls` and periodic `TokenLimit.period`, adding call scoping and recurring spending limits to access keys. -- The signed post-T3 key-authorization payload remains `SignedKeyAuthorization { authorization, signature }`, but `authorization` now uses the expanded `KeyAuthorization` shape and new RLP encoding. -- Low-level integrators that manually encode `key_authorization` must branch pre-T3 vs post-T3. The post-T3 digest and signed payload include `allowed_calls?` in addition to `expiry?` and `limits?`. -- Access-key validation gains two new execution checks: call scopes must pass before execution begins, and access-key-signed transactions may no longer perform contract creation anywhere in the batch. -- The Account Keychain precompile ABI changes in lockstep with T3 to support periodic limits, call-scoped authorizations, and new scope-management functions. -- Intrinsic gas for `key_authorization` changes to account for periodic-limit state and call-scope storage. See [TIP-1011](/protocol/tips/tip-1011#intrinsic-gas-for-key-authorization) for the post-T3 slot-counting rules. - - ## Specification ### Transaction Type @@ -72,13 +56,14 @@ pub struct Call { } // Key authorization for provisioning access keys -// RLP encoding: [chain_id, key_type, key_id, expiry?, limits?] +// RLP encoding: [chain_id, key_type, key_id, expiry?, limits?, allowed_calls?] pub struct KeyAuthorization { chain_id: u64, // Chain ID for replay protection (0 = valid on any chain) key_type: SignatureType, // Type of key: Secp256k1 (0), P256 (1), or WebAuthn (2) key_id: Address, // Key identifier (address derived from public key) - expiry: Option, // Unix timestamp when key expires (None = never expires) + expiry: Option, // Unix timestamp when key expires (omit / None for never expires) limits: Option>, // TIP20 spending limits (None = unlimited spending) + allowed_calls: Option>, // Call-scope allowlist (None = unrestricted; Some(empty) = scoped deny-all) } // Signed key authorization (authorization + root key signature) @@ -87,10 +72,23 @@ pub struct SignedKeyAuthorization { signature: PrimitiveSignature, // Root key's signature over keccak256(rlp(authorization)) } -// TIP20 spending limits for access keys +// TIP20 spending limit for access keys pub struct TokenLimit { token: Address, // TIP20 token address limit: U256, // Maximum spending amount for this token + period: u64, // Recurring period in seconds (0 = one-time, non-zero = recurring) +} + +// Call-scope allowlist entry: a target contract and its allowed selector rules +pub struct CallScope { + target: Address, // Target contract address + selector_rules: Vec, // Allowed selectors on that target (empty = any selector allowed) +} + +// Selector rule: a function selector and optional recipient allowlist (for recipient-bound TIP-20 selectors) +pub struct SelectorRule { + selector: [u8; 4], // 4-byte function selector + recipients: Vec
, // Allowed recipients (empty = any recipient) } ``` @@ -461,7 +459,7 @@ rlp([ - The `key_authorization` field is truly optional - when `None`, no bytes are encoded (backwards compatible) - The `calls` field is a list that must contain at least one Call (empty calls list is invalid) - The `sender_signature` field is the final field and contains the TempoSignature bytes (secp256k1, P256, WebAuthn, or Keychain) -- KeyAuthorization uses RLP trailing field semantics for optional `expiry` and `limits` +- KeyAuthorization uses RLP trailing field semantics for optional `expiry`, `limits`, and `allowed_calls` ### WebAuthn Signature Verification @@ -569,9 +567,15 @@ A sender can authorize a key by signing over a "key authorization" item that con - **Key ID** (address derived from the public key) - **Expiration** timestamp of when the key should expire (optional - None means never expires) - TIP20 token **spending limits** for the key (optional - None means unlimited spending): - - Limits deplete as tokens are spent - - Root key can update limits via `updateSpendingLimit()` without revoking the key + - Each limit carries a `period` (0 = one-time, non-zero = recurring in seconds). Recurring limits roll over to `max` when `current_timestamp >= periodEnd`. + - Root key can update limits via `updateSpendingLimit()` without revoking the key. Updates reset `remaining` and `max` to `newLimit` while preserving `period` and `periodEnd`. - Note: Spending limits only apply to TIP20 token transfers, not ETH or other asset transfers +- **Call scopes** for the key (optional - None means unrestricted): + - Each entry pins a `target` contract and a list of allowed selector rules. An empty selector list on a target means any selector is allowed on that target. + - Selector rules can additionally constrain TIP-20 recipient-bearing selectors to a recipient allowlist. + - `Some([])` (an empty top-level allowlist) means scoped deny-all. + +Access-key-signed transactions cannot perform contract creation. Calls within the batch that would `CREATE` or `CREATE2` (including via factory contracts) are rejected. Use the Root Key for deployment flows. #### RLP Encoding @@ -580,13 +584,14 @@ A sender can authorize a key by signing over a "key authorization" item that con The root key signs over the keccak256 hash of the RLP encoded `KeyAuthorization`: ``` -key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?])) +key_authorization_digest = keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?])) chain_id = u64 (0 = valid on any chain) key_type = 0 (Secp256k1) | 1 (P256) | 2 (WebAuthn) key_id = Address (derived from the public key) -expiry = Option (unix timestamp, None = never expires, stored as u64::MAX in precompile) -limits = Option> (None = unlimited spending) +expiry = Option (unix timestamp, None = never expires; omitted expiry is translated to u64::MAX when the protocol calls the precompile) +limits = Option> (None = unlimited spending; period = 0 means one-time) +allowed_calls = Option> (None = unrestricted; Some([]) = scoped deny-all) ``` **Signed Format:** @@ -594,12 +599,22 @@ limits = Option> (None = unlimited spending) The signed format (`SignedKeyAuthorization`) includes all fields with the `signature` appended: ``` -signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, signature]) +signed_key_authorization = rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?, signature]) ``` The `signature` is a `PrimitiveSignature` (secp256k1, P256, or WebAuthn) signed by the root key. -Note: `expiry` and `limits` use RLP trailing field semantics - they can be omitted entirely when None. +Note: `expiry`, `limits`, and `allowed_calls` use RLP trailing field semantics — they can be omitted entirely when None. + +:::warning[Expiry encoding] +For `key_authorization`, the canonical non-expiring encoding omits `expiry` (`None`). + +There is one decoder nuance: because `KeyAuthorization` uses canonical trailing optional fields, an explicit empty `expiry` placeholder (`0x80`) is also interpreted as `None` when another trailing optional field follows it. But a final `expiry` encoded as zero/empty is rejected, and a literal `0x00` is invalid RLP for this field. + +Do not hand-encode `expiry = 0` or rely on `Some(0)` as a sentinel. The supported encoding to target is omission, and the protocol translates omitted expiry to `u64::MAX` when materializing the `AccountKeychain.authorizeKey(...)` call. +::: + +Intrinsic gas for `key_authorization` accounts for the storage written for periodic-limit state and call-scope entries. See [TIP-1011](/protocol/tips/tip-1011#intrinsic-gas-for-key-authorization) for slot-counting rules. #### Keychain Precompile @@ -622,7 +637,7 @@ When a TempoTransaction is received, the protocol: 2. **Validates KeyAuthorization** (if present in transaction) - The `key_authorization` field in `TempoTransaction` provisions a NEW Access Key - Root Key MUST sign: - - The `key_authorization` digest: `keccak256(rlp([key_type, key_id, expiry, limits]))` + - The `key_authorization` digest: `keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?, allowed_calls?]))` - Access Key (being authorized) CAN sign the same tx which it is authorized in. - This enables "authorize and use" in a single transaction @@ -634,7 +649,7 @@ When a TempoTransaction is received, the protocol: 4. **Validates Key Authorization** (for Access Keys) - Queries precompile: `getKey(account, keyId)` returns `KeyInfo` - Checks key is active (not revoked) - - Checks expiry: `current_timestamp < expiry` (or `expiry == 0` for never expires) + - Checks expiry: `current_timestamp < expiry`; non-expiring keys are stored with `expiry = u64::MAX` - Rejects transaction if validation fails ##### Authorization Hierarchy Enforcement @@ -649,12 +664,13 @@ The protocol enforces a strict two-tier hierarchy: **Access Keys** (keyId != address(0)): - Secondary keys authorized by Root Key -- CANNOT call mutable precompile functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`) +- CANNOT call mutable precompile functions (`authorizeKey`, `revokeKey`, `updateSpendingLimit`, `setAllowedCalls`, `removeAllowedCalls`) - Precompile functions check: `transactionKey[msg.sender] == 0` before allowing mutations -- Subject to per-TIP20 token spending limits +- Subject to per-TIP20 token spending limits and call-scope checks during execution +- Cannot create contracts (`CREATE` and `CREATE2` are rejected anywhere in the call batch) - Can have expiry timestamps -When an Access Key attempts to call `authorizeKey()`, `revokeKey()`, or `updateSpendingLimit()`: +When an Access Key attempts to call any mutable keychain function: 1. Transaction executes normally until the precompile call 2. Precompile checks `getTransactionKey()` returns non-zero (Access Key) 3. Call reverts with `UnauthorizedCaller` error @@ -675,19 +691,33 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: 1. Protocol intercepts `transfer(to, amount)`, `transferWithMemo()`, `approve(spender, amount)`, and `startReward()` calls 2. For `transfer`/`transferWithMemo`, the full `amount` is checked against the remaining limit 3. For `approve`, only **increases** in approval (new approval minus previous allowance) are checked and counted against the limit -4. Queries: `getRemainingLimit(account, keyId, token)` -5. Checks: relevant amount (transfer amount or approval increase) `<= remaining_limit` +4. Queries: `getRemainingLimitWithPeriod(account, keyId, token)`, which returns `(remaining, periodEnd)` and reflects any periodic rollover +5. Checks: relevant amount (transfer amount or approval increase) `<= remaining` 6. If check fails: reverts with `SpendingLimitExceeded` 7. If check passes: decrements the limit by the relevant amount 8. Updates are stored in precompile state **Root Key Behavior:** Spending limit checks are skipped entirely (no limits apply) +**Recurring Limits:** When a `TokenLimit.period` is non-zero, the limit recurs. `remaining` rolls over to `max` once `current_timestamp >= periodEnd`, and `periodEnd` advances by `period`. Callers observe rollover state via `getRemainingLimitWithPeriod`. + **Limit Updates:** - Limits deplete as tokens are spent - Root Key can call `updateSpendingLimit(keyId, token, newLimit)` to set new limits -- Setting a new limit REPLACES the current remaining amount (does not add to it) -- Limits do not reset automatically (no time-based periods) +- Setting a new limit REPLACES both `remaining` and `max` with `newLimit`. The configured `period` and current `periodEnd` are preserved. + +##### Call Scope Enforcement + +When an Access Key has stored call scopes (`allowed_calls` was set at authorization, or `setAllowedCalls(...)` was called later), the protocol enforces them on top-level calls signed by that Access Key: + +1. For each call in the batch, look up the matching `(target, selector)` allowlist entry +2. If the target is not in the allowlist, or the selector is not allowed on that target, revert with `CallNotAllowed` +3. For recipient-bound TIP-20 selectors (e.g., `transfer`, `transferFrom`, `transferWithMemo`), additionally enforce that the call's recipient is in the rule's recipient allowlist (when non-empty) +4. Access keys with `allowed_calls = None` are unrestricted; `Some([])` is scoped deny-all + +##### Contract Creation Restriction + +Access-key-signed transactions cannot perform contract creation. Any `CREATE` or `CREATE2` (including via factory contracts or internal calls) reverts the transaction. Use the Root Key for deployment flows. ##### Creating and Using KeyAuthorization @@ -707,14 +737,26 @@ The protocol tracks and enforces spending limits for TIP20 token transfers: chain_id: 1, key_type: SignatureType.P256, // 1 key_id: keyId, // address derived from public key - expiry: timestamp + 86400, // 24 hours from now (or 0 for never) + expiry: timestamp + 86400, // 24 hours from now; omit this field for a non-expiring key authorization limits: [ - { token: USDG_ADDRESS, limit: 1000000000 }, // 1000 USDG (6 decimals) - { token: DAI_ADDRESS, limit: 500000000000000000000 } // 500 DAI (18 decimals) + // One-time limit (period = 0) + { token: USDG_ADDRESS, limit: 1000000000, period: 0 }, // 1000 USDG (6 decimals), one-time + // Recurring weekly limit (period = 604800 seconds) + { token: DAI_ADDRESS, limit: 500000000000000000000n, period: 604800 } // 500 DAI / week + ], + // Optional call scopes — omit for an unrestricted key + allowed_calls: [ + { + target: USDG_ADDRESS, + selector_rules: [ + // transfer(address,uint256) restricted to a single recipient + { selector: "0xa9059cbb", recipients: [TRUSTED_RECIPIENT] } + ] + } ] }; - // Compute digest: keccak256(rlp([chain_id, key_type, key_id, expiry, limits])) + // Compute digest: keccak256(rlp([chain_id, key_type, key_id, expiry, limits, allowed_calls])) const authDigest = computeAuthorizationDigest(keyAuth); ``` @@ -826,7 +868,7 @@ const tx = { }; ``` -**Note:** After updating, the remaining limit is set to the `newLimit` value, not added to the current remaining amount. +**Note:** After updating, both `remaining` and `max` are set to `newLimit`. The configured `period` and current `periodEnd` are preserved. ##### Querying Key State @@ -835,11 +877,19 @@ Applications can query key information and spending limits: ```typescript // Check if key is authorized and get info const keyInfo = await precompile.getKey(account, keyId); -// Returns: { signatureType, keyId, expiry } +// Returns: { signatureType, keyId, expiry, enforceLimits, isRevoked } + +// Check remaining spending limit and current period end for a token +const { remaining, periodEnd } = await precompile.getRemainingLimitWithPeriod( + account, keyId, USDG_ADDRESS +); +// Returns: (uint256 remaining, uint64 periodEnd). Reflects periodic rollover. -// Check remaining spending limit for a token -const remaining = await precompile.getRemainingLimit(account, keyId, USDG_ADDRESS); -// Returns: uint256 amount remaining +// Inspect call scopes +const { isScoped, scopes } = await precompile.getAllowedCalls(account, keyId); +// isScoped = false → key is unrestricted +// isScoped = true, scopes = [...] → key is scoped to those (target, selector, recipient) entries +// isScoped = true, scopes = [] → scoped deny-all (also returned for missing/revoked/expired keys) // Get which key signed current transaction (callable from contracts) const currentKey = await precompile.getTransactionKey(); @@ -1010,10 +1060,14 @@ def calculate_key_authorization_gas(key_auth: SignedKeyAuthorization) -> uint256 Args: key_auth: SignedKeyAuthorization with fields: - signature: PrimitiveSignature (root key's signature) - - limits: Optional[List[TokenLimit]] + - limits: Optional[List[TokenLimit]] # each carries a `period` + - allowed_calls: Optional[List[CallScope]] # call-scope allowlist Returns: gas_cost: uint256 + + Note: This is a simplified illustration. See TIP-1011 for the canonical + slot-counting rules covering periodic-limit state and call-scope storage. """ # Constants - KeyAuthorization pays FULL signature verification costs # (not the "additional" costs used for transaction signatures) @@ -1039,10 +1093,21 @@ def calculate_key_authorization_gas(key_auth: SignedKeyAuthorization) -> uint256 # Step 3: Overhead buffer gas += OVERHEAD_BUFFER # 5,000 - # Step 4: Per-limit storage cost + # Step 4: Per-limit storage cost (each TokenLimit carries period state) num_limits = len(key_auth.limits) if key_auth.limits else 0 gas += num_limits * COLD_SSTORE_SET_GAS # 22,000 per limit + # Step 5: Per-call-scope storage cost (target + selector + recipients). + # See TIP-1011 for exact slot accounting; this counts one slot per + # (target, selector, recipient) triple as a conservative approximation. + num_scope_slots = 0 + if key_auth.allowed_calls: + for scope in key_auth.allowed_calls: + for rule in scope.selector_rules: + # one slot for the (target, selector) entry, plus one per recipient + num_scope_slots += 1 + max(len(rule.recipients), 0) + gas += num_scope_slots * COLD_SSTORE_SET_GAS + return gas @@ -1142,3 +1207,40 @@ The introduction of 7702 delegated accounts already created complex cross-transa Because a single transaction can invalidate multiple others by spending balances of multiple accounts **Assessment:** While this transaction type introduces additional pre-execution validation costs, all costs are bounded to reasonable limits. The mempool complexity issues around cross-transaction dependencies already exist in Ethereum due to 7702 and accounts with code, making static validation inherently difficult. So the incremental cost from this transaction type is acceptable given these existing constraints. + +## T2 → T3 migration + +:::info[Migration appendix] +This section captures changes introduced by the [T3 network upgrade](/protocol/upgrades/t3) for integrators migrating from T2. The spec above is the canonical post-T3 specification; this appendix exists for reference and will be removed in a future docs revision. +::: + +T3 expanded access keys through [TIP-1011](/protocol/tips/tip-1011) in the following ways: + +- `KeyAuthorization` gained `allowed_calls` (call-scope allowlist). +- `TokenLimit` gained `period` (recurring vs. one-time spending limits). +- The signed payload `SignedKeyAuthorization { authorization, signature }` is unchanged in shape, but `authorization` now uses the expanded `KeyAuthorization` and new RLP encoding. Low-level integrators that manually encode `key_authorization` must branch pre-T3 vs post-T3 — the post-T3 digest and signed payload include `allowed_calls?` in addition to `expiry?` and `limits?`. +- A non-expiring `key_authorization` omits `expiry` in tx RLP. At the Account Keychain ABI boundary, the protocol translates that omission to `u64::MAX`. Literal `0` is not a valid non-expiring sentinel to rely on. +- Access-key validation gained two new execution checks: call scopes must pass before execution begins, and access-key-signed transactions may not perform contract creation anywhere in the batch. +- The Account Keychain precompile ABI changed in lockstep — `authorizeKey(...)` now takes a `KeyRestrictions` tuple, `getRemainingLimit(...)` is replaced by `getRemainingLimitWithPeriod(...)`, and `setAllowedCalls(...)` / `removeAllowedCalls(...)` / `getAllowedCalls(...)` are added. See the [Account Keychain spec](./AccountKeychain) for full details. +- Intrinsic gas for `key_authorization` accounts for periodic-limit state and call-scope storage. See [TIP-1011](/protocol/tips/tip-1011#intrinsic-gas-for-key-authorization) for the canonical slot-counting rules. + +### Pre-T3 KeyAuthorization (reference only) + +Before T3, `KeyAuthorization` did not include `allowed_calls`, and `TokenLimit` did not include `period`: + +```rust +pub struct KeyAuthorization { + chain_id: u64, + key_type: SignatureType, + key_id: Address, + expiry: Option, + limits: Option>, +} + +pub struct TokenLimit { + token: Address, + limit: U256, +} +``` + +The pre-T3 digest was `keccak256(rlp([chain_id, key_type, key_id, expiry?, limits?]))` and the signed payload was `rlp([chain_id, key_type, key_id, expiry?, limits?, signature])`. diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/sdk/foundry/index.mdx index de064221..3cfd098e 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/sdk/foundry/index.mdx @@ -197,7 +197,7 @@ forge script script/Deploy.s.sol \ --private-key $PRIVATE_KEY ``` -Use a root key for `forge create`. Post-T3, access keys can sign calls but not deployments. +Use a root key for `forge create`. Access keys can sign calls but not deployments. For more verification options including verifying existing contracts and API verification, see [Contract Verification](/quickstart/verify-contracts). @@ -299,36 +299,9 @@ cast send 'increment()' \ --tempo.root-account $ROOT_ADDRESS ``` -#### T2 -> T3 changes - -If you are migrating a pre-T3 integration, this is the key change: direct `authorizeKey(...)` calls must switch from the legacy ABI to the tuple-form TIP-1011 ABI shown above. If you need periodic limits or call scopes, fill those arrays instead of passing `[]`. The flattened seven-argument form is not equivalent. - If the access key will be used with passkey or WebAuthn signatures, pass `2` for `SignatureType`. `1` is only for raw P256 signatures. -Post-T3, access-key transactions also cannot create contracts, so use a root key for deployments or other flows that perform `CREATE`. - -### Check your `authorizeKey(...)` migration with `cast` - -Most users can skip this if they use `cast keychain` below or the tuple-form `cast send` example above. This is mainly for hand-encoded calldata. - -```bash -# Legacy pre-T3 selector: -cast sig 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])' - -# Incorrect flattened post-T3 form. This is not accepted onchain: -cast sig 'authorizeKey(address,uint8,uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])' - -# Correct post-T3 tuple-form selector: -cast sig 'authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))' - -# Build the exact post-T3 calldata locally: -cast calldata 'authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))' \ - $ACCESS_KEY_ADDR 0 '(1893456000,false,[],true,[])' -``` - -The expected selectors are `0x54063a55`, `0x203e2736`, and `0x980a6025`. - -After T3 activation, the legacy selector reverts with `LegacyAuthorizeKeySelectorChanged(0x980a6025)`. Use the tuple-form `cast send` example above when you are ready to broadcast. +Access-key transactions cannot create contracts, so use a root key for deployments or other flows that perform `CREATE`. ### Local Development with Anvil @@ -398,14 +371,17 @@ Prefer this over hand-encoding `authorizeKey(...)` calldata when you are working `cast keychain` only works on Tempo networks. ::: -#### T2 -> T3 changes +`cast keychain` authorization takes a future expiry timestamp, `webauthn` for passkey-backed access keys, optional `TOKEN:AMOUNT:PERIOD_SECONDS` limits for recurring budgets, and `--scope` for target, selector, and recipient restrictions. -Post-T3, `cast keychain` authorization flows should use a future expiry timestamp, `webauthn` for passkey-backed access keys, optional `TOKEN:AMOUNT:PERIOD_SECONDS` limits for recurring budgets, and `--scope` for target, selector, and recipient restrictions. +`cast keychain` sends the Account Keychain ABI directly, so a non-expiring key uses `18446744073709551615` (`type(uint64).max`) as the expiry value. This differs from tx-level `key_authorization`, where a non-expiring key is represented by omitting `expiry`. Do not pass `0`. ```bash -# Post-T3, access keys must be authorized with a future expiry timestamp. +# Access keys must be authorized with a future expiry timestamp. EXPIRY=$(($(date +%s) + 86400)) +# For a non-expiring key via direct precompile ABI / cast keychain, use: +NEVER_EXPIRES=18446744073709551615 + # Authorize a new access key (signature types: secp256k1, p256, webauthn): cast keychain authorize secp256k1 $EXPIRY \ --rpc-url $TEMPO_RPC_URL \ @@ -468,3 +444,34 @@ cast call 0xAAAAAAAA00000000000000000000000000000000 \ \ --rpc-url $TEMPO_RPC_URL ``` + +## T2 → T3 migration + +:::info[Migration appendix] +This appendix is kept for an integrator cooldown period after the T3 mainnet activation on Apr 27, 2026. It will be removed in a future docs revision. +::: + +If you are migrating a pre-T3 integration, this is the key change: direct `authorizeKey(...)` calls must switch from the legacy ABI to the tuple-form TIP-1011 ABI used in the [`cast send` example above](#interact--debug-with-cast). If you need periodic limits or call scopes, fill those arrays instead of passing `[]`. The flattened seven-argument form is not equivalent. + +### Check your `authorizeKey(...)` migration with `cast` + +Most users can skip this if they use [`cast keychain`](#cast-keychain) or the tuple-form `cast send` example above. This is mainly for hand-encoded calldata. + +```bash +# Legacy pre-T3 selector: +cast sig 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])' + +# Incorrect flattened post-T3 form. This is not accepted onchain: +cast sig 'authorizeKey(address,uint8,uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[])' + +# Correct post-T3 tuple-form selector: +cast sig 'authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))' + +# Build the exact post-T3 calldata locally: +cast calldata 'authorizeKey(address,uint8,(uint64,bool,(address,uint256,uint64)[],bool,(address,(bytes4,address[])[])[]))' \ + $ACCESS_KEY_ADDR 0 '(1893456000,false,[],true,[])' +``` + +The expected selectors are `0x54063a55`, `0x203e2736`, and `0x980a6025`. + +After T3 activation, the legacy selector reverts with `LegacyAuthorizeKeySelectorChanged(0x980a6025)`. Use the tuple-form `cast send` example above when you are ready to broadcast. diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index b5fee7f2..68546fc6 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -234,10 +234,6 @@ user's preferred fee token and the validator's preferred token. -#### T2 -> T3 changes - -If you manually encode the Tempo Transaction envelope, rename `authorization_list` to `aa_authorization_list`. The higher-level SDK calls in this section are otherwise unchanged. - :::info See a full guide on [paying fees in any stablecoin](/guide/payments/pay-fees-in-any-stablecoin). ::: @@ -1255,13 +1251,6 @@ transactions thereafter can be signed by the access key. -#### T2 -> T3 changes - -- `key_authorization` now supports periodic `TokenLimit.period` values and `allowed_calls` call scopes. -- Prefer T3-aware SDK helpers like `client.accessKey.authorizeSync`, `AccountKeychain.authorize_key`, `keychain.AuthorizeKey`, or `cast keychain authorize` instead of hard-coding the legacy `authorizeKey(...)` ABI. -- Low-level envelope encoders must rename `authorization_list` to `aa_authorization_list`. -- Access-key-signed transactions can no longer create contracts. Use the root key for deployments or other flows that execute `CREATE`. - :::info Learn more about [Access Keys](/protocol/transactions/spec-tempo-transaction#access-keys). :::