tempo_precompiles/account_keychain/
dispatch.rs1use 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}