From ee95f5a2d17edeb8f47a0b796a3363ccd3635d5f Mon Sep 17 00:00:00 2001 From: Shaun Wang Date: Wed, 30 Aug 2023 11:25:36 +1000 Subject: [PATCH] XVM error handling (#1011) * XVM error & revert handling. * Add integration tests. * Reentrance integration test checks 'ReentranceDenied' error. * Update EVM caller contract below ED failing test. * XVM precompile uses 'EvmDataWriter' to build output on revert * Fix XVM precompile tests. * fmt * Add docstring. * Integration tests: contract addr variables renaming. --- Cargo.lock | 5 + Cargo.toml | 1 + chain-extensions/types/xvm/src/lib.rs | 27 +- chain-extensions/xvm/src/lib.rs | 23 +- pallets/xvm/Cargo.toml | 5 +- pallets/xvm/src/lib.rs | 105 ++--- pallets/xvm/src/tests.rs | 30 +- precompiles/xvm/src/lib.rs | 31 +- precompiles/xvm/src/mock.rs | 24 +- precompiles/xvm/src/tests.rs | 14 +- primitives/src/xvm.rs | 92 ++++- tests/integration/Cargo.toml | 4 + .../ink-contracts/dummy_error.json | 391 ++++++++++++++++++ .../ink-contracts/dummy_error.wasm | Bin 0 -> 11361 bytes tests/integration/src/setup.rs | 7 + tests/integration/src/xvm.rs | 302 +++++++++++--- 16 files changed, 862 insertions(+), 199 deletions(-) create mode 100644 tests/integration/ink-contracts/dummy_error.json create mode 100644 tests/integration/ink-contracts/dummy_error.wasm diff --git a/Cargo.lock b/Cargo.lock index 7cbba0295f..28f586a9b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4465,6 +4465,8 @@ version = "0.1.0" dependencies = [ "astar-primitives", "astar-runtime", + "env_logger 0.10.0", + "fp-evm", "frame-support", "frame-system", "hex", @@ -4479,6 +4481,8 @@ dependencies = [ "pallet-proxy", "pallet-utility", "parity-scale-codec", + "precompile-utils", + "sha3", "shibuya-runtime", "shiden-runtime", "sp-core", @@ -8289,6 +8293,7 @@ dependencies = [ "log", "pallet-balances", "pallet-contracts", + "pallet-contracts-primitives", "pallet-evm", "pallet-insecure-randomness-collective-flip", "pallet-timestamp", diff --git a/Cargo.toml b/Cargo.toml index 6e3948a47f..fd6f3ee412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ array-bytes = "6.0.0" smallvec = "1.9.0" async-trait = "0.1.59" clap = { version = "4.2.5", features = ["derive"] } +env_logger = "0.10.0" futures = { version = "0.3.26" } serde = { version = "1.0.151", features = ["derive"] } serde_json = "1.0.92" diff --git a/chain-extensions/types/xvm/src/lib.rs b/chain-extensions/types/xvm/src/lib.rs index c42b71c4d5..9b6a3c9a4b 100644 --- a/chain-extensions/types/xvm/src/lib.rs +++ b/chain-extensions/types/xvm/src/lib.rs @@ -18,7 +18,10 @@ #![cfg_attr(not(feature = "std"), no_std)] -use astar_primitives::{xvm::CallError, Balance}; +use astar_primitives::{ + xvm::{FailureError, FailureReason, FailureRevert}, + Balance, +}; use parity_scale_codec::{Decode, Encode}; use sp_std::vec::Vec; @@ -31,18 +34,20 @@ pub enum XvmExecutionResult { Err(u32), } -impl From for XvmExecutionResult { - fn from(input: CallError) -> Self { - use CallError::*; - +impl From for XvmExecutionResult { + fn from(input: FailureReason) -> Self { // `0` is reserved for `Ok` let error_code = match input { - InvalidVmId => 1, - SameVmCallDenied => 2, - InvalidTarget => 3, - InputTooLarge => 4, - ReentranceDenied => 5, - ExecutionFailed(_) => 6, + // Revert failure: 1 - 127 + FailureReason::Revert(FailureRevert::InvalidTarget) => 1, + FailureReason::Revert(FailureRevert::InputTooLarge) => 2, + FailureReason::Revert(FailureRevert::VmRevert(_)) => 3, + + // Error failure: 128 - 255 + FailureReason::Error(FailureError::InvalidVmId) => 128, + FailureReason::Error(FailureError::SameVmCallDenied) => 129, + FailureReason::Error(FailureError::ReentranceDenied) => 130, + FailureReason::Error(FailureError::VmError(_)) => 131, }; Self::Err(error_code) } diff --git a/chain-extensions/xvm/src/lib.rs b/chain-extensions/xvm/src/lib.rs index 8b6c48b8f6..b5ff04a375 100644 --- a/chain-extensions/xvm/src/lib.rs +++ b/chain-extensions/xvm/src/lib.rs @@ -18,9 +18,14 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; +use alloc::format; + use astar_primitives::xvm::{Context, VmId, XvmCall}; use frame_support::dispatch::Encode; -use pallet_contracts::chain_extension::{ChainExtension, Environment, Ext, InitState, RetVal}; +use pallet_contracts::chain_extension::{ + ChainExtension, Environment, Ext, InitState, RetVal, ReturnFlags, +}; use sp_runtime::DispatchError; use sp_std::marker::PhantomData; use xvm_chain_extension_types::{XvmCallArgs, XvmExecutionResult}; @@ -92,9 +97,10 @@ where match TryInto::::try_into(vm_id) { Ok(id) => id, Err(err) => { - // TODO: Propagate error - let result = Into::::into(err); - return Ok(RetVal::Converging(result.into())); + return Ok(RetVal::Diverging { + flags: ReturnFlags::REVERT, + data: format!("{:?}", err).into(), + }); } } }; @@ -124,9 +130,12 @@ where "err: {:?}", err ); - // TODO Propagate error - let result = Into::::into(err.error); - Ok(RetVal::Converging(result.into())) + // `Diverging` is used instead of `Err` to make sure the control + // doesn't return to the caller. + Ok(RetVal::Diverging { + flags: ReturnFlags::REVERT, + data: format!("{:?}", err).into(), + }) } } } diff --git a/pallets/xvm/Cargo.toml b/pallets/xvm/Cargo.toml index d9f9bec944..0160aeb125 100644 --- a/pallets/xvm/Cargo.toml +++ b/pallets/xvm/Cargo.toml @@ -24,16 +24,17 @@ sp-std = { workspace = true } frame-benchmarking = { workspace = true, optional = true } # EVM +fp-evm = { workspace = true } pallet-evm = { workspace = true } # Substrate WASM VM support pallet-contracts = { workspace = true } +pallet-contracts-primitives = { workspace = true } # Astar astar-primitives = { workspace = true } [dev-dependencies] -fp-evm = { workspace = true } hex = { workspace = true } pallet-balances = { workspace = true, features = ["std"] } pallet-insecure-randomness-collective-flip = { workspace = true, features = ["std"] } @@ -46,9 +47,11 @@ std = [ "environmental/std", "log/std", "parity-scale-codec/std", + "fp-evm/std", "frame-support/std", "frame-system/std", "pallet-contracts/std", + "pallet-contracts-primitives/std", "pallet-evm/std", "pallet-insecure-randomness-collective-flip/std", "scale-info/std", diff --git a/pallets/xvm/src/lib.rs b/pallets/xvm/src/lib.rs index 0f15c8b7d8..6af17e3c7e 100644 --- a/pallets/xvm/src/lib.rs +++ b/pallets/xvm/src/lib.rs @@ -37,8 +37,13 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; +use alloc::format; + +use fp_evm::ExitReason; use frame_support::{ensure, traits::Currency, weights::Weight}; use pallet_contracts::{CollectEvents, DebugInfo, Determinism}; +use pallet_contracts_primitives::ReturnFlags; use pallet_evm::GasWeightMapping; use parity_scale_codec::Decode; use sp_core::{H160, U256}; @@ -49,7 +54,10 @@ use astar_primitives::{ ethereum_checked::{ AccountMapping, CheckedEthereumTransact, CheckedEthereumTx, EthereumTxInput, }, - xvm::{CallError, CallErrorWithWeight, CallInfo, CallResult, Context, VmId, XvmCall}, + xvm::{ + CallFailure, CallOutput, CallResult, Context, FailureError::*, FailureRevert::*, VmId, + XvmCall, + }, Balance, }; @@ -129,18 +137,12 @@ where ensure!( context.source_vm_id != vm_id, - CallErrorWithWeight { - error: CallError::SameVmCallDenied, - used_weight: overheads, - } + CallFailure::error(SameVmCallDenied, overheads) ); // Set `IN_XVM` to true & check reentrance. if IN_XVM.with(|in_xvm| in_xvm.replace(true)) { - return Err(CallErrorWithWeight { - error: CallError::ReentranceDenied, - used_weight: overheads, - }); + return Err(CallFailure::error(ReentranceDenied, overheads)); } let res = match vm_id { @@ -188,20 +190,12 @@ where ensure!( target.len() == H160::len_bytes(), - CallErrorWithWeight { - error: CallError::InvalidTarget, - used_weight: overheads, - } + CallFailure::revert(InvalidTarget, overheads) ); - let target_decoded = - Decode::decode(&mut target.as_ref()).map_err(|_| CallErrorWithWeight { - error: CallError::InvalidTarget, - used_weight: overheads, - })?; - let bounded_input = EthereumTxInput::try_from(input).map_err(|_| CallErrorWithWeight { - error: CallError::InputTooLarge, - used_weight: overheads, - })?; + let target_decoded = Decode::decode(&mut target.as_ref()) + .map_err(|_| CallFailure::revert(InvalidTarget, overheads))?; + let bounded_input = EthereumTxInput::try_from(input) + .map_err(|_| CallFailure::revert(InputTooLarge, overheads))?; let value_u256 = U256::from(value); // With overheads, less weight is available. @@ -220,10 +214,7 @@ where // Note the skip execution check should be exactly before `T::EthereumTransact::xvm_transact` // to benchmark the correct overheads. if skip_execution { - return Ok(CallInfo { - output: vec![], - used_weight: overheads, - }); + return Ok(CallOutput::new(vec![], overheads)); } let transact_result = T::EthereumTransact::xvm_transact(source, tx); @@ -232,28 +223,41 @@ where "EVM call result: {:?}", transact_result, ); - transact_result - .map(|(post_dispatch_info, call_info)| { + match transact_result { + Ok((post_dispatch_info, call_info)) => { let used_weight = post_dispatch_info .actual_weight .unwrap_or_default() .saturating_add(overheads); - CallInfo { - output: call_info.value, - used_weight, + match call_info.exit_reason { + ExitReason::Succeed(_) => Ok(CallOutput::new(call_info.value, used_weight)), + ExitReason::Revert(_) => { + // On revert, the `call_info.value` is the encoded error data. Refer to Contract + // ABI specification for details. https://docs.soliditylang.org/en/latest/abi-spec.html#errors + Err(CallFailure::revert(VmRevert(call_info.value), used_weight)) + } + ExitReason::Error(err) => Err(CallFailure::error( + VmError(format!("EVM call error: {:?}", err).into()), + used_weight, + )), + ExitReason::Fatal(err) => Err(CallFailure::error( + VmError(format!("EVM call error: {:?}", err).into()), + used_weight, + )), } - }) - .map_err(|e| { + } + Err(e) => { let used_weight = e .post_info .actual_weight .unwrap_or_default() .saturating_add(overheads); - CallErrorWithWeight { - error: CallError::ExecutionFailed(Into::<&str>::into(e.error).into()), + Err(CallFailure::error( + VmError(format!("EVM call error: {:?}", e.error).into()), used_weight, - } - }) + )) + } + } } fn wasm_call( @@ -272,10 +276,7 @@ where ); let dest = { - let error = CallErrorWithWeight { - error: CallError::InvalidTarget, - used_weight: overheads, - }; + let error = CallFailure::revert(InvalidTarget, overheads); let decoded = Decode::decode(&mut target.as_ref()).map_err(|_| error.clone())?; T::Lookup::lookup(decoded).map_err(|_| error) }?; @@ -286,10 +287,7 @@ where // Note the skip execution check should be exactly before `pallet_contracts::bare_call` // to benchmark the correct overheads. if skip_execution { - return Ok(CallInfo { - output: vec![], - used_weight: overheads, - }); + return Ok(CallOutput::new(vec![], overheads)); } let call_result = pallet_contracts::Pallet::::bare_call( @@ -307,14 +305,17 @@ where let used_weight = call_result.gas_consumed.saturating_add(overheads); match call_result.result { - Ok(success) => Ok(CallInfo { - output: success.data, - used_weight, - }), - Err(error) => Err(CallErrorWithWeight { - error: CallError::ExecutionFailed(Into::<&str>::into(error).into()), + Ok(val) => { + if val.flags.contains(ReturnFlags::REVERT) { + Err(CallFailure::revert(VmRevert(val.data), used_weight)) + } else { + Ok(CallOutput::new(val.data, used_weight)) + } + } + Err(error) => Err(CallFailure::error( + VmError(format!("WASM call error: {:?}", error).into()), used_weight, - }), + )), } } diff --git a/pallets/xvm/src/tests.rs b/pallets/xvm/src/tests.rs index ba7b32b585..d208e0ad98 100644 --- a/pallets/xvm/src/tests.rs +++ b/pallets/xvm/src/tests.rs @@ -48,10 +48,7 @@ fn calling_into_same_vm_is_not_allowed() { input.clone(), value ), - CallErrorWithWeight { - error: CallError::SameVmCallDenied, - used_weight: evm_used_weight - }, + CallFailure::error(SameVmCallDenied, evm_used_weight,), ); // Calling WASM from WASM @@ -65,10 +62,7 @@ fn calling_into_same_vm_is_not_allowed() { weights::SubstrateWeight::::wasm_call_overheads(); assert_noop!( Xvm::call(wasm_context, wasm_vm_id, ALICE, wasm_target, input, value), - CallErrorWithWeight { - error: CallError::SameVmCallDenied, - used_weight: wasm_used_weight - }, + CallFailure::error(SameVmCallDenied, wasm_used_weight,), ); }); } @@ -94,18 +88,12 @@ fn evm_call_fails_if_target_not_h160() { input.clone(), value ), - CallErrorWithWeight { - error: CallError::InvalidTarget, - used_weight - }, + CallFailure::revert(InvalidTarget, used_weight,), ); assert_noop!( Xvm::call(context, vm_id, ALICE, vec![1, 2, 3], input, value), - CallErrorWithWeight { - error: CallError::InvalidTarget, - used_weight - }, + CallFailure::revert(InvalidTarget, used_weight,), ); }); } @@ -131,10 +119,7 @@ fn evm_call_fails_if_input_too_large() { vec![1; 65_537], value ), - CallErrorWithWeight { - error: CallError::InputTooLarge, - used_weight - }, + CallFailure::revert(InputTooLarge, used_weight,), ); }); } @@ -193,10 +178,7 @@ fn wasm_call_fails_if_invalid_target() { assert_noop!( Xvm::call(context, vm_id, ALICE, target.encode(), input, value), - CallErrorWithWeight { - error: CallError::InvalidTarget, - used_weight - }, + CallFailure::revert(InvalidTarget, used_weight,), ); }); } diff --git a/precompiles/xvm/src/lib.rs b/precompiles/xvm/src/lib.rs index 6bb033dc1b..74a7893ac3 100644 --- a/precompiles/xvm/src/lib.rs +++ b/precompiles/xvm/src/lib.rs @@ -18,14 +18,16 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate alloc; +use alloc::format; + use astar_primitives::{ - xvm::{Context, VmId, XvmCall}, + xvm::{Context, FailureReason, VmId, XvmCall}, Balance, }; use fp_evm::{PrecompileHandle, PrecompileOutput}; use frame_support::dispatch::Dispatchable; use pallet_evm::{AddressMapping, GasWeightMapping, Precompile}; -use sp_runtime::codec::Encode; use sp_std::{marker::PhantomData, prelude::*}; use precompile_utils::{ @@ -37,6 +39,9 @@ mod mock; #[cfg(test)] mod tests; +// The selector on EVM revert, calculated by: `Keccak256::digest(b"Error(string)")[..4]` +const EVM_ERROR_MSG_SELECTOR: [u8; 4] = [8, 195, 121, 160]; + #[precompile_utils::generate_function_selector] #[derive(Debug, PartialEq)] pub enum Action { @@ -130,12 +135,22 @@ where "failure: {:?}", failure ); - Ok(succeed( - EvmDataWriter::new() - .write(false) - .write(Bytes(failure.error.encode())) - .build(), - )) + // On `FailureReason::Error` cases, use `revert` instead of `error` to + // allow error details propagate to caller. EVM implementation always reverts, + // no matter which one is used. + let message = match failure.reason { + FailureReason::Revert(failure_revert) => { + format!("{:?}", failure_revert) + } + FailureReason::Error(failure_error) => { + format!("{:?}", failure_error) + } + }; + let data = + EvmDataWriter::new_with_selector(u32::from_be_bytes(EVM_ERROR_MSG_SELECTOR)) + .write(Bytes(message.into_bytes())) + .build(); + Err(revert(data)) } } } diff --git a/precompiles/xvm/src/mock.rs b/precompiles/xvm/src/mock.rs index 33fd5d1a78..8aa1a57dc1 100644 --- a/precompiles/xvm/src/mock.rs +++ b/precompiles/xvm/src/mock.rs @@ -40,7 +40,9 @@ use sp_runtime::{ }; use sp_std::cell::RefCell; -use astar_primitives::xvm::{CallError::*, CallErrorWithWeight, CallInfo, CallResult}; +use astar_primitives::xvm::{ + CallFailure, CallOutput, CallResult, FailureError::*, FailureRevert::*, +}; pub type AccountId = TestAccount; pub type Balance = u128; @@ -268,32 +270,20 @@ impl XvmCall for MockXvmWithArgsCheck { ) -> CallResult { ensure!( vm_id != VmId::Evm, - CallErrorWithWeight { - error: SameVmCallDenied, - used_weight: Weight::zero() - } + CallFailure::error(SameVmCallDenied, Weight::zero()) ); ensure!( target.len() == 20, - CallErrorWithWeight { - error: InvalidTarget, - used_weight: Weight::zero() - } + CallFailure::revert(InvalidTarget, Weight::zero()), ); ensure!( input.len() <= 1024, - CallErrorWithWeight { - error: InputTooLarge, - used_weight: Weight::zero() - } + CallFailure::revert(InputTooLarge, Weight::zero()), ); WeightLimitCalledWith::set(context.weight_limit); - Ok(CallInfo { - output: vec![], - used_weight: Weight::zero(), - }) + Ok(CallOutput::new(vec![], Weight::zero())) } } diff --git a/precompiles/xvm/src/tests.rs b/precompiles/xvm/src/tests.rs index 37bbc56cf1..c51f80de3d 100644 --- a/precompiles/xvm/src/tests.rs +++ b/precompiles/xvm/src/tests.rs @@ -19,8 +19,6 @@ use crate::mock::*; use crate::*; -use astar_primitives::xvm::CallError; -use parity_scale_codec::Encode; use precompile_utils::testing::*; use precompile_utils::EvmDataWriter; use sp_core::U256; @@ -69,17 +67,15 @@ fn correct_arguments_works() { EvmDataWriter::new_with_selector(Action::XvmCall) .write(0x1Fu8) .write(Bytes(b"".to_vec())) - .write(Bytes(b"".to_vec())) + .write( + hex::decode("0000000000000000000000000000000000000000") + .expect("invalid hex"), + ) .write(U256::one()) .build(), ) .expect_no_logs() - .execute_returns( - EvmDataWriter::new() - .write(false) // the XVM call should succeed but the internal should fail - .write(Bytes(CallError::InvalidTarget.encode())) - .build(), - ); + .execute_some(); }) } diff --git a/primitives/src/xvm.rs b/primitives/src/xvm.rs index 4b08aa6967..1ef4ee50ad 100644 --- a/primitives/src/xvm.rs +++ b/primitives/src/xvm.rs @@ -33,7 +33,7 @@ pub enum VmId { } impl TryFrom for VmId { - type Error = CallError; + type Error = FailureReason; fn try_from(value: u8) -> Result { if value == VmId::Evm as u8 { @@ -41,48 +41,100 @@ impl TryFrom for VmId { } else if value == VmId::Wasm as u8 { Ok(VmId::Wasm) } else { - Err(CallError::InvalidVmId) + Err(FailureReason::Error(FailureError::InvalidVmId)) } } } /// XVM call info on success. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] -pub struct CallInfo { +pub struct CallOutput { /// Output of the call. pub output: Vec, /// Actual used weight. pub used_weight: Weight, } -/// XVM call error on failure. +impl CallOutput { + /// Create a new `CallOutput`. + pub fn new(output: Vec, used_weight: Weight) -> Self { + Self { + output, + used_weight, + } + } +} + +/// XVM call failure. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] -pub enum CallError { - /// Invalid VM id. - InvalidVmId, - /// Calling the contracts in the same VM is not allowed. - SameVmCallDenied, +pub struct CallFailure { + /// Failure reason. + pub reason: FailureReason, + /// Actual used weight. + pub used_weight: Weight, +} + +impl CallFailure { + /// Create a new `CallFailure` on revert. + pub fn revert(details: FailureRevert, used_weight: Weight) -> Self { + Self { + reason: FailureReason::Revert(details), + used_weight, + } + } + + /// Create a new `CallFailure` on error. + pub fn error(details: FailureError, used_weight: Weight) -> Self { + Self { + reason: FailureReason::Error(details), + used_weight, + } + } +} + +/// Failure reason of XVM calls. +/// +/// `Error` vs `Revert`: +/// - `Error` is for execution failed and the VM must stop. It maps to EVM +/// `ExitError/ExistFatal` and WASM `DispatchError`. +/// - `Revert` is for execution succeeded but the callee explicitly asked to +/// revert. It maps to EVM `ExitRevert` and WASM `REVERT` flag. It also includes +/// the case that wrong input was passed to XVM call, for instance invalid target, +/// as from VM/WASM perspective, it's an input guard condition failure. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum FailureReason { + /// XVM call failed with explicit revert. + Revert(FailureRevert), + /// XVM call failed with error. + Error(FailureError), +} + +/// Failure reason on revert. +#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum FailureRevert { /// Target contract address is invalid. InvalidTarget, /// Input is too large. InputTooLarge, - /// Reentrance is not allowed. - ReentranceDenied, - /// The call failed on EVM or WASM execution. - ExecutionFailed(Vec), + /// VM execution exit with revert. + VmRevert(Vec), } -/// XVM call error with used weight info. +/// Failure reason on error. #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] -pub struct CallErrorWithWeight { - /// Error info. - pub error: CallError, - /// Actual used weight. - pub used_weight: Weight, +pub enum FailureError { + /// Invalid VM id. + InvalidVmId, + /// Calling the contracts in the same VM is not allowed. + SameVmCallDenied, + /// Reentrance is not allowed. + ReentranceDenied, + /// The call failed with error on EVM or WASM execution. + VmError(Vec), } /// XVM call result. -pub type CallResult = Result; +pub type CallResult = Result; /// XVM context. /// diff --git a/tests/integration/Cargo.toml b/tests/integration/Cargo.toml index 834f6dd902..bacba8f98c 100644 --- a/tests/integration/Cargo.toml +++ b/tests/integration/Cargo.toml @@ -8,10 +8,13 @@ homepage.workspace = true repository.workspace = true [dependencies] +env_logger = { workspace = true } hex = { workspace = true } parity-scale-codec = { workspace = true } +sha3 = { workspace = true } # frontier +fp-evm = { workspace = true } pallet-evm = { workspace = true } # frame dependencies @@ -31,6 +34,7 @@ sp-runtime = { workspace = true } # astar dependencies pallet-ethereum-checked = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } +precompile-utils = { workspace = true } astar-primitives = { workspace = true } astar-runtime = { workspace = true, features = ["std"], optional = true } diff --git a/tests/integration/ink-contracts/dummy_error.json b/tests/integration/ink-contracts/dummy_error.json new file mode 100644 index 0000000000..45b7aad6b0 --- /dev/null +++ b/tests/integration/ink-contracts/dummy_error.json @@ -0,0 +1,391 @@ +{ + "source": { + "hash": "0x5f210bc642632bb2a7728b9ef6ce2883f288525db397fda7e2a6e5746fbd2410", + "language": "ink! 4.3.0", + "compiler": "rustc 1.68.0-nightly", + "build_info": { + "build_mode": "Debug", + "cargo_contract_version": "3.0.1", + "rust_toolchain": "nightly-aarch64-apple-darwin", + "wasm_opt_settings": { + "keep_debug_symbols": false, + "optimization_passes": "Z" + } + } + }, + "contract": { + "name": "dummy_error", + "version": "0.1.0", + "authors": [ + "[your_name] <[your_email]>" + ] + }, + "spec": { + "constructors": [ + { + "args": [], + "default": false, + "docs": [], + "label": "new", + "payable": false, + "returnType": { + "displayName": [ + "ink_primitives", + "ConstructorResult" + ], + "type": 0 + }, + "selector": "0x9bae9d5e" + } + ], + "docs": [], + "environment": { + "accountId": { + "displayName": [ + "AccountId" + ], + "type": 6 + }, + "balance": { + "displayName": [ + "Balance" + ], + "type": 9 + }, + "blockNumber": { + "displayName": [ + "BlockNumber" + ], + "type": 12 + }, + "chainExtension": { + "displayName": [ + "ChainExtension" + ], + "type": 13 + }, + "hash": { + "displayName": [ + "Hash" + ], + "type": 10 + }, + "maxEventTopics": 4, + "timestamp": { + "displayName": [ + "Timestamp" + ], + "type": 11 + } + }, + "events": [], + "lang_error": { + "displayName": [ + "ink", + "LangError" + ], + "type": 2 + }, + "messages": [ + { + "args": [], + "default": false, + "docs": [], + "label": "do_revert", + "mutates": false, + "payable": false, + "returnType": { + "displayName": [ + "ink", + "MessageResult" + ], + "type": 3 + }, + "selector": "0x0000002a" + } + ] + }, + "storage": { + "root": { + "layout": { + "struct": { + "fields": [], + "name": "DummyError" + } + }, + "root_key": "0x00000000" + } + }, + "types": [ + { + "id": 0, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 1 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 2 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 1 + }, + { + "name": "E", + "type": 2 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 1, + "type": { + "def": { + "tuple": [] + } + } + }, + { + "id": 2, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 1, + "name": "CouldNotReadInput" + } + ] + } + }, + "path": [ + "ink_primitives", + "LangError" + ] + } + }, + { + "id": 3, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 4 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 2 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 4 + }, + { + "name": "E", + "type": 2 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 4, + "type": { + "def": { + "variant": { + "variants": [ + { + "fields": [ + { + "type": 1 + } + ], + "index": 0, + "name": "Ok" + }, + { + "fields": [ + { + "type": 5 + } + ], + "index": 1, + "name": "Err" + } + ] + } + }, + "params": [ + { + "name": "T", + "type": 1 + }, + { + "name": "E", + "type": 5 + } + ], + "path": [ + "Result" + ] + } + }, + { + "id": 5, + "type": { + "def": { + "variant": { + "variants": [ + { + "index": 7, + "name": "DummyError" + } + ] + } + }, + "path": [ + "dummy_error", + "dummy_error", + "Error" + ] + } + }, + { + "id": 6, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 7, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "AccountId" + ] + } + }, + { + "id": 7, + "type": { + "def": { + "array": { + "len": 32, + "type": 8 + } + } + } + }, + { + "id": 8, + "type": { + "def": { + "primitive": "u8" + } + } + }, + { + "id": 9, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 10, + "type": { + "def": { + "composite": { + "fields": [ + { + "type": 7, + "typeName": "[u8; 32]" + } + ] + } + }, + "path": [ + "ink_primitives", + "types", + "Hash" + ] + } + }, + { + "id": 11, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 12, + "type": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 13, + "type": { + "def": { + "variant": {} + }, + "path": [ + "ink_env", + "types", + "NoChainExtension" + ] + } + } + ], + "version": "4" +} \ No newline at end of file diff --git a/tests/integration/ink-contracts/dummy_error.wasm b/tests/integration/ink-contracts/dummy_error.wasm new file mode 100644 index 0000000000000000000000000000000000000000..f72770149b96c0590dfea6eaf5112ee56c1c7133 GIT binary patch literal 11361 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 SJIKdh!)Ftp7Cyfc$o~P+g;X#A literal 0 HcmV?d00001 diff --git a/tests/integration/src/setup.rs b/tests/integration/src/setup.rs index 594ca745b0..e19400f15c 100644 --- a/tests/integration/src/setup.rs +++ b/tests/integration/src/setup.rs @@ -237,3 +237,10 @@ fn last_events(n: usize) -> Vec { pub fn expect_events(e: Vec) { assert_eq!(last_events(e.len()), e); } + +/// Initialize `env_logger` for tests. It will enable logging like `DEBUG` +/// and `TRACE` in runtime. +#[allow(dead_code)] +pub fn init_env_logger() { + let _ = env_logger::builder().is_test(true).try_init(); +} diff --git a/tests/integration/src/xvm.rs b/tests/integration/src/xvm.rs index fb3ba9b72b..4db3828cf4 100644 --- a/tests/integration/src/xvm.rs +++ b/tests/integration/src/xvm.rs @@ -18,11 +18,30 @@ use crate::setup::*; -use astar_primitives::xvm::{Context, VmId, XvmCall}; -use frame_support::{traits::Currency, weights::Weight}; +use sha3::{Digest, Keccak256}; + +use astar_primitives::{ + ethereum_checked::{CheckedEthereumTransact, CheckedEthereumTx, EthereumTxInput}, + xvm::{CallFailure, Context, FailureError, FailureReason, FailureRevert, VmId, XvmCall}, +}; +use fp_evm::{ExecutionInfoV2, ExitReason, ExitRevert}; +use frame_support::{dispatch::PostDispatchInfo, traits::Currency, weights::Weight}; +use pallet_contracts::{CollectEvents, DebugInfo, Determinism}; +use pallet_contracts_primitives::{ExecReturnValue, ReturnFlags}; use parity_scale_codec::Encode; +use precompile_utils::{Bytes, EvmDataWriter}; use sp_runtime::MultiAddress; +// Build EVM revert message error data. +fn evm_revert_message_error(msg: &str) -> Vec { + let hash = &Keccak256::digest(b"Error(string)")[..4]; + let selector = u32::from_be_bytes([hash[0], hash[1], hash[2], hash[3]]); + + EvmDataWriter::new_with_selector(selector) + .write(Bytes(msg.to_owned().into_bytes())) + .build() +} + /* pragma solidity >=0.8.2 <0.9.0; @@ -199,9 +218,9 @@ fn evm_payable_call_via_xvm_works() { #[test] fn wasm_payable_call_via_xvm_works() { new_test_ext().execute_with(|| { - let contract_addr = deploy_wasm_contract(WASM_PAYABLE_NAME); + let wasm_payable_addr = deploy_wasm_contract(WASM_PAYABLE_NAME); - let prev_balance = Balances::free_balance(&contract_addr); + let prev_balance = Balances::free_balance(&wasm_payable_addr); let value = UNIT; assert_ok!(Xvm::call( Context { @@ -210,13 +229,13 @@ fn wasm_payable_call_via_xvm_works() { }, VmId::Wasm, ALICE, - MultiAddress::::Id(contract_addr.clone()).encode(), + MultiAddress::::Id(wasm_payable_addr.clone()).encode(), // Calling `deposit` hex::decode("0000002a").expect("invalid selector hex"), value )); assert_eq!( - Balances::free_balance(contract_addr.clone()), + Balances::free_balance(wasm_payable_addr.clone()), value + prev_balance ); }); @@ -225,14 +244,14 @@ fn wasm_payable_call_via_xvm_works() { #[test] fn calling_wasm_payable_from_evm_fails_if_caller_contract_balance_below_ed() { new_test_ext().execute_with(|| { - let wasm_payable_addr = deploy_wasm_contract(WASM_PAYABLE_NAME); - let call_wasm_payable_addr = deploy_evm_contract(CALL_WASM_PAYBLE); + let _ = deploy_wasm_contract(WASM_PAYABLE_NAME); + let evm_caller_addr = deploy_evm_contract(CALL_WASM_PAYBLE); let value = 1_000_000_000; assert_ok!(EVM::call( RuntimeOrigin::root(), alith(), - call_wasm_payable_addr.clone(), + evm_caller_addr.clone(), // to: 0x00a8f69d59df362b69a8d4acdb9001eb3e1b8d067b8fdaa70081aed945bde5c48c // input: 0x0000002a (deposit) // value: 1000000000 @@ -245,18 +264,16 @@ fn calling_wasm_payable_from_evm_fails_if_caller_contract_balance_below_ed() { vec![], )); - // TODO: after XVM error propagation finished, assert `pallet-evm` execution error - // and update balance assertions. - - // Transfer to EVM contract ok. assert_eq!( - Balances::free_balance(&account_id_from(call_wasm_payable_addr)), - value, + System::events().iter().last().expect("no event found").event, + RuntimeEvent::EVM( + pallet_evm::Event::ExecutedFailed { address: evm_caller_addr }, + ), ); - // Transfer from EVM contract to wasm Contract err. + // EVM caller contract balance should be unchanged. assert_eq!( - Balances::free_balance(&wasm_payable_addr), - ExistentialDeposit::get(), + Balances::free_balance(&account_id_from(evm_caller_addr)), + 0, ); }); } @@ -264,17 +281,17 @@ fn calling_wasm_payable_from_evm_fails_if_caller_contract_balance_below_ed() { #[test] fn calling_wasm_payable_from_evm_works() { new_test_ext().execute_with(|| { - let wasm_payable_addr = deploy_wasm_contract(WASM_PAYABLE_NAME); - let call_wasm_payable_addr = deploy_evm_contract(CALL_WASM_PAYBLE); + let wasm_payable_callee_addr = deploy_wasm_contract(WASM_PAYABLE_NAME); + let evm_caller_addr = deploy_evm_contract(CALL_WASM_PAYBLE); - let _ = Balances::deposit_creating(&account_id_from(call_wasm_payable_addr.clone()), ExistentialDeposit::get()); + let _ = Balances::deposit_creating(&account_id_from(evm_caller_addr.clone()), ExistentialDeposit::get()); - let prev_wasm_payable_balance = Balances::free_balance(&wasm_payable_addr); + let prev_wasm_payable_balance = Balances::free_balance(&wasm_payable_callee_addr); let value = 1_000_000_000; assert_ok!(EVM::call( RuntimeOrigin::root(), alith(), - call_wasm_payable_addr.clone(), + evm_caller_addr.clone(), // to: 0x00a8f69d59df362b69a8d4acdb9001eb3e1b8d067b8fdaa70081aed945bde5c48c // input: 0x0000002a (deposit) // value: 1000000000 @@ -286,7 +303,7 @@ fn calling_wasm_payable_from_evm_works() { None, vec![], )); - let recieved = Balances::free_balance(&wasm_payable_addr) - prev_wasm_payable_balance; + let recieved = Balances::free_balance(&wasm_payable_callee_addr) - prev_wasm_payable_balance; assert_eq!(recieved, value); }); } @@ -294,18 +311,18 @@ fn calling_wasm_payable_from_evm_works() { #[test] fn calling_evm_payable_from_wasm_works() { new_test_ext().execute_with(|| { - let evm_payable_addr = deploy_evm_contract(EVM_PAYABLE); - let wasm_address = deploy_wasm_contract(CALL_EVM_PAYBLE_NAME); + let evm_payable_callee_addr = deploy_evm_contract(EVM_PAYABLE); + let wasm_caller_addr = deploy_wasm_contract(CALL_EVM_PAYBLE_NAME); let value = UNIT; // TODO: after Account Unification finished, remove this mock account. // It is needed now because currently the `AccountMapping` and `AddressMapping` are // both one way mapping. - let mock_unified_wasm_account = account_id_from(h160_from(wasm_address.clone())); + let mock_unified_wasm_account = account_id_from(h160_from(wasm_caller_addr.clone())); let _ = Balances::deposit_creating(&mock_unified_wasm_account, value); - let evm_payable = evm_payable_addr.as_ref().to_vec(); + let evm_payable = evm_payable_callee_addr.as_ref().to_vec(); let deposit_func = hex::decode("d0e30db0").expect("invalid deposit function hex"); let input = hex::decode("0000002a") .expect("invalid selector hex") @@ -316,7 +333,7 @@ fn calling_evm_payable_from_wasm_works() { .collect::>(); assert_ok!(Contracts::call( RuntimeOrigin::signed(ALICE), - MultiAddress::Id(wasm_address.clone()), + MultiAddress::Id(wasm_caller_addr.clone()), value, Weight::from_parts(10_000_000_000, 1024 * 1024), None, @@ -324,7 +341,7 @@ fn calling_evm_payable_from_wasm_works() { )); assert_eq!( - Balances::free_balance(account_id_from(evm_payable_addr)), + Balances::free_balance(account_id_from(evm_payable_callee_addr)), value ); @@ -339,8 +356,8 @@ fn calling_evm_payable_from_wasm_works() { fn reentrance_not_allowed() { new_test_ext().execute_with(|| { // Call path: WASM -> EVM -> WASM - let call_evm_payable_address = deploy_wasm_contract(CALL_EVM_PAYBLE_NAME); - let call_wasm_payable_addr = deploy_evm_contract(CALL_WASM_PAYBLE); + let wasm_caller_addr = deploy_wasm_contract(CALL_EVM_PAYBLE_NAME); + let evm_caller_addr = deploy_evm_contract(CALL_WASM_PAYBLE); let _ = deploy_wasm_contract(WASM_PAYABLE_NAME); // to: 0x00a8f69d59df362b69a8d4acdb9001eb3e1b8d067b8fdaa70081aed945bde5c48c @@ -350,29 +367,214 @@ fn reentrance_not_allowed() { let input = hex::decode("0000002a") .expect("invalid selector hex") .iter() - .chain(call_wasm_payable_addr.as_ref().to_vec().encode().iter()) + .chain(evm_caller_addr.as_ref().to_vec().encode().iter()) .chain(call_wasm_payable_input.encode().iter()) .cloned() .collect::>(); - assert_ok!( - Contracts::call( - RuntimeOrigin::signed(ALICE), - MultiAddress::Id(call_evm_payable_address.clone()), - 0, - Weight::from_parts(10_000_000_000, 1024 * 1024), - None, - input, - ) + + // assert `ReentranceDenied` error + let result = Contracts::bare_call( + ALICE, + wasm_caller_addr, + 0, + Weight::from_parts(10_000_000_000, 1024 * 1024), + None, + input, + DebugInfo::Skip, + CollectEvents::Skip, + Determinism::Enforced, ); + match result.result { + Ok(ExecReturnValue { flags, data }) => { + assert!(flags.contains(ReturnFlags::REVERT)); + + let reentrance_msg_error = evm_revert_message_error(&format!("{:?}", FailureError::ReentranceDenied)); + let error_string = String::from_utf8(data).expect("invalid utf8"); + assert!(error_string.contains(&format!("{:?}", reentrance_msg_error))); + } + _ => panic!("unexpected wasm call result"), + } + }); +} + +/* - // TODO: after XVM error propagation finished, replace with assert `ReentranceDenied` - // error. - let wasm_entrance_count = System::events().iter().filter(|record| { - match record.event { - RuntimeEvent::Contracts(pallet_contracts::Event::Called { .. }) => true, - _ => false, +pragma solidity >=0.8.2 <0.9.0; + +contract ShinyError { + error TooShiny(uint256 a, uint256 star); + + function revert_with_err_msg() public pure { + revert("too shiny"); + } + + function revert_with_err_type() public pure { + revert TooShiny(1, 2); + } +} + + */ +const EVM_DUMMY_ERROR: &'static str = "608060405234801561001057600080fd5b50610231806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806328fd58ae1461003b578063cb1c03b214610045575b600080fd5b61004361004f565b005b61004d61008a565b005b6040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161008190610128565b60405180910390fd5b600160026040517f2cdac97f0000000000000000000000000000000000000000000000000000000081526004016100c29291906101d2565b60405180910390fd5b600082825260208201905092915050565b7f746f6f207368696e790000000000000000000000000000000000000000000000600082015250565b60006101126009836100cb565b915061011d826100dc565b602082019050919050565b6000602082019050818103600083015261014181610105565b9050919050565b6000819050919050565b6000819050919050565b6000819050919050565b600061018161017c61017784610148565b61015c565b610152565b9050919050565b61019181610166565b82525050565b6000819050919050565b60006101bc6101b76101b284610197565b61015c565b610152565b9050919050565b6101cc816101a1565b82525050565b60006040820190506101e76000830185610188565b6101f460208301846101c3565b939250505056fea26469706673582212203b6d6f183650a1e330bb63d34c4d28865e8356715721534381292e37b07c8dd664736f6c63430008120033"; + +#[test] +fn evm_call_via_xvm_fails_if_revert() { + new_test_ext().execute_with(|| { + let evm_callee_addr = deploy_evm_contract(EVM_DUMMY_ERROR); + + let result = Xvm::call( + Context { + source_vm_id: VmId::Wasm, + weight_limit: Weight::from_parts(1_000_000_000, 1024 * 1024), + }, + VmId::Evm, + ALICE, + evm_callee_addr.as_ref().to_vec(), + // Calling `revert_with_err_msg` + hex::decode("28fd58ae").expect("invalid selector hex"), + 0, + ); + match result { + Err(CallFailure { + reason: FailureReason::Revert(FailureRevert::VmRevert(data)), + .. + }) => { + assert_eq!(data, evm_revert_message_error("too shiny")); } - }).count(); - assert_eq!(wasm_entrance_count, 1); + _ => panic!("unexpected evm call result: {:?}", result), + } + + let result1 = Xvm::call( + Context { + source_vm_id: VmId::Wasm, + weight_limit: Weight::from_parts(1_000_000_000, 1024 * 1024), + }, + VmId::Evm, + ALICE, + evm_callee_addr.as_ref().to_vec(), + // Calling `revert_with_err_type` + hex::decode("cb1c03b2").expect("invalid selector hex"), + 0, + ); + match result1 { + Err(CallFailure { + reason: FailureReason::Revert(FailureRevert::VmRevert(data)), + .. + }) => { + // data with error type `TooShiny(uint256,uint256)` on revert: selector(4) ++ payload(32) ++ paylaod(32) + let mut encoded = [0u8; 4 + 32 + 32]; + encoded[..4].copy_from_slice(&Keccak256::digest(b"TooShiny(uint256,uint256)")[..4]); + U256::from(1).to_big_endian(&mut encoded[4..36]); + U256::from(2).to_big_endian(&mut encoded[36..]); + assert_eq!(data, encoded); + } + _ => panic!("unexpected evm call result: {:?}", result1), + } + }); +} + +const WASM_DUMMY_ERROR_NAME: &'static str = "dummy_error"; + +#[test] +fn wasm_call_via_xvm_fails_if_revert() { + new_test_ext().execute_with(|| { + let wasm_callee_addr = deploy_wasm_contract(WASM_DUMMY_ERROR_NAME); + let input = hex::decode("0000002a").expect("invalid selector hex"); + let result = Xvm::call( + Context { + source_vm_id: VmId::Evm, + weight_limit: Weight::from_parts(10_000_000_000, 1024 * 1024), + }, + VmId::Wasm, + ALICE, + MultiAddress::::Id(wasm_callee_addr.clone()).encode(), + input, + 0, + ); + match result { + Err(CallFailure { + reason: FailureReason::Revert(FailureRevert::VmRevert(data)), + .. + }) => { + // `DummyError` error index is set `7` in wasm contract. + assert_eq!(data.last(), Some(&7u8)); + } + _ => panic!("unexpected wasm call result: {:?}", result), + } + }); +} + +#[test] +fn evm_caller_reverts_if_wasm_callee_reverted() { + new_test_ext().execute_with(|| { + let _ = deploy_wasm_contract(WASM_DUMMY_ERROR_NAME); + let evm_caller_addr = deploy_evm_contract(CALL_WASM_PAYBLE); + + // to: 0x00a0565d335eb7545deeb25563471219e6f0c9b9bb504a112a5f26fe61237c5a23 + // input: 0x0000002a (do_revert) + // value: 0 + let input = hex::decode("4012b914000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002100a0565d335eb7545deeb25563471219e6f0c9b9bb504a112a5f26fe61237c5a230000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000002a00000000000000000000000000000000000000000000000000000000").expect("invalid call input hex"); + let tx = CheckedEthereumTx { + target: evm_caller_addr.clone(), + input: EthereumTxInput::try_from(input).expect("input too large"), + value: U256::zero(), + gas_limit: U256::from(1_000_000), + maybe_access_list: None, + }; + + // Note `EVM::call` won't log details of the revert error, so we need to + // use `EthereumChecked` here for error checks. + match EthereumChecked::xvm_transact(alith(), tx) { + Ok((PostDispatchInfo { .. }, ExecutionInfoV2 { exit_reason, value, .. })) => { + assert_eq!(exit_reason, ExitReason::Revert(ExitRevert::Reverted)); + + // The last item `7` of `[0, 1, 7]` indicates the `DummyError` error index. + let revert_msg_error = evm_revert_message_error("FailureRevert::VmRevert([0, 1, 7])"); + assert_eq!(value, revert_msg_error); + }, + _ => panic!("unexpected evm call result"), + } + }); +} + +#[test] +fn wasm_caller_reverts_if_evm_callee_reverted() { + new_test_ext().execute_with(|| { + let evm_callee_addr = deploy_evm_contract(EVM_DUMMY_ERROR); + let wasm_caller_addr = deploy_wasm_contract(CALL_EVM_PAYBLE_NAME); + + // Calling `revert_with_err_msg` + let revert_func = hex::decode("28fd58ae").expect("invalid selector hex"); + let input = hex::decode("0000002a") + .expect("invalid selector hex") + .iter() + .chain(evm_callee_addr.as_ref().to_vec().encode().iter()) + .chain(revert_func.encode().iter()) + .cloned() + .collect::>(); + + // assert `too shiny` error + let result = Contracts::bare_call( + ALICE, + wasm_caller_addr, + 0, + Weight::from_parts(10_000_000_000, 1024 * 1024), + None, + input, + DebugInfo::Skip, + CollectEvents::Skip, + Determinism::Enforced, + ); + match result.result { + Ok(ExecReturnValue { flags, data }) => { + assert!(flags.contains(ReturnFlags::REVERT)); + + let revert_failure = FailureReason::Revert(FailureRevert::VmRevert( + evm_revert_message_error("too shiny"), + )); + let error_string = String::from_utf8(data).expect("invalid utf8"); + assert!(error_string.contains(&format!("{:?}", revert_failure))); + } + _ => panic!("unexpected wasm call result"), + } }); }