1use super::{AccountKeychain, KeyRestrictions, TokenLimit, authorizeKeyCall};
4use crate::{Precompile, SelectorSchedule, charge_input_cost, dispatch_call, mutate_void, view};
5use alloy::{
6 primitives::Address,
7 sol_types::{SolCall, SolInterface},
8};
9use revm::precompile::PrecompileResult;
10use tempo_chainspec::hardfork::TempoHardfork;
11use tempo_contracts::precompiles::{
12 AccountKeychainError,
13 IAccountKeychain::{self, IAccountKeychainCalls},
14};
15
16const T3_ADDED: &[[u8; 4]] = &[
17 authorizeKeyCall::SELECTOR,
18 IAccountKeychain::setAllowedCallsCall::SELECTOR,
19 IAccountKeychain::removeAllowedCallsCall::SELECTOR,
20 IAccountKeychain::getRemainingLimitWithPeriodCall::SELECTOR,
21 IAccountKeychain::getAllowedCallsCall::SELECTOR,
22];
23const T3_DROPPED: &[[u8; 4]] = &[IAccountKeychain::getRemainingLimitCall::SELECTOR];
24const T5_ADDED: &[[u8; 4]] = &[
25 IAccountKeychain::authorizeKey_2Call::SELECTOR,
26 IAccountKeychain::burnKeyAuthorizationWitnessCall::SELECTOR,
27 IAccountKeychain::isKeyAuthorizationWitnessBurnedCall::SELECTOR,
28];
29const T6_ADDED: &[[u8; 4]] = &[
30 IAccountKeychain::authorizeAdminKeyCall::SELECTOR,
31 IAccountKeychain::isAdminKeyCall::SELECTOR,
32];
33
34impl Precompile for AccountKeychain {
35 fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
36 if let Some(err) = charge_input_cost(&mut self.storage, calldata) {
37 return err;
38 }
39
40 dispatch_call(
41 calldata,
42 &[
43 SelectorSchedule::new(TempoHardfork::T3)
44 .with_added(T3_ADDED)
45 .with_dropped(T3_DROPPED),
46 SelectorSchedule::new(TempoHardfork::T5).with_added(T5_ADDED),
47 SelectorSchedule::new(TempoHardfork::T6).with_added(T6_ADDED),
48 ],
49 IAccountKeychainCalls::abi_decode,
50 |call| match call {
51 IAccountKeychainCalls::authorizeKey_0(call) => {
52 if self.storage.spec().is_t3() {
53 return self.storage.error_result(
54 AccountKeychainError::legacy_authorize_key_selector_changed(
55 authorizeKeyCall::SELECTOR.into(),
56 ),
57 );
58 }
59
60 let call = authorizeKeyCall {
61 keyId: call.keyId,
62 signatureType: call.signatureType,
63 config: KeyRestrictions {
64 expiry: call.expiry,
65 enforceLimits: call.enforceLimits,
66 limits: call
67 .limits
68 .into_iter()
69 .map(|limit| TokenLimit {
70 token: limit.token,
71 amount: limit.amount,
72 period: 0,
73 })
74 .collect(),
75 allowAnyCalls: true,
76 allowedCalls: vec![],
77 },
78 };
79
80 mutate_void(call, msg_sender, |sender, c| {
81 self.authorize_key(sender, c.keyId, c.signatureType, c.config, None)
82 })
83 }
84 IAccountKeychainCalls::authorizeKey_1(call) => {
85 mutate_void(call, msg_sender, |sender, c| {
86 self.authorize_key(sender, c.keyId, c.signatureType, c.config, None)
87 })
88 }
89 IAccountKeychainCalls::authorizeKey_2(call) => {
90 mutate_void(call, msg_sender, |sender, c| {
91 self.authorize_key(
92 sender,
93 c.keyId,
94 c.signatureType,
95 c.config,
96 Some(c.witness),
97 )
98 })
99 }
100 IAccountKeychainCalls::authorizeAdminKey(call) => {
101 mutate_void(call, msg_sender, |sender, c| {
102 self.authorize_admin_key(sender, c.keyId, c.signatureType, Some(c.witness))
103 })
104 }
105 IAccountKeychainCalls::burnKeyAuthorizationWitness(call) => {
106 mutate_void(call, msg_sender, |sender, c| {
107 self.burn_key_authorization_witness(sender, c)
108 })
109 }
110 IAccountKeychainCalls::revokeKey(call) => {
111 mutate_void(call, msg_sender, |sender, c| self.revoke_key(sender, c))
112 }
113 IAccountKeychainCalls::updateSpendingLimit(call) => {
114 mutate_void(call, msg_sender, |sender, c| {
115 self.update_spending_limit(sender, c)
116 })
117 }
118 IAccountKeychainCalls::setAllowedCalls(call) => {
119 mutate_void(call, msg_sender, |sender, c| {
120 self.set_allowed_calls(sender, c)
121 })
122 }
123 IAccountKeychainCalls::removeAllowedCalls(call) => {
124 mutate_void(call, msg_sender, |sender, c| {
125 self.remove_allowed_calls(sender, c)
126 })
127 }
128 IAccountKeychainCalls::getKey(call) => view(call, |c| self.get_key(c)),
129 IAccountKeychainCalls::getRemainingLimit(call) => {
130 view(call, |c| self.get_remaining_limit(c))
131 }
132 IAccountKeychainCalls::getRemainingLimitWithPeriod(call) => {
133 view(call, |c| self.get_remaining_limit_with_period(c))
134 }
135 IAccountKeychainCalls::getAllowedCalls(call) => {
136 view(call, |c| self.get_allowed_calls(c))
137 }
138 IAccountKeychainCalls::isKeyAuthorizationWitnessBurned(call) => {
139 view(call, |c| self.is_key_authorization_witness_burned(c))
140 }
141 IAccountKeychainCalls::isAdminKey(call) => {
142 view(call, |c| self.is_admin_key(c.account, c.keyId))
143 }
144 IAccountKeychainCalls::getTransactionKey(call) => {
145 view(call, |c| self.get_transaction_key(c, msg_sender))
146 }
147 },
148 )
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use crate::{
156 Precompile,
157 account_keychain::{getRemainingLimitCall, getRemainingLimitWithPeriodCall},
158 storage::{Handler, StorageCtx, hashmap::HashMapStorageProvider},
159 test_util::{assert_full_coverage, check_selector_coverage},
160 };
161 use alloy::{
162 primitives::{B256, U256},
163 sol_types::{SolCall, SolError},
164 };
165 use tempo_chainspec::hardfork::TempoHardfork;
166 use tempo_contracts::precompiles::{UnknownFunctionSelector, legacyAuthorizeKeyCall};
167
168 #[test]
169 fn test_account_keychain_selector_coverage() -> eyre::Result<()> {
170 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
171 StorageCtx::enter(&mut storage, || {
172 let mut fee_manager = AccountKeychain::new();
173 let selectors: Vec<_> = IAccountKeychainCalls::SELECTORS
174 .iter()
175 .copied()
176 .filter(|selector| *selector != getRemainingLimitCall::SELECTOR)
177 .collect();
178
179 let unsupported = check_selector_coverage(
180 &mut fee_manager,
181 &selectors,
182 "IAccountKeychain",
183 IAccountKeychainCalls::name_by_selector,
184 );
185
186 assert_full_coverage([unsupported]);
187
188 Ok(())
189 })
190 }
191
192 #[test]
193 fn test_legacy_authorize_key_selector_supported_pre_t3() -> eyre::Result<()> {
194 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
195 let account = Address::random();
196 let key_id = Address::random();
197 let token = Address::random();
198
199 StorageCtx::enter(&mut storage, || {
200 let mut keychain = AccountKeychain::new();
201 keychain.initialize()?;
202
203 let calldata = legacyAuthorizeKeyCall {
204 keyId: key_id,
205 signatureType:
206 tempo_contracts::precompiles::IAccountKeychain::SignatureType::Secp256k1,
207 expiry: u64::MAX,
208 enforceLimits: true,
209 limits: vec![
210 tempo_contracts::precompiles::IAccountKeychain::LegacyTokenLimit {
211 token,
212 amount: U256::from(100),
213 },
214 ],
215 }
216 .abi_encode();
217
218 let _ = keychain.call(&calldata, account)?;
219
220 let key = keychain.keys[account][key_id].read()?;
221 assert_eq!(key.expiry, u64::MAX);
222
223 let limit_key = AccountKeychain::spending_limit_key(account, key_id);
224 let remaining = keychain.spending_limits[limit_key][token].read()?.remaining;
225 assert_eq!(remaining, U256::from(100));
226
227 Ok(())
228 })
229 }
230
231 #[test]
232 fn test_new_authorize_key_selector_rejected_pre_t3() -> eyre::Result<()> {
233 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
234 let account = Address::random();
235
236 StorageCtx::enter(&mut storage, || {
237 let mut keychain = AccountKeychain::new();
238 keychain.initialize()?;
239
240 let calldata = authorizeKeyCall {
241 keyId: Address::random(),
242 signatureType: IAccountKeychain::SignatureType::Secp256k1,
243 config: KeyRestrictions {
244 expiry: u64::MAX,
245 enforceLimits: true,
246 limits: vec![TokenLimit {
247 token: Address::random(),
248 amount: U256::from(100),
249 period: 0,
250 }],
251 allowAnyCalls: true,
252 allowedCalls: vec![],
253 },
254 }
255 .abi_encode();
256
257 let result = keychain.call(&calldata, account)?;
258 assert!(result.is_revert());
259
260 Ok(())
261 })
262 }
263
264 #[test]
265 fn test_legacy_authorize_key_selector_rejected_post_t3() -> eyre::Result<()> {
266 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
267 let account = Address::random();
268
269 StorageCtx::enter(&mut storage, || {
270 let mut keychain = AccountKeychain::new();
271 keychain.initialize()?;
272
273 let calldata = legacyAuthorizeKeyCall {
274 keyId: Address::random(),
275 signatureType: IAccountKeychain::SignatureType::Secp256k1,
276 expiry: u64::MAX,
277 enforceLimits: false,
278 limits: vec![],
279 }
280 .abi_encode();
281
282 let result = keychain.call(&calldata, account)?;
283 assert!(result.is_revert());
284 let decoded =
285 IAccountKeychain::LegacyAuthorizeKeySelectorChanged::abi_decode(&result.bytes)?;
286 assert_eq!(decoded.newSelector, authorizeKeyCall::SELECTOR);
287
288 Ok(())
289 })
290 }
291
292 #[test]
293 fn test_get_remaining_limit_uses_legacy_return_shape_pre_t3() -> eyre::Result<()> {
294 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
295 let account = Address::random();
296 let key_id = Address::random();
297 let token = Address::random();
298
299 StorageCtx::enter(&mut storage, || {
300 let mut keychain = AccountKeychain::new();
301 keychain.initialize()?;
302
303 let authorize_calldata = legacyAuthorizeKeyCall {
304 keyId: key_id,
305 signatureType: IAccountKeychain::SignatureType::Secp256k1,
306 expiry: u64::MAX,
307 enforceLimits: true,
308 limits: vec![IAccountKeychain::LegacyTokenLimit {
309 token,
310 amount: U256::from(123),
311 }],
312 }
313 .abi_encode();
314 let _ = keychain.call(&authorize_calldata, account)?;
315
316 let get_limit_calldata = getRemainingLimitCall {
317 account,
318 keyId: key_id,
319 token,
320 }
321 .abi_encode();
322
323 let output = keychain.call(&get_limit_calldata, account)?;
324 assert!(!output.is_revert());
325 assert_eq!(
326 output.bytes.len(),
327 32,
328 "pre-T3 should return legacy uint256"
329 );
330
331 let remaining = getRemainingLimitCall::abi_decode_returns(&output.bytes)?;
332 assert_eq!(remaining, U256::from(123));
333
334 Ok(())
335 })
336 }
337
338 #[test]
339 fn test_get_remaining_limit_with_period_rejected_pre_t3() -> eyre::Result<()> {
340 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
341 let account = Address::random();
342
343 StorageCtx::enter(&mut storage, || {
344 let mut keychain = AccountKeychain::new();
345 keychain.initialize()?;
346
347 let calldata = getRemainingLimitWithPeriodCall {
348 account,
349 keyId: Address::random(),
350 token: Address::random(),
351 }
352 .abi_encode();
353
354 let result = keychain.call(&calldata, account)?;
355 assert!(result.is_revert());
356
357 Ok(())
358 })
359 }
360
361 #[test]
362 fn test_get_remaining_limit_returns_unknown_selector_post_t3() -> eyre::Result<()> {
363 let account = Address::random();
364 let key_id = Address::random();
365 let token = Address::random();
366
367 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
368 StorageCtx::enter(&mut storage, || {
369 let mut keychain = AccountKeychain::new();
370 keychain.initialize()?;
371
372 let calldata = getRemainingLimitCall {
373 account,
374 keyId: key_id,
375 token,
376 }
377 .abi_encode();
378
379 let result = keychain.call(&calldata, account)?;
380 assert!(
381 result.is_revert(),
382 "expected revert for dropped selector post-T3"
383 );
384
385 let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?;
386 assert_eq!(
387 decoded.selector.as_slice(),
388 &getRemainingLimitCall::SELECTOR,
389 );
390
391 Ok(())
392 })
393 }
394
395 #[test]
396 fn test_t5_witness_selectors_rejected_pre_t5() -> eyre::Result<()> {
397 let account = Address::random();
398 let witness = B256::repeat_byte(0x53);
399
400 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T4);
401 StorageCtx::enter(&mut storage, || {
402 let mut keychain = AccountKeychain::new();
403 keychain.initialize()?;
404
405 for (selector, calldata) in [
406 (
407 IAccountKeychain::authorizeKey_2Call::SELECTOR,
408 IAccountKeychain::authorizeKey_2Call {
409 keyId: Address::random(),
410 signatureType: IAccountKeychain::SignatureType::Secp256k1,
411 config: KeyRestrictions {
412 expiry: u64::MAX,
413 enforceLimits: false,
414 limits: vec![],
415 allowAnyCalls: true,
416 allowedCalls: vec![],
417 },
418 witness,
419 }
420 .abi_encode(),
421 ),
422 (
423 IAccountKeychain::burnKeyAuthorizationWitnessCall::SELECTOR,
424 IAccountKeychain::burnKeyAuthorizationWitnessCall { witness }.abi_encode(),
425 ),
426 (
427 IAccountKeychain::isKeyAuthorizationWitnessBurnedCall::SELECTOR,
428 IAccountKeychain::isKeyAuthorizationWitnessBurnedCall { account, witness }
429 .abi_encode(),
430 ),
431 ] {
432 let result = keychain.call(&calldata, account)?;
433 assert!(result.is_revert(), "expected T5 selector to revert pre-T5");
434
435 let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?;
436 assert_eq!(decoded.selector.as_slice(), &selector);
437 }
438
439 Ok(())
440 })
441 }
442
443 #[test]
444 fn test_t3_selector_with_malformed_data_returns_unknown_selector_error() -> eyre::Result<()> {
445 let selector = getRemainingLimitWithPeriodCall::SELECTOR;
446 let calldata = selector.to_vec();
447
448 let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
449 StorageCtx::enter(&mut storage, || {
450 let mut keychain = AccountKeychain::new();
451
452 let result = keychain.call(&calldata, Address::ZERO)?;
453 assert!(result.is_revert(), "expected revert");
454
455 let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?;
456 assert_eq!(decoded.selector.as_slice(), &selector);
457
458 Ok(())
459 })
460 }
461}