tempo_precompiles/tip_account_registrar/
mod.rs1pub mod dispatch;
2
3pub use tempo_contracts::precompiles::ITipAccountRegistrar;
4use tempo_precompiles_macros::contract;
5
6use crate::error::Result;
7use alloy::{
8 eips::eip7702::constants::SECP256K1N_HALF,
9 primitives::{Address, B512, U256},
10};
11use revm::{precompile::secp256k1::ecrecover, state::Bytecode};
12use tempo_contracts::{
13 DEFAULT_7702_DELEGATE_ADDRESS,
14 precompiles::{TIP_ACCOUNT_REGISTRAR, TIPAccountRegistrarError},
15};
16
17#[contract(addr = TIP_ACCOUNT_REGISTRAR)]
18pub struct TipAccountRegistrar {}
19
20impl TipAccountRegistrar {
21 pub fn initialize(&mut self) -> Result<()> {
25 self.__initialize()
26 }
27
28 pub fn delegate_to_default_v1(
32 &mut self,
33 call: ITipAccountRegistrar::delegateToDefault_0Call,
34 ) -> Result<Address> {
35 let ITipAccountRegistrar::delegateToDefault_0Call { hash, signature } = call;
36
37 self.storage.deduct_gas(3_000)?;
40 let (sig, v) = validate_signature(&signature)?;
41
42 let signer = match ecrecover(&sig, v, &hash) {
43 Ok(recovered_addr) => Address::from_word(recovered_addr),
44 Err(_) => {
45 return Err(TIPAccountRegistrarError::invalid_signature().into());
46 }
47 };
48
49 let cost = self.storage.with_account_info(signer, |info| {
52 if info.nonce != 0 {
53 Err(TIPAccountRegistrarError::nonce_not_zero().into())
54 } else if !info.is_empty_code_hash() {
55 Err(TIPAccountRegistrarError::code_not_empty().into())
56 } else if info.is_empty() {
57 Ok(revm::primitives::eip7702::PER_EMPTY_ACCOUNT_COST)
58 } else {
59 Ok(revm::primitives::eip7702::PER_AUTH_BASE_COST)
60 }
61 })?;
62 self.storage.deduct_gas(cost)?;
63
64 self.storage
66 .set_code(signer, Bytecode::new_eip7702(DEFAULT_7702_DELEGATE_ADDRESS))?;
67
68 Ok(signer)
69 }
70
71 pub fn delegate_to_default_v2(
76 &mut self,
77 call: ITipAccountRegistrar::delegateToDefault_1Call,
78 ) -> Result<Address> {
79 let ITipAccountRegistrar::delegateToDefault_1Call { message, signature } = call;
80
81 let hash = alloy::primitives::keccak256(&message);
83
84 self.delegate_to_default_v1(ITipAccountRegistrar::delegateToDefault_0Call {
86 hash,
87 signature,
88 })
89 }
90}
91
92fn validate_signature(signature: &[u8]) -> Result<(B512, u8)> {
95 if signature.len() != 65 {
96 return Err(TIPAccountRegistrarError::invalid_signature().into());
97 }
98
99 let sig: &[u8; 64] = signature[0..64].try_into().unwrap();
105 let mut v = signature[64];
106 v = match v {
108 27 | 28 => v - 27,
109 0 | 1 => v,
110 _ => {
111 return Err(TIPAccountRegistrarError::invalid_signature().into());
112 }
113 };
114
115 let s = U256::from_be_slice(&sig[32..64]);
117 if s > SECP256K1N_HALF {
118 return Err(TIPAccountRegistrarError::invalid_signature().into());
119 }
120
121 Ok((sig.into(), v))
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::{
128 Precompile,
129 error::TempoPrecompileError,
130 storage::{StorageCtx, hashmap::HashMapStorageProvider},
131 };
132 use alloy::sol_types::{SolCall, SolError};
133 use alloy_signer::SignerSync;
134 use alloy_signer_local::PrivateKeySigner;
135 use tempo_chainspec::hardfork::TempoHardfork;
136 use tempo_contracts::precompiles::{TIPAccountRegistrarError, UnknownFunctionSelector};
137
138 #[test]
139 fn test_delegate_to_default_v1_pre_moderato() -> eyre::Result<()> {
140 let mut storage = HashMapStorageProvider::new(1);
141 StorageCtx::enter(&mut storage, || {
142 let signer = PrivateKeySigner::random();
144 let expected_address = signer.address();
145 let hash = alloy::primitives::keccak256(b"test");
146
147 let mut registrar = TipAccountRegistrar::new();
148
149 let signature = signer.sign_hash_sync(&hash).unwrap();
150 let call = ITipAccountRegistrar::delegateToDefault_0Call {
151 hash,
152 signature: signature.as_bytes().into(),
153 };
154
155 let result = registrar.delegate_to_default_v1(call);
156 assert!(result.is_ok());
157
158 let recovered_address = result.unwrap();
159 assert_eq!(recovered_address, expected_address);
160
161 let account_info_after = registrar
162 .storage
163 .get_account_info(expected_address)
164 .expect("Failed to get account info");
165 assert_eq!(
166 account_info_after.code,
167 Some(Bytecode::new_eip7702(DEFAULT_7702_DELEGATE_ADDRESS)),
168 );
169
170 Ok(())
171 })
172 }
173
174 #[test]
175 fn test_delegate_to_default_v1_rejected_post_moderato() -> eyre::Result<()> {
176 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
177 StorageCtx::enter(&mut storage, || {
178 let hash = alloy::primitives::keccak256(b"test");
180 let mut registrar = TipAccountRegistrar::new();
181
182 let signer = PrivateKeySigner::random();
183 let signature = signer.sign_hash_sync(&hash).unwrap();
184
185 let call = ITipAccountRegistrar::delegateToDefault_0Call {
187 hash,
188 signature: signature.as_bytes().into(),
189 };
190 let calldata = call.abi_encode();
191
192 let result = registrar.call(&calldata, signer.address());
194 assert!(result.is_ok());
195 let output = result.unwrap();
196 assert!(output.reverted);
197
198 let decoded_error = UnknownFunctionSelector::abi_decode(&output.bytes);
200 assert!(
201 decoded_error.is_ok(),
202 "Should decode as UnknownFunctionSelector"
203 );
204
205 let error = decoded_error.unwrap();
207 assert_eq!(
208 error.selector.as_slice(),
209 &ITipAccountRegistrar::delegateToDefault_0Call::SELECTOR
210 );
211
212 Ok(())
213 })
214 }
215
216 #[test]
217 fn test_delegate_to_default_v2_post_moderato() -> eyre::Result<()> {
218 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
219 StorageCtx::enter(&mut storage, || {
220 let signer = PrivateKeySigner::random();
222 let expected_address = signer.address();
223
224 let mut registrar = TipAccountRegistrar::new();
225
226 let message = b"Hello, Tempo! I want to delegate my account.";
227 let message_hash = alloy::primitives::keccak256(message);
228 let signature = signer.sign_hash_sync(&message_hash).unwrap();
229
230 let call = ITipAccountRegistrar::delegateToDefault_1Call {
231 message: message.to_vec().into(),
232 signature: signature.as_bytes().into(),
233 };
234
235 let result = registrar.delegate_to_default_v2(call);
236 assert!(result.is_ok());
237
238 let recovered_address = result.unwrap();
239 assert_eq!(recovered_address, expected_address);
240
241 let account_info_after = registrar
242 .storage
243 .get_account_info(expected_address)
244 .expect("Failed to get account info");
245 assert_eq!(
246 account_info_after.code,
247 Some(Bytecode::new_eip7702(DEFAULT_7702_DELEGATE_ADDRESS)),
248 );
249
250 Ok(())
251 })
252 }
253
254 #[test]
255 fn test_delegate_to_default_v2_rejected_pre_moderato() -> eyre::Result<()> {
256 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Adagio);
257 StorageCtx::enter(&mut storage, || {
258 let mut registrar = TipAccountRegistrar::new();
260
261 let signer = PrivateKeySigner::random();
262 let message = b"Hello, Tempo!";
263 let message_hash = alloy::primitives::keccak256(message);
264 let signature = signer.sign_hash_sync(&message_hash).unwrap();
265
266 let call = ITipAccountRegistrar::delegateToDefault_1Call {
268 message: message.to_vec().into(),
269 signature: signature.as_bytes().into(),
270 };
271 let calldata = call.abi_encode();
272
273 let result = registrar.call(&calldata, signer.address());
275 assert!(matches!(
276 result,
277 Err(revm::precompile::PrecompileError::Other(ref msg)) if msg.contains("Unknown function selector")
278 ));
279
280 Ok(())
281 })
282 }
283
284 #[test]
285 fn test_malformed_signature_v1() -> eyre::Result<()> {
286 let mut storage = HashMapStorageProvider::new(1);
287 StorageCtx::enter(&mut storage, || {
288 let hash = alloy::primitives::keccak256(b"test");
289 let mut registrar = TipAccountRegistrar::new();
290
291 let call = ITipAccountRegistrar::delegateToDefault_0Call {
293 hash,
294 signature: vec![0u8; 64].into(),
295 };
296
297 let result = registrar.delegate_to_default_v1(call);
298 assert!(result.is_err());
299 assert!(matches!(
300 result.unwrap_err(),
301 TempoPrecompileError::TIPAccountRegistrarError(
302 TIPAccountRegistrarError::InvalidSignature(_)
303 )
304 ));
305
306 let call = ITipAccountRegistrar::delegateToDefault_0Call {
308 hash,
309 signature: vec![0u8; 66].into(),
310 };
311
312 let result = registrar.delegate_to_default_v1(call);
313 assert!(result.is_err());
314 assert!(matches!(
315 result.unwrap_err(),
316 TempoPrecompileError::TIPAccountRegistrarError(
317 TIPAccountRegistrarError::InvalidSignature(_)
318 )
319 ));
320
321 Ok(())
322 })
323 }
324
325 #[test]
326 fn test_invalid_signature_v1() -> eyre::Result<()> {
327 let mut storage = HashMapStorageProvider::new(1);
328 StorageCtx::enter(&mut storage, || {
329 let hash = alloy::primitives::keccak256(b"test");
330 let mut registrar = TipAccountRegistrar::new();
331
332 let mut invalid_signature = vec![0u8; 65];
334 invalid_signature[64] = 30;
335
336 let call = ITipAccountRegistrar::delegateToDefault_0Call {
337 hash,
338 signature: invalid_signature.into(),
339 };
340
341 let result = registrar.delegate_to_default_v1(call);
342 assert!(result.is_err());
343 assert!(matches!(
344 result.unwrap_err(),
345 TempoPrecompileError::TIPAccountRegistrarError(
346 TIPAccountRegistrarError::InvalidSignature(_)
347 )
348 ));
349
350 Ok(())
351 })
352 }
353
354 #[test]
355 fn test_nonce_gt_zero_v1() -> eyre::Result<()> {
356 let mut storage = HashMapStorageProvider::new(1);
357 StorageCtx::enter(&mut storage, || {
358 let signer = PrivateKeySigner::random();
359 let expected_address = signer.address();
360 let hash = alloy::primitives::keccak256(b"test");
361
362 let mut registrar = TipAccountRegistrar::new();
363
364 registrar.storage.set_nonce(expected_address, 1);
365 let signature = signer.sign_hash_sync(&hash).unwrap();
366 let call = ITipAccountRegistrar::delegateToDefault_0Call {
367 hash,
368 signature: signature.as_bytes().into(),
369 };
370
371 let result = registrar.delegate_to_default_v1(call);
372 assert!(matches!(
373 result.unwrap_err(),
374 TempoPrecompileError::TIPAccountRegistrarError(
375 TIPAccountRegistrarError::NonceNotZero(_)
376 )
377 ));
378
379 let account_info_after = registrar
380 .storage
381 .get_account_info(expected_address)
382 .expect("Failed to get account info");
383 assert!(account_info_after.is_empty_code_hash());
384
385 Ok(())
386 })
387 }
388
389 #[test]
390 fn test_delegate_to_default_v2_different_messages_different_signers() -> eyre::Result<()> {
391 let mut storage = HashMapStorageProvider::new(1).with_spec(TempoHardfork::Moderato);
392 StorageCtx::enter(&mut storage, || {
393 let signer = PrivateKeySigner::random();
394 let expected_address = signer.address();
395
396 let mut registrar = TipAccountRegistrar::new();
397
398 let message1 = b"Message 1";
400 let hash1 = alloy::primitives::keccak256(message1);
401 let signature1 = signer.sign_hash_sync(&hash1).unwrap();
402
403 let message2 = b"Message 2";
405
406 let call = ITipAccountRegistrar::delegateToDefault_1Call {
407 message: message2.to_vec().into(),
408 signature: signature1.as_bytes().into(),
409 };
410
411 let result = registrar.delegate_to_default_v2(call);
414
415 let recovered_addr = result.expect("ecrecover should succeed with valid signature");
418 assert_ne!(
419 recovered_addr, expected_address,
420 "Should recover a different address when signature is for different message"
421 );
422
423 Ok(())
424 })
425 }
426}