diff --git a/Cargo.lock b/Cargo.lock index 555dca189..afcf35e1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3817,6 +3817,7 @@ dependencies = [ "pallet-humanode-session", "pallet-im-online", "pallet-multisig", + "pallet-native-to-evm-swap", "pallet-pot", "pallet-session", "pallet-sudo", @@ -3829,8 +3830,8 @@ dependencies = [ "parity-scale-codec", "precompile-bioauth", "precompile-bls12381", - "precompile-currency-swap", "precompile-evm-accounts-mapping", + "precompile-evm-to-native-swap", "precompile-native-currency", "precompile-utils", "primitives-auth-ticket", @@ -6289,6 +6290,29 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-native-to-evm-swap" +version = "0.1.0" +dependencies = [ + "assert_matches", + "ethereum", + "fp-ethereum", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal", + "num_enum 0.7.3", + "pallet-balances", + "pallet-ethereum", + "pallet-evm", + "pallet-evm-balances", + "pallet-evm-system", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", +] + [[package]] name = "pallet-pot" version = "0.1.0" @@ -6816,7 +6840,7 @@ dependencies = [ ] [[package]] -name = "precompile-currency-swap" +name = "precompile-evm-accounts-mapping" version = "0.1.0" dependencies = [ "fp-evm", @@ -6824,21 +6848,18 @@ dependencies = [ "frame-system", "hex-literal", "mockall", - "num_enum 0.7.3", - "pallet-balances", - "pallet-evm", - "pallet-evm-balances", - "pallet-evm-system", - "pallet-timestamp", + "pallet-evm-accounts-mapping", "parity-scale-codec", - "precompile-utils", - "primitives-currency-swap", + "primitives-ethereum", "scale-info", "sp-core", + "sp-io", + "sp-runtime", + "sp-std", ] [[package]] -name = "precompile-evm-accounts-mapping" +name = "precompile-evm-to-native-swap" version = "0.1.0" dependencies = [ "fp-evm", @@ -6846,14 +6867,16 @@ dependencies = [ "frame-system", "hex-literal", "mockall", - "pallet-evm-accounts-mapping", + "num_enum 0.7.3", + "pallet-balances", + "pallet-evm", + "pallet-evm-balances", + "pallet-evm-system", + "pallet-timestamp", "parity-scale-codec", - "primitives-ethereum", + "precompile-utils", "scale-info", "sp-core", - "sp-io", - "sp-runtime", - "sp-std", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index d3ae8efba..11594f0d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,6 +150,7 @@ fc-mapping-sync = { git = "https://github.com/humanode-network/frontier", tag = fc-rpc = { git = "https://github.com/humanode-network/frontier", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } fc-rpc-core = { git = "https://github.com/humanode-network/frontier", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } fc-storage = { git = "https://github.com/humanode-network/frontier", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } +fp-ethereum = { git = "https://github.com/humanode-network/frontier", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } fp-evm = { git = "https://github.com/humanode-network/frontier", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } fp-rpc = { git = "https://github.com/humanode-network/frontier", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } fp-self-contained = { git = "https://github.com/humanode-network/frontier", tag = "locked/polkadot-v0.9.43-2025-03-22", default-features = false } diff --git a/crates/humanode-runtime/Cargo.toml b/crates/humanode-runtime/Cargo.toml index 5dad34cb0..528727410 100644 --- a/crates/humanode-runtime/Cargo.toml +++ b/crates/humanode-runtime/Cargo.toml @@ -32,13 +32,14 @@ pallet-evm-balances = { path = "../pallet-evm-balances", default-features = fals pallet-evm-system = { path = "../pallet-evm-system", default-features = false } pallet-humanode-offences = { path = "../pallet-humanode-offences", default-features = false } pallet-humanode-session = { path = "../pallet-humanode-session", default-features = false } +pallet-native-to-evm-swap = { path = "../pallet-native-to-evm-swap", default-features = false } pallet-pot = { path = "../pallet-pot", default-features = false } pallet-token-claims = { path = "../pallet-token-claims", default-features = false } pallet-vesting = { path = "../pallet-vesting", default-features = false } precompile-bioauth = { path = "../precompile-bioauth", default-features = false } precompile-bls12381 = { path = "../precompile-bls12381", default-features = false } -precompile-currency-swap = { path = "../precompile-currency-swap", default-features = false } precompile-evm-accounts-mapping = { path = "../precompile-evm-accounts-mapping", default-features = false } +precompile-evm-to-native-swap = { path = "../precompile-evm-to-native-swap", default-features = false } precompile-native-currency = { path = "../precompile-native-currency", default-features = false } precompile-utils = { path = "../precompile-utils", default-features = false } primitives-auth-ticket = { path = "../primitives-auth-ticket", default-features = false } @@ -131,6 +132,7 @@ runtime-benchmarks = [ "pallet-humanode-session/runtime-benchmarks", "pallet-im-online/runtime-benchmarks", "pallet-multisig/runtime-benchmarks", + "pallet-native-to-evm-swap/runtime-benchmarks", "pallet-sudo/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-token-claims/runtime-benchmarks", @@ -190,6 +192,7 @@ std = [ "pallet-humanode-session/std", "pallet-im-online/std", "pallet-multisig/std", + "pallet-native-to-evm-swap/std", "pallet-pot/std", "pallet-session/std", "pallet-sudo/std", @@ -201,8 +204,8 @@ std = [ "pallet-vesting/std", "precompile-bioauth/std", "precompile-bls12381/std", - "precompile-currency-swap/std", "precompile-evm-accounts-mapping/std", + "precompile-evm-to-native-swap/std", "precompile-native-currency/std", "precompile-utils/std", "primitives-auth-ticket/std", @@ -261,6 +264,7 @@ try-runtime = [ "pallet-humanode-session/try-runtime", "pallet-im-online/try-runtime", "pallet-multisig/try-runtime", + "pallet-native-to-evm-swap/try-runtime", "pallet-pot/try-runtime", "pallet-session/try-runtime", "pallet-sudo/try-runtime", diff --git a/crates/humanode-runtime/src/currency_swap.rs b/crates/humanode-runtime/src/evm_swap.rs similarity index 84% rename from crates/humanode-runtime/src/currency_swap.rs rename to crates/humanode-runtime/src/evm_swap.rs index d69fdef9a..46672ba5c 100644 --- a/crates/humanode-runtime/src/currency_swap.rs +++ b/crates/humanode-runtime/src/evm_swap.rs @@ -12,6 +12,18 @@ parameter_types! { pub EvmToNativeSwapBridgePotAccountId: EvmAccountId = EvmToNativeSwapBridgePot::account_id(); } +pub struct EvmToNativeSwapConfig; + +impl precompile_evm_to_native_swap::Config for EvmToNativeSwapConfig { + type AccountId = AccountId; + type EvmAccountId = EvmAccountId; + type NativeToken = Balances; + type EvmToken = EvmBalances; + type BalanceConverterEvmToNative = Identity; + type BridgePotNative = NativeToEvmSwapBridgePotAccountId; + type BridgePotEvm = EvmToNativeSwapBridgePotAccountId; +} + pub type NativeToEvmOneToOne = bridge_pot_currency_swap::CurrencySwap; diff --git a/crates/humanode-runtime/src/frontier_precompiles.rs b/crates/humanode-runtime/src/frontier_precompiles.rs index 2b578a5af..3482dbfe2 100644 --- a/crates/humanode-runtime/src/frontier_precompiles.rs +++ b/crates/humanode-runtime/src/frontier_precompiles.rs @@ -12,14 +12,14 @@ use precompile_bls12381::{ Bls12381G1Add, Bls12381G1Mul, Bls12381G1MultiExp, Bls12381G2Add, Bls12381G2Mul, Bls12381G2MultiExp, Bls12381MapG1, Bls12381MapG2, Bls12381Pairing, }; -use precompile_currency_swap::CurrencySwap; use precompile_evm_accounts_mapping::EvmAccountsMapping; +use precompile_evm_to_native_swap::EvmToNativeSwap; use precompile_native_currency::NativeCurrency; use precompile_utils::EvmData; use sp_core::{H160, U256}; use sp_std::marker::PhantomData; -use crate::{currency_swap, AccountId, ConstU64, EvmAccountId}; +use crate::{evm_swap, ConstU64}; /// A set of constant values used to indicate precompiles. pub mod precompiles_constants { @@ -74,8 +74,8 @@ pub mod precompiles_constants { pub const EVM_ACCOUNTS_MAPPING: u64 = 2049; /// `NativeCurrency` precompile constant. pub const NATIVE_CURRENCY: u64 = 2050; - /// `CurrencySwap` precompile constant. - pub const CURRENCY_SWAP: u64 = 2304; + /// `EvmToNativeSwap` precompile constant. + pub const EVM_TO_NATIVE_SWAP: u64 = 2304; } use precompiles_constants::*; @@ -117,7 +117,7 @@ where BIOAUTH, EVM_ACCOUNTS_MAPPING, NATIVE_CURRENCY, - CURRENCY_SWAP + EVM_TO_NATIVE_SWAP, ] .into_iter() .map(hash) @@ -172,15 +172,11 @@ where a if a == hash(NATIVE_CURRENCY) => { Some(NativeCurrency::>::execute(handle)) } - a if a == hash(CURRENCY_SWAP) => { - Some(CurrencySwap::< - currency_swap::EvmToNativeOneToOne, - EvmAccountId, - AccountId, - // TODO(#697): implement proper dynamic gas cost estimation. - ConstU64<200>, - >::execute(handle)) - } + a if a == hash(EVM_TO_NATIVE_SWAP) => Some(EvmToNativeSwap::< + evm_swap::EvmToNativeSwapConfig, + // TODO(#697): implement proper dynamic gas cost estimation. + ConstU64<200>, + >::execute(handle)), // Fallback _ => None, } diff --git a/crates/humanode-runtime/src/lib.rs b/crates/humanode-runtime/src/lib.rs index dc54fd4ee..6b1af25fa 100644 --- a/crates/humanode-runtime/src/lib.rs +++ b/crates/humanode-runtime/src/lib.rs @@ -85,12 +85,12 @@ use frontier_precompiles::{precompiles_constants, FrontierPrecompiles}; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod constants; -mod currency_swap; mod deauthentication_reason; #[cfg(test)] mod dev_utils; mod display_moment; pub mod eth_sig; +mod evm_swap; mod find_author; mod fixed_supply; pub mod robonode; @@ -625,13 +625,24 @@ impl pallet_evm_balances::Config for Runtime { type Balance = Balance; type ExistentialDeposit = ConstU128<1>; type AccountStore = EvmSystem; - type DustRemoval = currency_swap::TreasuryPotProxy; + type DustRemoval = evm_swap::TreasuryPotProxy; } impl pallet_currency_swap::Config for Runtime { type RuntimeEvent = RuntimeEvent; type AccountIdTo = EvmAccountId; - type CurrencySwap = currency_swap::NativeToEvmOneToOne; + type CurrencySwap = evm_swap::NativeToEvmOneToOne; + type WeightInfo = (); +} + +impl pallet_native_to_evm_swap::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type EvmAccountId = EvmAccountId; + type NativeToken = Balances; + type EvmToken = EvmBalances; + type BalanceConverterNativeToEvm = Identity; + type BridgePotNative = NativeToEvmSwapBridgePotAccountId; + type BridgePotEvm = EvmToNativeSwapBridgePotAccountId; type WeightInfo = (); } @@ -661,7 +672,7 @@ impl pallet_evm::Config for Runtime { type ChainId = EthereumChainId; type BlockGasLimit = BlockGasLimit; type OnChargeTransaction = - fixed_supply::EvmTransactionCharger; + fixed_supply::EvmTransactionCharger; type OnCreate = (); type FindAuthor = find_author::FindAuthorTruncated< find_author::FindAuthorFromSession, @@ -787,7 +798,7 @@ frame_support::parameter_types! { frontier_precompiles::hash(precompiles_constants::BIOAUTH), frontier_precompiles::hash(precompiles_constants::EVM_ACCOUNTS_MAPPING), frontier_precompiles::hash(precompiles_constants::NATIVE_CURRENCY), - frontier_precompiles::hash(precompiles_constants::CURRENCY_SWAP), + frontier_precompiles::hash(precompiles_constants::EVM_TO_NATIVE_SWAP), ]; } @@ -842,6 +853,7 @@ construct_runtime!( EvmBalancesErc20Support: pallet_erc20_support = 37, DummyPrecompilesCode: pallet_dummy_precompiles_code = 38, HumanodeOffences: pallet_humanode_offences = 39, + NativeToEvmSwap: pallet_native_to_evm_swap = 40, } ); diff --git a/crates/humanode-runtime/src/tests/currency_swap.rs b/crates/humanode-runtime/src/tests/currency_swap.rs index fc615e770..fbbf581db 100644 --- a/crates/humanode-runtime/src/tests/currency_swap.rs +++ b/crates/humanode-runtime/src/tests/currency_swap.rs @@ -164,86 +164,3 @@ fn currency_swap_native_call_works() { ); }) } - -/// This test verifies that the swap precompile call works in the happy path. -#[test] -fn currency_swap_precompile_call_works() { - // Build the state from the config. - new_test_ext_with().execute_with(move || { - let alice_balance_before = Balances::total_balance(&account_id("Alice")); - let native_to_evm_swap_bridge_pot_before = - Balances::total_balance(&NativeToEvmSwapBridgePot::account_id()); - let alice_evm_balance_before = EvmBalances::total_balance(&evm_account_id("EvmAlice")); - let evm_to_native_swap_bridge_pot_before = - EvmBalances::total_balance(&EvmToNativeSwapBridgePot::account_id()); - let fees_pot_balance_before = Balances::total_balance(&FeesPot::account_id()); - let swap_balance: Balance = 1000; - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(precompile_currency_swap::Action::Swap) - .write(H256::from(account_id("Alice").as_ref())) - .build(); - - let expected_gas_usage: u64 = 21216 + 560; - let expected_fee: Balance = - u128::from(expected_gas_usage) * u128::try_from(*GAS_PRICE).unwrap(); - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - evm_account_id("EvmAlice"), - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_000, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); - assert_eq!( - execinfo.logs, - vec![LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( - precompile_currency_swap::SELECTOR_LOG_SWAP, - evm_account_id("EvmAlice"), - H256::from(account_id("Alice").as_ref()), - EvmDataWriter::new().write(swap_balance).build(), - )] - ); - - // Assert state changes. - assert!(BalancedCurrencySwapBridgesInitializer::is_balanced().unwrap()); - assert_eq!( - Balances::total_balance(&FeesPot::account_id()), - fees_pot_balance_before + expected_fee - ); - assert_eq!( - Balances::total_balance(&account_id("Alice")), - alice_balance_before + swap_balance - ); - assert_eq!( - Balances::total_balance(&NativeToEvmSwapBridgePot::account_id()), - native_to_evm_swap_bridge_pot_before - swap_balance - expected_fee - ); - assert_eq!( - EvmBalances::total_balance(&evm_account_id("EvmAlice")), - alice_evm_balance_before - swap_balance - expected_fee - ); - assert_eq!( - EvmBalances::total_balance(&EvmToNativeSwapBridgePot::account_id()), - evm_to_native_swap_bridge_pot_before + swap_balance + expected_fee - ); - }) -} diff --git a/crates/humanode-runtime/src/tests/evm_swap.rs b/crates/humanode-runtime/src/tests/evm_swap.rs new file mode 100644 index 000000000..d4b88b82d --- /dev/null +++ b/crates/humanode-runtime/src/tests/evm_swap.rs @@ -0,0 +1,240 @@ +//! Tests to verify evm swap related basic operations. + +// Allow simple integer arithmetic in tests. +#![allow(clippy::arithmetic_side_effects)] + +use frame_support::{assert_ok, once_cell::sync::Lazy, traits::fungible::Inspect}; +use precompile_utils::{EvmDataWriter, LogsBuilder}; +use sp_core::H160; + +use super::*; +use crate::dev_utils::*; +use crate::opaque::SessionKeys; + +pub(crate) static PRECOMPILE_ADDRESS: Lazy = Lazy::new(|| H160::from_low_u64_be(0x900)); +pub(crate) static GAS_PRICE: Lazy = + Lazy::new(|| ::FeeCalculator::min_gas_price().0); + +const INIT_BALANCE: Balance = 10u128.pow(18 + 6); + +/// Build test externalities from the custom genesis. +/// Using this call requires manual assertions on the genesis init logic. +fn new_test_ext_with() -> sp_io::TestExternalities { + let authorities = [authority_keys("Alice")]; + let bootnodes = vec![account_id("Alice")]; + + let endowed_accounts = [account_id("Alice"), account_id("Bob")]; + let pot_accounts = vec![FeesPot::account_id()]; + + let evm_endowed_accounts = vec![evm_account_id("EvmAlice"), evm_account_id("EvmBob")]; + // Build test genesis. + let config = GenesisConfig { + balances: BalancesConfig { + balances: { + endowed_accounts + .iter() + .cloned() + .chain(pot_accounts) + .map(|k| (k, INIT_BALANCE)) + .chain([ + (TreasuryPot::account_id(), 10 * INIT_BALANCE), + ( + TokenClaimsPot::account_id(), + >::minimum_balance(), + ), + ( + NativeToEvmSwapBridgePot::account_id(), + >::minimum_balance(), + ), + ]) + .collect() + }, + }, + session: SessionConfig { + keys: authorities + .iter() + .map(|x| { + ( + x.0.clone(), + x.0.clone(), + SessionKeys { + babe: x.1.clone(), + grandpa: x.2.clone(), + im_online: x.3.clone(), + }, + ) + }) + .collect::>(), + }, + babe: BabeConfig { + authorities: vec![], + epoch_config: Some(BABE_GENESIS_EPOCH_CONFIG), + }, + bootnodes: BootnodesConfig { + bootnodes: bootnodes.try_into().unwrap(), + }, + evm: EVMConfig { + accounts: { + let init_genesis_account = fp_evm::GenesisAccount { + balance: INIT_BALANCE.into(), + code: Default::default(), + nonce: Default::default(), + storage: Default::default(), + }; + + evm_endowed_accounts + .into_iter() + .map(|k| (k, init_genesis_account.clone())) + .chain([( + EvmToNativeSwapBridgePot::account_id(), + fp_evm::GenesisAccount { + balance: >::minimum_balance() + .into(), + code: Default::default(), + nonce: Default::default(), + storage: Default::default(), + }, + )]) + .collect() + }, + }, + ..Default::default() + }; + let storage = config.build_storage().unwrap(); + + // Make test externalities from the storage. + storage.into() +} + +/// This test verifies that bridges initialization has been applied at genesis. +#[test] +fn currencies_are_balanced() { + // Build the state from the config. + new_test_ext_with().execute_with(move || { + assert_eq!( + BalancedCurrencySwapBridgesInitializer::last_initializer_version(), + pallet_balanced_currency_swap_bridges_initializer::CURRENT_BRIDGES_INITIALIZER_VERSION + ); + assert!(BalancedCurrencySwapBridgesInitializer::is_balanced().unwrap()); + }) +} + +/// This test verifies that native to evm swap call works in the happy path. +#[test] +fn native_to_evm_swap_call_works() { + // Build the state from the config. + new_test_ext_with().execute_with(move || { + let alice_balance_before = Balances::total_balance(&account_id("Alice")); + let bridge_pot_native_account_balance_before = + Balances::total_balance(&NativeToEvmSwapBridgePot::account_id()); + let alice_evm_balance_before = EvmBalances::total_balance(&evm_account_id("EvmAlice")); + let bridge_pot_evm_account_balance_before = + EvmBalances::total_balance(&EvmToNativeSwapBridgePot::account_id()); + let swap_balance: Balance = 1000; + + // Make swap. + assert_ok!(NativeToEvmSwap::swap( + Some(account_id("Alice")).into(), + evm_account_id("EvmAlice"), + swap_balance + )); + + // Assert state changes. + assert!(BalancedCurrencySwapBridgesInitializer::is_balanced().unwrap()); + assert_eq!( + Balances::total_balance(&account_id("Alice")), + alice_balance_before - swap_balance + ); + assert_eq!( + Balances::total_balance(&NativeToEvmSwapBridgePot::account_id()), + bridge_pot_native_account_balance_before + swap_balance + ); + assert_eq!( + EvmBalances::total_balance(&evm_account_id("EvmAlice")), + alice_evm_balance_before + swap_balance + ); + assert_eq!( + EvmBalances::total_balance(&EvmToNativeSwapBridgePot::account_id()), + bridge_pot_evm_account_balance_before - swap_balance + ); + }) +} + +/// This test verifies that ewm to native swap precompile call works in the happy path. +#[test] +fn ewm_to_native_precompile_call_works() { + // Build the state from the config. + new_test_ext_with().execute_with(move || { + let alice_balance_before = Balances::total_balance(&account_id("Alice")); + let bridge_pot_native_account_balance_before = + Balances::total_balance(&NativeToEvmSwapBridgePot::account_id()); + let alice_evm_balance_before = EvmBalances::total_balance(&evm_account_id("EvmAlice")); + let bridge_pot_evm_account_balance_before = + EvmBalances::total_balance(&EvmToNativeSwapBridgePot::account_id()); + let fees_pot_balance_before = Balances::total_balance(&FeesPot::account_id()); + let swap_balance: Balance = 1000; + + let expected_gas_usage: u64 = 21216 + 560; + let expected_fee: Balance = + Balance::from(expected_gas_usage) * Balance::try_from(*GAS_PRICE).unwrap(); + + // Invoke the function under test. + let execinfo = ::Runner::call( + evm_account_id("EvmAlice"), + *PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(precompile_evm_to_native_swap::Action::Swap) + .write(H256::from(account_id("Alice").as_ref())) + .build(), + swap_balance.into(), + 50_000, // a reasonable upper bound for tests + Some(*GAS_PRICE), + Some(*GAS_PRICE), + None, + Vec::new(), + true, + true, + None, + None, + ::config(), + ) + .unwrap(); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) + ); + assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); + assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); + assert_eq!( + execinfo.logs, + vec![LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( + precompile_evm_to_native_swap::SELECTOR_LOG_SWAP, + evm_account_id("EvmAlice"), + H256::from(account_id("Alice").as_ref()), + EvmDataWriter::new().write(swap_balance).build(), + )] + ); + + // Assert state changes. + assert!(BalancedCurrencySwapBridgesInitializer::is_balanced().unwrap()); + assert_eq!( + Balances::total_balance(&FeesPot::account_id()), + fees_pot_balance_before + expected_fee + ); + assert_eq!( + Balances::total_balance(&account_id("Alice")), + alice_balance_before + swap_balance + ); + assert_eq!( + Balances::total_balance(&NativeToEvmSwapBridgePot::account_id()), + bridge_pot_native_account_balance_before - swap_balance - expected_fee + ); + assert_eq!( + EvmBalances::total_balance(&evm_account_id("EvmAlice")), + alice_evm_balance_before - swap_balance - expected_fee + ); + assert_eq!( + EvmBalances::total_balance(&EvmToNativeSwapBridgePot::account_id()), + bridge_pot_evm_account_balance_before + swap_balance + expected_fee + ); + }) +} diff --git a/crates/humanode-runtime/src/tests/mod.rs b/crates/humanode-runtime/src/tests/mod.rs index 9502ad313..f53e136ff 100644 --- a/crates/humanode-runtime/src/tests/mod.rs +++ b/crates/humanode-runtime/src/tests/mod.rs @@ -1,7 +1,7 @@ use super::*; mod claims_and_vesting; -mod currency_swap; +mod evm_swap; mod fees; mod fixed_supply; mod genesis_config; diff --git a/crates/pallet-native-to-evm-swap/Cargo.toml b/crates/pallet-native-to-evm-swap/Cargo.toml new file mode 100644 index 000000000..349fe2f48 --- /dev/null +++ b/crates/pallet-native-to-evm-swap/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "pallet-native-to-evm-swap" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +codec = { workspace = true, features = ["derive"] } +ethereum = { workspace = true } +fp-ethereum = { workspace = true } +fp-evm = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +num_enum = { workspace = true } +pallet-ethereum = { workspace = true } +pallet-evm = { workspace = true } +scale-info = { workspace = true, features = ["derive"] } +sp-core = { workspace = true } + +[dev-dependencies] +pallet-evm-balances = { path = "../pallet-evm-balances", features = ["default"] } +pallet-evm-system = { path = "../pallet-evm-system", features = ["default"] } + +assert_matches = { workspace = true } +hex-literal = { workspace = true } +pallet-balances = { workspace = true, features = ["default"] } +pallet-timestamp = { workspace = true, features = ["default"] } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-ethereum/runtime-benchmarks", + "pallet-evm/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", +] +std = [ + "codec/std", + "ethereum/std", + "fp-ethereum/std", + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "num_enum/std", + "pallet-balances/std", + "pallet-ethereum/std", + "pallet-evm/std", + "pallet-timestamp/std", + "scale-info/std", + "sp-core/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "pallet-ethereum/try-runtime", + "pallet-evm-balances/try-runtime", + "pallet-evm-system/try-runtime", + "pallet-evm/try-runtime", + "pallet-timestamp/try-runtime", +] diff --git a/crates/pallet-native-to-evm-swap/src/lib.rs b/crates/pallet-native-to-evm-swap/src/lib.rs new file mode 100644 index 000000000..21d0bf414 --- /dev/null +++ b/crates/pallet-native-to-evm-swap/src/lib.rs @@ -0,0 +1,249 @@ +//! A substrate pallet containing native to EVM swap tokens integration. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::{ + fungible::{Inspect, Mutate}, + tokens::{Preservation, Provenance}, +}; +pub use pallet::*; +use sp_core::{Get, H160, U256}; +pub use weights::*; + +pub mod weights; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/// Utility alias for easy access to the [`Inspect::Balance`] of the [`Config::NativeToken`] type. +type NativeBalanceOf = + <::NativeToken as Inspect<::AccountId>>::Balance; + +/// Utility alias for easy access to the [`Inspect::Balance`] of the [`Config::EvmToken`] type. +type EvmBalanceOf = <::EvmToken as Inspect<::EvmAccountId>>::Balance; + +// We have to temporarily allow some clippy lints. Later on we'll send patches to substrate to +// fix them at their end. +#[allow(clippy::missing_docs_in_private_items)] +#[frame_support::pallet] +pub mod pallet { + use fp_ethereum::ValidatedTransaction; + use frame_support::{ + dispatch::PostDispatchInfo, + pallet_prelude::*, + sp_runtime::traits::{Convert, UniqueSaturatedInto}, + storage::with_storage_layer, + }; + use frame_system::pallet_prelude::*; + use pallet_evm::GasWeightMapping; + use sp_core::H256; + + use super::*; + + #[pallet::pallet] + pub struct Pallet(_); + + /// Configuration trait of this pallet. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_ethereum::Config { + /// Overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The EVM user account identifier type. + type EvmAccountId: Parameter + Into; + + /// Native token. + /// + /// TODO(#1462): switch from `Mutate` to `Balanced` fungible interface. + type NativeToken: Inspect + Mutate; + + /// EVM token. + /// + /// TODO(#1462): switch from `Mutate` to `Balanced` fungible interface. + type EvmToken: Inspect + Mutate; + + /// The converter to determine how the balance amount should be converted from native + /// to EVM token. + type BalanceConverterNativeToEvm: Convert, EvmBalanceOf>; + + /// The bridge pot native account. + type BridgePotNative: Get; + + /// The bridge pot EVM account. + type BridgePotEvm: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Balances were swapped. + BalancesSwapped { + /// The account id balances withdrawed from. + from: T::AccountId, + /// The withdrawed balances amount. + withdrawed_amount: NativeBalanceOf, + /// The account id balances deposited to. + to: T::EvmAccountId, + /// The deposited balances amount. + deposited_amount: EvmBalanceOf, + /// The corresponding transaction hash executed in EVM. + evm_transaction_hash: H256, + }, + } + + /// Possible error conditions during tokens swap. + #[pallet::error] + pub enum Error { + /// Ethereum transfer execution has not succeeded. + EthereumExecutionNotSucceeded, + } + + #[pallet::call(weight(::WeightInfo))] + impl Pallet { + /// Swap balances. + #[pallet::call_index(0)] + pub fn swap( + origin: OriginFor, + to: T::EvmAccountId, + #[pallet::compact] amount: NativeBalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + with_storage_layer(move || { + Self::do_swap(who, to, amount, Preservation::Expendable)?; + + Ok(()) + }) + } + + /// Same as the swap call, but with a check that the swap will not kill the origin account. + #[pallet::call_index(1)] + pub fn swap_keep_alive( + origin: OriginFor, + to: T::EvmAccountId, + #[pallet::compact] amount: NativeBalanceOf, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + with_storage_layer(move || { + Self::do_swap(who, to, amount, Preservation::Preserve)?; + + Ok(()) + }) + } + } + + impl Pallet { + /// General swap balances implementation. + pub fn do_swap( + who: T::AccountId, + to: T::EvmAccountId, + amount: NativeBalanceOf, + preservation: Preservation, + ) -> DispatchResult { + let estimated_swapped_balance = T::BalanceConverterNativeToEvm::convert(amount); + T::EvmToken::can_deposit(&to, estimated_swapped_balance, Provenance::Extant) + .into_result()?; + + T::EvmToken::can_withdraw(&T::BridgePotEvm::get(), estimated_swapped_balance) + // Bridge pot EVM account shouldn't be killed. + .into_result(true)?; + + T::NativeToken::transfer(&who, &T::BridgePotNative::get(), amount, preservation)?; + + let evm_transaction_hash = Self::execute_ethereum_transfer( + T::BridgePotEvm::get().into(), + to.clone().into(), + estimated_swapped_balance, + )?; + + Self::deposit_event(Event::BalancesSwapped { + from: who, + withdrawed_amount: amount, + to, + deposited_amount: estimated_swapped_balance, + evm_transaction_hash, + }); + + Ok(()) + } + + /// Execute ethereum transfer from source address to target EVM address with provided + /// balance value to be sent. + fn execute_ethereum_transfer( + source_address: H160, + target_address: H160, + value: EvmBalanceOf, + ) -> Result { + let transaction = ethereum_transfer_transaction::( + source_address, + target_address, + value.unique_saturated_into(), + ); + let transaction_hash = transaction.hash(); + + let (post_info, call_info) = + pallet_ethereum::ValidatedTransaction::::apply(source_address, transaction) + .map_err(|dispatch_error_with_post_info| dispatch_error_with_post_info.error)?; + + match call_info { + fp_evm::CallOrCreateInfo::Call(execution_info) => { + match execution_info.exit_reason { + fp_evm::ExitReason::Succeed(_) => { + // We are fine. + } + _ => return Err(Error::::EthereumExecutionNotSucceeded.into()), + } + } + // We use explicitly `ethereum::TransactionAction::Call` in prepared transaction. + fp_evm::CallOrCreateInfo::Create(_) => unreachable!(), + } + + debug_assert!( + post_info + == PostDispatchInfo { + actual_weight: Some( + T::GasWeightMapping::gas_to_weight( + T::config().gas_transaction_call.unique_saturated_into(), + true, + ) + ), + pays_fee: Pays::No + }, + "we must ensure that actual weight corresponds to gas used for simple transfer call" + ); + + Ok(transaction_hash) + } + } +} + +/// A helper function to prepare simple ethereum transfer transaction. +pub(crate) fn ethereum_transfer_transaction( + source_address: H160, + target_address: H160, + value: u128, +) -> pallet_ethereum::Transaction { + pallet_ethereum::Transaction::EIP1559(ethereum::EIP1559Transaction { + chain_id: T::ChainId::get(), + nonce: pallet_evm::Pallet::::account_basic(&source_address) + .0 + .nonce, + max_priority_fee_per_gas: 0.into(), + max_fee_per_gas: 0.into(), + gas_limit: T::config().gas_transaction_call.into(), + action: ethereum::TransactionAction::Call(target_address), + value: U256::from(value), + input: Default::default(), + access_list: Default::default(), + odd_y_parity: false, + r: Default::default(), + s: Default::default(), + }) +} diff --git a/crates/precompile-currency-swap/src/mock.rs b/crates/pallet-native-to-evm-swap/src/mock.rs similarity index 60% rename from crates/precompile-currency-swap/src/mock.rs rename to crates/pallet-native-to-evm-swap/src/mock.rs index 32c5242d1..2b755dfd7 100644 --- a/crates/precompile-currency-swap/src/mock.rs +++ b/crates/pallet-native-to-evm-swap/src/mock.rs @@ -1,31 +1,37 @@ -//! The mock for the precompile. +use std::collections::BTreeMap; -// Allow simple integer arithmetic in tests. -#![allow(clippy::arithmetic_side_effects)] - -use fp_evm::{IsPrecompileResult, PrecompileHandle}; use frame_support::{ once_cell::sync::Lazy, - sp_io, + parameter_types, sp_io, sp_runtime::{ - self, testing::Header, - traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, DispatchError, + traits::{BlakeTwo256, Identity, IdentityLookup}, + BuildStorage, }, - traits::{ConstU16, ConstU32, ConstU64}, + traits::{ConstU128, ConstU32, ConstU64}, weights::Weight, }; -use frame_system as system; -use mockall::mock; -use sp_core::{ConstU128, H160, H256, U256}; +use pallet_ethereum::PostLogContent as EthereumPostLogContent; +use sp_core::{Get, H160, H256, U256}; + +use crate::{self as pallet_native_to_evm_swap}; + +pub const INIT_BALANCE: u128 = 10_000_000_000_000_000; +// Add some tokens to test swap with full balance. +pub const BRIDGE_INIT_BALANCE: u128 = INIT_BALANCE + 100; + +pub fn alice() -> AccountId { + AccountId::from(hex_literal::hex!( + "1100000000000000000000000000000000000000000000000000000000000011" + )) +} type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; -pub(crate) type AccountId = sp_runtime::AccountId32; -pub(crate) type EvmAccountId = H160; -pub(crate) type Balance = u128; +pub type AccountId = frame_support::sp_runtime::AccountId32; +pub type EvmAccountId = H160; +pub type Balance = u128; // Configure a mock runtime to test the pallet. frame_support::construct_runtime!( @@ -41,10 +47,12 @@ frame_support::construct_runtime!( EvmSystem: pallet_evm_system, EvmBalances: pallet_evm_balances, EVM: pallet_evm, + Ethereum: pallet_ethereum, + NativeToEvmSwap: pallet_native_to_evm_swap, } ); -impl system::Config for Test { +impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); type BlockLength = (); @@ -56,7 +64,7 @@ impl system::Config for Test { type Hash = H256; type Hashing = BlakeTwo256; type AccountId = AccountId; - type Lookup = IdentityLookup; + type Lookup = IdentityLookup; type Header = Header; type RuntimeEvent = RuntimeEvent; type BlockHashCount = ConstU64<250>; @@ -66,21 +74,11 @@ impl system::Config for Test { type OnNewAccount = (); type OnKilledAccount = (); type SystemWeightInfo = (); - type SS58Prefix = ConstU16<1>; + type SS58Prefix = (); type OnSetCode = (); type MaxConsumers = ConstU32<16>; } -frame_support::parameter_types! { - pub const MinimumPeriod: u64 = 1000; -} -impl pallet_timestamp::Config for Test { - type Moment = u64; - type OnTimestampSet = (); - type MinimumPeriod = MinimumPeriod; - type WeightInfo = (); -} - impl pallet_balances::Config for Test { type Balance = Balance; type RuntimeEvent = RuntimeEvent; @@ -115,8 +113,18 @@ impl pallet_evm_balances::Config for Test { type DustRemoval = (); } -pub(crate) static GAS_PRICE: Lazy = Lazy::new(|| 1_000_000_000u128.into()); +parameter_types! { + pub const MinimumPeriod: u64 = 1000; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} +pub static GAS_PRICE: Lazy = Lazy::new(|| 1_000_000_000u128.into()); pub struct FixedGasPrice; impl fp_evm::FeeCalculator for FixedGasPrice { fn min_gas_price() -> (U256, Weight) { @@ -125,11 +133,10 @@ impl fp_evm::FeeCalculator for FixedGasPrice { } } -frame_support::parameter_types! { +parameter_types! { pub BlockGasLimit: U256 = U256::max_value(); pub GasLimitPovSizeRatio: u64 = 0; pub WeightPerGas: Weight = Weight::from_parts(20_000, 0); - pub MockPrecompiles: MockPrecompileSet = MockPrecompileSet; } impl pallet_evm::Config for Test { @@ -147,8 +154,8 @@ impl pallet_evm::Config for Test { type AddressMapping = pallet_evm::IdentityAddressMapping; type Currency = EvmBalances; type RuntimeEvent = RuntimeEvent; - type PrecompilesType = MockPrecompileSet; - type PrecompilesValue = MockPrecompiles; + type PrecompilesType = (); + type PrecompilesValue = (); type ChainId = (); type BlockGasLimit = BlockGasLimit; type Runner = pallet_evm::runner::stack::Runner; @@ -160,69 +167,77 @@ impl pallet_evm::Config for Test { type WeightInfo = (); } -type CurrencySwapPrecompile = - crate::CurrencySwap>; - -/// The precompile set containing the precompile under test. -pub struct MockPrecompileSet; - -pub(crate) static PRECOMPILE_ADDRESS: Lazy = Lazy::new(|| H160::from_low_u64_be(0x900)); - -impl pallet_evm::PrecompileSet for MockPrecompileSet { - /// Tries to execute a precompile in the precompile set. - /// If the provided address is not a precompile, returns None. - fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { - use pallet_evm::Precompile; - let address = handle.code_address(); +parameter_types! { + pub const PostBlockAndTxnHashes: EthereumPostLogContent = EthereumPostLogContent::BlockAndTxnHashes; +} - if address == *PRECOMPILE_ADDRESS { - return Some(CurrencySwapPrecompile::execute(handle)); - } +impl pallet_ethereum::Config for Test { + type RuntimeEvent = RuntimeEvent; + type StateRoot = pallet_ethereum::IntermediateStateRoot; + type PostLogContent = PostBlockAndTxnHashes; + type ExtraDataLength = ConstU32<30>; +} - None - } +pub struct BridgePotNative; - /// Check if the given address is a precompile. Should only be called to - /// perform the check while not executing the precompile afterward, since - /// `execute` already performs a check internally. - fn is_precompile(&self, address: H160, _gas: u64) -> IsPrecompileResult { - IsPrecompileResult::Answer { - is_precompile: address == *PRECOMPILE_ADDRESS, - extra_cost: 0, - } +impl Get for BridgePotNative { + fn get() -> AccountId { + AccountId::from(hex_literal::hex!( + "1000000000000000000000000000000000000000000000000000000000000001" + )) } } -mock! { - #[derive(Debug)] - pub CurrencySwap {} - impl primitives_currency_swap::CurrencySwap for CurrencySwap { - type From = EvmBalances; - type To = Balances; - type Error = DispatchError; +pub struct BridgePotEvm; - fn swap( - imbalance: primitives_currency_swap::FromNegativeImbalanceFor, - ) -> Result< - primitives_currency_swap::ToNegativeImbalanceFor, - primitives_currency_swap::ErrorFor, - >; - - fn estimate_swapped_balance( - balance: primitives_currency_swap::FromBalanceFor, - ) -> primitives_currency_swap::ToBalanceFor; +impl Get for BridgePotEvm { + fn get() -> EvmAccountId { + EvmAccountId::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )) } } -pub fn new_test_ext() -> sp_io::TestExternalities { - let genesis_config = GenesisConfig::default(); - new_test_ext_with(genesis_config) +impl pallet_native_to_evm_swap::Config for Test { + type RuntimeEvent = RuntimeEvent; + type EvmAccountId = EvmAccountId; + type NativeToken = Balances; + type EvmToken = EvmBalances; + type BalanceConverterNativeToEvm = Identity; + type BridgePotNative = BridgePotNative; + type BridgePotEvm = BridgePotEvm; + type WeightInfo = (); } -// This function basically just builds a genesis storage key/value store according to -// our desired mockup. -pub fn new_test_ext_with(genesis_config: GenesisConfig) -> sp_io::TestExternalities { - let storage = genesis_config.build_storage().unwrap(); +pub fn new_test_ext() -> sp_io::TestExternalities { + // Build genesis. + let config = GenesisConfig { + balances: BalancesConfig { + balances: vec![ + (BridgePotNative::get(), BRIDGE_INIT_BALANCE), + (alice(), INIT_BALANCE), + ], + }, + evm: EVMConfig { + accounts: { + let mut map = BTreeMap::new(); + map.insert( + BridgePotEvm::get(), + fp_evm::GenesisAccount { + balance: BRIDGE_INIT_BALANCE.into(), + code: Default::default(), + nonce: Default::default(), + storage: Default::default(), + }, + ); + map + }, + }, + ..Default::default() + }; + let storage = config.build_storage().unwrap(); + + // Make test externalities from the storage. storage.into() } diff --git a/crates/pallet-native-to-evm-swap/src/tests.rs b/crates/pallet-native-to-evm-swap/src/tests.rs new file mode 100644 index 000000000..eae4a22b4 --- /dev/null +++ b/crates/pallet-native-to-evm-swap/src/tests.rs @@ -0,0 +1,296 @@ +// Allow simple integer arithmetic in tests. +#![allow(clippy::arithmetic_side_effects)] + +use fp_evm::{ExitReason, ExitSucceed}; +use frame_support::{ + assert_noop, assert_ok, + dispatch::DispatchError, + sp_runtime::{ArithmeticError, TokenError}, + traits::fungible::Unbalanced, +}; +use sp_core::Get; + +use crate::{mock::*, *}; + +/// Returns source swap native account used in tests. +fn source_swap_native_account() -> AccountId { + alice() +} + +/// Returns target swap evm account used in tests. +fn target_swap_evm_account() -> EvmAccountId { + EvmAccountId::from(hex_literal::hex!( + "7700000000000000000000000000000000000077" + )) +} + +/// A helper enum to identify call used in tests. +enum TestCall { + Swap, + SwapKeepAlive, +} + +/// A helper function to run succeeded test and assert state changes. +fn run_succeeded_test_and_assert( + call: TestCall, + swap_balance: Balance, + is_origin_should_be_killed: bool, +) { + let source_swap_native_account_balance_before = + Balances::total_balance(&source_swap_native_account()); + let bridge_pot_native_account_balance_before = Balances::total_balance(&BridgePotNative::get()); + let bridge_pot_evm_account_balance_before = EvmBalances::total_balance(&BridgePotEvm::get()); + let target_swap_evm_account_balance_before = + EvmBalances::total_balance(&target_swap_evm_account()); + + // We should remember expected evm transaction hash before execution as nonce is increased + // after the execution. + let expected_evm_transaction_hash = ethereum_transfer_transaction::( + BridgePotEvm::get(), + target_swap_evm_account(), + swap_balance, + ) + .hash(); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + assert_ok!(match call { + TestCall::Swap => NativeToEvmSwap::swap( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + swap_balance + ), + TestCall::SwapKeepAlive => NativeToEvmSwap::swap_keep_alive( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + swap_balance + ), + }); + + // Assert state changes. + + // Verify that source swap native balance either has been decreased by swap value or reduced to 0 + // due to left balance becomes less than existential deposit. + if is_origin_should_be_killed { + assert_eq!(::total_balance(&source_swap_native_account()), 0); + } else { + assert_eq!( + ::total_balance(&source_swap_native_account()), + source_swap_native_account_balance_before - swap_balance, + ); + } + + // Verify that bridge pot native balance has been increased by swap value. + assert_eq!( + Balances::total_balance(&BridgePotNative::get()), + bridge_pot_native_account_balance_before + swap_balance, + ); + // Verify that bridge pot evm balance has been decreased by swap value. + assert_eq!( + EvmBalances::total_balance(&BridgePotEvm::get()), + bridge_pot_evm_account_balance_before - swap_balance, + ); + // Verify that target swap evm balance has been increased by swap value. + assert_eq!( + ::total_balance(&target_swap_evm_account()), + target_swap_evm_account_balance_before + swap_balance + ); + // Verifyt that we have a corresponding native to evm swap event. + System::assert_has_event(RuntimeEvent::NativeToEvmSwap(Event::BalancesSwapped { + from: source_swap_native_account(), + withdrawed_amount: swap_balance, + to: target_swap_evm_account(), + deposited_amount: swap_balance, + evm_transaction_hash: expected_evm_transaction_hash, + })); + // Verify that we have a corresponding ethereum event. + System::assert_has_event(RuntimeEvent::Ethereum(pallet_ethereum::Event::Executed { + from: BridgePotEvm::get(), + to: target_swap_evm_account(), + transaction_hash: expected_evm_transaction_hash, + exit_reason: ExitReason::Succeed(ExitSucceed::Stopped), + extra_data: vec![], + })); +} + +/// This test verifies that the `swap` call works in the happy path. +#[test] +fn swap_works() { + new_test_ext().execute_with_ext(|_| { + run_succeeded_test_and_assert(TestCall::Swap, 100, false); + }); +} + +/// This test verifies that `swap` call works as expected in case origin left balances amount +/// is less than existential deposit. The origin account should be killed. +#[test] +fn swap_works_kill_origin() { + new_test_ext().execute_with_ext(|_| { + run_succeeded_test_and_assert(TestCall::Swap, INIT_BALANCE - 1, true); + }); +} + +/// This test verifies that `swap_keep_alive` call works in the happy path. +#[test] +fn swap_keep_alive_works() { + new_test_ext().execute_with_ext(|_| { + run_succeeded_test_and_assert(TestCall::SwapKeepAlive, 100, false); + }); +} + +/// This test verifies that `swap_keep_alive` call fails in case origin left balances amount +/// is less than existential deposit. The call should prevent swap operation. +#[test] +fn swap_keep_alive_fails_kill_origin() { + new_test_ext().execute_with_ext(|_| { + // Invoke the function under test. + assert_noop!( + NativeToEvmSwap::swap_keep_alive( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + INIT_BALANCE - 1, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + }); +} + +/// This test verifies that both calls fail in case source account has no the sufficient balance. +#[test] +fn swap_both_fails_source_no_funds() { + new_test_ext().execute_with_ext(|_| { + // Invoke the `swap` under test. + assert_noop!( + NativeToEvmSwap::swap( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + INIT_BALANCE + 1, + ), + DispatchError::Token(TokenError::FundsUnavailable) + ); + + // Invoke the `swap_keep_alive` under test. + assert_noop!( + NativeToEvmSwap::swap_keep_alive( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + INIT_BALANCE + 1, + ), + DispatchError::Token(TokenError::FundsUnavailable) + ); + }); +} + +/// This test verifies that both calls fail in case target deposit results into overflow. +#[test] +fn swap_both_fails_target_overflow() { + new_test_ext().execute_with_ext(|_| { + EvmBalances::write_balance(&target_swap_evm_account(), Balance::MAX).unwrap(); + + // Invoke the `swap` under test. + assert_noop!( + NativeToEvmSwap::swap( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + 100, + ), + DispatchError::Arithmetic(ArithmeticError::Overflow) + ); + + // Invoke the `swap_keep_alive` under test. + assert_noop!( + NativeToEvmSwap::swap_keep_alive( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + 100, + ), + DispatchError::Arithmetic(ArithmeticError::Overflow) + ); + }); +} + +/// This test verifies that both calls fail in case bridge evm account would be killed. +#[test] +fn swap_both_fails_bridge_evm_killed() { + new_test_ext().execute_with_ext(|_| { + Balances::write_balance(&source_swap_native_account(), BRIDGE_INIT_BALANCE).unwrap(); + + // Invoke the `swap` under test. + assert_noop!( + NativeToEvmSwap::swap( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + BRIDGE_INIT_BALANCE, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + + // Invoke the `swap_keep_alive` under test. + assert_noop!( + NativeToEvmSwap::swap_keep_alive( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + BRIDGE_INIT_BALANCE, + ), + DispatchError::Token(TokenError::NotExpendable) + ); + }); +} + +/// This test verifies that both calls fail in case bridge evm account has no funds. +#[test] +fn swap_both_fails_bridge_evm_no_funds() { + new_test_ext().execute_with_ext(|_| { + EvmBalances::write_balance(&BridgePotEvm::get(), 0).unwrap(); + + // Invoke the `swap` under test. + assert_noop!( + NativeToEvmSwap::swap( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + 100, + ), + DispatchError::Token(TokenError::FundsUnavailable) + ); + + // Invoke the `swap_keep_alive` under test. + assert_noop!( + NativeToEvmSwap::swap_keep_alive( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + 100, + ), + DispatchError::Token(TokenError::FundsUnavailable) + ); + }); +} + +/// This test verifies that both calls fail in case bridge native balance results into overflow. +#[test] +fn swap_both_fails_bridge_native_overflow() { + new_test_ext().execute_with_ext(|_| { + Balances::write_balance(&BridgePotNative::get(), Balance::MAX).unwrap(); + + // Invoke the `swap` under test. + assert_noop!( + NativeToEvmSwap::swap( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + 100, + ), + DispatchError::Arithmetic(ArithmeticError::Overflow) + ); + + // Invoke the `swap_keep_alive` under test. + assert_noop!( + NativeToEvmSwap::swap_keep_alive( + RuntimeOrigin::signed(source_swap_native_account()), + target_swap_evm_account(), + 100, + ), + DispatchError::Arithmetic(ArithmeticError::Overflow) + ); + }); +} diff --git a/crates/pallet-native-to-evm-swap/src/weights.rs b/crates/pallet-native-to-evm-swap/src/weights.rs new file mode 100644 index 000000000..02d1fe381 --- /dev/null +++ b/crates/pallet-native-to-evm-swap/src/weights.rs @@ -0,0 +1,22 @@ +//! Weights definition for pallet-native-to-evm-swap. + +use frame_support::weights::Weight; + +/// Weight functions needed for pallet-native-to-evm-swap. +pub trait WeightInfo { + /// A function to calculate required weights for swap call. + fn swap() -> Weight; + + /// A function to calculate required weights for `swap_keep_alive` call. + fn swap_keep_alive() -> Weight; +} + +impl WeightInfo for () { + fn swap() -> Weight { + Weight::zero() + } + + fn swap_keep_alive() -> Weight { + Weight::zero() + } +} diff --git a/crates/precompile-currency-swap/src/lib.rs b/crates/precompile-currency-swap/src/lib.rs deleted file mode 100644 index ecb6ab715..000000000 --- a/crates/precompile-currency-swap/src/lib.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! A precompile to swap EVM tokens with native chain tokens. - -#![cfg_attr(not(feature = "std"), no_std)] - -use frame_support::{ - sp_runtime, - sp_std::{marker::PhantomData, prelude::*}, - traits::{ - fungible::Inspect, - tokens::{currency::Currency, Provenance}, - }, -}; -use pallet_evm::{ - ExitError, ExitRevert, Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput, - PrecompileResult, -}; -use precompile_utils::{ - keccak256, succeed, EvmDataWriter, EvmResult, LogExt, LogsBuilder, PrecompileHandleExt, -}; -use sp_core::{Get, H160, H256, U256}; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod tests; - -/// Solidity selector of the Swap log, which is the Keccak of the Log signature. -pub const SELECTOR_LOG_SWAP: [u8; 32] = keccak256!("Swap(address,bytes32,uint256)"); - -/// Possible actions for this interface. -#[precompile_utils::generate_function_selector] -#[derive(Debug, PartialEq)] -pub enum Action { - /// Swap EVM tokens to native tokens. - Swap = "swap(bytes32)", -} - -/// Exposes the currency swap interface to EVM. -pub struct CurrencySwap( - PhantomData<(CurrencySwapT, AccountIdFrom, AccountIdTo, GasCost)>, -) -where - AccountIdFrom: From, - AccountIdTo: From<[u8; 32]>, - CurrencySwapT: primitives_currency_swap::CurrencySwap, - FromBalanceFor: TryFrom, - GasCost: Get; - -impl Precompile - for CurrencySwap -where - AccountIdFrom: From, - AccountIdTo: From<[u8; 32]>, - CurrencySwapT: primitives_currency_swap::CurrencySwap, - FromBalanceFor: TryFrom, - GasCost: Get, -{ - fn execute(handle: &mut impl PrecompileHandle) -> PrecompileResult { - handle.record_cost(GasCost::get())?; - - let selector = handle - .read_selector() - .map_err(|_| PrecompileFailure::Error { - exit_status: ExitError::Other("invalid function selector".into()), - })?; - - match selector { - Action::Swap => Self::swap(handle), - } - } -} - -/// Utility alias for easy access to the [`Currency::Balance`] of -/// the [`primitives_currency_swap::CurrencySwap::From`] type. -type FromBalanceFor = - as Currency>::Balance; - -/// Utility alias for easy access to [`primitives_currency_swap::CurrencySwap::From`] type. -type FromCurrencyFor = - >::From; - -impl - CurrencySwap -where - AccountIdFrom: From, - AccountIdTo: From<[u8; 32]>, - CurrencySwapT: primitives_currency_swap::CurrencySwap, - FromBalanceFor: TryFrom, - GasCost: Get, -{ - /// Swap EVM tokens to native tokens. - fn swap(handle: &mut impl PrecompileHandle) -> EvmResult { - let mut input = handle.read_input()?; - - let fp_evm::Context { - address, - apparent_value: value, - .. - } = handle.context(); - - // Here we must withdraw from self (i.e. from the precompile address, not from the caller - // address), since the funds have already been transferred to us (precompile) as this point. - let from: AccountIdFrom = (*address).into(); - - let value_u256 = *value; - let value: FromBalanceFor = - (*value).try_into().map_err(|_| PrecompileFailure::Error { - exit_status: ExitError::Other("value is out of bounds".into()), - })?; - - input - .expect_arguments(1) - .map_err(|_| PrecompileFailure::Error { - exit_status: ExitError::Other("exactly one argument is expected".into()), - })?; - - let to_h256: H256 = input.read()?; - let to: [u8; 32] = to_h256.into(); - let to: AccountIdTo = to.into(); - - let junk_data = input.read_till_end()?; - if !junk_data.is_empty() { - return Err(PrecompileFailure::Error { - exit_status: ExitError::Other("junk at the end of input".into()), - }); - } - - let estimated_swapped_balance = CurrencySwapT::estimate_swapped_balance(value); - CurrencySwapT::To::can_deposit(&to, estimated_swapped_balance, Provenance::Extant) - .into_result() - .map_err(|error| match error { - sp_runtime::DispatchError::Token(sp_runtime::TokenError::BelowMinimum) => { - PrecompileFailure::Error { - exit_status: ExitError::OutOfFund, - } - } - _ => PrecompileFailure::Error { - exit_status: ExitError::Other("unable to deposit funds".into()), - }, - })?; - - let imbalance = CurrencySwapT::From::withdraw( - &from, - value, - frame_support::traits::WithdrawReasons::TRANSFER, - frame_support::traits::ExistenceRequirement::AllowDeath, - ) - .map_err(|error| match error { - sp_runtime::DispatchError::Token(sp_runtime::TokenError::FundsUnavailable) => { - PrecompileFailure::Error { - exit_status: ExitError::OutOfFund, - } - } - _ => PrecompileFailure::Error { - exit_status: ExitError::Other("unable to withdraw funds".into()), - }, - })?; - - let imbalance = CurrencySwapT::swap(imbalance).map_err(|error| { - // Here we undo the withdrawal to avoid having a dangling imbalance. - CurrencySwapT::From::resolve_creating(&from, error.incoming_imbalance); - PrecompileFailure::Revert { - exit_status: ExitRevert::Reverted, - output: "unable to swap the currency".into(), - } - })?; - - CurrencySwapT::To::resolve_creating(&to, imbalance); - - let logs_builder = LogsBuilder::new(handle.context().address); - - logs_builder - .log3( - SELECTOR_LOG_SWAP, - handle.context().caller, - to_h256, - EvmDataWriter::new().write(value_u256).build(), - ) - .record(handle)?; - - Ok(succeed(EvmDataWriter::new().write(true).build())) - } -} diff --git a/crates/precompile-currency-swap/src/tests.rs b/crates/precompile-currency-swap/src/tests.rs deleted file mode 100644 index ff6f10b6c..000000000 --- a/crates/precompile-currency-swap/src/tests.rs +++ /dev/null @@ -1,1058 +0,0 @@ -#![allow(clippy::arithmetic_side_effects)] // not a problem in tests - -use mockall::predicate; -use pallet_evm::Runner; -use precompile_utils::EvmDataWriter; - -use crate::{mock::*, *}; - -/// A utility that performs gas to fee computation. -/// Might not be explicitly correct, but does the job. -fn gas_to_fee(gas: u64) -> Balance { - u128::from(gas) * u128::try_from(*GAS_PRICE).unwrap() -} - -/// This test verifies that the swap precompile call works in the happy path. -#[test] -fn swap_works() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 10 * 10u128.pow(18); - - let expected_gas_usage: u64 = 21216 + 200; - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx - .expect() - .once() - .with(predicate::eq(swap_balance)) - .return_const(swap_balance); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx - .expect() - .once() - .with(predicate::eq( - >::NegativeImbalance::new(swap_balance), - )) - .return_once(move |_| { - Ok(>::NegativeImbalance::new( - swap_balance, - )) - }); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(Action::Swap) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_000, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); - assert_eq!( - execinfo.logs, - vec![LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( - SELECTOR_LOG_SWAP, - alice_evm, - H256::from(alice.as_ref()), - EvmDataWriter::new().write(swap_balance).build(), - )] - ); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - swap_balance - expected_fee - ); - assert_eq!( - >::total_balance(&alice), - swap_balance - ); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call works when we transfer *almost* the full -/// account balance. -/// Almost because we leave one token left on the source account. -#[test] -fn swap_works_almost_full_balance() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - - let expected_gas_usage: u64 = 21216 + 200; - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 100 * 10u128.pow(18) - expected_fee - 1; - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx - .expect() - .once() - .with(predicate::eq(swap_balance)) - .return_const(swap_balance); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx - .expect() - .once() - .with(predicate::eq( - >::NegativeImbalance::new(swap_balance), - )) - .return_once(move |_| { - Ok(>::NegativeImbalance::new( - swap_balance, - )) - }); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(Action::Swap) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - expected_gas_usage, // the exact amount of fee we'll be using - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); - assert_eq!( - execinfo.logs, - vec![LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( - SELECTOR_LOG_SWAP, - alice_evm, - H256::from(alice.as_ref()), - EvmDataWriter::new().write(swap_balance).build(), - )] - ); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - swap_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice_evm), 1); - assert_eq!( - >::total_balance(&alice), - swap_balance - ); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when called without -/// the sufficient balance. -/// The fee is not consumed, and neither is the value. -#[test] -fn swap_fail_no_funds() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 1000 * 10u128.pow(18); // more than we have - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx.expect().never(); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx.expect().never(); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(Action::Swap) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - - let storage_root = frame_support::storage_root(sp_runtime::StateVersion::V1); - let execerr = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_000, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap_err(); - assert!(matches!(execerr.error, pallet_evm::Error::BalanceLow)); - assert_eq!( - storage_root, - frame_support::storage_root(sp_runtime::StateVersion::V1), - "storage changed" - ); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when -/// estimated swapped balance less or equal than target currency existential deposit. -/// All fee (up to specified max fee limit!) will be consumed, but not the value. -#[test] -fn swap_fail_below_ed() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - - let expected_gas_usage: u64 = 50_123; // all fee will be consumed - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 10 * 10u128.pow(18); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx - .expect() - .once() - .with(predicate::eq(swap_balance)) - .return_const(1_u128); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx.expect().never(); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(Action::Swap) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_123, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Error(fp_evm::ExitError::OutOfFund) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, Vec::::new()); - assert_eq!(execinfo.logs, Vec::new()); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when the currency swap -/// implementation fails. -/// The fee is consumed (and not all of it - just what was actually used), but the value is not. -/// The error message is checked to be correct. -#[test] -fn swap_fail_trait_error() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 10 * 10u128.pow(18); - - let expected_gas_usage: u64 = 21216 + 200; - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx - .expect() - .once() - .with(predicate::eq(swap_balance)) - .return_const(swap_balance); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx - .expect() - .once() - .with(predicate::eq( - >::NegativeImbalance::new(swap_balance), - )) - .return_once(move |incoming_imbalance| { - Err(primitives_currency_swap::Error { - cause: sp_runtime::DispatchError::Other("test"), - incoming_imbalance, - }) - }); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(Action::Swap) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_000, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Revert(ExitRevert::Reverted) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, "unable to swap the currency".as_bytes()); - assert_eq!(execinfo.logs, Vec::new()); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call works when we transfer the full account balance. -#[test] -fn swap_works_full_balance() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - - let expected_gas_usage: u64 = 21216 + 200; - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 100 * 10u128.pow(18) - expected_fee; - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx - .expect() - .once() - .with(predicate::eq(swap_balance)) - .return_const(swap_balance); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx - .expect() - .once() - .with(predicate::eq( - >::NegativeImbalance::new(swap_balance), - )) - .return_once(move |_| { - Ok(>::NegativeImbalance::new( - swap_balance, - )) - }); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(Action::Swap) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - expected_gas_usage, // the exact amount of fee we'll be using - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); - assert_eq!( - execinfo.logs, - vec![LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( - SELECTOR_LOG_SWAP, - alice_evm, - H256::from(alice.as_ref()), - EvmDataWriter::new().write(swap_balance).build(), - )] - ); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - swap_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice_evm), 0); - assert_eq!( - >::total_balance(&alice), - swap_balance - ); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when a bad selector is -/// passed. -/// All fee (up to specified max fee limit!) will be consumed, but not the value. -#[test] -fn swap_fail_bad_selector() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 10 * 10u128.pow(18); - - let expected_gas_usage: u64 = 50_123; // all fee will be consumed - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx.expect().never(); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx.expect().never(); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(123u32) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_123, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Error(fp_evm::ExitError::Other("invalid function selector".into())) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, Vec::::new()); - assert_eq!(execinfo.logs, Vec::new()); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when the call value -/// is overflowing the underlying balance type. -/// This test actually unable to invoke the condition, as it fails prior to that error due to -/// a failing balance check. Nonetheless, this behaviour is verified in this test. -/// The test name could be misleading, but the idea here is that this test is a demonstration of how -/// we tried to test the value overflow and could not. -/// Fee will be consumed, but not the value. -#[test] -fn swap_fail_value_overflow() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = u128::MAX; - let swap_balance_u256: U256 = U256::from(u128::MAX) + U256::from(1); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx.expect().never(); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx.expect().never(); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(123u32) - .write(H256::from(alice.as_ref())) - .build(); - - // Invoke the function under test. - let config = ::config(); - let storage_root = frame_support::storage_root(sp_runtime::StateVersion::V1); - let execerr = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance_u256, - 50_000, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap_err(); - assert!(matches!(execerr.error, pallet_evm::Error::BalanceLow)); - assert_eq!( - storage_root, - frame_support::storage_root(sp_runtime::StateVersion::V1), - "storage changed" - ); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when the call has no -/// arguments. -/// All fee (up to specified max fee limit!) will be consumed, but not the value. -#[test] -fn swap_fail_no_arguments() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 10 * 10u128.pow(18); - - let expected_gas_usage: u64 = 50_123; // all fee will be consumed - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx.expect().never(); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx.expect().never(); - - // Prepare EVM call. - let input = EvmDataWriter::new_with_selector(Action::Swap).build(); - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_123, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Error(fp_evm::ExitError::Other( - "exactly one argument is expected".into() - )) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, Vec::::new()); - assert_eq!(execinfo.logs, Vec::new()); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when the call has -/// an incomplete argument. -/// All fee (up to specified max fee limit!) will be consumed, but not the value. -#[test] -fn swap_fail_short_argument() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 10 * 10u128.pow(18); - - let expected_gas_usage: u64 = 50_123; // all fee will be consumed - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx.expect().never(); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx.expect().never(); - - // Prepare EVM call. - let mut input = EvmDataWriter::new_with_selector(Action::Swap).build(); - input.extend_from_slice(&hex_literal::hex!("1000")); // bad input - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_123, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Error(fp_evm::ExitError::Other( - "exactly one argument is expected".into() - )) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, Vec::::new()); - assert_eq!(execinfo.logs, Vec::new()); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} - -/// This test verifies that the swap precompile call behaves as expected when the call has -/// extra data after the end of the first argument. -/// All fee (up to specified max fee limit!) will be consumed, but not the value. -#[test] -fn swap_fail_trailing_junk() { - new_test_ext().execute_with_ext(|_| { - let alice_evm = H160::from(hex_literal::hex!( - "1000000000000000000000000000000000000001" - )); - let alice = AccountId::from(hex_literal::hex!( - "1000000000000000000000000000000000000000000000000000000000000001" - )); - let alice_evm_balance = 100 * 10u128.pow(18); - let swap_balance = 10 * 10u128.pow(18); - - let expected_gas_usage: u64 = 50_123; // all fee will be consumed - let expected_fee: Balance = gas_to_fee(expected_gas_usage); - - // Prepare the test state. - EvmBalances::make_free_balance_be(&alice_evm, alice_evm_balance); - - // Check test preconditions. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - ); - assert_eq!(>::total_balance(&alice), 0); - - // Set block number to enable events. - System::set_block_number(1); - - // Set mock expectations. - let estimate_swapped_balance_ctx = MockCurrencySwap::estimate_swapped_balance_context(); - estimate_swapped_balance_ctx.expect().never(); - let swap_ctx = MockCurrencySwap::swap_context(); - swap_ctx.expect().never(); - - // Prepare EVM call. - let mut input = EvmDataWriter::new_with_selector(Action::Swap) - .write(H256::from(alice.as_ref())) - .build(); - input.extend_from_slice(&hex_literal::hex!("1000")); // bad input - - // Invoke the function under test. - let config = ::config(); - let execinfo = ::Runner::call( - alice_evm, - *PRECOMPILE_ADDRESS, - input, - swap_balance.into(), - 50_123, // a reasonable upper bound for tests - Some(*GAS_PRICE), - Some(*GAS_PRICE), - None, - Vec::new(), - true, - true, - None, - None, - config, - ) - .unwrap(); - assert_eq!( - execinfo.exit_reason, - fp_evm::ExitReason::Error(fp_evm::ExitError::Other("junk at the end of input".into())) - ); - assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); - assert_eq!(execinfo.value, Vec::::new()); - assert_eq!(execinfo.logs, Vec::new()); - - // Assert state changes. - assert_eq!( - >::total_balance(&alice_evm), - alice_evm_balance - expected_fee - ); - assert_eq!(>::total_balance(&alice), 0); - assert_eq!( - >::total_balance(&PRECOMPILE_ADDRESS), - 0 - ); - - // Assert mock invocations. - estimate_swapped_balance_ctx.checkpoint(); - swap_ctx.checkpoint(); - }); -} diff --git a/crates/precompile-currency-swap/Cargo.toml b/crates/precompile-evm-to-native-swap/Cargo.toml similarity index 88% rename from crates/precompile-currency-swap/Cargo.toml rename to crates/precompile-evm-to-native-swap/Cargo.toml index a81ea63bf..8d3d75254 100644 --- a/crates/precompile-currency-swap/Cargo.toml +++ b/crates/precompile-evm-to-native-swap/Cargo.toml @@ -1,12 +1,11 @@ [package] -name = "precompile-currency-swap" +name = "precompile-evm-to-native-swap" version = "0.1.0" edition = "2021" publish = false [dependencies] precompile-utils = { path = "../precompile-utils", default-features = false } -primitives-currency-swap = { path = "../primitives-currency-swap", default-features = false } codec = { workspace = true, features = ["derive"] } fp-evm = { workspace = true } @@ -41,7 +40,6 @@ std = [ "pallet-evm/std", "pallet-timestamp/std", "precompile-utils/std", - "primitives-currency-swap/std", "scale-info/std", "sp-core/std", ] diff --git a/crates/precompile-currency-swap/CurrencySwap.sol b/crates/precompile-evm-to-native-swap/EvmToNativeSwap.sol similarity index 54% rename from crates/precompile-currency-swap/CurrencySwap.sol rename to crates/precompile-evm-to-native-swap/EvmToNativeSwap.sol index cc8a9cb40..11a704d6e 100644 --- a/crates/precompile-currency-swap/CurrencySwap.sol +++ b/crates/precompile-evm-to-native-swap/EvmToNativeSwap.sol @@ -3,14 +3,14 @@ pragma solidity >=0.7.0 <0.9.0; /** - * @title Currency Swap Interface + * @title Evm to Native tokens Swap Interface * * An interface enabling swapping the funds from EVM accounts to * native Substrate accounts. * * Address: 0x0000000000000000000000000000000000000900 */ -interface CurrencySwap { +interface EvmToNativeSwap { /** * Transfer the funds from an EVM account to native substrate account. * Selector: 76467cbd @@ -19,4 +19,14 @@ interface CurrencySwap { * @return success Whether or not the swap was successful. */ function swap(bytes32 nativeAddress) external payable returns (bool success); + + /** + * Event emitted when a transfer has been performed. + * Selector: 69d31d1d87c1206beee49fbab11570a7f001121cf21fbb234d5b0a2473fa5c58 + * + * @param from The EVM account id the tokens withdrawed from. + * @param to The Native account id the tokens deposited to. + * @param value The amount of tokens swapped. + */ + event Swap(address indexed from, bytes32 indexed to, uint256 value); } diff --git a/crates/precompile-evm-to-native-swap/src/lib.rs b/crates/precompile-evm-to-native-swap/src/lib.rs new file mode 100644 index 000000000..d9417ff39 --- /dev/null +++ b/crates/precompile-evm-to-native-swap/src/lib.rs @@ -0,0 +1,213 @@ +//! A precompile to swap EVM tokens to native chain tokens using fungible interfaces. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + dispatch::DispatchError, + sp_runtime::traits::Convert, + sp_std::{marker::PhantomData, prelude::*}, + traits::{ + fungible::{Inspect, Mutate}, + tokens::{Preservation, Provenance}, + }, +}; +use pallet_evm::{ + ExitError, Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileResult, +}; +use precompile_utils::{ + keccak256, succeed, EvmDataWriter, EvmResult, LogExt, LogsBuilder, PrecompileHandleExt, +}; +use sp_core::{Get, H160, H256, U256}; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/// Utility alias for easy access to the [`Inspect::Balance`] of the [`Config::NativeToken`] type. +type NativeBalanceOf = + <::NativeToken as Inspect<::AccountId>>::Balance; + +/// Utility alias for easy access to the [`Inspect::Balance`] of the [`Config::EvmToken`] type. +type EvmBalanceOf = <::EvmToken as Inspect<::EvmAccountId>>::Balance; + +/// The config for EVM to native swap logic. +pub trait Config { + /// The native user account identifier type. + type AccountId: From<[u8; 32]>; + + /// The EVM user account identifier type. + type EvmAccountId: From; + + /// Native token. + /// + /// TODO(#1462): switch from `Mutate` to `Balanced` fungible interface. + type NativeToken: Inspect + Mutate; + + /// EVM token. + /// + /// TODO(#1462): switch from `Mutate` to `Balanced` fungible interface. + type EvmToken: Inspect + Mutate; + + /// The converter to determine how the balance amount should be converted from EVM + /// to native token. + type BalanceConverterEvmToNative: Convert, NativeBalanceOf>; + + /// The bridge pot native account. + type BridgePotNative: Get; + + /// The bridge pot EVM account. + type BridgePotEvm: Get; +} + +/// Solidity selector of the Swap log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_SWAP: [u8; 32] = keccak256!("Swap(address,bytes32,uint256)"); + +/// Possible actions for this interface. +#[precompile_utils::generate_function_selector] +#[derive(Debug, PartialEq)] +pub enum Action { + /// Swap EVM tokens to native tokens. + Swap = "swap(bytes32)", +} + +/// Exposes the swap interface. +pub struct EvmToNativeSwap(PhantomData<(C, GasCost)>) +where + C: Config, + EvmBalanceOf: TryFrom, + C::EvmAccountId: From, + C::AccountId: From<[u8; 32]>, + GasCost: Get; + +impl Precompile for EvmToNativeSwap +where + C: Config, + EvmBalanceOf: TryFrom, + C::EvmAccountId: From, + C::AccountId: From<[u8; 32]>, + GasCost: Get, +{ + fn execute(handle: &mut impl PrecompileHandle) -> PrecompileResult { + handle.record_cost(GasCost::get())?; + + let selector = handle + .read_selector() + .map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("invalid function selector".into()), + })?; + + match selector { + Action::Swap => Self::swap(handle), + } + } +} + +impl EvmToNativeSwap +where + C: Config, + EvmBalanceOf: TryFrom, + C::EvmAccountId: From, + C::AccountId: From<[u8; 32]>, + GasCost: Get, +{ + /// Swap EVM tokens to native chain tokens. + fn swap(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + + let fp_evm::Context { + address, + apparent_value: value, + .. + } = handle.context(); + + let value_u256 = *value; + let value: EvmBalanceOf = (*value).try_into().map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("value is out of bounds".into()), + })?; + + input + .expect_arguments(1) + .map_err(|_| PrecompileFailure::Error { + exit_status: ExitError::Other("exactly one argument is expected".into()), + })?; + + let to_h256: H256 = input.read()?; + let to: [u8; 32] = to_h256.into(); + let to: C::AccountId = to.into(); + + let junk_data = input.read_till_end()?; + if !junk_data.is_empty() { + return Err(PrecompileFailure::Error { + exit_status: ExitError::Other("junk at the end of input".into()), + }); + } + + // Here we must withdraw from self (i.e. from the precompile address, not from the caller + // address), since the funds have already been transferred to us (precompile) as this point. + let from: C::EvmAccountId = (*address).into(); + + let estimated_swapped_balance = C::BalanceConverterEvmToNative::convert(value); + + C::NativeToken::can_deposit(&to, estimated_swapped_balance, Provenance::Extant) + .into_result() + .map_err(process_dispatch_error)?; + + C::EvmToken::transfer( + &from, + &C::BridgePotEvm::get(), + value, + Preservation::Expendable, + ) + .map_err(process_dispatch_error)?; + + C::NativeToken::transfer( + &C::BridgePotNative::get(), + &to, + estimated_swapped_balance, + // Bridge pot native account shouldn't be killed. + Preservation::Preserve, + ) + .map_err(process_dispatch_error)?; + + let logs_builder = LogsBuilder::new(handle.context().address); + + logs_builder + .log3( + SELECTOR_LOG_SWAP, + handle.context().caller, + to_h256, + EvmDataWriter::new().write(value_u256).build(), + ) + .record(handle)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } +} + +/// A helper function to process dispatch related errors. +fn process_dispatch_error(error: DispatchError) -> PrecompileFailure { + match error { + DispatchError::Token(frame_support::sp_runtime::TokenError::FundsUnavailable) => { + PrecompileFailure::Error { + exit_status: ExitError::OutOfFund, + } + } + DispatchError::Token(frame_support::sp_runtime::TokenError::BelowMinimum) => { + PrecompileFailure::Error { + exit_status: ExitError::Other( + "resulted balance is less than existential deposit".into(), + ), + } + } + DispatchError::Token(frame_support::sp_runtime::TokenError::NotExpendable) => { + PrecompileFailure::Error { + exit_status: ExitError::Other("account would be killed".into()), + } + } + _ => PrecompileFailure::Error { + exit_status: ExitError::Other("unable to execute swap".into()), + }, + } +} diff --git a/crates/precompile-evm-to-native-swap/src/mock.rs b/crates/precompile-evm-to-native-swap/src/mock.rs new file mode 100644 index 000000000..aaf37c79f --- /dev/null +++ b/crates/precompile-evm-to-native-swap/src/mock.rs @@ -0,0 +1,275 @@ +use std::collections::BTreeMap; + +use frame_support::{ + once_cell::sync::Lazy, + parameter_types, sp_io, + sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, Identity, IdentityLookup}, + BuildStorage, + }, + traits::{ConstU128, ConstU32, ConstU64}, + weights::Weight, +}; +use precompile_utils::precompile_set::{PrecompileAt, PrecompileSetBuilder}; +use sp_core::{Get, H160, H256, U256}; + +use crate::{Config, EvmToNativeSwap}; + +pub const INIT_BALANCE: u128 = 10_000_000_000_000_000; +// Add some tokens to test swap with full balance. +pub const BRIDGE_INIT_BALANCE: u128 = INIT_BALANCE + 100; + +pub fn alice() -> EvmAccountId { + EvmAccountId::from(hex_literal::hex!( + "1100000000000000000000000000000000000011" + )) +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +pub type AccountId = frame_support::sp_runtime::AccountId32; +pub type EvmAccountId = H160; +pub type Balance = u128; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + EvmSystem: pallet_evm_system, + EvmBalances: pallet_evm_balances, + EVM: pallet_evm, + } +); + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<2>; // 2 because we test the account kills via 1 balance + type AccountStore = System; + type MaxLocks = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxReserves = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; + type ReserveIdentifier = [u8; 8]; + type WeightInfo = (); +} + +impl pallet_evm_system::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = EvmAccountId; + type Index = u64; + type AccountData = pallet_evm_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); +} + +impl pallet_evm_balances::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = EvmAccountId; + type Balance = Balance; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = EvmSystem; + type DustRemoval = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 1000; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub static GAS_PRICE: Lazy = Lazy::new(|| 1_000_000_000u128.into()); + +pub struct FixedGasPrice; +impl fp_evm::FeeCalculator for FixedGasPrice { + fn min_gas_price() -> (U256, Weight) { + // Return some meaningful gas price and weight + (*GAS_PRICE, Weight::from_parts(7u64, 0)) + } +} + +pub static PRECOMPILE_ADDRESS: Lazy = Lazy::new(|| H160::from_low_u64_be(0x900)); + +pub struct BridgePotNative; + +impl Get for BridgePotNative { + fn get() -> AccountId { + AccountId::from(hex_literal::hex!( + "1000000000000000000000000000000000000000000000000000000000000001" + )) + } +} + +pub struct BridgePotEvm; + +impl Get for BridgePotEvm { + fn get() -> EvmAccountId { + EvmAccountId::from(hex_literal::hex!( + "1000000000000000000000000000000000000001" + )) + } +} + +pub struct PrecompileConfig; + +impl Config for PrecompileConfig { + type AccountId = AccountId; + type EvmAccountId = EvmAccountId; + type NativeToken = Balances; + type EvmToken = EvmBalances; + type BalanceConverterEvmToNative = Identity; + type BridgePotNative = BridgePotNative; + type BridgePotEvm = BridgePotEvm; +} + +pub type EvmToNativeSwapPrecompile = EvmToNativeSwap>; + +pub type Precompiles = + PrecompileSetBuilder>; + +parameter_types! { + pub BlockGasLimit: U256 = U256::max_value(); + pub GasLimitPovSizeRatio: u64 = 0; + pub WeightPerGas: Weight = Weight::from_parts(20_000, 0); + pub PrecompileAddress: H160 = *PRECOMPILE_ADDRESS; + pub PrecompilesValue: Precompiles = Precompiles::new(); +} + +impl pallet_evm::Config for Test { + type AccountProvider = EvmSystem; + type FeeCalculator = FixedGasPrice; + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type CallOrigin = pallet_evm::EnsureAddressNever< + ::AccountId, + >; + type WithdrawOrigin = pallet_evm::EnsureAddressNever< + ::AccountId, + >; + type AddressMapping = pallet_evm::IdentityAddressMapping; + type Currency = EvmBalances; + type RuntimeEvent = RuntimeEvent; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = (); + type OnCreate = (); + type FindAuthor = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type Timestamp = Timestamp; + type WeightInfo = (); +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + // Build genesis. + let config = GenesisConfig { + balances: BalancesConfig { + balances: vec![(BridgePotNative::get(), BRIDGE_INIT_BALANCE)], + }, + evm: EVMConfig { + accounts: { + let mut map = BTreeMap::new(); + map.insert( + BridgePotEvm::get(), + fp_evm::GenesisAccount { + balance: BRIDGE_INIT_BALANCE.into(), + code: Default::default(), + nonce: Default::default(), + storage: Default::default(), + }, + ); + map.insert( + alice(), + fp_evm::GenesisAccount { + balance: INIT_BALANCE.into(), + code: Default::default(), + nonce: Default::default(), + storage: Default::default(), + }, + ); + map + }, + }, + ..Default::default() + }; + let storage = config.build_storage().unwrap(); + + // Make test externalities from the storage. + storage.into() +} + +pub fn runtime_lock() -> std::sync::MutexGuard<'static, ()> { + static MOCK_RUNTIME_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + // Ignore the poisoning for the tests that panic. + // We only care about concurrency here, not about the poisoning. + match MOCK_RUNTIME_MUTEX.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +pub trait TestExternalitiesExt { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R; +} + +impl TestExternalitiesExt for frame_support::sp_io::TestExternalities { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R, + { + let guard = runtime_lock(); + let result = self.execute_with(|| execute(&guard)); + drop(guard); + result + } +} diff --git a/crates/precompile-evm-to-native-swap/src/tests.rs b/crates/precompile-evm-to-native-swap/src/tests.rs new file mode 100644 index 000000000..a33ff1d76 --- /dev/null +++ b/crates/precompile-evm-to-native-swap/src/tests.rs @@ -0,0 +1,462 @@ +// Allow simple integer arithmetic in tests. +#![allow(clippy::arithmetic_side_effects)] + +use fp_evm::{ExitError, ExitReason}; +use frame_support::{assert_noop, traits::fungible::Unbalanced}; +use pallet_evm::Runner; +use precompile_utils::{EvmDataWriter, LogsBuilder}; +use sp_core::H256; + +use crate::{mock::*, *}; + +/// Returns source swap evm account used in tests. +fn source_swap_evm_account() -> EvmAccountId { + EvmAccountId::from(hex_literal::hex!( + "1100000000000000000000000000000000000011" + )) +} + +/// Returns target swap native account used in tests. +fn target_swap_native_account() -> AccountId { + AccountId::from(hex_literal::hex!( + "7700000000000000000000000000000000000000000000000000000000000077" + )) +} + +/// A utility that performs gas to fee computation. +fn gas_to_fee(gas: u64) -> Balance { + Balance::from(gas) * Balance::try_from(*GAS_PRICE).unwrap() +} + +/// A helper function to run succeeded test and assert state changes. +fn run_succeeded_test_and_assert( + swap_balance: Balance, + expected_gas_usage: u64, + expected_fee: Balance, +) { + let source_swap_evm_account_balance_before = + EvmBalances::total_balance(&source_swap_evm_account()); + let bridge_pot_evm_account_balance_before = EvmBalances::total_balance(&BridgePotEvm::get()); + let bridge_pot_native_account_balance_before = Balances::total_balance(&BridgePotNative::get()); + let target_swap_native_account_balance_before = + Balances::total_balance(&target_swap_native_account()); + + // Invoke the function under test. + let execinfo = ::Runner::call( + source_swap_evm_account(), + *PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + swap_balance.into(), + expected_gas_usage, // the exact amount of fee we'll be using + Some(*GAS_PRICE), + Some(*GAS_PRICE), + None, + Vec::new(), + true, + true, + None, + None, + ::config(), + ) + .unwrap(); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) + ); + assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); + assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); + assert_eq!( + execinfo.logs, + vec![LogsBuilder::new(*PRECOMPILE_ADDRESS).log3( + SELECTOR_LOG_SWAP, + source_swap_evm_account(), + H256::from(target_swap_native_account().as_ref()), + EvmDataWriter::new().write(swap_balance).build(), + )] + ); + + // Assert state changes. + + // Verify that source swap evm balance has been decreased by swap value and fee. + assert_eq!( + ::total_balance(&source_swap_evm_account()), + source_swap_evm_account_balance_before - swap_balance - expected_fee, + ); + // Verify that bridge pot evm balance has been increased by swap value. + assert_eq!( + EvmBalances::total_balance(&BridgePotEvm::get()), + bridge_pot_evm_account_balance_before + swap_balance, + ); + // Verify that target swap native balance has been increased by swap value. + assert_eq!( + ::total_balance(&target_swap_native_account()), + target_swap_native_account_balance_before + swap_balance + ); + // Verify that bridge pot native balance has been decreased by swap value. + assert_eq!( + Balances::total_balance(&BridgePotNative::get()), + bridge_pot_native_account_balance_before - swap_balance, + ); + // Verify that precompile balance remains the same. + assert_eq!(EvmBalances::total_balance(&*PRECOMPILE_ADDRESS), 0); +} + +/// This test verifies that the swap precompile call works in the happy path. +#[test] +fn swap_works() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 21216 + 200; + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + run_succeeded_test_and_assert(100, expected_gas_usage, expected_fee); + }); +} + +/// This test verifies that the swap precompile call works when we transfer *almost* the full +/// account balance. +/// Almost because we leave one token left on the source account. +#[test] +fn swap_works_almost_full_balance() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 21216 + 200; + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + run_succeeded_test_and_assert( + INIT_BALANCE - expected_fee - 1, + expected_gas_usage, + expected_fee, + ); + }); +} + +/// This test verifies that the swap precompile call works when we transfer the full account balance. +#[test] +fn swap_works_full_balance() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 21216 + 200; + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + run_succeeded_test_and_assert( + INIT_BALANCE - expected_fee, + expected_gas_usage, + expected_fee, + ); + }); +} + +/// A helper function to run failed test and assert state changes. +fn run_failed_test_and_assert( + input: Vec, + value: U256, + expected_gas_usage: u64, + expected_fee: Balance, + expected_exit_reason: fp_evm::ExitReason, + expected_exit_value: Vec, +) { + let source_swap_evm_account_balance_before = + EvmBalances::total_balance(&source_swap_evm_account()); + let bridge_pot_evm_account_balance_before = EvmBalances::total_balance(&BridgePotEvm::get()); + let bridge_pot_native_account_balance_before = Balances::total_balance(&BridgePotNative::get()); + let target_swap_native_account_balance_before = + Balances::total_balance(&target_swap_native_account()); + + // Invoke the function under test. + let execinfo = ::Runner::call( + source_swap_evm_account(), + *PRECOMPILE_ADDRESS, + input, + value, + 50_000, // a reasonable upper bound for tests + Some(*GAS_PRICE), + Some(*GAS_PRICE), + None, + Vec::new(), + true, + true, + None, + None, + ::config(), + ) + .unwrap(); + assert_eq!(execinfo.exit_reason, expected_exit_reason); + assert_eq!(execinfo.used_gas.standard, expected_gas_usage.into()); + assert_eq!(execinfo.value, expected_exit_value); + assert_eq!(execinfo.logs, vec![]); + + // Verify that source swap evm balance is reduced just by spent fee. + assert_eq!( + ::total_balance(&source_swap_evm_account()), + source_swap_evm_account_balance_before - expected_fee, + ); + // Verify that bridge pot evm balance remains the same. + assert_eq!( + EvmBalances::total_balance(&BridgePotEvm::get()), + bridge_pot_evm_account_balance_before, + ); + // Verify that target swap native balance remains the same. + assert_eq!( + ::total_balance(&target_swap_native_account()), + target_swap_native_account_balance_before + ); + // Verify that bridge pot native balance remains the same. + assert_eq!( + Balances::total_balance(&BridgePotNative::get()), + bridge_pot_native_account_balance_before, + ); + // Verify that precompile balance remains the same. + assert_eq!(EvmBalances::total_balance(&*PRECOMPILE_ADDRESS), 0); +} + +/// This test verifies that the swap precompile call fails when estimated swapped balance is +/// less or equal than native token existential deposit. +#[test] +fn swap_fail_target_balance_below_ed() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + run_failed_test_and_assert( + EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + U256::from(1), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other( + "resulted balance is less than existential deposit".into(), + )), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails when estimated swapped balance results +/// into target swap native account balance overflow. +#[test] +fn swap_fail_target_balance_overflow() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + Balances::write_balance(&target_swap_native_account(), Balance::MAX).unwrap(); + + run_failed_test_and_assert( + EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + U256::from(1), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other("unable to execute swap".into())), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails when swapped balance results into +/// bridge pot evm account balance overflow. +#[test] +fn swap_fail_bridge_evm_overflow() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + EvmBalances::write_balance(&BridgePotEvm::get(), Balance::MAX).unwrap(); + + run_failed_test_and_assert( + EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + U256::from(100), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other("unable to execute swap".into())), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails when swap results into killing bridge +/// pot native account. +#[test] +fn swap_fail_bridge_native_killed() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + EvmBalances::write_balance( + &source_swap_evm_account(), + INIT_BALANCE + BRIDGE_INIT_BALANCE, + ) + .unwrap(); + + run_failed_test_and_assert( + EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + U256::from(BRIDGE_INIT_BALANCE), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other("account would be killed".into())), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails when a bad selector is passed. +#[test] +fn swap_fail_bad_selector() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + run_failed_test_and_assert( + EvmDataWriter::new_with_selector(111_u32) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + U256::from(1), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other("invalid function selector".into())), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails when the call has no +/// arguments. +#[test] +fn swap_fail_no_arguments() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + run_failed_test_and_assert( + EvmDataWriter::new_with_selector(Action::Swap).build(), + U256::from(1), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other("exactly one argument is expected".into())), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails the call has an incomplete argument. +#[test] +fn swap_fail_short_argument() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + let mut input = EvmDataWriter::new_with_selector(Action::Swap).build(); + input.extend_from_slice(&hex_literal::hex!("1000")); // bad input + + run_failed_test_and_assert( + input, + U256::from(1), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other("exactly one argument is expected".into())), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails when the call has extra data after +/// the end of the first argument. +#[test] +fn swap_fail_trailing_junk() { + new_test_ext().execute_with_ext(|_| { + let expected_gas_usage: u64 = 50_000; // all passed gas will be consumed + let expected_fee: Balance = gas_to_fee(expected_gas_usage); + + let mut input = EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(); + input.extend_from_slice(&hex_literal::hex!("1000")); // bad input + + run_failed_test_and_assert( + input, + U256::from(1), + expected_gas_usage, + expected_fee, + ExitReason::Error(ExitError::Other("junk at the end of input".into())), + EvmDataWriter::new().build(), + ); + }); +} + +/// This test verifies that the swap precompile call fails when called without the sufficient balance. +#[test] +fn runner_fail_source_balance_no_funds() { + new_test_ext().execute_with_ext(|_| { + assert_noop!( + Err::<(), DispatchError>( + ::Runner::call( + source_swap_evm_account(), + *PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + U256::from(INIT_BALANCE + 1), + 50_000, // a reasonable upper bound for tests + Some(*GAS_PRICE), + Some(*GAS_PRICE), + None, + Vec::new(), + true, + true, + None, + None, + ::config(), + ) + .unwrap_err() + .error + .into() + ), + pallet_evm::Error::::BalanceLow + ); + }); +} + +/// This test verifies that the swap precompile call fails when the call value is overflowing the +/// underlying balance type. +/// This test actually unable to invoke the condition, as it fails prior to that error due to +/// a failing balance check. Nonetheless, this behaviour is verified in this test. +/// The test name could be misleading, but the idea here is that this test is a demonstration of how +/// we tried to test the value overflow and could not. +#[test] +fn runner_fail_value_overflow() { + new_test_ext().execute_with_ext(|_| { + // Invoke the function under test. + assert_noop!( + Err::<(), DispatchError>( + ::Runner::call( + source_swap_evm_account(), + *PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::Swap) + .write(H256::from(target_swap_native_account().as_ref())) + .build(), + U256::MAX, + 50_000, // a reasonable upper bound for tests + Some(*GAS_PRICE), + Some(*GAS_PRICE), + None, + Vec::new(), + true, + true, + None, + None, + ::config(), + ) + .unwrap_err() + .error + .into() + ), + pallet_evm::Error::::BalanceLow + ); + }); +} diff --git a/utils/checks/snapshots/features.yaml b/utils/checks/snapshots/features.yaml index 0e9b0c6fb..6139346c5 100644 --- a/utils/checks/snapshots/features.yaml +++ b/utils/checks/snapshots/features.yaml @@ -2008,6 +2008,10 @@ - name: pallet-multisig 4.0.0-dev features: - std +- name: pallet-native-to-evm-swap 0.1.0 + features: + - default + - std - name: pallet-pot 0.1.0 features: - default @@ -2156,11 +2160,11 @@ features: - default - std -- name: precompile-currency-swap 0.1.0 +- name: precompile-evm-accounts-mapping 0.1.0 features: - default - std -- name: precompile-evm-accounts-mapping 0.1.0 +- name: precompile-evm-to-native-swap 0.1.0 features: - default - std diff --git a/utils/e2e-tests/ts/lib/abis/evmSwap.ts b/utils/e2e-tests/ts/lib/abis/evmSwap.ts new file mode 100644 index 000000000..d4f5aa657 --- /dev/null +++ b/utils/e2e-tests/ts/lib/abis/evmSwap.ts @@ -0,0 +1,43 @@ +export default { + abi: [ + { + inputs: [ + { + internalType: "bytes32", + name: "nativeAddress", + type: "bytes32", + }, + ], + name: "swap", + outputs: [ + { + internalType: "bool", + name: "success", + type: "bool", + }, + ], + stateMutability: "payable", + type: "function", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "from", + type: "address", + }, + { indexed: true, internalType: "address", name: "to", type: "bytes32" }, + { + indexed: false, + internalType: "uint256", + name: "value", + type: "uint256", + }, + ], + name: "Swap", + type: "event", + }, + ], +} as const; diff --git a/utils/e2e-tests/ts/lib/substrateUtils.ts b/utils/e2e-tests/ts/lib/substrateUtils.ts new file mode 100644 index 000000000..dc902396b --- /dev/null +++ b/utils/e2e-tests/ts/lib/substrateUtils.ts @@ -0,0 +1,24 @@ +//! Common substrate utils. + +import * as substrate from "../lib/substrate"; + +type SystemAccount = { + data: { + free: bigint; + }; +}; + +/// A helper function to get balance of native account. +export const getNativeBalance = async ( + substrateApi: substrate.Api, + nativeAccount: string, +) => { + const systemAccount = (await substrateApi.query["system"]?.["account"]?.( + nativeAccount, + )) as unknown as SystemAccount; + + const free = systemAccount.data.free; + + // We should explicitly convert to native bigint for math operations. + return BigInt(free); +}; diff --git a/utils/e2e-tests/ts/tests/swap/evmToNative.ts b/utils/e2e-tests/ts/tests/swap/evmToNative.ts new file mode 100644 index 000000000..9c5abeff4 --- /dev/null +++ b/utils/e2e-tests/ts/tests/swap/evmToNative.ts @@ -0,0 +1,140 @@ +import { describe, it, expect } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as eth from "../../lib/ethViem"; +import "../../lib/expect"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import evmSwap from "../../lib/abis/evmSwap"; +import { decodeEventLog } from "viem"; +import * as substrate from "../../lib/substrate"; +import { getNativeBalance } from "../../lib/substrateUtils"; + +const evmToNativeSwapPrecompileAddress = + "0x0000000000000000000000000000000000000900"; +const bridgePotEvmAddress = "0x6d6f646c686d63732f656e310000000000000000"; +const bridgePotNativeAccount = + "hmpwhPbL5XJM1pYFVL6wRPkUP5gHQyvC6R5jMkziwnGTQ6hFr"; +const feesPotNativeAccount = + "hmpwhPbL5XJTYPWXPMkacfqGhJ3eoQRPLKphajpvcot5Q5zkk"; + +describe("evm to native tokens swap", () => { + let node: RunNodeState; + let ethPublicClient: eth.PublicClientWebSocket; + let ethDevClients: eth.DevClientsWebSocket; + let substrateApi: substrate.Api; + beforeEachWithCleanup(async (cleanup) => { + node = runNode({ args: ["--dev", "--tmp"] }, cleanup.push); + + await node.waitForBoot; + + ethPublicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + ethDevClients = eth.devClientsFromNodeWebSocket(node, cleanup.push); + substrateApi = await substrate.apiFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + it("success", async () => { + const [alice, _] = ethDevClients; + + const swapBalance = 1_000_000n; + const targetSwapNativeAccount = + "0x7700000000000000000000000000000000000000000000000000000000000077"; + const targetSwapNativeAccountSs58 = + "hmqAEn816d1W6TxbT7Md2Zc4hq1AUXFiLEs8yXW5BCUHFx54W"; + + const sourceSwapEvmBalanceBefore = await ethPublicClient.getBalance({ + address: alice.account.address, + }); + const bridgePotEvmBalanceBefore = await ethPublicClient.getBalance({ + address: bridgePotEvmAddress, + }); + const bridgePotNativeBalanceBefore = await getNativeBalance( + substrateApi, + bridgePotNativeAccount, + ); + const targetSwapNativeBalanceBefore = await getNativeBalance( + substrateApi, + targetSwapNativeAccountSs58, + ); + const feesPotNativeBalanceBefore = await getNativeBalance( + substrateApi, + feesPotNativeAccount, + ); + + const swapTxHash = await alice.writeContract({ + abi: evmSwap.abi, + address: evmToNativeSwapPrecompileAddress, + functionName: "swap", + args: [targetSwapNativeAccount], + value: swapBalance, + }); + + const swapTxReceipt = await ethPublicClient.waitForTransactionReceipt({ + hash: swapTxHash, + timeout: 18_000, + }); + + expect(swapTxReceipt.status).toBe("success"); + + const logs = swapTxReceipt.logs; + expect(logs.length).toEqual(1); + + const swapLog = logs[0]!; + const event = decodeEventLog({ + abi: evmSwap.abi, + data: swapLog.data, + topics: swapLog.topics, + }); + + expect(event).toEqual({ + eventName: "Swap", + args: { + from: alice.account.address, + to: targetSwapNativeAccount, + value: swapBalance, + }, + }); + + const fee = + swapTxReceipt.cumulativeGasUsed * swapTxReceipt.effectiveGasPrice; + + const sourceSwapEvmBalanceAfter = await ethPublicClient.getBalance({ + address: alice.account.address, + }); + expect(sourceSwapEvmBalanceAfter).toEqual( + sourceSwapEvmBalanceBefore - swapBalance - fee, + ); + + const bridgePotEvmBalanceAfter = await ethPublicClient.getBalance({ + address: bridgePotEvmAddress, + }); + expect(bridgePotEvmBalanceAfter).toEqual( + bridgePotEvmBalanceBefore + swapBalance + fee, + ); + + const bridgePotNativeBalanceAfter = await getNativeBalance( + substrateApi, + bridgePotNativeAccount, + ); + expect(bridgePotNativeBalanceAfter).toEqual( + bridgePotNativeBalanceBefore - swapBalance - fee, + ); + + const targetSwapNativeBalanceAfter = await getNativeBalance( + substrateApi, + targetSwapNativeAccountSs58, + ); + expect(targetSwapNativeBalanceAfter).toEqual( + targetSwapNativeBalanceBefore + swapBalance, + ); + + const feesPotNativeBalanceAfter = await getNativeBalance( + substrateApi, + feesPotNativeAccount, + ); + expect(feesPotNativeBalanceAfter).toEqual(feesPotNativeBalanceBefore + fee); + + const evmSwapPrecompileBalance = await ethPublicClient.getBalance({ + address: evmToNativeSwapPrecompileAddress, + }); + expect(evmSwapPrecompileBalance).toEqual(0n); + }); +}); diff --git a/utils/e2e-tests/ts/tests/swap/nativeToEvm.ts b/utils/e2e-tests/ts/tests/swap/nativeToEvm.ts new file mode 100644 index 000000000..eb175f020 --- /dev/null +++ b/utils/e2e-tests/ts/tests/swap/nativeToEvm.ts @@ -0,0 +1,217 @@ +import { expect, it, describe, assert } from "vitest"; +import { RunNodeState, runNode } from "../../lib/node"; +import * as substrate from "../../lib/substrate"; +import { beforeEachWithCleanup } from "../../lib/lifecycle"; +import { Keyring } from "@polkadot/api"; +import { Codec, IEvent } from "@polkadot/types/types"; +import sendAndWait from "../../lib/substrateSendAndAwait"; +import * as eth from "../../lib/ethViem"; +import { getNativeBalance } from "../../lib/substrateUtils"; + +type EvmSwapBalancesSwappedEvent = Record< + "from" | "withdrawedAmount" | "to" | "depositedAmount" | "evmTransactionHash", + Codec +>; + +type EthereumExecutedEvent = Record< + "from" | "to" | "transactionHash" | "exitReason", + Codec +>; + +type TransactionPaymentEvent = Record<"who" | "actualFee", Codec>; + +const bridgePotEvmAddress = "0x6d6f646c686d63732f656e310000000000000000"; +const bridgePotNativeAccount = + "hmpwhPbL5XJM1pYFVL6wRPkUP5gHQyvC6R5jMkziwnGTQ6hFr"; +const feesPotNativeAccount = + "hmpwhPbL5XJTYPWXPMkacfqGhJ3eoQRPLKphajpvcot5Q5zkk"; + +describe("native to evm tokens swap", () => { + let node: RunNodeState; + let substrateApi: substrate.Api; + let ethPublicClient: eth.PublicClientWebSocket; + beforeEachWithCleanup(async (cleanup) => { + node = runNode({ args: ["--dev", "--tmp"] }, cleanup.push); + + await node.waitForBoot; + + substrateApi = await substrate.apiFromNodeWebSocket(node, cleanup.push); + ethPublicClient = eth.publicClientFromNodeWebSocket(node, cleanup.push); + }, 60 * 1000); + + it("success", async () => { + const keyring = new Keyring({ type: "sr25519", ss58Format: 5234 }); + const alice = keyring.addFromUri("//Alice"); + + const targetSwapEvmAddress = "0x1100000000000000000000000000000000000011"; + const swapBalance = 1_000_000n; + + const swap = substrateApi.tx["nativeToEvmSwap"]?.["swap"]; + assert(swap); + + const sourceSwapNativeBalanceBefore = await getNativeBalance( + substrateApi, + alice.address, + ); + const bridgePotNativeBalanceBefore = await getNativeBalance( + substrateApi, + bridgePotNativeAccount, + ); + const targetSwapEvmBalanceBefore = await ethPublicClient.getBalance({ + address: targetSwapEvmAddress, + }); + const bridgePotEvmBalanceBefore = await ethPublicClient.getBalance({ + address: bridgePotEvmAddress, + }); + const feesPotNativeBalanceBefore = await getNativeBalance( + substrateApi, + feesPotNativeAccount, + ); + + const { isCompleted, internalError, events, status, dispatchError } = + await sendAndWait(swap(targetSwapEvmAddress, swapBalance), { + signWith: alice, + }); + + expect(isCompleted).toBe(true); + expect(status.isInBlock).toBe(true); + expect(dispatchError).toBe(undefined); + expect(internalError).toBe(undefined); + + const expectedEvents = [ + // Executed fee withdraw from source swap native account. + ["balances", "Withdraw"], + // Executed swap balance transfer from source swap to bridge pot native account. + ["balances", "Transfer"], + // Executed new target swap EVM account creation in accounts records. + ["evmSystem", "NewAccount"], + // Executed EVM balances explicitly related event that an account is created with some free balance. + ["evmBalances", "Endowed"], + // Executed swap balance transfer from bridge EVM to target swap address. + ["evmBalances", "Transfer"], + // Executed ethereum transaction. + ["ethereum", "Executed"], + // Executed native to EVM swap. + ["nativeToEvmSwap", "BalancesSwapped"], + // Executed fee deposit to fees pot native account. + ["balances", "Deposit"], + // Executed pot explicitly related event that some balance is deposited. + ["feesPot", "Deposit"], + // Executed transaction payment event. + ["transactionPayment", "TransactionFeePaid"], + // Executed extrinsic success event. + ["system", "ExtrinsicSuccess"], + ] as const; + + expectedEvents.forEach((value, idx) => { + const [section, method] = value; + expect(events[idx]).toBeDefined(); + expect(events[idx]?.event?.section).toBe(section); + expect(events[idx]?.event?.method).toBe(method); + }); + + expect(events).toHaveLength(expectedEvents.length); + + const ethereumExecutedEvent = events[5]?.event as unknown as IEvent< + Codec[], + EthereumExecutedEvent + >; + const nativeToEvmSwapBalancesSwappedEvent = events[6] + ?.event as unknown as IEvent; + const transactionPaymentEvent = events[9]?.event as unknown as IEvent< + Codec[], + TransactionPaymentEvent + >; + + // Ethereum execution checks. + const executedEvmTransaction = await ethPublicClient.getTransaction({ + hash: ethereumExecutedEvent.data.transactionHash.toPrimitive() as `0x${string}`, + }); + expect(executedEvmTransaction.from).toEqual(bridgePotEvmAddress); + expect(executedEvmTransaction.to).toEqual(targetSwapEvmAddress); + expect(executedEvmTransaction.value).toEqual(swapBalance); + expect(executedEvmTransaction.gasPrice).toEqual(0n); + expect(executedEvmTransaction.maxFeePerGas).toEqual(0n); + expect(executedEvmTransaction.maxPriorityFeePerGas).toEqual(0n); + expect(executedEvmTransaction.gas).toEqual(21000n); + expect(executedEvmTransaction.input).toEqual("0x"); + + // Events related asserts. + expect(nativeToEvmSwapBalancesSwappedEvent.data.from.toPrimitive()).toEqual( + alice.address, + ); + expect( + BigInt( + nativeToEvmSwapBalancesSwappedEvent.data.withdrawedAmount.toPrimitive() as + | string + | number, + ), + ).toEqual(swapBalance); + expect(nativeToEvmSwapBalancesSwappedEvent.data.to.toPrimitive()).toEqual( + targetSwapEvmAddress, + ); + expect( + BigInt( + nativeToEvmSwapBalancesSwappedEvent.data.depositedAmount.toPrimitive() as + | string + | number, + ), + ).toEqual(swapBalance); + expect(nativeToEvmSwapBalancesSwappedEvent.data.evmTransactionHash).toEqual( + ethereumExecutedEvent.data.transactionHash, + ); + expect(ethereumExecutedEvent.data.from.toPrimitive()).toEqual( + bridgePotEvmAddress, + ); + expect(ethereumExecutedEvent.data.to.toPrimitive()).toEqual( + targetSwapEvmAddress, + ); + expect(ethereumExecutedEvent.data.exitReason.toPrimitive()).toEqual({ + succeed: "Stopped", + }); + + // Balances changes related checks. + const fee = BigInt( + transactionPaymentEvent.data.actualFee.toPrimitive() as string | number, + ); + expect(transactionPaymentEvent.data.who.toPrimitive()).toEqual( + alice.address, + ); + + const sourceSwapNativeBalanceAfter = await getNativeBalance( + substrateApi, + alice.address, + ); + expect(sourceSwapNativeBalanceAfter).toEqual( + sourceSwapNativeBalanceBefore - swapBalance - fee, + ); + + const bridgePotNativeBalanceAfter = await getNativeBalance( + substrateApi, + bridgePotNativeAccount, + ); + expect(bridgePotNativeBalanceAfter).toEqual( + bridgePotNativeBalanceBefore + swapBalance, + ); + + const targetSwapEvmBalanceAfter = await ethPublicClient.getBalance({ + address: targetSwapEvmAddress, + }); + expect(targetSwapEvmBalanceAfter).toEqual( + targetSwapEvmBalanceBefore + swapBalance, + ); + + const bridgePotEvmBalanceAfter = await ethPublicClient.getBalance({ + address: bridgePotEvmAddress, + }); + expect(bridgePotEvmBalanceAfter).toEqual( + bridgePotEvmBalanceBefore - swapBalance, + ); + + const feesPotNativeBalanceAfter = await getNativeBalance( + substrateApi, + feesPotNativeAccount, + ); + expect(feesPotNativeBalanceAfter).toEqual(feesPotNativeBalanceBefore + fee); + }); +});