diff --git a/Cargo.lock b/Cargo.lock index bcea145a7..d97db8612 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5974,6 +5974,20 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-evm-balances" +version = "1.0.0-dev" +source = "git+https://github.com/humanode-network/frontier?branch=locked/polkadot-v0.9.38#07429a3b559c7277fa20024587f67ae809556054" +dependencies = [ + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" @@ -6002,6 +6016,21 @@ dependencies = [ "sp-io", ] +[[package]] +name = "pallet-evm-system" +version = "1.0.0-dev" +source = "git+https://github.com/humanode-network/frontier?branch=locked/polkadot-v0.9.38#07429a3b559c7277fa20024587f67ae809556054" +dependencies = [ + "fp-evm", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-grandpa" version = "4.0.0-dev" @@ -6635,6 +6664,28 @@ dependencies = [ "sp-std", ] +[[package]] +name = "precompile-currency-swap" +version = "0.1.0" +dependencies = [ + "fp-evm", + "frame-support", + "frame-system", + "hex-literal", + "mockall", + "num_enum 0.6.0", + "pallet-balances", + "pallet-evm", + "pallet-evm-balances", + "pallet-evm-system", + "pallet-timestamp", + "parity-scale-codec", + "precompile-utils", + "primitives-currency-swap", + "scale-info", + "sp-core", +] + [[package]] name = "precompile-evm-accounts-mapping" version = "0.1.0" diff --git a/crates/precompile-currency-swap/Cargo.toml b/crates/precompile-currency-swap/Cargo.toml new file mode 100644 index 000000000..5818a81a8 --- /dev/null +++ b/crates/precompile-currency-swap/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "precompile-currency-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 = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +fp-evm = { git = "https://github.com/humanode-network/frontier", branch = "locked/polkadot-v0.9.38", default-features = false } +frame-support = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +num_enum = { version = "0.6.0", default-features = false } +pallet-evm = { git = "https://github.com/humanode-network/frontier", branch = "locked/polkadot-v0.9.38", default-features = false } +scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } +sp-core = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } + +[dev-dependencies] +frame-system = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +hex-literal = "0.4" +mockall = "0.11" +pallet-balances = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } +pallet-evm = { default-features = false, git = "https://github.com/humanode-network/frontier", branch = "locked/polkadot-v0.9.38" } +pallet-evm-balances = { default-features = false, git = "https://github.com/humanode-network/frontier", branch = "locked/polkadot-v0.9.38" } +pallet-evm-system = { default-features = false, git = "https://github.com/humanode-network/frontier", branch = "locked/polkadot-v0.9.38" } +pallet-timestamp = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "locked/polkadot-v0.9.38" } + +[features] +default = ["std"] +std = [ + "codec/std", + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "num_enum/std", + "pallet-balances/std", + "pallet-evm-balances/std", + "pallet-evm-system/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-currency-swap/CurrencySwap.sol new file mode 100644 index 000000000..cc8a9cb40 --- /dev/null +++ b/crates/precompile-currency-swap/CurrencySwap.sol @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity >=0.7.0 <0.9.0; + +/** + * @title Currency Swap Interface + * + * An interface enabling swapping the funds from EVM accounts to + * native Substrate accounts. + * + * Address: 0x0000000000000000000000000000000000000900 + */ +interface CurrencySwap { + /** + * Transfer the funds from an EVM account to native substrate account. + * Selector: 76467cbd + * + * @param nativeAddress The native address to send the funds to. + * @return success Whether or not the swap was successful. + */ + function swap(bytes32 nativeAddress) external payable returns (bool success); +} diff --git a/crates/precompile-currency-swap/src/lib.rs b/crates/precompile-currency-swap/src/lib.rs new file mode 100644 index 000000000..34ad450a8 --- /dev/null +++ b/crates/precompile-currency-swap/src/lib.rs @@ -0,0 +1,146 @@ +//! 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::tokens::currency::Currency, +}; +use pallet_evm::{ + ExitError, ExitRevert, Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput, + PrecompileResult, +}; +use precompile_utils::{succeed, EvmDataWriter, EvmResult, PrecompileHandleExt}; +use sp_core::{Get, H160, H256, U256}; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +/// 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: 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 = input.read()?; + let to: [u8; 32] = to.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 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::NoFunds) => { + PrecompileFailure::Error { + exit_status: ExitError::OutOfFund, + } + } + _ => PrecompileFailure::Error { + exit_status: ExitError::Other("unable to withdraw funds".into()), + }, + })?; + + let imbalance = CurrencySwapT::swap(imbalance).map_err(|_| PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output: "unable to swap the currency".into(), + })?; + + CurrencySwapT::To::resolve_creating(&to, imbalance); + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } +} diff --git a/crates/precompile-currency-swap/src/mock.rs b/crates/precompile-currency-swap/src/mock.rs new file mode 100644 index 000000000..a71652f26 --- /dev/null +++ b/crates/precompile-currency-swap/src/mock.rs @@ -0,0 +1,237 @@ +//! The mock for the precompile. + +// Allow simple integer arithmetic in tests. +#![allow(clippy::integer_arithmetic)] + +use fp_evm::PrecompileHandle; +use frame_support::{ + once_cell::sync::Lazy, + sp_io, + sp_runtime::{ + self, + testing::Header, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, DispatchError, + }, + traits::{ConstU16, ConstU32, ConstU64, Currency}, + weights::Weight, +}; +use frame_system as system; +use mockall::mock; +use sp_core::{ConstU128, H160, H256, U256}; + +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; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum 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 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 = ConstU16<1>; + 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; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<2>; // 2 because we test the account kills via 1 balance + type AccountStore = System; + type MaxLocks = (); + type MaxReserves = (); + 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 = (); +} + +pub(crate) 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_ref_time(7u64)) + } +} + +frame_support::parameter_types! { + pub BlockGasLimit: U256 = U256::max_value(); + pub WeightPerGas: Weight = Weight::from_ref_time(20_000); + pub MockPrecompiles: MockPrecompileSet = MockPrecompileSet; +} + +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 = MockPrecompileSet; + type PrecompilesValue = MockPrecompiles; + type ChainId = (); + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = (); + type OnCreate = (); + type FindAuthor = (); +} + +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(); + + if address == *PRECOMPILE_ADDRESS { + return Some(CurrencySwapPrecompile::execute(handle)); + } + + None + } + + /// 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) -> bool { + address == *PRECOMPILE_ADDRESS + } +} + +mock! { + #[derive(Debug)] + pub CurrencySwap {} + impl primitives_currency_swap::CurrencySwap for CurrencySwap { + type From = EvmBalances; + type To = Balances; + type Error = DispatchError; + + fn swap( + imbalance: >::NegativeImbalance, + ) -> Result<>::NegativeImbalance, DispatchError>; + } +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let genesis_config = GenesisConfig::default(); + new_test_ext_with(genesis_config) +} + +// 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(); + 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-currency-swap/src/tests.rs b/crates/precompile-currency-swap/src/tests.rs new file mode 100644 index 000000000..0644f5cf5 --- /dev/null +++ b/crates/precompile-currency-swap/src/tests.rs @@ -0,0 +1,778 @@ +#![allow(clippy::integer_arithmetic)] // 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + config, + ) + .unwrap(); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) + ); + assert_eq!(execinfo.used_gas, expected_gas_usage.into()); + assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); + assert_eq!(execinfo.logs, Vec::new()); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - swap_balance - expected_fee + ); + assert_eq!(Balances::total_balance(&alice), swap_balance); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + config, + ) + .unwrap(); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Succeed(fp_evm::ExitSucceed::Returned) + ); + assert_eq!(execinfo.used_gas, expected_gas_usage.into()); + assert_eq!(execinfo.value, EvmDataWriter::new().write(true).build()); + assert_eq!(execinfo.logs, Vec::new()); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - swap_balance - expected_fee + ); + assert_eq!(EvmBalances::total_balance(&alice_evm), 1); + assert_eq!(Balances::total_balance(&alice), swap_balance); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + let swap_ctx = MockCurrencySwap::swap_context(); + swap_ctx + .expect() + .once() + .with(predicate::eq( + >::NegativeImbalance::new(swap_balance), + )) + .return_once(move |_| Err(sp_runtime::DispatchError::Other("test"))); + + // 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, + config, + ) + .unwrap(); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Revert(ExitRevert::Reverted) + ); + assert_eq!(execinfo.used_gas, 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!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - expected_fee + ); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + swap_ctx.checkpoint(); + }); +} + +/// This test verifies that the swap precompile call is unable to swap the whole account balance. +/// This is not so much the desired behaviour, but rather an undesired effect of the current +/// implementation that is nonetheless specified and verified with tests. +/// The precense of this test ensures that when/if this behaviour is fixed, this test will start +/// failing and will have to be replaced with another test that verified the new behaviour. +/// See also: [`swap_works_almost_full_balance`]. +#[test] +fn swap_fail_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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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(), + expected_gas_usage, // the exact amount of fee we'll be using + Some(*GAS_PRICE), + Some(*GAS_PRICE), + None, + Vec::new(), + true, + true, + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + config, + ) + .unwrap(); + assert_eq!( + execinfo.exit_reason, + fp_evm::ExitReason::Error(fp_evm::ExitError::Other("invalid function selector".into())) + ); + assert_eq!(execinfo.used_gas, expected_gas_usage.into()); + assert_eq!(execinfo.value, Vec::::new()); + assert_eq!(execinfo.logs, Vec::new()); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - expected_fee + ); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + 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, expected_gas_usage.into()); + assert_eq!(execinfo.value, Vec::::new()); + assert_eq!(execinfo.logs, Vec::new()); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - expected_fee + ); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + 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, expected_gas_usage.into()); + assert_eq!(execinfo.value, Vec::::new()); + assert_eq!(execinfo.logs, Vec::new()); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - expected_fee + ); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + 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!(EvmBalances::total_balance(&alice_evm), alice_evm_balance); + assert_eq!(Balances::total_balance(&alice), 0); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + 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, + 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, expected_gas_usage.into()); + assert_eq!(execinfo.value, Vec::::new()); + assert_eq!(execinfo.logs, Vec::new()); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice_evm), + alice_evm_balance - expected_fee + ); + assert_eq!(Balances::total_balance(&alice), 0); + + // Assert mock invocations. + swap_ctx.checkpoint(); + }); +}