From ed3c3c2a360b935f9a7d3d34f8ab9feb0ed10c1f Mon Sep 17 00:00:00 2001 From: Derek Cofausper <256792747+decofe@users.noreply.github.com> Date: Fri, 3 Apr 2026 12:40:57 +0000 Subject: [PATCH 1/2] feat: add T3 hardfork support - IAccountKeychain: T3 ABI with call scopes (CallScope, SelectorRule), periodic token limits (TokenLimit.period), KeyRestrictions struct, authorizeKey V2, setAllowedCalls, removeAllowedCalls, getAllowedCalls, getRemainingLimitWithPeriod, new errors and AccessKeySpend event - StdPrecompiles: add ADDRESS_REGISTRY, SIGNATURE_VERIFIER, and VALIDATOR_CONFIG_V2 addresses and typed constants Co-Authored-By: grandizzy <38490174+grandizzy@users.noreply.github.com> Amp-Thread-ID: https://ampcode.com/threads/T-019d5352-e28f-72dd-b87b-befc7fce5cf8 --- README.md | 6 +- src/StdPrecompiles.sol | 9 +++ src/interfaces/IAccountKeychain.sol | 117 ++++++++++++++++++++++++---- 3 files changed, 116 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 10945bf..4123e30 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ forge install tempoxyz/tempo-std
src
├── interfaces
-│ ├── IAccountKeychain.sol: Account Keychain | Docs | Implementation
-│ ├── IAddressRegistry.sol: TIP-1022 Virtual Address Registry | Implementation
+│ ├── IAccountKeychain.sol: Account Keychain (T3: call scopes, periodic limits) | Docs | Implementation
+│ ├── IAddressRegistry.sol: TIP-1022 Virtual Address Registry (T3+) | Implementation
│ ├── IFeeAMM.sol: Fee AMM | Docs | Implementation
│ ├── IFeeManager.sol: Fee AMM Management | Docs | Implementation
│ ├── INonce.sol: 2D Nonce Management for Tempo Transactions | Implementation
-│ ├── ISignatureVerifier.sol: TIP-1020 Signature Verification Precompile | Implementation
+│ ├── ISignatureVerifier.sol: TIP-1020 Signature Verification Precompile (T3+) | Implementation
│ ├── IStablecoinDEX.sol: Stablecoin DEX | Docs | Implementation
│ ├── ITempoStreamChannel.sol: Streaming payment channel escrow (concept) | Implementation
│ ├── ITIP20Factory.sol: TIP-20: Factory Contract | Docs | Implementation
diff --git a/src/StdPrecompiles.sol b/src/StdPrecompiles.sol
index e0e90d3..513b6c5 100644
--- a/src/StdPrecompiles.sol
+++ b/src/StdPrecompiles.sol
@@ -2,12 +2,15 @@
pragma solidity >=0.8.13 <0.9.0;
import {IAccountKeychain} from "./interfaces/IAccountKeychain.sol";
+import {IAddressRegistry} from "./interfaces/IAddressRegistry.sol";
import {IFeeManager} from "./interfaces/IFeeManager.sol";
+import {ISignatureVerifier} from "./interfaces/ISignatureVerifier.sol";
import {ITIP403Registry} from "./interfaces/ITIP403Registry.sol";
import {ITIP20Factory} from "./interfaces/ITIP20Factory.sol";
import {ITIP20RewardsRegistry} from "./interfaces/ITIP20RewardsRegistry.sol";
import {IStablecoinDEX} from "./interfaces/IStablecoinDEX.sol";
import {IValidatorConfig} from "./interfaces/IValidatorConfig.sol";
+import {IValidatorConfigV2} from "./interfaces/IValidatorConfigV2.sol";
import {INonce} from "./interfaces/INonce.sol";
/// @title Standard Precompiles Library for Tempo
@@ -22,6 +25,9 @@ library StdPrecompiles {
address internal constant NONCE_ADDRESS = 0x4e4F4E4345000000000000000000000000000000;
address internal constant VALIDATOR_CONFIG_ADDRESS = 0xCccCcCCC00000000000000000000000000000000;
address internal constant ACCOUNT_KEYCHAIN_ADDRESS = 0xaAAAaaAA00000000000000000000000000000000;
+ address internal constant VALIDATOR_CONFIG_V2_ADDRESS = 0xCcCCCCcC00000000000000000000000000000001;
+ address internal constant ADDRESS_REGISTRY_ADDRESS = 0xfDC0000000000000000000000000000000000000;
+ address internal constant SIGNATURE_VERIFIER_ADDRESS = 0x5165300000000000000000000000000000000000;
IFeeManager internal constant TIP_FEE_MANAGER = IFeeManager(TIP_FEE_MANAGER_ADDRESS);
ITIP403Registry internal constant TIP403_REGISTRY = ITIP403Registry(TIP403_REGISTRY_ADDRESS);
@@ -32,4 +38,7 @@ library StdPrecompiles {
INonce internal constant NONCE_PRECOMPILE = INonce(NONCE_ADDRESS);
IValidatorConfig internal constant VALIDATOR_CONFIG = IValidatorConfig(VALIDATOR_CONFIG_ADDRESS);
IAccountKeychain internal constant ACCOUNT_KEYCHAIN = IAccountKeychain(ACCOUNT_KEYCHAIN_ADDRESS);
+ IValidatorConfigV2 internal constant VALIDATOR_CONFIG_V2 = IValidatorConfigV2(VALIDATOR_CONFIG_V2_ADDRESS);
+ IAddressRegistry internal constant ADDRESS_REGISTRY = IAddressRegistry(ADDRESS_REGISTRY_ADDRESS);
+ ISignatureVerifier internal constant SIGNATURE_VERIFIER = ISignatureVerifier(SIGNATURE_VERIFIER_ADDRESS);
}
diff --git a/src/interfaces/IAccountKeychain.sol b/src/interfaces/IAccountKeychain.sol
index 0d57737..8ba08b1 100644
--- a/src/interfaces/IAccountKeychain.sol
+++ b/src/interfaces/IAccountKeychain.sol
@@ -9,9 +9,11 @@ pragma solidity >=0.8.13 <0.9.0;
* The Account Keychain allows accounts to authorize secondary keys (Access Keys) that can sign
* transactions on behalf of the account. Access Keys can be scoped by:
* - Expiry timestamp (when the key becomes invalid)
- * - Per-TIP20 token spending limits that deplete as the key spends
+ * - Per-TIP20 token spending limits (one-time or periodic) that deplete as the key spends
+ * - Call scopes restricting which contracts/selectors the key may call (T3+)
*
- * Only the Root Key can call authorizeKey, revokeKey, and updateSpendingLimit.
+ * Only the Root Key can call authorizeKey, revokeKey, updateSpendingLimit, setAllowedCalls,
+ * and removeAllowedCalls.
* This restriction is enforced by the protocol at transaction validation time.
* Access Keys attempting to call these functions will fail with UnauthorizedCaller.
*
@@ -30,10 +32,38 @@ interface IAccountKeychain {
WebAuthn
}
+ /// @notice Legacy token spending limit structure used before T3
+ struct LegacyTokenLimit {
+ address token; // TIP20 token address
+ uint256 amount; // Spending limit amount
+ }
+
/// @notice Token spending limit structure
struct TokenLimit {
address token; // TIP20 token address
uint256 amount; // Spending limit amount
+ uint64 period; // Period duration in seconds (0 = one-time limit, >0 = periodic reset)
+ }
+
+ /// @notice Selector-level recipient rule
+ struct SelectorRule {
+ bytes4 selector; // 4-byte function selector
+ address[] recipients; // Empty means no recipient restriction for this selector
+ }
+
+ /// @notice Per-target call scope
+ struct CallScope {
+ address target; // Target contract address
+ SelectorRule[] selectorRules; // Empty means any selector is allowed for this target
+ }
+
+ /// @notice Optional access-key restrictions configured at authorization time
+ struct KeyRestrictions {
+ uint64 expiry; // Unix timestamp when key expires (use type(uint64).max for never)
+ bool enforceLimits; // Whether spending limits are enforced for this key
+ TokenLimit[] limits; // Token spending limits
+ bool allowAnyCalls; // true = unrestricted calls (allowedCalls must be empty), false = allowedCalls defines scope
+ CallScope[] allowedCalls; // Call scopes when allowAnyCalls is false
}
/// @notice Key information structure
@@ -60,29 +90,37 @@ interface IAccountKeychain {
address indexed account, address indexed publicKey, address indexed token, uint256 newLimit
);
+ /// @notice Emitted when an access key spends tokens
+ event AccessKeySpend(
+ address indexed account, address indexed publicKey, address indexed token, uint256 amount, uint256 remainingLimit
+ );
+
/*//////////////////////////////////////////////////////////////
ERRORS
//////////////////////////////////////////////////////////////*/
+ error UnauthorizedCaller();
error KeyAlreadyExists();
error KeyNotFound();
- error KeyInactive();
error KeyExpired();
- error KeyAlreadyRevoked();
error SpendingLimitExceeded();
+ error InvalidSpendingLimit();
error InvalidSignatureType();
error ZeroPublicKey();
error ExpiryInPast();
- error UnauthorizedCaller();
+ error KeyAlreadyRevoked();
+ error SignatureTypeMismatch(uint8 expected, uint8 actual);
+ error CallNotAllowed();
+ error InvalidCallScope();
+ error LegacyAuthorizeKeySelectorChanged(bytes4 newSelector);
/*//////////////////////////////////////////////////////////////
MANAGEMENT FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
- * @notice Authorize a new key for the caller's account
+ * @notice Legacy authorize-key entrypoint used before T3
* @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 (use type(uint64).max for never expires)
@@ -94,13 +132,21 @@ interface IAccountKeychain {
SignatureType signatureType,
uint64 expiry,
bool enforceLimits,
- TokenLimit[] calldata limits
+ LegacyTokenLimit[] calldata limits
) external;
+ /**
+ * @notice Authorize a new key for the caller's account with T3 extensions
+ * @dev MUST only be called in transactions signed by the Root Key
+ * @param keyId The key identifier (address derived from public key)
+ * @param signatureType Signature type of the key (0: Secp256k1, 1: P256, 2: WebAuthn)
+ * @param config Access-key expiry and optional limits / call restrictions
+ */
+ function authorizeKey(address keyId, SignatureType signatureType, KeyRestrictions calldata config) 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;
@@ -108,13 +154,30 @@ interface IAccountKeychain {
/**
* @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;
+ /**
+ * @notice Set or replace allowed calls for one or more key+target pairs
+ * @dev MUST only be called in transactions signed by the Root Key.
+ * Reverts if `scopes` is empty; use `removeAllowedCalls` to delete target scopes.
+ * `scope.selectorRules = []` allows any selector on that target.
+ * @param keyId The key ID to configure
+ * @param scopes The call scopes to set
+ */
+ function setAllowedCalls(address keyId, CallScope[] calldata scopes) external;
+
+ /**
+ * @notice Remove any configured call scope for a key+target pair
+ * @dev MUST only be called in transactions signed by the Root Key
+ * @param keyId The key ID to update
+ * @param target The target contract to remove from allowed calls
+ */
+ function removeAllowedCalls(address keyId, address target) external;
+
/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/
@@ -128,13 +191,41 @@ interface IAccountKeychain {
function getKey(address account, address keyId) external view returns (KeyInfo memory);
/**
- * @notice Get remaining spending limit for a key-token pair
+ * @notice Get remaining spending limit for a key-token pair (legacy)
* @param account The account address
* @param keyId The key ID
* @param token The token address
- * @return Remaining spending amount
+ * @return remaining Remaining spending amount
+ */
+ function getRemainingLimit(address account, address keyId, address token) external view returns (uint256 remaining);
+
+ /**
+ * @notice Get remaining spending limit together with the active period end
+ * @param account The account address
+ * @param keyId The key ID
+ * @param token The token address
+ * @return remaining Remaining spending amount
+ * @return periodEnd Period end timestamp for periodic limits (0 for one-time)
+ */
+ function getRemainingLimitWithPeriod(address account, address keyId, address token)
+ external
+ view
+ returns (uint256 remaining, uint64 periodEnd);
+
+ /**
+ * @notice Returns whether an account key is call-scoped and, if so, the configured call scopes
+ * @dev `isScoped = false` means unrestricted. `isScoped = true && scopes.length == 0`
+ * means scoped deny-all. Missing, revoked, or expired access keys also return scoped
+ * deny-all so callers do not observe stale persisted scope state.
+ * @param account The account address
+ * @param keyId The key ID
+ * @return isScoped Whether the key is call-scoped
+ * @return scopes The configured call scopes
*/
- function getRemainingLimit(address account, address keyId, address token) external view returns (uint256);
+ function getAllowedCalls(address account, address keyId)
+ external
+ view
+ returns (bool isScoped, CallScope[] memory scopes);
/**
* @notice Get the transaction key used in the current transaction
From 80ac343cffb5d04aa69972bb8cf853bd36ffcd7b Mon Sep 17 00:00:00 2001
From: Derek Cofausper <256792747+decofe@users.noreply.github.com>
Date: Fri, 3 Apr 2026 12:42:42 +0000
Subject: [PATCH 2/2] chore: forge fmt
Co-Authored-By: grandizzy <38490174+grandizzy@users.noreply.github.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d5352-e28f-72dd-b87b-befc7fce5cf8
---
src/interfaces/IAccountKeychain.sol | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/interfaces/IAccountKeychain.sol b/src/interfaces/IAccountKeychain.sol
index 8ba08b1..e7297b9 100644
--- a/src/interfaces/IAccountKeychain.sol
+++ b/src/interfaces/IAccountKeychain.sol
@@ -92,7 +92,11 @@ interface IAccountKeychain {
/// @notice Emitted when an access key spends tokens
event AccessKeySpend(
- address indexed account, address indexed publicKey, address indexed token, uint256 amount, uint256 remainingLimit
+ address indexed account,
+ address indexed publicKey,
+ address indexed token,
+ uint256 amount,
+ uint256 remainingLimit
);
/*//////////////////////////////////////////////////////////////