1#[cfg(any(test, feature = "test-utils"))]
4use crate::error::TempoPrecompileError;
5use crate::{
6 PATH_USD_ADDRESS, Precompile, Result,
7 address_registry::{AddressRegistry, IAddressRegistry},
8 storage::{ContractStorage, StorageCtx, hashmap::HashMapStorageProvider},
9 tip20::{self, ITIP20, TIP20Token},
10 tip20_factory::{self, TIP20Factory},
11};
12use alloy::{
13 primitives::{Address, B256, U256, address, hex_literal::hex},
14 sol_types::SolError,
15};
16use revm::precompile::PrecompileError;
17#[cfg(any(test, feature = "test-utils"))]
18use tempo_contracts::precompiles::TIP20Error;
19use tempo_contracts::precompiles::{TIP20_FACTORY_ADDRESS, UnknownFunctionSelector};
20use tempo_primitives::{MasterId, TempoAddressExt, UserTag};
21
22pub fn check_selector_coverage<P: Precompile>(
27 precompile: &mut P,
28 selectors: &[[u8; 4]],
29 interface_name: &str,
30 name_lookup: impl Fn([u8; 4]) -> Option<&'static str>,
31) -> Vec<([u8; 4], &'static str)> {
32 let mut unsupported_selectors = Vec::new();
33
34 for selector in selectors.iter() {
35 let mut calldata = selector.to_vec();
36 calldata.extend_from_slice(&[0u8; 32]);
38
39 let result = precompile.call(&calldata, Address::ZERO);
40
41 let is_unsupported_old = matches!(&result,
43 Err(PrecompileError::Fatal(msg)) if msg.contains("Unknown function selector")
44 );
45
46 let is_unsupported_new = if let Ok(output) = &result {
48 output.is_revert() && UnknownFunctionSelector::abi_decode(&output.bytes).is_ok()
49 } else {
50 false
51 };
52
53 if (is_unsupported_old || is_unsupported_new)
54 && let Some(name) = name_lookup(*selector)
55 {
56 unsupported_selectors.push((*selector, name));
57 }
58 }
59
60 if !unsupported_selectors.is_empty() {
62 eprintln!("Unsupported {interface_name} selectors:");
63 for (selector, name) in &unsupported_selectors {
64 eprintln!(" - {name} ({selector:?})");
65 }
66 }
67
68 unsupported_selectors
69}
70
71pub fn assert_full_coverage(results: impl IntoIterator<Item = Vec<([u8; 4], &'static str)>>) {
75 let all_unsupported: Vec<_> = results
76 .into_iter()
77 .flat_map(|r| r.into_iter())
78 .map(|(_, name)| name)
79 .collect();
80
81 assert!(
82 all_unsupported.is_empty(),
83 "Found {} unsupported selectors: {:?}",
84 all_unsupported.len(),
85 all_unsupported
86 );
87}
88
89pub fn setup_storage() -> (HashMapStorageProvider, Address) {
91 (HashMapStorageProvider::new(1), Address::random())
92}
93
94#[derive(Default, Clone)]
96#[cfg(any(test, feature = "test-utils"))]
97enum Action {
98 #[default]
99 PathUSD,
101
102 CreateToken {
104 name: &'static str,
105 symbol: &'static str,
106 currency: String,
107 },
108 ConfigureToken { address: Address },
110}
111
112#[derive(Default)]
136#[cfg(any(test, feature = "test-utils"))]
137pub struct TIP20Setup {
138 action: Action,
139 quote_token: Option<Address>,
140 admin: Option<Address>,
141 salt: Option<B256>,
142 roles: Vec<(Address, B256)>,
143 mints: Vec<(Address, U256)>,
144 approvals: Vec<(Address, Address, U256)>,
145 reward_opt_ins: Vec<Address>,
146 distribute_rewards: Vec<U256>,
147 clear_events: bool,
148}
149
150#[cfg(any(test, feature = "test-utils"))]
151impl TIP20Setup {
152 pub fn path_usd(admin: Address) -> Self {
154 Self {
155 action: Action::PathUSD,
156 admin: Some(admin),
157 ..Default::default()
158 }
159 }
160
161 pub fn create(name: &'static str, symbol: &'static str, admin: Address) -> Self {
165 Self {
166 action: Action::CreateToken {
167 name,
168 symbol,
169 currency: "USD".into(),
170 },
171 admin: Some(admin),
172 ..Default::default()
173 }
174 }
175
176 pub fn config(address: Address) -> Self {
178 Self {
179 action: Action::ConfigureToken { address },
180 ..Default::default()
181 }
182 }
183
184 pub fn clear_events(mut self) -> Self {
188 self.clear_events = true;
189 self
190 }
191
192 pub fn currency(mut self, currency: impl Into<String>) -> Self {
194 if let Action::CreateToken {
195 currency: ref mut c,
196 ..
197 } = self.action
198 {
199 *c = currency.into();
200 }
201 self
202 }
203
204 pub fn quote_token(mut self, token: Address) -> Self {
206 self.quote_token = Some(token);
207 self
208 }
209
210 pub fn with_salt(mut self, salt: B256) -> Self {
212 self.salt = Some(salt);
213 self
214 }
215
216 pub fn with_admin(mut self, admin: Address) -> Self {
218 self.admin = Some(admin);
219 self
220 }
221
222 pub fn with_issuer(self, account: Address) -> Self {
224 self.with_role(account, *tip20::ISSUER_ROLE)
225 }
226
227 pub fn with_role(mut self, account: Address, role: B256) -> Self {
229 self.roles.push((account, role));
230 self
231 }
232
233 pub fn with_mint(mut self, to: Address, amount: U256) -> Self {
237 self.mints.push((to, amount));
238 self
239 }
240
241 pub fn with_approval(mut self, owner: Address, spender: Address, amount: U256) -> Self {
243 self.approvals.push((owner, spender, amount));
244 self
245 }
246
247 pub fn with_reward_opt_in(mut self, user: Address) -> Self {
249 self.reward_opt_ins.push(user);
250 self
251 }
252
253 pub fn with_reward(mut self, amount: U256) -> Self {
255 self.distribute_rewards.push(amount);
256 self
257 }
258
259 fn path_usd_inner(&self) -> Result<TIP20Token> {
261 if is_initialized(PATH_USD_ADDRESS)? {
262 return TIP20Token::from_address(PATH_USD_ADDRESS);
263 }
264
265 let admin = self
266 .admin
267 .expect("pathUSD is uninitialized and requires an admin");
268
269 Self::factory()?.create_token_reserved_address(
270 PATH_USD_ADDRESS,
271 "pathUSD",
272 "pathUSD",
273 "USD",
274 Address::ZERO,
275 admin,
276 )?;
277
278 TIP20Token::from_address(PATH_USD_ADDRESS)
279 }
280
281 pub fn factory() -> Result<TIP20Factory> {
283 let mut factory = TIP20Factory::new();
284 if !is_initialized(TIP20_FACTORY_ADDRESS)? {
285 factory.initialize()?;
286 }
287 Ok(factory)
288 }
289
290 pub fn apply(self) -> Result<TIP20Token> {
292 let mut token = match self.action.clone() {
293 Action::PathUSD => self.path_usd_inner()?,
294 Action::CreateToken {
295 name,
296 symbol,
297 currency,
298 } => {
299 let mut factory = Self::factory()?;
300 self.path_usd_inner()?;
301
302 let admin = self.admin.expect("initializing a token requires an admin");
303 let quote = self.quote_token.unwrap_or(PATH_USD_ADDRESS);
304 let salt = self.salt.unwrap_or_else(B256::random);
305 let token_address = factory.create_token(
306 admin,
307 tip20_factory::ITIP20Factory::createTokenCall {
308 name: name.to_string(),
309 symbol: symbol.to_string(),
310 currency,
311 quoteToken: quote,
312 admin,
313 salt,
314 },
315 )?;
316 TIP20Token::from_address(token_address)?
317 }
318 Action::ConfigureToken { address } => {
319 assert!(
320 is_initialized(address)?,
321 "token not initialized, use `fn create` instead"
322 );
323 TIP20Token::from_address(address)?
324 }
325 };
326
327 for (account, role) in self.roles {
329 token.grant_role_internal(account, role)?;
330 }
331
332 for (to, amount) in self.mints {
334 let admin = self.admin.unwrap_or_else(|| {
335 get_tip20_admin(token.address()).expect("unable to get token admin")
336 });
337 token.mint(admin, ITIP20::mintCall { to, amount })?;
338 }
339
340 for (owner, spender, amount) in self.approvals {
342 token.approve(owner, ITIP20::approveCall { spender, amount })?;
343 }
344
345 for user in self.reward_opt_ins {
347 token.set_reward_recipient(user, ITIP20::setRewardRecipientCall { recipient: user })?;
348 }
349
350 for amount in self.distribute_rewards {
352 let admin = self.admin.unwrap_or_else(|| {
353 get_tip20_admin(token.address()).expect("unable to get token admin")
354 });
355 token.distribute_reward(admin, ITIP20::distributeRewardCall { amount })?;
356 }
357
358 if self.clear_events {
359 token.clear_emitted_events();
360 }
361
362 Ok(token)
363 }
364
365 pub fn expect_err(self, expected: TempoPrecompileError) {
367 let result = self.apply();
368 assert!(result.is_err_and(|err| err == expected));
369 }
370
371 pub fn expect_tip20_err(self, expected: TIP20Error) {
373 let result = self.apply();
374 assert!(result.is_err_and(|err| err == TempoPrecompileError::TIP20(expected)));
375 }
376}
377
378#[cfg(any(test, feature = "test-utils"))]
380fn is_initialized(address: Address) -> Result<bool> {
381 crate::storage::StorageCtx.has_bytecode(address)
382}
383
384#[cfg(any(test, feature = "test-utils"))]
386fn get_tip20_admin(token: Address) -> Option<Address> {
387 use alloy::{primitives::Log, sol_types::SolEvent};
388 use tempo_contracts::precompiles::ITIP20Factory;
389
390 let events = StorageCtx.get_events(TIP20_FACTORY_ADDRESS);
391 for log_data in events {
392 let log = Log::new_unchecked(
393 TIP20_FACTORY_ADDRESS,
394 log_data.topics().to_vec(),
395 log_data.data.clone(),
396 );
397 if let Ok(event) = ITIP20Factory::TokenCreated::decode_log(&log)
398 && event.token == token
399 {
400 return Some(event.admin);
401 }
402 }
403
404 None
405}
406
407pub fn gen_word_from(values: &[&str]) -> U256 {
422 let mut bytes = Vec::new();
423
424 for value in values {
425 let hex_str = value.strip_prefix("0x").unwrap_or(value);
426
427 assert!(
429 hex_str.len() % 2 == 0,
430 "Hex string '{value}' has odd length"
431 );
432
433 for i in (0..hex_str.len()).step_by(2) {
434 let byte_str = &hex_str[i..i + 2];
435 let byte = u8::from_str_radix(byte_str, 16)
436 .unwrap_or_else(|e| panic!("Invalid hex in '{value}': {e}"));
437 bytes.push(byte);
438 }
439 }
440
441 assert!(
442 bytes.len() <= 32,
443 "Total bytes ({}) exceed 32-byte slot limit",
444 bytes.len()
445 );
446
447 let mut slot_bytes = [0u8; 32];
449 let start_idx = 32 - bytes.len();
450 slot_bytes[start_idx..].copy_from_slice(&bytes);
451
452 U256::from_be_bytes(slot_bytes)
453}
454
455pub const VIRTUAL_MASTER: Address = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
460pub const VIRTUAL_SALT: [u8; 32] =
461 hex!("00000000000000000000000000000000000000000000000000000000abf52baf");
462
463pub fn register_virtual_master(registry: &mut AddressRegistry) -> Result<(MasterId, Address)> {
465 let master_id = registry.register_virtual_master(
466 VIRTUAL_MASTER,
467 IAddressRegistry::registerVirtualMasterCall {
468 salt: VIRTUAL_SALT.into(),
469 },
470 )?;
471 let virtual_addr = Address::new_virtual(master_id, UserTag::new(hex!("010203040506")));
472 Ok((master_id, virtual_addr))
473}