Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions crates/precompiles/src/address_registry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
}
206 changes: 191 additions & 15 deletions crates/precompiles/src/tip20/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1141,6 +1141,7 @@ impl TIP20Token {
/// recipients, `virtual_addr` carries the original virtual address for event emission.
///
/// [TIP-1022]: <https://docs.tempo.xyz/protocol/tip1022>
#[derive(Debug, PartialEq)]
pub(crate) struct Recipient {
/// The effective (resolved) address where the balance is credited.
pub(crate) target: Address,
Expand Down Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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>(())
})?;
}
Expand All @@ -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 {
Expand All @@ -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);

Expand All @@ -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>(())
})?;
}
Expand All @@ -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 {
Expand All @@ -2728,39 +2886,57 @@ 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 {
assert_eq!(token.get_balance(virtual_addr)?, amount);
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>(())
})?;
}
Ok(())
}

#[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(())
})
Expand Down
Loading