1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#![allow(clippy::too_many_arguments)]
pub use IAccountKeychain::{
IAccountKeychainErrors as AccountKeychainError, IAccountKeychainEvents as AccountKeychainEvent,
authorizeKey_0Call as legacyAuthorizeKeyCall, authorizeKey_1Call as authorizeKeyCall,
getAllowedCallsReturn, getRemainingLimitWithPeriodCall,
getRemainingLimitWithPeriodReturn as getRemainingLimitReturn,
};
crate::sol! {
/// Account Keychain interface for managing authorized keys
///
/// This precompile allows accounts to authorize secondary keys with:
/// - Different signature types (secp256k1, P256, WebAuthn)
/// - Expiry times for key rotation
/// - Per-token spending limits for security
///
/// Only the main account key can authorize/revoke keys, while secondary keys
/// can be used for regular transactions within their spending limits.
#[derive(Debug, PartialEq, Eq)]
#[sol(abi)]
interface IAccountKeychain {
enum SignatureType {
Secp256k1,
P256,
WebAuthn,
}
/// Legacy token spending limit structure used before T3.
struct LegacyTokenLimit {
address token;
uint256 amount;
}
/// Token spending limit structure
struct TokenLimit {
address token;
uint256 amount;
uint64 period;
}
/// Selector-level recipient rule.
struct SelectorRule {
bytes4 selector;
/// Empty means no recipient restriction for this selector.
/// To block the selector entirely, remove the selector rule instead of passing `[]`.
address[] recipients;
}
/// Per-target call scope.
struct CallScope {
address target;
/// Empty means no selector restriction for this target.
/// To block the target entirely, omit this scope from `allowedCalls` or call
/// `removeAllowedCalls` for incremental updates.
SelectorRule[] selectorRules;
}
/// Optional access-key restrictions configured at authorization time.
struct KeyRestrictions {
uint64 expiry;
bool enforceLimits;
TokenLimit[] limits;
/// `true` means the key is unrestricted and `allowedCalls` must be empty.
/// `false` means `allowedCalls` defines the full call scope (including deny-all with `[]`).
bool allowAnyCalls;
CallScope[] allowedCalls;
}
/// Key information structure
struct KeyInfo {
SignatureType signatureType;
address keyId;
uint64 expiry;
bool enforceLimits;
bool isRevoked;
}
/// Emitted when a new key is authorized
event KeyAuthorized(address indexed account, address indexed publicKey, uint8 signatureType, uint64 expiry);
/// Emitted when a key is revoked
event KeyRevoked(address indexed account, address indexed publicKey);
/// Emitted when a spending limit is updated
event SpendingLimitUpdated(address indexed account, address indexed publicKey, address indexed token, uint256 newLimit);
event AccessKeySpend(
address indexed account,
address indexed publicKey,
address indexed token,
uint256 amount,
uint256 remainingLimit
);
/// Legacy authorize-key entrypoint used before T3.
function authorizeKey(
address keyId,
SignatureType signatureType,
uint64 expiry,
bool enforceLimits,
LegacyTokenLimit[] calldata limits
) external;
/// Authorize a new key for the caller's account with T3 extensions.
/// @param keyId The key identifier (address derived from public key)
/// @param signatureType 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;
/// Revoke an authorized key
/// @param publicKey The public key to revoke
function revokeKey(address keyId) external;
/// Update spending limit for a key-token pair
/// @param publicKey The public key
/// @param token The token address
/// @param newLimit The new spending limit
function updateSpendingLimit(
address keyId,
address token,
uint256 newLimit
) external;
/// Set or replace allowed calls for one or more key+target pairs.
/// @dev Reverts if `scopes` is empty; use `removeAllowedCalls` to delete target scopes.
/// @dev `scope.selectorRules = []` does NOT block the target; it allows any selector on that target.
/// @dev To block the target entirely, call `removeAllowedCalls`. To block one selector,
/// omit that selector rule from `scope.selectorRules`.
function setAllowedCalls(
address keyId,
CallScope[] calldata scopes
) external;
/// Remove any configured call scope for a key+target pair.
function removeAllowedCalls(address keyId, address target) external;
/// Get key information
/// @param account The account address
/// @param publicKey The public key
/// @return Key information
function getKey(address account, address keyId) external view returns (KeyInfo memory);
/// Get remaining spending limit using the legacy pre-T3 return shape.
/// @param account The account address
/// @param publicKey The public key
/// @param token The token address
function getRemainingLimit(
address account,
address keyId,
address token
) external view returns (uint256 remaining);
/// Get remaining spending limit together with the active period end.
/// @param account The account address
/// @param publicKey The public key
/// @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);
/// 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.
/// @dev Missing, revoked, or expired access keys also return scoped deny-all so callers do
/// not observe stale persisted scope state.
function getAllowedCalls(
address account,
address keyId
) external view returns (bool isScoped, CallScope[] memory scopes);
/// Get the key used in the current transaction
/// @return The keyId used in the current transaction
function getTransactionKey() external view returns (address);
// Errors
error UnauthorizedCaller();
error KeyAlreadyExists();
error KeyNotFound();
error KeyExpired();
error SpendingLimitExceeded();
error InvalidSpendingLimit();
error InvalidSignatureType();
error ZeroPublicKey();
error ExpiryInPast();
error KeyAlreadyRevoked();
error SignatureTypeMismatch(uint8 expected, uint8 actual);
error CallNotAllowed();
error InvalidCallScope();
error LegacyAuthorizeKeySelectorChanged(bytes4 newSelector);
}
}
impl AccountKeychainError {
/// Creates an error for signature type mismatch.
pub const fn signature_type_mismatch(expected: u8, actual: u8) -> Self {
Self::SignatureTypeMismatch(IAccountKeychain::SignatureTypeMismatch { expected, actual })
}
/// Creates an error for unauthorized caller.
pub const fn unauthorized_caller() -> Self {
Self::UnauthorizedCaller(IAccountKeychain::UnauthorizedCaller {})
}
/// Creates an error for key already exists.
pub const fn key_already_exists() -> Self {
Self::KeyAlreadyExists(IAccountKeychain::KeyAlreadyExists {})
}
/// Creates an error for key not found.
pub const fn key_not_found() -> Self {
Self::KeyNotFound(IAccountKeychain::KeyNotFound {})
}
/// Creates an error for key expired.
pub const fn key_expired() -> Self {
Self::KeyExpired(IAccountKeychain::KeyExpired {})
}
/// Creates an error for spending limit exceeded.
pub const fn spending_limit_exceeded() -> Self {
Self::SpendingLimitExceeded(IAccountKeychain::SpendingLimitExceeded {})
}
/// Creates an error for spending limits that exceed the TIP-20 u128 supply cap.
pub const fn invalid_spending_limit() -> Self {
Self::InvalidSpendingLimit(IAccountKeychain::InvalidSpendingLimit {})
}
/// Creates an error for invalid signature type.
pub const fn invalid_signature_type() -> Self {
Self::InvalidSignatureType(IAccountKeychain::InvalidSignatureType {})
}
/// Creates an error for zero public key.
pub const fn zero_public_key() -> Self {
Self::ZeroPublicKey(IAccountKeychain::ZeroPublicKey {})
}
/// Creates an error for expiry timestamp in the past.
pub const fn expiry_in_past() -> Self {
Self::ExpiryInPast(IAccountKeychain::ExpiryInPast {})
}
/// Creates an error for when a key_id has already been revoked.
/// Once revoked, a key_id can never be re-authorized for the same account.
/// This prevents replay attacks where a revoked key's authorization is reused.
pub const fn key_already_revoked() -> Self {
Self::KeyAlreadyRevoked(IAccountKeychain::KeyAlreadyRevoked {})
}
/// Creates an error for disallowed call attempts by scoped access keys.
pub const fn call_not_allowed() -> Self {
Self::CallNotAllowed(IAccountKeychain::CallNotAllowed {})
}
/// Creates an error for invalid scope configuration.
pub const fn invalid_call_scope() -> Self {
Self::InvalidCallScope(IAccountKeychain::InvalidCallScope {})
}
/// Creates an error for the legacy authorize-key selector being unavailable on T3+.
pub fn legacy_authorize_key_selector_changed(new_selector: [u8; 4]) -> Self {
Self::LegacyAuthorizeKeySelectorChanged(
IAccountKeychain::LegacyAuthorizeKeySelectorChanged {
newSelector: new_selector.into(),
},
)
}
}