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
1 change: 1 addition & 0 deletions crates/anvil/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod revert;
mod sign;
mod simulate;
mod state;
mod tempo;
mod traces;
mod transaction;
mod txpool;
Expand Down
244 changes: 244 additions & 0 deletions crates/anvil/tests/it/tempo.rs
Original file line number Diff line number Diff line change
@@ -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<Address> = 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<Address> = 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<Address> = 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<Address> = 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<Address> = 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);
}
12 changes: 12 additions & 0 deletions crates/evm/core/src/tempo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
Loading