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