Skip to main content

tempo_precompiles/account_keychain/
dispatch.rs

1//! ABI dispatch for the [`AccountKeychain`] precompile.
2
3use 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}