1pub mod dispatch;
2
3use tempo_contracts::precompiles::{AccountKeychainError, AccountKeychainEvent};
4pub use tempo_contracts::precompiles::{
5 IAccountKeychain,
6 IAccountKeychain::{
7 KeyInfo, SignatureType, TokenLimit, authorizeKeyCall, getKeyCall, getRemainingLimitCall,
8 getTransactionKeyCall, revokeKeyCall, updateSpendingLimitCall,
9 },
10};
11
12use crate::{
13 ACCOUNT_KEYCHAIN_ADDRESS,
14 error::Result,
15 storage::{Mapping, PrecompileStorageProvider, Storable, double_mapping_slot},
16};
17use alloy::primitives::{Address, B256, Bytes, IntoLogData, U256};
18use revm::{
19 interpreter::instructions::utility::{IntoAddress, IntoU256},
20 state::Bytecode,
21};
22use tempo_precompiles_macros::{Storable, contract};
23
24#[derive(Debug, Clone, Default, PartialEq, Eq, Storable)]
32pub struct AuthorizedKey {
33 pub signature_type: u8,
35 pub expiry: u64,
37 pub enforce_limits: bool,
39 pub is_revoked: bool,
42}
43
44impl AuthorizedKey {
45 pub fn decode_from_slot(slot_value: U256) -> Self {
50 Self::from_evm_words([slot_value]).unwrap()
51 }
52}
53
54#[contract]
56pub struct AccountKeychain {
57 keys: Mapping<Address, Mapping<Address, AuthorizedKey>>,
59 spending_limits: Mapping<B256, Mapping<Address, U256>>,
62}
63
64const TRANSACTION_KEY_SLOT: U256 = U256::ZERO;
67
68pub fn compute_keys_slot(account: Address, key_id: Address) -> U256 {
76 double_mapping_slot(account, key_id, slots::KEYS)
77}
78
79impl<'a, S: PrecompileStorageProvider> AccountKeychain<'a, S> {
80 pub fn new(storage: &'a mut S) -> Self {
84 Self::_new(ACCOUNT_KEYCHAIN_ADDRESS, storage)
85 }
86
87 fn tload_transaction_key(&mut self) -> Result<Address> {
89 let value = self.storage.tload(self.address, TRANSACTION_KEY_SLOT)?;
90 Ok(value.into_address())
91 }
92
93 fn tstore_transaction_key(&mut self, key_id: Address) -> Result<()> {
95 self.storage.tstore(
96 ACCOUNT_KEYCHAIN_ADDRESS,
97 TRANSACTION_KEY_SLOT,
98 key_id.into_u256(),
99 )?;
100 Ok(())
101 }
102
103 fn spending_limit_key(account: Address, key_id: Address) -> B256 {
105 use alloy::primitives::keccak256;
106 let mut data = [0u8; 40];
107 data[..20].copy_from_slice(account.as_slice());
108 data[20..].copy_from_slice(key_id.as_slice());
109 keccak256(data)
110 }
111
112 pub fn initialize(&mut self) -> Result<()> {
114 self.storage.set_code(
115 ACCOUNT_KEYCHAIN_ADDRESS,
116 Bytecode::new_legacy(Bytes::from_static(&[0xef])),
117 )?;
118
119 Ok(())
120 }
121
122 pub fn authorize_key(&mut self, msg_sender: Address, call: authorizeKeyCall) -> Result<()> {
125 let transaction_key = self.tload_transaction_key()?;
127
128 if transaction_key != Address::ZERO {
130 return Err(AccountKeychainError::unauthorized_caller().into());
131 }
132
133 if call.keyId == Address::ZERO {
135 return Err(AccountKeychainError::zero_public_key().into());
136 }
137
138 let existing_key = self.sload_keys(msg_sender, call.keyId)?;
140 if existing_key.expiry > 0 {
141 return Err(AccountKeychainError::key_already_exists().into());
142 }
143
144 if existing_key.is_revoked {
146 return Err(AccountKeychainError::key_already_revoked().into());
147 }
148
149 let signature_type = match call.signatureType {
151 SignatureType::Secp256k1 => 0,
152 SignatureType::P256 => 1,
153 SignatureType::WebAuthn => 2,
154 _ => return Err(AccountKeychainError::invalid_signature_type().into()),
155 };
156
157 let new_key = AuthorizedKey {
159 signature_type,
160 expiry: call.expiry,
161 enforce_limits: call.enforceLimits,
162 is_revoked: false,
163 };
164
165 self.sstore_keys(msg_sender, call.keyId, new_key)?;
166
167 if call.enforceLimits {
169 let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
170 for limit in call.limits {
171 self.sstore_spending_limits(limit_key, limit.token, limit.amount)?;
172 }
173 }
174
175 let mut public_key_bytes = [0u8; 32];
177 public_key_bytes[12..].copy_from_slice(call.keyId.as_slice());
178 self.storage.emit_event(
179 ACCOUNT_KEYCHAIN_ADDRESS,
180 AccountKeychainEvent::KeyAuthorized(IAccountKeychain::KeyAuthorized {
181 account: msg_sender,
182 publicKey: B256::from(public_key_bytes),
183 signatureType: signature_type,
184 expiry: call.expiry,
185 })
186 .into_log_data(),
187 )?;
188
189 Ok(())
190 }
191
192 pub fn revoke_key(&mut self, msg_sender: Address, call: revokeKeyCall) -> Result<()> {
198 let transaction_key = self.tload_transaction_key()?;
199
200 if transaction_key != Address::ZERO {
201 return Err(AccountKeychainError::unauthorized_caller().into());
202 }
203
204 let key = self.sload_keys(msg_sender, call.keyId)?;
205
206 if key.expiry == 0 {
208 return Err(AccountKeychainError::key_not_found().into());
209 }
210
211 let revoked_key = AuthorizedKey {
215 is_revoked: true,
216 ..Default::default()
217 };
218 self.sstore_keys(msg_sender, call.keyId, revoked_key)?;
219
220 let mut public_key_bytes = [0u8; 32];
224 public_key_bytes[12..].copy_from_slice(call.keyId.as_slice());
225 self.storage.emit_event(
226 ACCOUNT_KEYCHAIN_ADDRESS,
227 AccountKeychainEvent::KeyRevoked(IAccountKeychain::KeyRevoked {
228 account: msg_sender,
229 publicKey: B256::from(public_key_bytes),
230 })
231 .into_log_data(),
232 )?;
233
234 Ok(())
235 }
236
237 pub fn update_spending_limit(
242 &mut self,
243 msg_sender: Address,
244 call: updateSpendingLimitCall,
245 ) -> Result<()> {
246 let transaction_key = self.tload_transaction_key()?;
247
248 if transaction_key != Address::ZERO {
249 return Err(AccountKeychainError::unauthorized_caller().into());
250 }
251
252 let mut key = self.load_active_key(msg_sender, call.keyId)?;
254
255 let current_timestamp = self.storage.timestamp().saturating_to::<u64>();
256 if current_timestamp >= key.expiry {
257 return Err(AccountKeychainError::key_expired().into());
258 }
259
260 if !key.enforce_limits {
262 key.enforce_limits = true;
263 self.sstore_keys(msg_sender, call.keyId, key)?;
264 }
265
266 let limit_key = Self::spending_limit_key(msg_sender, call.keyId);
268 self.sstore_spending_limits(limit_key, call.token, call.newLimit)?;
269
270 let mut public_key_bytes = [0u8; 32];
272 public_key_bytes[12..].copy_from_slice(call.keyId.as_slice());
273 self.storage.emit_event(
274 ACCOUNT_KEYCHAIN_ADDRESS,
275 AccountKeychainEvent::SpendingLimitUpdated(IAccountKeychain::SpendingLimitUpdated {
276 account: msg_sender,
277 publicKey: B256::from(public_key_bytes),
278 token: call.token,
279 newLimit: call.newLimit,
280 })
281 .into_log_data(),
282 )?;
283
284 Ok(())
285 }
286
287 pub fn get_key(&mut self, call: getKeyCall) -> Result<KeyInfo> {
289 let key = self.sload_keys(call.account, call.keyId)?;
290
291 if key.expiry == 0 || key.is_revoked {
293 return Ok(KeyInfo {
294 signatureType: SignatureType::Secp256k1,
295 keyId: Address::ZERO,
296 expiry: 0,
297 enforceLimits: false,
298 isRevoked: key.is_revoked,
299 });
300 }
301
302 let signature_type = match key.signature_type {
304 0 => SignatureType::Secp256k1,
305 1 => SignatureType::P256,
306 2 => SignatureType::WebAuthn,
307 _ => SignatureType::Secp256k1, };
309
310 Ok(KeyInfo {
311 signatureType: signature_type,
312 keyId: call.keyId,
313 expiry: key.expiry,
314 enforceLimits: key.enforce_limits,
315 isRevoked: key.is_revoked,
316 })
317 }
318
319 pub fn get_remaining_limit(&mut self, call: getRemainingLimitCall) -> Result<U256> {
321 let limit_key = Self::spending_limit_key(call.account, call.keyId);
322 self.sload_spending_limits(limit_key, call.token)
323 }
324
325 pub fn get_transaction_key(
327 &mut self,
328 _call: getTransactionKeyCall,
329 _msg_sender: Address,
330 ) -> Result<Address> {
331 self.tload_transaction_key()
332 }
333
334 pub fn set_transaction_key(&mut self, key_id: Address) -> Result<()> {
345 self.tstore_transaction_key(key_id)?;
346 Ok(())
347 }
348
349 fn load_active_key(&mut self, account: Address, key_id: Address) -> Result<AuthorizedKey> {
358 let key = self.sload_keys(account, key_id)?;
359
360 if key.is_revoked {
361 return Err(AccountKeychainError::key_already_revoked().into());
362 }
363
364 if key.expiry == 0 {
365 return Err(AccountKeychainError::key_not_found().into());
366 }
367
368 Ok(key)
369 }
370
371 pub fn validate_keychain_authorization(
376 &mut self,
377 account: Address,
378 key_id: Address,
379 current_timestamp: u64,
380 ) -> Result<()> {
381 let key = self.load_active_key(account, key_id)?;
382
383 if current_timestamp >= key.expiry {
384 return Err(AccountKeychainError::key_expired().into());
385 }
386
387 Ok(())
388 }
389
390 pub fn verify_and_update_spending(
392 &mut self,
393 account: Address,
394 key_id: Address,
395 token: Address,
396 amount: U256,
397 ) -> Result<()> {
398 if key_id == Address::ZERO {
400 return Ok(());
401 }
402
403 let key = self.load_active_key(account, key_id)?;
405
406 if !key.enforce_limits {
408 return Ok(());
409 }
410
411 let limit_key = Self::spending_limit_key(account, key_id);
413 let remaining = self.sload_spending_limits(limit_key, token)?;
414
415 if amount > remaining {
416 return Err(AccountKeychainError::spending_limit_exceeded().into());
417 }
418
419 self.sstore_spending_limits(limit_key, token, remaining - amount)?;
421
422 Ok(())
423 }
424
425 pub fn authorize_transfer(
440 &mut self,
441 account: Address,
442 token: Address,
443 amount: U256,
444 ) -> Result<()> {
445 let transaction_key = self.tload_transaction_key()?;
447
448 if transaction_key == Address::ZERO {
450 return Ok(());
451 }
452
453 self.verify_and_update_spending(account, transaction_key, token, amount)
455 }
456
457 pub fn authorize_approve(
473 &mut self,
474 account: Address,
475 token: Address,
476 old_approval: U256,
477 new_approval: U256,
478 ) -> Result<()> {
479 let transaction_key = self.tload_transaction_key()?;
481
482 if transaction_key == Address::ZERO {
484 return Ok(());
485 }
486
487 let approval_increase = new_approval.saturating_sub(old_approval);
491
492 if approval_increase.is_zero() {
494 return Ok(());
495 }
496
497 self.verify_and_update_spending(account, transaction_key, token, approval_increase)
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use crate::storage::hashmap::HashMapStorageProvider;
506 use alloy::primitives::{Address, U256};
507 use tempo_contracts::precompiles::IAccountKeychain::SignatureType;
508
509 #[test]
510 fn test_transaction_key_transient_storage() {
511 let mut storage = HashMapStorageProvider::new(1); let mut keychain = AccountKeychain::new(&mut storage);
513
514 let initial_key = keychain.tload_transaction_key().unwrap();
516 assert_eq!(
517 initial_key,
518 Address::ZERO,
519 "Initial transaction key should be zero"
520 );
521
522 let access_key_addr = Address::from([0x01; 20]);
524 keychain.set_transaction_key(access_key_addr).unwrap();
525
526 let loaded_key = keychain.tload_transaction_key().unwrap();
528 assert_eq!(loaded_key, access_key_addr, "Transaction key should be set");
529
530 let get_tx_key_call = getTransactionKeyCall {};
532 let result = keychain
533 .get_transaction_key(get_tx_key_call, Address::ZERO)
534 .unwrap();
535 assert_eq!(
536 result, access_key_addr,
537 "getTransactionKey should return the set key"
538 );
539
540 keychain.set_transaction_key(Address::ZERO).unwrap();
542 let cleared_key = keychain.tload_transaction_key().unwrap();
543 assert_eq!(
544 cleared_key,
545 Address::ZERO,
546 "Transaction key should be cleared"
547 );
548 }
549
550 #[test]
551 fn test_admin_operations_blocked_with_access_key() {
552 let mut storage = HashMapStorageProvider::new(1); let mut keychain = AccountKeychain::new(&mut storage);
554
555 keychain.initialize().unwrap();
557
558 let msg_sender = Address::from([0x01; 20]);
559 let existing_key = Address::from([0x02; 20]);
560 let access_key = Address::from([0x03; 20]);
561 let token = Address::from([0x04; 20]);
562
563 keychain.set_transaction_key(Address::ZERO).unwrap();
565 let setup_call = authorizeKeyCall {
566 keyId: existing_key,
567 signatureType: SignatureType::Secp256k1,
568 expiry: u64::MAX,
569 enforceLimits: true,
570 limits: vec![],
571 };
572 keychain.authorize_key(msg_sender, setup_call).unwrap();
573
574 keychain.set_transaction_key(access_key).unwrap();
576
577 let auth_call = authorizeKeyCall {
579 keyId: Address::from([0x05; 20]),
580 signatureType: SignatureType::P256,
581 expiry: u64::MAX,
582 enforceLimits: true,
583 limits: vec![],
584 };
585 let auth_result = keychain.authorize_key(msg_sender, auth_call);
586 assert!(
587 auth_result.is_err(),
588 "authorize_key should fail when using access key"
589 );
590 assert_unauthorized_error(auth_result.unwrap_err());
591
592 let revoke_call = revokeKeyCall {
594 keyId: existing_key,
595 };
596 let revoke_result = keychain.revoke_key(msg_sender, revoke_call);
597 assert!(
598 revoke_result.is_err(),
599 "revoke_key should fail when using access key"
600 );
601 assert_unauthorized_error(revoke_result.unwrap_err());
602
603 let update_call = updateSpendingLimitCall {
605 keyId: existing_key,
606 token,
607 newLimit: U256::from(1000),
608 };
609 let update_result = keychain.update_spending_limit(msg_sender, update_call);
610 assert!(
611 update_result.is_err(),
612 "update_spending_limit should fail when using access key"
613 );
614 assert_unauthorized_error(update_result.unwrap_err());
615
616 fn assert_unauthorized_error(error: crate::error::TempoPrecompileError) {
618 match error {
619 crate::error::TempoPrecompileError::AccountKeychainError(e) => {
620 assert!(
621 matches!(e, AccountKeychainError::UnauthorizedCaller(_)),
622 "Expected UnauthorizedCaller error, got: {e:?}"
623 );
624 }
625 _ => panic!("Expected AccountKeychainError, got: {error:?}"),
626 }
627 }
628 }
629
630 #[test]
631 fn test_replay_protection_revoked_key_cannot_be_reauthorized() {
632 let mut storage = HashMapStorageProvider::new(1);
633 let mut keychain = AccountKeychain::new(&mut storage);
634 keychain.initialize().unwrap();
635
636 let account = Address::from([0x01; 20]);
637 let key_id = Address::from([0x02; 20]);
638
639 keychain.set_transaction_key(Address::ZERO).unwrap();
641
642 let auth_call = authorizeKeyCall {
644 keyId: key_id,
645 signatureType: SignatureType::Secp256k1,
646 expiry: u64::MAX,
647 enforceLimits: false,
648 limits: vec![],
649 };
650 keychain.authorize_key(account, auth_call.clone()).unwrap();
651
652 let key_info = keychain
654 .get_key(getKeyCall {
655 account,
656 keyId: key_id,
657 })
658 .unwrap();
659 assert_eq!(key_info.expiry, u64::MAX);
660 assert!(!key_info.isRevoked);
661
662 let revoke_call = revokeKeyCall { keyId: key_id };
664 keychain.revoke_key(account, revoke_call).unwrap();
665
666 let key_info = keychain
668 .get_key(getKeyCall {
669 account,
670 keyId: key_id,
671 })
672 .unwrap();
673 assert_eq!(key_info.expiry, 0);
674 assert!(key_info.isRevoked);
675
676 let replay_result = keychain.authorize_key(account, auth_call);
679 assert!(
680 replay_result.is_err(),
681 "Re-authorizing a revoked key should fail"
682 );
683
684 match replay_result.unwrap_err() {
686 crate::error::TempoPrecompileError::AccountKeychainError(e) => {
687 assert!(
688 matches!(e, AccountKeychainError::KeyAlreadyRevoked(_)),
689 "Expected KeyAlreadyRevoked error, got: {e:?}"
690 );
691 }
692 e => panic!("Expected AccountKeychainError, got: {e:?}"),
693 }
694 }
695
696 #[test]
697 fn test_different_key_id_can_be_authorized_after_revocation() {
698 let mut storage = HashMapStorageProvider::new(1);
699 let mut keychain = AccountKeychain::new(&mut storage);
700 keychain.initialize().unwrap();
701
702 let account = Address::from([0x01; 20]);
703 let key_id_1 = Address::from([0x02; 20]);
704 let key_id_2 = Address::from([0x03; 20]);
705
706 keychain.set_transaction_key(Address::ZERO).unwrap();
708
709 let auth_call_1 = authorizeKeyCall {
711 keyId: key_id_1,
712 signatureType: SignatureType::Secp256k1,
713 expiry: u64::MAX,
714 enforceLimits: false,
715 limits: vec![],
716 };
717 keychain.authorize_key(account, auth_call_1).unwrap();
718
719 keychain
721 .revoke_key(account, revokeKeyCall { keyId: key_id_1 })
722 .unwrap();
723
724 let auth_call_2 = authorizeKeyCall {
726 keyId: key_id_2,
727 signatureType: SignatureType::P256,
728 expiry: 1000,
729 enforceLimits: true,
730 limits: vec![],
731 };
732 keychain.authorize_key(account, auth_call_2).unwrap();
733
734 let key_info = keychain
736 .get_key(getKeyCall {
737 account,
738 keyId: key_id_2,
739 })
740 .unwrap();
741 assert_eq!(key_info.expiry, 1000);
742 assert!(!key_info.isRevoked);
743 }
744}