diff --git a/prdoc/pr_5664.prdoc b/prdoc/pr_5664.prdoc new file mode 100644 index 0000000000000000000000000000000000000000..7fbe15006da360aad837f958525feb628677d739 --- /dev/null +++ b/prdoc/pr_5664.prdoc @@ -0,0 +1,11 @@ +title: Calling an address without associated code is a balance transfer + +doc: + - audience: Runtime Dev + description: | + This makes pallet_revive behave like EVM where a balance transfer + is just a call to a plain wallet. + +crates: + - name: pallet-revive + bump: patch diff --git a/substrate/frame/revive/fixtures/contracts/call_return_code.rs b/substrate/frame/revive/fixtures/contracts/call_return_code.rs index 25370459acb4fc654f99ec82d561e1c6c8f39746..d0d7c1bee2a5f9a65a226dd76f767ac44b463b3d 100644 --- a/substrate/frame/revive/fixtures/contracts/call_return_code.rs +++ b/substrate/frame/revive/fixtures/contracts/call_return_code.rs @@ -34,6 +34,7 @@ pub extern "C" fn call() { input!( 100, callee_addr: &[u8; 20], + value: &[u8; 32], input: [u8], ); @@ -44,7 +45,7 @@ pub extern "C" fn call() { 0u64, // How much ref_time to devote for the execution. 0 = all. 0u64, // How much proof_size to devote for the execution. 0 = all. None, // No deposit limit. - &u256_bytes(100u64), // Value transferred to the contract. + value, // Value transferred to the contract. input, None, ) { diff --git a/substrate/frame/revive/src/exec.rs b/substrate/frame/revive/src/exec.rs index 468f5aa8240e8d796a85b71646878c23a7405e17..587d89b31984a49d14cf5b9303f112eb506a53b7 100644 --- a/substrate/frame/revive/src/exec.rs +++ b/substrate/frame/revive/src/exec.rs @@ -64,6 +64,8 @@ pub type ExecResult = Result<ExecReturnValue, ExecError>; /// Type for variable sized storage key. Used for transparent hashing. type VarSizedKey = BoundedVec<u8, ConstU32<{ limits::STORAGE_KEY_BYTES }>>; +const FRAME_ALWAYS_EXISTS_ON_INSTANTIATE: &str = "The return value is only `None` if no contract exists at the specified address. This cannot happen on instantiate or delegate; qed"; + /// Combined key type for both fixed and variable sized storage keys. pub enum Key { /// Variant for fixed sized keys. @@ -591,26 +593,6 @@ enum CachedContract<T: Config> { Terminated, } -impl<T: Config> CachedContract<T> { - /// Return `Some(ContractInfo)` if the contract is in cached state. `None` otherwise. - fn into_contract(self) -> Option<ContractInfo<T>> { - if let CachedContract::Cached(contract) = self { - Some(contract) - } else { - None - } - } - - /// Return `Some(&mut ContractInfo)` if the contract is in cached state. `None` otherwise. - fn as_contract(&mut self) -> Option<&mut ContractInfo<T>> { - if let CachedContract::Cached(contract) = self { - Some(contract) - } else { - None - } - } -} - impl<T: Config> Frame<T> { /// Return the `contract_info` of the current contract. fn contract_info(&mut self) -> &mut ContractInfo<T> { @@ -668,6 +650,24 @@ macro_rules! top_frame_mut { } impl<T: Config> CachedContract<T> { + /// Return `Some(ContractInfo)` if the contract is in cached state. `None` otherwise. + fn into_contract(self) -> Option<ContractInfo<T>> { + if let CachedContract::Cached(contract) = self { + Some(contract) + } else { + None + } + } + + /// Return `Some(&mut ContractInfo)` if the contract is in cached state. `None` otherwise. + fn as_contract(&mut self) -> Option<&mut ContractInfo<T>> { + if let CachedContract::Cached(contract) = self { + Some(contract) + } else { + None + } + } + /// Load the `contract_info` from storage if necessary. fn load(&mut self, account_id: &T::AccountId) { if let CachedContract::Invalidated = self { @@ -717,20 +717,20 @@ where value: BalanceOf<T>, input_data: Vec<u8>, debug_message: Option<&'a mut DebugBuffer>, - ) -> Result<ExecReturnValue, ExecError> { - let (mut stack, executable) = Self::new( - FrameArgs::Call { - dest: T::AddressMapper::to_account_id(&dest), - cached_info: None, - delegated_call: None, - }, - origin, + ) -> ExecResult { + let dest = T::AddressMapper::to_account_id(&dest); + if let Some((mut stack, executable)) = Self::new( + FrameArgs::Call { dest: dest.clone(), cached_info: None, delegated_call: None }, + origin.clone(), gas_meter, storage_meter, value, debug_message, - )?; - stack.run(executable, input_data) + )? { + stack.run(executable, input_data) + } else { + Self::transfer_no_contract(&origin, &dest, value) + } } /// Create and run a new call stack by instantiating a new contract. @@ -765,7 +765,8 @@ where storage_meter, value, debug_message, - )?; + )? + .expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE); let address = T::AddressMapper::to_address(&stack.top_frame().account_id); stack.run(executable, input_data).map(|ret| (address, ret)) } @@ -792,9 +793,13 @@ where debug_message, ) .unwrap() + .unwrap() } /// Create a new call stack. + /// + /// Returns `None` when calling a non existant contract. This is not an error case + /// since this will result in a value transfer. fn new( args: FrameArgs<T, E>, origin: Origin<T>, @@ -802,8 +807,8 @@ where storage_meter: &'a mut storage::meter::Meter<T>, value: BalanceOf<T>, debug_message: Option<&'a mut DebugBuffer>, - ) -> Result<(Self, E), ExecError> { - let (first_frame, executable) = Self::new_frame( + ) -> Result<Option<(Self, E)>, ExecError> { + let Some((first_frame, executable)) = Self::new_frame( args, value, gas_meter, @@ -811,7 +816,10 @@ where storage_meter, BalanceOf::<T>::zero(), false, - )?; + )? + else { + return Ok(None); + }; let stack = Self { origin, @@ -826,7 +834,7 @@ where _phantom: Default::default(), }; - Ok((stack, executable)) + Ok(Some((stack, executable))) } /// Construct a new frame. @@ -841,15 +849,20 @@ where storage_meter: &mut storage::meter::GenericMeter<T, S>, deposit_limit: BalanceOf<T>, read_only: bool, - ) -> Result<(Frame<T>, E), ExecError> { + ) -> Result<Option<(Frame<T>, E)>, ExecError> { let (account_id, contract_info, executable, delegate_caller, entry_point) = match frame_args { FrameArgs::Call { dest, cached_info, delegated_call } => { let contract = if let Some(contract) = cached_info { contract } else { - <ContractInfoOf<T>>::get(T::AddressMapper::to_address(&dest)) - .ok_or(<Error<T>>::ContractNotFound)? + if let Some(contract) = + <ContractInfoOf<T>>::get(T::AddressMapper::to_address(&dest)) + { + contract + } else { + return Ok(None) + } }; let (executable, delegate_caller) = @@ -896,7 +909,7 @@ where read_only, }; - Ok((frame, executable)) + Ok(Some((frame, executable))) } /// Create a subsequent nested frame. @@ -907,7 +920,7 @@ where gas_limit: Weight, deposit_limit: BalanceOf<T>, read_only: bool, - ) -> Result<E, ExecError> { + ) -> Result<Option<E>, ExecError> { if self.frames.len() as u32 == limits::CALL_STACK_DEPTH { return Err(Error::<T>::MaxCallDepthReached.into()); } @@ -929,7 +942,7 @@ where let frame = top_frame_mut!(self); let nested_gas = &mut frame.nested_gas; let nested_storage = &mut frame.nested_storage; - let (frame, executable) = Self::new_frame( + if let Some((frame, executable)) = Self::new_frame( frame_args, value_transferred, nested_gas, @@ -937,15 +950,18 @@ where nested_storage, deposit_limit, read_only, - )?; - self.frames.try_push(frame).map_err(|_| Error::<T>::MaxCallDepthReached)?; - Ok(executable) + )? { + self.frames.try_push(frame).map_err(|_| Error::<T>::MaxCallDepthReached)?; + Ok(Some(executable)) + } else { + Ok(None) + } } /// Run the current (top) frame. /// /// This can be either a call or an instantiate. - fn run(&mut self, executable: E, input_data: Vec<u8>) -> Result<ExecReturnValue, ExecError> { + fn run(&mut self, executable: E, input_data: Vec<u8>) -> ExecResult { let frame = self.top_frame(); let entry_point = frame.entry_point; let delegated_code_hash = @@ -954,13 +970,15 @@ where self.transient_storage.start_transaction(); let do_transaction = || { + let caller = self.caller(); + let frame = top_frame_mut!(self); + // We need to charge the storage deposit before the initial transfer so that // it can create the account in case the initial transfer is < ed. if entry_point == ExportedFunction::Constructor { // Root origin can't be used to instantiate a contract, so it is safe to assume that // if we reached this point the origin has an associated account. let origin = &self.origin.account_id()?; - let frame = top_frame_mut!(self); frame.nested_storage.charge_instantiate( origin, &frame.account_id, @@ -969,11 +987,15 @@ where )?; // Needs to be incremented before calling into the code so that it is visible // in case of recursion. - <System<T>>::inc_account_nonce(self.caller().account_id()?); + <System<T>>::inc_account_nonce(caller.account_id()?); } // Every non delegate call or instantiate also optionally transfers the balance. - self.initial_transfer()?; + // If it is a delegate call, then we've already transferred tokens in the + // last non-delegate frame. + if delegated_code_hash.is_none() { + Self::transfer_from_origin(&caller, &frame.account_id, frame.value_transferred)?; + } let contract_address = T::AddressMapper::to_address(&top_frame!(self).account_id); @@ -1166,40 +1188,40 @@ where } /// Transfer some funds from `from` to `to`. - fn transfer( - preservation: Preservation, - from: &T::AccountId, - to: &T::AccountId, - value: BalanceOf<T>, - ) -> DispatchResult { - if !value.is_zero() && from != to { - T::Currency::transfer(from, to, value, preservation) + fn transfer(from: &T::AccountId, to: &T::AccountId, value: BalanceOf<T>) -> DispatchResult { + // this avoids events to be emitted for zero balance transfers + if !value.is_zero() { + T::Currency::transfer(from, to, value, Preservation::Preserve) .map_err(|_| Error::<T>::TransferFailed)?; } Ok(()) } - // The transfer as performed by a call or instantiate. - fn initial_transfer(&self) -> DispatchResult { - let frame = self.top_frame(); - - // If it is a delegate call, then we've already transferred tokens in the - // last non-delegate frame. - if frame.delegate_caller.is_some() { - return Ok(()); - } - - let value = frame.value_transferred; - - // Get the account id from the caller. - // If the caller is root there is no account to transfer from, and therefore we can't take - // any `value` other than 0. - let caller = match self.caller() { + /// Same as `transfer` but `from` is an `Origin`. + fn transfer_from_origin( + from: &Origin<T>, + to: &T::AccountId, + value: BalanceOf<T>, + ) -> DispatchResult { + // If the from address is root there is no account to transfer from, and therefore we can't + // take any `value` other than 0. + let from = match from { Origin::Signed(caller) => caller, Origin::Root if value.is_zero() => return Ok(()), Origin::Root => return DispatchError::RootNotAllowed.into(), }; - Self::transfer(Preservation::Preserve, &caller, &frame.account_id, value) + Self::transfer(from, to, value) + } + + /// Same as `transfer_from_origin` but creates an `ExecReturnValue` on success. + fn transfer_no_contract( + from: &Origin<T>, + to: &T::AccountId, + value: BalanceOf<T>, + ) -> ExecResult { + Self::transfer_from_origin(from, to, value) + .map(|_| ExecReturnValue::default()) + .map_err(Into::into) } /// Reference to the current (top) frame. @@ -1255,13 +1277,14 @@ where input_data: Vec<u8>, allows_reentry: bool, read_only: bool, - ) -> Result<ExecReturnValue, ExecError> { + ) -> ExecResult { // Before pushing the new frame: Protect the caller contract against reentrancy attacks. // It is important to do this before calling `allows_reentry` so that a direct recursion // is caught by it. self.top_frame_mut().allows_reentry = allows_reentry; let dest = T::AddressMapper::to_account_id(dest); + let value = value.try_into().map_err(|_| Error::<T>::BalanceConversionFailed)?; let try_call = || { if !self.allows_reentry(&dest) { @@ -1278,15 +1301,22 @@ where CachedContract::Cached(contract) => Some(contract.clone()), _ => None, }); - let executable = self.push_frame( - FrameArgs::Call { dest, cached_info, delegated_call: None }, - value.try_into().map_err(|_| Error::<T>::BalanceConversionFailed)?, + if let Some(executable) = self.push_frame( + FrameArgs::Call { dest: dest.clone(), cached_info, delegated_call: None }, + value, gas_limit, deposit_limit.try_into().map_err(|_| Error::<T>::BalanceConversionFailed)?, // Enable read-only access if requested; cannot disable it if already set. read_only || self.is_read_only(), - )?; - self.run(executable, input_data) + )? { + self.run(executable, input_data) + } else { + Self::transfer_no_contract( + &Origin::from_account_id(self.account_id().clone()), + &dest, + value, + ) + } }; // We need to make sure to reset `allows_reentry` even on failure. @@ -1319,7 +1349,7 @@ where BalanceOf::<T>::zero(), self.is_read_only(), )?; - self.run(executable, input_data) + self.run(executable.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE), input_data) } fn instantiate( @@ -1346,7 +1376,8 @@ where self.is_read_only(), )?; let address = T::AddressMapper::to_address(&self.top_frame().account_id); - self.run(executable, input_data).map(|ret| (address, ret)) + self.run(executable.expect(FRAME_ALWAYS_EXISTS_ON_INSTANTIATE), input_data) + .map(|ret| (address, ret)) } fn terminate(&mut self, beneficiary: &H160) -> DispatchResult { @@ -1379,7 +1410,6 @@ where fn transfer(&mut self, to: &H160, value: U256) -> DispatchResult { Self::transfer( - Preservation::Preserve, &self.top_frame().account_id, &T::AddressMapper::to_account_id(to), value.try_into().map_err(|_| Error::<T>::BalanceConversionFailed)?, @@ -1854,7 +1884,7 @@ mod tests { set_balance(&ALICE, 100); set_balance(&BOB, 0); - MockStack::transfer(Preservation::Preserve, &ALICE, &BOB, 55).unwrap(); + MockStack::transfer(&ALICE, &BOB, 55).unwrap(); assert_eq!(get_balance(&ALICE), 45); assert_eq!(get_balance(&BOB), 55); @@ -1974,7 +2004,7 @@ mod tests { ExtBuilder::default().build().execute_with(|| { set_balance(&origin, 0); - let result = MockStack::transfer(Preservation::Preserve, &origin, &dest, 100); + let result = MockStack::transfer(&origin, &dest, 100); assert_eq!(result, Err(Error::<Test>::TransferFailed.into())); assert_eq!(get_balance(&origin), 0); @@ -2848,15 +2878,35 @@ mod tests { } #[test] - fn recursive_call_during_constructor_fails() { + fn recursive_call_during_constructor_is_balance_transfer() { let code = MockLoader::insert(Constructor, |ctx, _| { let account_id = ctx.ext.account_id().clone(); let addr = <Test as Config>::AddressMapper::to_address(&account_id); + let balance = ctx.ext.balance(); - assert_matches!( - ctx.ext.call(Weight::zero(), U256::zero(), &addr, U256::zero(), vec![], - true, false), Err(ExecError{error, ..}) if error == <Error<Test>>::ContractNotFound.into() - ); + // Calling ourselves during the constructor will trigger a balance + // transfer since no contract exist yet. + assert_ok!(ctx.ext.call( + Weight::zero(), + U256::zero(), + &addr, + (balance - 1).into(), + vec![], + true, + false + )); + + // Should also work with call data set as it is ignored when no + // contract is deployed. + assert_ok!(ctx.ext.call( + Weight::zero(), + U256::zero(), + &addr, + 1u32.into(), + vec![1, 2, 3, 4], + true, + false + )); exec_success() }); @@ -2879,7 +2929,7 @@ mod tests { executable, &mut gas_meter, &mut storage_meter, - min_balance, + 10, vec![], Some(&[0; 32]), None, @@ -2888,6 +2938,51 @@ mod tests { }); } + #[test] + fn cannot_send_more_balance_than_available_to_self() { + let code_hash = MockLoader::insert(Call, |ctx, _| { + let account_id = ctx.ext.account_id().clone(); + let addr = <Test as Config>::AddressMapper::to_address(&account_id); + let balance = ctx.ext.balance(); + + assert_err!( + ctx.ext.call( + Weight::zero(), + U256::zero(), + &addr, + (balance + 1).into(), + vec![], + true, + false + ), + <Error<Test>>::TransferFailed + ); + exec_success() + }); + + ExtBuilder::default() + .with_code_hashes(MockLoader::code_hashes()) + .build() + .execute_with(|| { + let min_balance = <Test as Config>::Currency::minimum_balance(); + let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT); + set_balance(&ALICE, min_balance * 10); + place_contract(&BOB, code_hash); + let origin = Origin::from_account_id(ALICE); + let mut storage_meter = storage::meter::Meter::new(&origin, 0, 0).unwrap(); + MockStack::run_call( + origin, + BOB_ADDR, + &mut gas_meter, + &mut storage_meter, + 0, + vec![], + None, + ) + .unwrap(); + }); + } + #[test] fn printing_works() { let code_hash = MockLoader::insert(Call, |ctx, _| { diff --git a/substrate/frame/revive/src/primitives.rs b/substrate/frame/revive/src/primitives.rs index 1b48527d23d7fdf3080f16117203f8f17fa28636..67bc144c3dd233924928695afdbe5a5057ce46a9 100644 --- a/substrate/frame/revive/src/primitives.rs +++ b/substrate/frame/revive/src/primitives.rs @@ -106,7 +106,7 @@ pub enum ContractAccessError { } /// Output of a contract call or instantiation which ran to completion. -#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo)] +#[derive(Clone, PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, Default)] pub struct ExecReturnValue { /// Flags passed along by `seal_return`. Empty when `seal_return` was never called. pub flags: ReturnFlags, diff --git a/substrate/frame/revive/src/tests.rs b/substrate/frame/revive/src/tests.rs index 73914c9aae0730f62bd0c4c1368f2fba64cbcfa0..19d6eabd577bdd518f5a839702ec1b1185046f12 100644 --- a/substrate/frame/revive/src/tests.rs +++ b/substrate/frame/revive/src/tests.rs @@ -50,7 +50,6 @@ use codec::{Decode, Encode}; use frame_support::{ assert_err, assert_err_ignore_postinfo, assert_err_with_weight, assert_noop, assert_ok, derive_impl, - dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo}, pallet_prelude::EnsureOrigin, parameter_types, storage::child, @@ -159,6 +158,12 @@ pub mod test_utils { // Assert that contract code is stored, and get its size. PristineCode::<Test>::try_get(&code_hash).unwrap().len() } + pub fn u256_bytes(u: u64) -> [u8; 32] { + let mut buffer = [0u8; 32]; + let bytes = u.to_le_bytes(); + buffer[..8].copy_from_slice(&bytes); + buffer + } } mod builder { @@ -589,25 +594,15 @@ mod run_tests { use pretty_assertions::{assert_eq, assert_ne}; use sp_core::U256; - // Perform a call to a plain account. - // The actual transfer fails because we can only call contracts. - // Then we check that at least the base costs where charged (no runtime gas costs.) #[test] - fn calling_plain_account_fails() { + fn calling_plain_account_is_balance_transfer() { ExtBuilder::default().build().execute_with(|| { let _ = <Test as Config>::Currency::set_balance(&ALICE, 100_000_000); - let base_cost = <<Test as Config>::WeightInfo as WeightInfo>::call(); - - assert_eq!( - builder::call(BOB_ADDR).build(), - Err(DispatchErrorWithPostInfo { - error: Error::<Test>::ContractNotFound.into(), - post_info: PostDispatchInfo { - actual_weight: Some(base_cost), - pays_fee: Default::default(), - }, - }) - ); + assert!(!<ContractInfoOf<Test>>::contains_key(BOB_ADDR)); + assert_eq!(test_utils::get_balance(&BOB_CONTRACT_ID), 0); + let result = builder::bare_call(BOB_ADDR).value(42).build_and_unwrap_result(); + assert_eq!(test_utils::get_balance(&BOB_CONTRACT_ID), 42); + assert_eq!(result, Default::default()); }); } @@ -1472,6 +1467,8 @@ mod run_tests { #[test] fn call_return_code() { + use test_utils::u256_bytes; + let (caller_code, _caller_hash) = compile_module("call_return_code").unwrap(); let (callee_code, _callee_hash) = compile_module("ok_trap_revert").unwrap(); ExtBuilder::default().existential_deposit(50).build().execute_with(|| { @@ -1481,28 +1478,60 @@ mod run_tests { let bob = builder::bare_instantiate(Code::Upload(caller_code)) .value(min_balance * 100) - .data(vec![0]) .build_and_unwrap_contract(); - <Test as Config>::Currency::set_balance(&bob.account_id, min_balance); // Contract calls into Django which is no valid contract + // This will be a balance transfer into a new account + // with more than the contract has which will make the transfer fail + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&u256_bytes(min_balance * 200)) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Sending less than the minimum balance will also make the transfer fail let result = builder::bare_call(bob.addr) - .data(AsRef::<[u8]>::as_ref(&DJANGO).to_vec()) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&u256_bytes(42)) + .cloned() + .collect(), + ) .build_and_unwrap_result(); - assert_return_code!(result, RuntimeReturnCode::NotCallable); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Sending at least the minimum balance should result in success but + // no code called. + assert_eq!(test_utils::get_balance(Ð_DJANGO), 0); + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&u256_bytes(55)) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::Success); + assert_eq!(test_utils::get_balance(Ð_DJANGO), 55); let django = builder::bare_instantiate(Code::Upload(callee_code)) .origin(RuntimeOrigin::signed(CHARLIE)) .value(min_balance * 100) - .data(vec![0]) .build_and_unwrap_contract(); - <Test as Config>::Currency::set_balance(&django.account_id, min_balance); - // Contract has only the minimal balance so any transfer will fail. + // Sending more than the contract has will make the transfer fail. let result = builder::bare_call(bob.addr) .data( AsRef::<[u8]>::as_ref(&django.addr) .iter() + .chain(&u256_bytes(min_balance * 300)) .chain(&0u32.to_le_bytes()) .cloned() .collect(), @@ -1516,6 +1545,7 @@ mod run_tests { .data( AsRef::<[u8]>::as_ref(&django.addr) .iter() + .chain(&u256_bytes(5)) .chain(&1u32.to_le_bytes()) .cloned() .collect(), @@ -1528,6 +1558,7 @@ mod run_tests { .data( AsRef::<[u8]>::as_ref(&django.addr) .iter() + .chain(&u256_bytes(5)) .chain(&2u32.to_le_bytes()) .cloned() .collect(), diff --git a/substrate/frame/revive/src/wasm/runtime.rs b/substrate/frame/revive/src/wasm/runtime.rs index 528b0ababfa01f5e5ca28d2198ee182c031ed2fb..d9257d38b66ea864103df4643512f4843952d400 100644 --- a/substrate/frame/revive/src/wasm/runtime.rs +++ b/substrate/frame/revive/src/wasm/runtime.rs @@ -581,8 +581,7 @@ impl<'a, E: Ext, M: PolkaVmInstance<E::T>> Runtime<'a, E, M> { None => Some(Err(Error::<E::T>::InvalidCallFlags.into())), Some(flags) => Some(Ok(ExecReturnValue { flags, data })), }, - Err(TrapReason::Termination) => - Some(Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() })), + Err(TrapReason::Termination) => Some(Ok(Default::default())), Err(TrapReason::SupervisorError(error)) => Some(Err(error.into())), } }, diff --git a/substrate/frame/revive/uapi/src/flags.rs b/substrate/frame/revive/uapi/src/flags.rs index 17b91ce2b3be8225924dd6ddfe908ad9592a381a..763a89d6c030489b3867417357c8310cbfa686f3 100644 --- a/substrate/frame/revive/uapi/src/flags.rs +++ b/substrate/frame/revive/uapi/src/flags.rs @@ -20,6 +20,7 @@ use bitflags::bitflags; bitflags! { /// Flags used by a contract to customize exit behaviour. #[cfg_attr(feature = "scale", derive(codec::Encode, codec::Decode, scale_info::TypeInfo))] + #[derive(Default)] pub struct ReturnFlags: u32 { /// If this bit is set all changes made by the contract execution are rolled back. const REVERT = 0x0000_0001;