diff --git a/crates/anvil/tests/it/main.rs b/crates/anvil/tests/it/main.rs index e208093cfe62b..c4879e36d5240 100644 --- a/crates/anvil/tests/it/main.rs +++ b/crates/anvil/tests/it/main.rs @@ -19,6 +19,7 @@ mod revert; mod sign; mod simulate; mod state; +mod tempo; mod traces; mod transaction; mod txpool; diff --git a/crates/anvil/tests/it/tempo.rs b/crates/anvil/tests/it/tempo.rs new file mode 100644 index 0000000000000..c9a8e55b0c4bc --- /dev/null +++ b/crates/anvil/tests/it/tempo.rs @@ -0,0 +1,244 @@ +//! Tests for Tempo-specific features in Anvil. +//! +//! This module tests Tempo's payment-native protocol features including: +//! - TIP20 fee tokens (PathUSD, AlphaUSD, BetaUSD, ThetaUSD) +//! - Tempo precompiles initialization (sentinel bytecode) +//! - Native value transfer rejection +//! - Basic transaction behavior in Tempo mode + +use alloy_network::{ReceiptResponse, TransactionBuilder}; +use alloy_primitives::{Address, Bytes, U256}; +use alloy_provider::Provider; +use alloy_rpc_types::TransactionRequest; +use alloy_serde::WithOtherFields; +use alloy_sol_types::sol; +use anvil::{NodeConfig, spawn}; +use foundry_evm::core::tempo::{ + ALPHA_USD_ADDRESS, BETA_USD_ADDRESS, PATH_USD_ADDRESS, TEMPO_PRECOMPILE_ADDRESSES, + TEMPO_TIP20_TOKENS, THETA_USD_ADDRESS, +}; + +const PATH_USD: Address = PATH_USD_ADDRESS; +const ALPHA_USD: Address = ALPHA_USD_ADDRESS; +const BETA_USD: Address = BETA_USD_ADDRESS; +const THETA_USD: Address = THETA_USD_ADDRESS; + +/// Gas limit for TIP20 transfer calls (precompile interactions need more gas). +const TIP20_TRANSFER_GAS: u64 = 300_000; + +sol! { + #[sol(rpc)] + interface IERC20 { + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function balanceOf(address account) external view returns (uint256); + function transfer(address to, uint256 amount) external returns (bool); + } +} + +// ============================================================================ +// Tempo Genesis: Precompile Initialization +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_tempo_precompiles_have_code() { + let (api, _handle) = spawn(NodeConfig::test_tempo()).await; + + // Tempo precompiles should have sentinel bytecode (0xef) + for addr in TEMPO_PRECOMPILE_ADDRESSES { + let code = api.get_code(*addr, None).await.unwrap(); + assert!(!code.is_empty(), "Precompile {addr} should have code"); + } + + // All TIP20 token addresses should also have code + for addr in TEMPO_TIP20_TOKENS { + let code = api.get_code(*addr, None).await.unwrap(); + assert!(!code.is_empty(), "Token {addr} should have code deployed"); + } +} + +// ============================================================================ +// Tempo Genesis: Fee Token Metadata +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_tip20_token_metadata() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let tokens = [ + (PATH_USD, "PathUSD", "PathUSD"), + (ALPHA_USD, "AlphaUSD", "AlphaUSD"), + (BETA_USD, "BetaUSD", "BetaUSD"), + (THETA_USD, "ThetaUSD", "ThetaUSD"), + ]; + + for (addr, expected_name, expected_symbol) in tokens { + let token = IERC20::new(addr, &provider); + let name = token.name().call().await.unwrap(); + let symbol = token.symbol().call().await.unwrap(); + let decimals = token.decimals().call().await.unwrap(); + + assert_eq!(name, expected_name, "Token at {addr} name mismatch"); + assert_eq!(symbol, expected_symbol, "Token at {addr} symbol mismatch"); + assert_eq!(decimals, 6, "All TIP20 tokens should use 6 decimals"); + } +} + +// ============================================================================ +// Tempo Genesis: Fee Token Balances +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_fee_token_balances_minted_to_dev_accounts() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let dev_accounts: Vec
= handle.dev_accounts().collect(); + assert!(!dev_accounts.is_empty()); + + for account in dev_accounts.iter().take(3) { + for token_addr in [PATH_USD, ALPHA_USD, BETA_USD, THETA_USD] { + let token = IERC20::new(token_addr, &provider); + let balance = token.balanceOf(*account).call().await.unwrap(); + assert!( + balance > U256::ZERO, + "Account {account} should have {token_addr} balance, got 0" + ); + } + } +} + +// ============================================================================ +// Tempo Genesis: Dev Account Balance +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_dev_accounts_have_balance() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let genesis_balance = handle.genesis_balance(); + + for account in handle.dev_accounts() { + let balance = provider.get_balance(account).await.unwrap(); + assert_eq!(balance, genesis_balance, "Dev account {account} should have genesis balance"); + } +} + +// ============================================================================ +// Tempo Transactions: Native Value Transfer Rejected +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_native_value_transfer_rejected() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let accounts: Vec
= handle.dev_accounts().collect(); + let from = accounts[0]; + let to = accounts[1]; + + let tx = TransactionRequest::default() + .from(from) + .to(to) + .value(U256::from(1_000_000_000_000_000_000u64)); // 1 ETH + + let tx = WithOtherFields::new(tx); + let result = provider.send_transaction(tx).await; + assert!(result.is_err(), "Native ETH transfers should be rejected in Tempo mode"); + + let err = result.unwrap_err().to_string(); + assert!( + err.contains("native value transfer not allowed"), + "Expected 'native value transfer not allowed' error, got: {err}" + ); +} + +// ============================================================================ +// Tempo Transactions: Zero-Value EIP-1559 Tx Succeeds +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_zero_value_tx_succeeds() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let accounts: Vec
= handle.dev_accounts().collect(); + let sender = accounts[0]; + let recipient = accounts[1]; + + // TIP20 transfer (value=0, only calldata) + let token = IERC20::new(PATH_USD, &provider); + let transfer_call = token.transfer(recipient, U256::from(1_000_000)); + let calldata: Bytes = transfer_call.calldata().clone(); + + let tx = TransactionRequest::default() + .from(sender) + .to(PATH_USD) + .with_input(calldata) + .with_gas_limit(TIP20_TRANSFER_GAS); + + let tx = WithOtherFields::new(tx); + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + assert!(receipt.status()); +} + +// ============================================================================ +// Tempo Transactions: Contract Deployment +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_contract_deployment() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let accounts: Vec
= handle.dev_accounts().collect(); + let sender = accounts[0]; + + // Minimal contract: PUSH1 0x00 PUSH1 0x00 RETURN (returns empty) + let bytecode = Bytes::from(vec![0x60, 0x00, 0x60, 0x00, 0xf3]); + + let tx = + TransactionRequest::default().from(sender).with_input(bytecode).with_gas_limit(100_000); + + let tx = WithOtherFields::new(tx); + let receipt = provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + assert!(receipt.status()); + assert!(receipt.contract_address.is_some(), "Should have deployed a contract"); +} + +// ============================================================================ +// Tempo Transactions: Nonce Increments +// ============================================================================ + +#[tokio::test(flavor = "multi_thread")] +async fn test_nonce_increments() { + let (_api, handle) = spawn(NodeConfig::test_tempo()).await; + let provider = handle.http_provider(); + + let accounts: Vec
= handle.dev_accounts().collect(); + let from = accounts[0]; + let to = accounts[1]; + + let nonce_before = provider.get_transaction_count(from).await.unwrap(); + assert_eq!(nonce_before, 0); + + // Send a TIP20 transfer + let token = IERC20::new(ALPHA_USD, &provider); + let transfer_call = token.transfer(to, U256::from(1000)); + let calldata: Bytes = transfer_call.calldata().clone(); + + let tx = TransactionRequest::default() + .from(from) + .to(ALPHA_USD) + .with_input(calldata) + .with_gas_limit(TIP20_TRANSFER_GAS); + + let tx = WithOtherFields::new(tx); + provider.send_transaction(tx).await.unwrap().get_receipt().await.unwrap(); + + let nonce_after = provider.get_transaction_count(from).await.unwrap(); + assert_eq!(nonce_after, 1); +} diff --git a/crates/evm/core/src/tempo.rs b/crates/evm/core/src/tempo.rs index 784a23bf8dfc6..45f44e6f9c40a 100644 --- a/crates/evm/core/src/tempo.rs +++ b/crates/evm/core/src/tempo.rs @@ -43,6 +43,18 @@ pub const SIGNATURE_VERIFIER_ADDRESS: Address = pub const ADDRESS_REGISTRY_ADDRESS: Address = address!("0xFDC0000000000000000000000000000000000000"); +/// All well-known Tempo precompile addresses. +pub const TEMPO_PRECOMPILE_ADDRESSES: &[Address] = &[ + NONCE_PRECOMPILE_ADDRESS, + STABLECOIN_DEX_ADDRESS, + TIP20_FACTORY_ADDRESS, + TIP403_REGISTRY_ADDRESS, + TIP_FEE_MANAGER_ADDRESS, + VALIDATOR_CONFIG_ADDRESS, + VALIDATOR_CONFIG_V2_ADDRESS, + ACCOUNT_KEYCHAIN_ADDRESS, +]; + /// All well-known TIP20 fee token addresses on Tempo networks. pub const TEMPO_TIP20_TOKENS: &[Address] = &[PATH_USD_ADDRESS, ALPHA_USD_ADDRESS, BETA_USD_ADDRESS, THETA_USD_ADDRESS];