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];
24
25impl Precompile for AccountKeychain {
26    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
27        if let Some(err) = charge_input_cost(&mut self.storage, calldata) {
28            return err;
29        }
30
31        dispatch_call(
32            calldata,
33            &[SelectorSchedule::new(TempoHardfork::T3)
34                .with_added(T3_ADDED)
35                .with_dropped(T3_DROPPED)],
36            IAccountKeychainCalls::abi_decode,
37            |call| match call {
38                IAccountKeychainCalls::authorizeKey_0(call) => {
39                    if self.storage.spec().is_t3() {
40                        return self.storage.error_result(
41                            AccountKeychainError::legacy_authorize_key_selector_changed(
42                                authorizeKeyCall::SELECTOR,
43                            ),
44                        );
45                    }
46
47                    let call = authorizeKeyCall {
48                        keyId: call.keyId,
49                        signatureType: call.signatureType,
50                        config: KeyRestrictions {
51                            expiry: call.expiry,
52                            enforceLimits: call.enforceLimits,
53                            limits: call
54                                .limits
55                                .into_iter()
56                                .map(|limit| TokenLimit {
57                                    token: limit.token,
58                                    amount: limit.amount,
59                                    period: 0,
60                                })
61                                .collect(),
62                            allowAnyCalls: true,
63                            allowedCalls: vec![],
64                        },
65                    };
66
67                    mutate_void(call, msg_sender, |sender, c| self.authorize_key(sender, c))
68                }
69                IAccountKeychainCalls::authorizeKey_1(call) => {
70                    mutate_void(call, msg_sender, |sender, c| self.authorize_key(sender, c))
71                }
72                IAccountKeychainCalls::revokeKey(call) => {
73                    mutate_void(call, msg_sender, |sender, c| self.revoke_key(sender, c))
74                }
75                IAccountKeychainCalls::updateSpendingLimit(call) => {
76                    mutate_void(call, msg_sender, |sender, c| {
77                        self.update_spending_limit(sender, c)
78                    })
79                }
80                IAccountKeychainCalls::setAllowedCalls(call) => {
81                    mutate_void(call, msg_sender, |sender, c| {
82                        self.set_allowed_calls(sender, c)
83                    })
84                }
85                IAccountKeychainCalls::removeAllowedCalls(call) => {
86                    mutate_void(call, msg_sender, |sender, c| {
87                        self.remove_allowed_calls(sender, c)
88                    })
89                }
90                IAccountKeychainCalls::getKey(call) => view(call, |c| self.get_key(c)),
91                IAccountKeychainCalls::getRemainingLimit(call) => {
92                    view(call, |c| self.get_remaining_limit(c))
93                }
94                IAccountKeychainCalls::getRemainingLimitWithPeriod(call) => {
95                    view(call, |c| self.get_remaining_limit_with_period(c))
96                }
97                IAccountKeychainCalls::getAllowedCalls(call) => {
98                    view(call, |c| self.get_allowed_calls(c))
99                }
100                IAccountKeychainCalls::getTransactionKey(call) => {
101                    view(call, |c| self.get_transaction_key(c, msg_sender))
102                }
103            },
104        )
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111    use crate::{
112        Precompile,
113        account_keychain::{getRemainingLimitCall, getRemainingLimitWithPeriodCall},
114        storage::{Handler, StorageCtx, hashmap::HashMapStorageProvider},
115        test_util::{assert_full_coverage, check_selector_coverage},
116    };
117    use alloy::{
118        primitives::U256,
119        sol_types::{SolCall, SolError},
120    };
121    use tempo_chainspec::hardfork::TempoHardfork;
122    use tempo_contracts::precompiles::{UnknownFunctionSelector, legacyAuthorizeKeyCall};
123
124    #[test]
125    fn test_account_keychain_selector_coverage() -> eyre::Result<()> {
126        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
127        StorageCtx::enter(&mut storage, || {
128            let mut fee_manager = AccountKeychain::new();
129            let selectors: Vec<_> = IAccountKeychainCalls::SELECTORS
130                .iter()
131                .copied()
132                .filter(|selector| *selector != getRemainingLimitCall::SELECTOR)
133                .collect();
134
135            let unsupported = check_selector_coverage(
136                &mut fee_manager,
137                &selectors,
138                "IAccountKeychain",
139                IAccountKeychainCalls::name_by_selector,
140            );
141
142            assert_full_coverage([unsupported]);
143
144            Ok(())
145        })
146    }
147
148    #[test]
149    fn test_legacy_authorize_key_selector_supported_pre_t3() -> eyre::Result<()> {
150        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
151        let account = Address::random();
152        let key_id = Address::random();
153        let token = Address::random();
154
155        StorageCtx::enter(&mut storage, || {
156            let mut keychain = AccountKeychain::new();
157            keychain.initialize()?;
158
159            let calldata = legacyAuthorizeKeyCall {
160                keyId: key_id,
161                signatureType:
162                    tempo_contracts::precompiles::IAccountKeychain::SignatureType::Secp256k1,
163                expiry: u64::MAX,
164                enforceLimits: true,
165                limits: vec![
166                    tempo_contracts::precompiles::IAccountKeychain::LegacyTokenLimit {
167                        token,
168                        amount: U256::from(100),
169                    },
170                ],
171            }
172            .abi_encode();
173
174            let _ = keychain.call(&calldata, account)?;
175
176            let key = keychain.keys[account][key_id].read()?;
177            assert_eq!(key.expiry, u64::MAX);
178
179            let limit_key = AccountKeychain::spending_limit_key(account, key_id);
180            let remaining = keychain.spending_limits[limit_key][token].read()?.remaining;
181            assert_eq!(remaining, U256::from(100));
182
183            Ok(())
184        })
185    }
186
187    #[test]
188    fn test_new_authorize_key_selector_rejected_pre_t3() -> eyre::Result<()> {
189        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
190        let account = Address::random();
191
192        StorageCtx::enter(&mut storage, || {
193            let mut keychain = AccountKeychain::new();
194            keychain.initialize()?;
195
196            let calldata = authorizeKeyCall {
197                keyId: Address::random(),
198                signatureType: IAccountKeychain::SignatureType::Secp256k1,
199                config: KeyRestrictions {
200                    expiry: u64::MAX,
201                    enforceLimits: true,
202                    limits: vec![TokenLimit {
203                        token: Address::random(),
204                        amount: U256::from(100),
205                        period: 0,
206                    }],
207                    allowAnyCalls: true,
208                    allowedCalls: vec![],
209                },
210            }
211            .abi_encode();
212
213            let result = keychain.call(&calldata, account)?;
214            assert!(result.is_revert());
215
216            Ok(())
217        })
218    }
219
220    #[test]
221    fn test_legacy_authorize_key_selector_rejected_post_t3() -> eyre::Result<()> {
222        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
223        let account = Address::random();
224
225        StorageCtx::enter(&mut storage, || {
226            let mut keychain = AccountKeychain::new();
227            keychain.initialize()?;
228
229            let calldata = legacyAuthorizeKeyCall {
230                keyId: Address::random(),
231                signatureType: IAccountKeychain::SignatureType::Secp256k1,
232                expiry: u64::MAX,
233                enforceLimits: false,
234                limits: vec![],
235            }
236            .abi_encode();
237
238            let result = keychain.call(&calldata, account)?;
239            assert!(result.is_revert());
240            let decoded =
241                IAccountKeychain::LegacyAuthorizeKeySelectorChanged::abi_decode(&result.bytes)?;
242            assert_eq!(decoded.newSelector, authorizeKeyCall::SELECTOR);
243
244            Ok(())
245        })
246    }
247
248    #[test]
249    fn test_get_remaining_limit_uses_legacy_return_shape_pre_t3() -> eyre::Result<()> {
250        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
251        let account = Address::random();
252        let key_id = Address::random();
253        let token = Address::random();
254
255        StorageCtx::enter(&mut storage, || {
256            let mut keychain = AccountKeychain::new();
257            keychain.initialize()?;
258
259            let authorize_calldata = legacyAuthorizeKeyCall {
260                keyId: key_id,
261                signatureType: IAccountKeychain::SignatureType::Secp256k1,
262                expiry: u64::MAX,
263                enforceLimits: true,
264                limits: vec![IAccountKeychain::LegacyTokenLimit {
265                    token,
266                    amount: U256::from(123),
267                }],
268            }
269            .abi_encode();
270            let _ = keychain.call(&authorize_calldata, account)?;
271
272            let get_limit_calldata = getRemainingLimitCall {
273                account,
274                keyId: key_id,
275                token,
276            }
277            .abi_encode();
278
279            let output = keychain.call(&get_limit_calldata, account)?;
280            assert!(!output.is_revert());
281            assert_eq!(
282                output.bytes.len(),
283                32,
284                "pre-T3 should return legacy uint256"
285            );
286
287            let remaining = getRemainingLimitCall::abi_decode_returns(&output.bytes)?;
288            assert_eq!(remaining, U256::from(123));
289
290            Ok(())
291        })
292    }
293
294    #[test]
295    fn test_get_remaining_limit_with_period_rejected_pre_t3() -> eyre::Result<()> {
296        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1C);
297        let account = Address::random();
298
299        StorageCtx::enter(&mut storage, || {
300            let mut keychain = AccountKeychain::new();
301            keychain.initialize()?;
302
303            let calldata = getRemainingLimitWithPeriodCall {
304                account,
305                keyId: Address::random(),
306                token: Address::random(),
307            }
308            .abi_encode();
309
310            let result = keychain.call(&calldata, account)?;
311            assert!(result.is_revert());
312
313            Ok(())
314        })
315    }
316
317    #[test]
318    fn test_get_remaining_limit_returns_unknown_selector_post_t3() -> eyre::Result<()> {
319        let account = Address::random();
320        let key_id = Address::random();
321        let token = Address::random();
322
323        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
324        StorageCtx::enter(&mut storage, || {
325            let mut keychain = AccountKeychain::new();
326            keychain.initialize()?;
327
328            let calldata = getRemainingLimitCall {
329                account,
330                keyId: key_id,
331                token,
332            }
333            .abi_encode();
334
335            let result = keychain.call(&calldata, account)?;
336            assert!(
337                result.is_revert(),
338                "expected revert for dropped selector post-T3"
339            );
340
341            let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?;
342            assert_eq!(
343                decoded.selector.as_slice(),
344                &getRemainingLimitCall::SELECTOR,
345            );
346
347            Ok(())
348        })
349    }
350
351    #[test]
352    fn test_t3_selector_with_malformed_data_returns_unknown_selector_error() -> eyre::Result<()> {
353        let selector = getRemainingLimitWithPeriodCall::SELECTOR;
354        let calldata = selector.to_vec();
355
356        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
357        StorageCtx::enter(&mut storage, || {
358            let mut keychain = AccountKeychain::new();
359
360            let result = keychain.call(&calldata, Address::ZERO)?;
361            assert!(result.is_revert(), "expected revert");
362
363            let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?;
364            assert_eq!(decoded.selector.as_slice(), &selector);
365
366            Ok(())
367        })
368    }
369}