diff --git a/crates/precompiles/src/address_registry/mod.rs b/crates/precompiles/src/address_registry/mod.rs index 4191eb8009..228e3bd214 100644 --- a/crates/precompiles/src/address_registry/mod.rs +++ b/crates/precompiles/src/address_registry/mod.rs @@ -424,4 +424,64 @@ mod tests { Ok(()) }) } + + #[test] + fn test_resolve_virtual_address_view() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3); + let (master, salt) = (VIRTUAL_MASTER, VIRTUAL_SALT.into()); + + StorageCtx::enter(&mut storage, || { + let mut registry = AddressRegistry::new(); + + // Non-virtual → zero + assert_eq!( + registry.resolve_virtual_address(Address::random())?, + Address::ZERO + ); + + // Unregistered virtual → zero + let unregistered = make_virtual_address(MasterId::ZERO, UserTag::ZERO); + assert_eq!( + registry.resolve_virtual_address(unregistered)?, + Address::ZERO + ); + + // Registered virtual → master + let master_id = registry.register_virtual_master( + master, + IAddressRegistry::registerVirtualMasterCall { salt }, + )?; + let virtual_addr = make_virtual_address(master_id, UserTag::new(hex!("aabbccddeeff"))); + assert_eq!(registry.resolve_virtual_address(virtual_addr)?, master); + + Ok(()) + }) + } + + #[test] + fn test_resolve_recipient_pre_t3_returns_literal() -> eyre::Result<()> { + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2); + let virtual_addr = make_virtual_address(MasterId::ZERO, UserTag::ZERO); + + StorageCtx::enter(&mut storage, || { + let registry = AddressRegistry::new(); + assert_eq!(registry.resolve_recipient(virtual_addr)?, virtual_addr); + Ok(()) + }) + } + + #[test] + fn test_is_valid_master_address() { + // Zero → invalid + assert!(!is_valid_master_address(Address::ZERO)); + // Virtual address → invalid + assert!(!is_valid_master_address(make_virtual_address( + MasterId::ZERO, + UserTag::ZERO + ))); + // TIP-20 prefix → invalid + assert!(!is_valid_master_address(crate::PATH_USD_ADDRESS)); + // Normal address → valid + assert!(is_valid_master_address(Address::repeat_byte(0x42))); + } } diff --git a/crates/precompiles/src/tip20/mod.rs b/crates/precompiles/src/tip20/mod.rs index 99cf163264..b7b011cb88 100644 --- a/crates/precompiles/src/tip20/mod.rs +++ b/crates/precompiles/src/tip20/mod.rs @@ -1141,6 +1141,7 @@ impl TIP20Token { /// recipients, `virtual_addr` carries the original virtual address for event emission. /// /// [TIP-1022]: +#[derive(Debug, PartialEq)] pub(crate) struct Recipient { /// The effective (resolved) address where the balance is credited. pub(crate) target: Address, @@ -1209,6 +1210,121 @@ impl Recipient { } } +#[cfg(test)] +mod recipient_tests { + use super::*; + use crate::{ + address_registry::{AddressRegistry, MasterId, UserTag}, + error::TempoPrecompileError, + storage::{StorageCtx, hashmap::HashMapStorageProvider}, + test_util::{VIRTUAL_MASTER, make_virtual_address, register_virtual_master}, + }; + use alloy::primitives::{Address, U256}; + use tempo_chainspec::hardfork::TempoHardfork; + + #[test] + fn test_resolve() -> eyre::Result<()> { + // direct (non-virtual) + let addr = Address::repeat_byte(0x11); + assert_eq!( + Recipient::direct(addr), + Recipient { + target: addr, + virtual_addr: None + } + ); + + // T3: non-virtual → direct + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3); + StorageCtx::enter(&mut storage, || { + let r = Recipient::resolve(addr)?; + assert_eq!( + r, + Recipient { + target: addr, + virtual_addr: None + } + ); + + // T3: registered virtual → master + let mut registry = AddressRegistry::new(); + let (_, virtual_addr) = register_virtual_master(&mut registry)?; + let r = Recipient::resolve(virtual_addr)?; + assert_eq!( + r, + Recipient { + target: VIRTUAL_MASTER, + virtual_addr: Some(virtual_addr) + } + ); + + // T3: unregistered virtual → error + let unregistered = make_virtual_address(MasterId::ZERO, UserTag::ZERO); + assert!(Recipient::resolve(unregistered).is_err()); + + Ok::<_, TempoPrecompileError>(()) + })?; + + // Pre-T3: virtual address passed through as literal + let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2); + StorageCtx::enter(&mut storage, || { + let virtual_addr = make_virtual_address(MasterId::ZERO, UserTag::ZERO); + let r = Recipient::resolve(virtual_addr)?; + assert_eq!( + r, + Recipient { + target: virtual_addr, + virtual_addr: None + } + ); + Ok::<_, TempoPrecompileError>(()) + })?; + Ok(()) + } + + #[test] + fn test_validate() { + assert!(Recipient::direct(Address::ZERO).validate().is_err()); + assert!( + Recipient::direct(crate::PATH_USD_ADDRESS) + .validate() + .is_err() + ); + assert!( + Recipient::direct(Address::repeat_byte(0x11)) + .validate() + .is_ok() + ); + } + + #[test] + fn test_build_events() { + let from = Address::repeat_byte(0x01); + let target = Address::repeat_byte(0x02); + let vaddr = Address::repeat_byte(0x03); + let amount = U256::from(42); + + let direct = Recipient::direct(target); + let virt = Recipient { + target, + virtual_addr: Some(vaddr), + }; + + // transfer event uses virtual_addr when present, target otherwise + assert!(matches!(direct.build_transfer_event(from, amount), + TIP20Event::Transfer(ITIP20::Transfer { to, .. }) if to == target)); + assert!(matches!(virt.build_transfer_event(from, amount), + TIP20Event::Transfer(ITIP20::Transfer { to, .. }) if to == vaddr)); + + // virtual transfer event: None for direct, Some(virtual→master) for virtual + assert!(direct.build_virtual_transfer_event(amount).is_none()); + let hop = virt.build_virtual_transfer_event(amount).unwrap(); + assert!(matches!(hop, + TIP20Event::Transfer(ITIP20::Transfer { from, to, .. }) + if from == vaddr && to == target)); + } +} + #[cfg(test)] pub(crate) mod tests { use alloy::primitives::{Address, FixedBytes, IntoLogData, U256}; @@ -2592,12 +2708,18 @@ pub(crate) mod tests { StorageCtx::enter(&mut storage, || { let mut registry = AddressRegistry::new(); let (_, virtual_addr) = register_virtual_master(&mut registry)?; + let credited = if hardfork.is_t3() { + VIRTUAL_MASTER + } else { + virtual_addr + }; let mut token = TIP20Setup::create("Test", "TST", admin) .with_issuer(admin) .clear_events() .apply()?; + // mint token.mint( admin, ITIP20::mintCall { @@ -2635,6 +2757,18 @@ pub(crate) mod tests { assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO); } + // mintWithMemo: same resolution behavior + let pre = token.get_balance(credited)?; + token.mint_with_memo( + admin, + ITIP20::mintWithMemoCall { + to: virtual_addr, + amount, + memo: FixedBytes::ZERO, + }, + )?; + assert_eq!(token.get_balance(credited)? - pre, amount); + Ok::<_, TempoPrecompileError>(()) })?; } @@ -2653,13 +2787,19 @@ pub(crate) mod tests { StorageCtx::enter(&mut storage, || { let mut registry = AddressRegistry::new(); let (_, virtual_addr) = register_virtual_master(&mut registry)?; + let credited = if hardfork.is_t3() { + VIRTUAL_MASTER + } else { + virtual_addr + }; let mut token = TIP20Setup::create("Test", "TST", admin) .with_issuer(admin) - .with_mint(sender, amount) + .with_mint(sender, amount * U256::from(2)) .clear_events() .apply()?; + // transfer token.transfer( sender, ITIP20::transferCall { @@ -2669,7 +2809,6 @@ pub(crate) mod tests { )?; if hardfork.is_t3() { - assert_eq!(token.get_balance(sender)?, U256::ZERO); assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount); assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO); @@ -2691,6 +2830,18 @@ pub(crate) mod tests { assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO); } + // transferWithMemo: same resolution behavior + let pre = token.get_balance(credited)?; + token.transfer_with_memo( + sender, + ITIP20::transferWithMemoCall { + to: virtual_addr, + amount, + memo: FixedBytes::ZERO, + }, + )?; + assert_eq!(token.get_balance(credited)? - pre, amount); + Ok::<_, TempoPrecompileError>(()) })?; } @@ -2710,14 +2861,21 @@ pub(crate) mod tests { StorageCtx::enter(&mut storage, || { let mut registry = AddressRegistry::new(); let (_, virtual_addr) = register_virtual_master(&mut registry)?; + let credited = if hardfork.is_t3() { + VIRTUAL_MASTER + } else { + virtual_addr + }; + let total = amount * U256::from(2); let mut token = TIP20Setup::create("Test", "TST", admin) .with_issuer(admin) - .with_mint(owner, amount) - .with_approval(owner, spender, amount) + .with_mint(owner, total) + .with_approval(owner, spender, total) .clear_events() .apply()?; + // transferFrom token.transfer_from( spender, ITIP20::transferFromCall { @@ -2728,7 +2886,6 @@ pub(crate) mod tests { )?; if hardfork.is_t3() { - assert_eq!(token.get_balance(owner)?, U256::ZERO); assert_eq!(token.get_balance(VIRTUAL_MASTER)?, amount); assert_eq!(token.get_balance(virtual_addr)?, U256::ZERO); } else { @@ -2736,6 +2893,19 @@ pub(crate) mod tests { assert_eq!(token.get_balance(VIRTUAL_MASTER)?, U256::ZERO); } + // transferFromWithMemo: same resolution behavior + let pre = token.get_balance(credited)?; + token.transfer_from_with_memo( + spender, + ITIP20::transferFromWithMemoCall { + from: owner, + to: virtual_addr, + amount, + memo: FixedBytes::ZERO, + }, + )?; + assert_eq!(token.get_balance(credited)? - pre, amount); + Ok::<_, TempoPrecompileError>(()) })?; } @@ -2743,24 +2913,30 @@ pub(crate) mod tests { } #[test] - fn test_mint_to_unregistered_virtual_reverts_on_t3() -> eyre::Result<()> { + #[rustfmt::skip] + fn test_unregistered_virtual_reverts_on_t3() -> eyre::Result<()> { let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3); let admin = Address::random(); - let unregistered_virtual = make_virtual_address(MasterId::ZERO, UserTag::ZERO); + let sender = Address::random(); + let spender = Address::random(); + let to = make_virtual_address(MasterId::ZERO, UserTag::ZERO); + let amount = U256::from(100); + let memo = FixedBytes::ZERO; StorageCtx::enter(&mut storage, || { let mut token = TIP20Setup::create("Test", "TST", admin) .with_issuer(admin) + .with_mint(sender, amount) + .with_approval(sender, spender, amount) .apply()?; - let result = token.mint( - admin, - ITIP20::mintCall { - to: unregistered_virtual, - amount: U256::from(100), - }, - ); - assert!(result.is_err()); + // All 6 entrypoints should revert for an unregistered virtual address + assert!(token.mint(admin, ITIP20::mintCall { to, amount }).is_err()); + assert!(token.mint_with_memo(admin, ITIP20::mintWithMemoCall { to, amount, memo }).is_err()); + assert!(token.transfer(sender, ITIP20::transferCall { to, amount }).is_err()); + assert!(token.transfer_with_memo(sender, ITIP20::transferWithMemoCall { to, amount, memo }).is_err()); + assert!(token.transfer_from(spender, ITIP20::transferFromCall { from: sender, to, amount }).is_err()); + assert!(token.transfer_from_with_memo(spender, ITIP20::transferFromWithMemoCall { from: sender, to, amount, memo }).is_err()); Ok(()) })