diff --git a/prdoc/pr_10554.prdoc b/prdoc/pr_10554.prdoc new file mode 100644 index 0000000000000..5298a312dd691 --- /dev/null +++ b/prdoc/pr_10554.prdoc @@ -0,0 +1,14 @@ +title: '[pallet-revive] add EVM gas call syscalls' +doc: +- audience: Runtime Dev + description: |- + This PR adds two new syscalls for calls accepting EVM gas instead of Weight and Deposit. + + This is an important change for the initial release as it will align PVM contracts closer to EVM (the problem can't be solved in the Solidity compiler). +crates: +- name: pallet-revive-fixtures + bump: minor +- name: pallet-revive + bump: minor +- name: pallet-revive-uapi + bump: minor diff --git a/substrate/frame/revive/fixtures/contracts/call_with_gas.rs b/substrate/frame/revive/fixtures/contracts/call_with_gas.rs new file mode 100644 index 0000000000000..10d918c9ed06d --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/call_with_gas.rs @@ -0,0 +1,42 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] +#![no_main] + +include!("../panic_handler.rs"); + +use uapi::{input, CallFlags, HostFn, HostFnImpl as api}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + 256, + callee_addr: &[u8; 20], + gas: u64, + ); + + let mut value = [0; 32]; + api::value_transferred(&mut value); + + api::call_evm(CallFlags::empty(), callee_addr, gas, &value, &[], None).unwrap(); +} diff --git a/substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs b/substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs new file mode 100644 index 0000000000000..cffc33efb6b99 --- /dev/null +++ b/substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs @@ -0,0 +1,52 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] +#![no_main] +include!("../panic_handler.rs"); + +use uapi::{input, CallFlags, HostFn, HostFnImpl as api, StorageFlags}; + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn deploy() {} + +#[no_mangle] +#[polkavm_derive::polkavm_export] +pub extern "C" fn call() { + input!( + address: &[u8; 20], + gas: u64, + ); + + let mut key = [0u8; 32]; + key[0] = 1u8; + + let mut value = [0u8; 32]; + let value = &mut &mut value[..]; + value[0] = 2u8; + + api::set_storage(StorageFlags::empty(), &key, value); + api::get_storage(StorageFlags::empty(), &key, value).unwrap(); + assert!(value[0] == 2u8); + + api::delegate_call_evm(CallFlags::empty(), address, gas, &[], None).unwrap(); + + api::get_storage(StorageFlags::empty(), &key, value).unwrap(); + assert!(value[0] == 1u8); +} + diff --git a/substrate/frame/revive/src/tests/pvm.rs b/substrate/frame/revive/src/tests/pvm.rs new file mode 100644 index 0000000000000..787a0824e0a81 --- /dev/null +++ b/substrate/frame/revive/src/tests/pvm.rs @@ -0,0 +1,5613 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! The pallet-revive PVM specific integration test suite. + +use super::{ + precompiles, + precompiles::{INoInfo, NoInfo}, +}; +use crate::{ + address::{create1, create2, AddressMapper}, + assert_refcount, assert_return_code, + evm::{fees::InfoT, CallTrace, CallTracer, CallType}, + exec::Key, + limits, + metering::TransactionLimits, + precompiles::alloy::sol_types::{ + sol_data::{Bool, FixedBytes}, + SolType, + }, + storage::{DeletionQueueManager, WriteOutcome}, + test_utils::{builder::Contract, WEIGHT_LIMIT}, + tests::{ + builder, initialize_block, test_utils::*, Balances, CodeHashLockupDepositPercent, + Contracts, DepositPerByte, DepositPerItem, ExtBuilder, InstantiateAccount, RuntimeCall, + RuntimeEvent, RuntimeOrigin, System, Test, UploadAccount, DEPOSIT_PER_BYTE, *, + }, + tracing::trace, + weights::WeightInfo, + AccountInfo, AccountInfoOf, BalanceWithDust, Code, Config, ContractInfo, DebugSettings, + DeletionQueueCounter, Error, ExecConfig, HoldReason, Origin, Pallet, StorageDeposit, +}; +use assert_matches::assert_matches; +use codec::Encode; +use frame_support::{ + assert_err, assert_err_ignore_postinfo, assert_noop, assert_ok, + storage::child, + traits::{ + fungible::{Balanced, BalancedHold, Inspect, Mutate}, + tokens::Preservation, + OnIdle, OnInitialize, + }, + weights::{Weight, WeightMeter}, +}; +use frame_system::{EventRecord, Phase}; +use pallet_revive_fixtures::compile_module; +use pallet_revive_uapi::{ReturnErrorCode as RuntimeReturnCode, ReturnFlags}; +use pretty_assertions::{assert_eq, assert_ne}; +use sp_core::U256; +use sp_io::hashing::blake2_256; +use sp_runtime::{ + testing::H256, AccountId32, BoundedVec, DispatchError, SaturatedConversion, TokenError, +}; + +#[test] +fn eth_call_transfer_with_dust_works() { + let (binary, _) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + ::FeeInfo::deposit_txfee(::Currency::issue(10_000_000_000)); + + let balance = + Pallet::::convert_native_to_evm(BalanceWithDust::new_unchecked::(100, 10)); + assert_ok!(builder::eth_call(addr) + .origin(Origin::EthTransaction(ALICE).into()) + .value(balance) + .build()); + + assert_eq!(Pallet::::evm_balance(&addr), balance); + }); +} + +#[test] +fn set_evm_balance_for_eoa_works() { + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let native_with_dust = BalanceWithDust::new_unchecked::(100, 10); + let evm_balance = Pallet::::convert_native_to_evm(native_with_dust); + let _ = Pallet::::set_evm_balance(&ALICE_ADDR, evm_balance); + + assert_eq!(Pallet::::evm_balance(&ALICE_ADDR), evm_balance); + }); +} + +#[test] +fn set_evm_balance_works() { + let (binary, _) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + let native_with_dust = BalanceWithDust::new_unchecked::(100, 10); + let evm_value = Pallet::::convert_native_to_evm(native_with_dust); + + assert_ok!(Pallet::::set_evm_balance(&addr, evm_value)); + + assert_eq!(Pallet::::evm_balance(&addr), evm_value); + }); +} + +#[test] +fn contract_call_transfer_with_dust_works() { + let (binary_caller, _code_hash_caller) = compile_module("call_with_value").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)) + .native_value(200) + .build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + let balance = + Pallet::::convert_native_to_evm(BalanceWithDust::new_unchecked::(100, 10)); + assert_ok!(builder::call(addr_caller).data((balance, addr_callee).encode()).build()); + + assert_eq!(Pallet::::evm_balance(&addr_callee), balance); + }); +} + +#[test] +fn deposit_limit_enforced_on_plain_transfer() { + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, 1_000_000); + + // sending balance to a new account should fail when the limit is lower than the ed + let result = builder::bare_call(CHARLIE_ADDR) + .native_value(1) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: Default::default(), + deposit_limit: 190, + }) + .build(); + assert_err!(result.result, >::StorageDepositLimitExhausted); + assert_eq!(result.storage_deposit, StorageDeposit::Charge(0)); + assert_eq!(get_balance(&CHARLIE), 0); + + // works when the account is prefunded + let result = builder::bare_call(BOB_ADDR) + .native_value(1) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: Default::default(), + deposit_limit: 0, + }) + .build(); + assert_ok!(result.result); + assert_eq!(result.storage_deposit, StorageDeposit::Charge(0)); + assert_eq!(get_balance(&BOB), 1_000_001); + + // also works allowing enough deposit + let result = builder::bare_call(CHARLIE_ADDR) + .native_value(1) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: Default::default(), + deposit_limit: 200, + }) + .build(); + assert_ok!(result.result); + assert_eq!(result.storage_deposit, StorageDeposit::Charge(200)); + assert_eq!(get_balance(&CHARLIE), 201); + }); +} + +#[test] +fn instantiate_and_call_and_deposit_event() { + let (binary, code_hash) = compile_module("event_and_return_on_deploy").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let value = 100; + + // We determine the storage deposit limit after uploading because it depends on ALICEs + // free balance which is changed by uploading a module. + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + binary, + deposit_limit::(), + )); + + // Drop previous events + initialize_block(2); + + // Check at the end to get hash on error easily + let Contract { addr, account_id } = builder::bare_instantiate(Code::Existing(code_hash)) + .native_value(value) + .build_and_unwrap_contract(); + assert!(AccountInfoOf::::contains_key(&addr)); + + let hold_balance = contract_base_deposit(&addr); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: account_id.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: account_id.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: value, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::ContractEmitted { + contract: addr, + data: vec![1, 2, 3, 4], + topics: vec![H256::repeat_byte(42)], + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Instantiated { + deployer: ALICE_ADDR, + contract: addr + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: ALICE, + dest: account_id.clone(), + transferred: hold_balance, + }), + topics: vec![], + }, + ] + ); + }); +} + +#[test] +fn create1_address_from_extrinsic() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + binary.clone(), + deposit_limit::(), + )); + + assert_eq!(System::account_nonce(&ALICE), 0); + System::inc_account_nonce(&ALICE); + + for nonce in 1..3 { + let Contract { addr, .. } = builder::bare_instantiate(Code::Existing(code_hash)) + .salt(None) + .build_and_unwrap_contract(); + assert!(AccountInfoOf::::contains_key(&addr)); + assert_eq!( + addr, + create1(&::AddressMapper::to_address(&ALICE), nonce - 1) + ); + } + assert_eq!(System::account_nonce(&ALICE), 3); + + for nonce in 3..6 { + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary.clone())) + .salt(None) + .build_and_unwrap_contract(); + assert!(AccountInfoOf::::contains_key(&addr)); + assert_eq!( + addr, + create1(&::AddressMapper::to_address(&ALICE), nonce - 1) + ); + } + assert_eq!(System::account_nonce(&ALICE), 6); + }); +} + +#[test] +fn deposit_event_max_value_limit() { + let (binary, _code_hash) = compile_module("event_size").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + // Create + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(30_000) + .build_and_unwrap_contract(); + + // Call contract with allowed event size. + assert_ok!(builder::call(addr) + .weight_limit(WEIGHT_LIMIT.set_ref_time(WEIGHT_LIMIT.ref_time() * 2)) // we are copying a huge buffer, + .data(limits::EVENT_BYTES.encode()) + .build()); + + // Call contract with too large a evene size + assert_err_ignore_postinfo!( + builder::call(addr) + .weight_limit(Weight::from_parts(u64::MAX, u64::MAX)) + .data((limits::EVENT_BYTES + 1).encode()) + .build(), + Error::::ValueTooLarge, + ); + }); +} + +// Fail out of fuel (ref_time weight) in the engine. +#[test] +fn run_out_of_fuel_engine() { + let (binary, _code_hash) = compile_module("run_out_of_gas").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(100 * min_balance) + .build_and_unwrap_contract(); + + // Call the contract with a fixed gas limit. It must run out of gas because it just + // loops forever. + assert_err_ignore_postinfo!( + builder::call(addr) + .weight_limit(Weight::from_parts(10_000_000_000, u64::MAX)) + .build(), + Error::::OutOfGas, + ); + }); +} + +// Fail out of fuel (ref_time weight) in the host. +#[test] +fn run_out_of_fuel_host() { + use crate::precompiles::Precompile; + use alloy_core::sol_types::SolInterface; + + let precompile_addr = H160(NoInfo::::MATCHER.base_address()); + let input = INoInfo::INoInfoCalls::consumeMaxGas(INoInfo::consumeMaxGasCall {}).abi_encode(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let result = builder::bare_call(precompile_addr).data(input).build().result; + assert_err!(result, >::OutOfGas); + }); +} + +#[test] +fn gas_syncs_work() { + let (code, _code_hash) = compile_module("gas_price_n").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let contract = builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let result = builder::bare_call(contract.addr).data(0u32.encode()).build(); + assert_ok!(result.result); + let engine_consumed_noop = result.weight_consumed.ref_time(); + + let result = builder::bare_call(contract.addr).data(1u32.encode()).build(); + assert_ok!(result.result); + let weight_consumed_once = result.weight_consumed.ref_time(); + let host_consumed_once = ::WeightInfo::seal_gas_price().ref_time(); + let engine_consumed_once = weight_consumed_once - host_consumed_once - engine_consumed_noop; + + let result = builder::bare_call(contract.addr).data(2u32.encode()).build(); + assert_ok!(result.result); + let weight_consumed_twice = result.weight_consumed.ref_time(); + let host_consumed_twice = host_consumed_once * 2; + let engine_consumed_twice = + weight_consumed_twice - host_consumed_twice - engine_consumed_noop; + + // Second contract just repeats first contract's instructions twice. + // If runtime syncs gas with the engine properly, this should pass. + assert_eq!(engine_consumed_twice, engine_consumed_once * 2); + }); +} + +/// Check that contracts with the same account id have different trie ids. +/// Check the `Nonce` storage item for more information. +#[test] +fn instantiate_unique_trie_id() { + let (binary, code_hash) = compile_module("self_destruct_by_precompile").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + binary.clone(), + deposit_limit::(), + ) + .unwrap(); + + // Instantiate the contract and store its trie id for later comparison. + let Contract { addr, .. } = + builder::bare_instantiate(Code::Existing(code_hash)).build_and_unwrap_contract(); + let trie_id = get_contract(&addr).trie_id; + + // Try to instantiate it again without termination should yield an error. + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).build(), + >::DuplicateContract, + ); + + // Terminate the contract. + assert_ok!(builder::call(addr).build()); + + // Re-Instantiate after termination. + Contracts::upload_code(RuntimeOrigin::signed(ALICE), binary, deposit_limit::()) + .unwrap(); + assert_ok!(builder::instantiate(code_hash).build()); + + // Trie ids shouldn't match or we might have a collision + assert_ne!(trie_id, get_contract(&addr).trie_id); + }); +} + +#[test] +fn storage_work() { + let (code, _code_hash) = compile_module("storage").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + builder::bare_call(addr).build_and_unwrap_result(); + }); +} + +#[cfg(not(feature = "runtime-benchmarks"))] +#[test] +fn storage_precompile_only_delegate_call() { + let (code, _code_hash) = compile_module("storage_precompile_only_delegate_call").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + let ret = builder::bare_call(addr).build_and_unwrap_result(); + assert!(ret.did_revert()); + }); +} + +#[test] +fn storage_max_value_limit() { + let (binary, _code_hash) = compile_module("storage_size").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + // Create + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(30_000) + .build_and_unwrap_contract(); + get_contract(&addr); + + // Call contract with allowed storage value. + assert_ok!(builder::call(addr) + .weight_limit(WEIGHT_LIMIT.set_ref_time(WEIGHT_LIMIT.ref_time() * 2)) // we are copying a huge buffer + .data(limits::STORAGE_BYTES.encode()) + .build()); + + // Call contract with too large a storage value. + assert_err_ignore_postinfo!( + builder::call(addr).data((limits::STORAGE_BYTES + 1).encode()).build(), + Error::::ValueTooLarge, + ); + }); +} + +#[test] +fn clear_storage_on_zero_value() { + let (code, _code_hash) = compile_module("clear_storage_on_zero_value").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + builder::bare_call(addr).build_and_unwrap_result(); + }); +} + +#[test] +fn transient_storage_work() { + let (code, _code_hash) = compile_module("transient_storage").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + builder::bare_call(addr).build_and_unwrap_result(); + }); +} + +#[test] +fn transient_storage_limit_in_call() { + let (binary_caller, _code_hash_caller) = + compile_module("create_transient_storage_and_call").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("set_transient_storage").unwrap(); + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + // Call contracts with storage values within the limit. + // Caller and Callee contracts each set a transient storage value of size 100. + assert_ok!(builder::call(addr_caller) + .data((100u32, 100u32, &addr_callee).encode()) + .build(),); + + // Call a contract with a storage value that is too large. + // Limit exceeded in the caller contract. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data((4u32 * 1024u32, 200u32, &addr_callee).encode()) + .build(), + >::OutOfTransientStorage, + ); + + // Call a contract with a storage value that is too large. + // Limit exceeded in the callee contract. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data((50u32, 4 * 1024u32, &addr_callee).encode()) + .build(), + >::ContractTrapped + ); + }); +} + +#[test] +fn deploy_and_call_other_contract() { + let (caller_binary, _caller_code_hash) = compile_module("caller_contract").unwrap(); + let (callee_binary, callee_code_hash) = compile_module("return_with_data").unwrap(); + let code_load_weight = crate::vm::code_load_weight(callee_binary.len() as u32); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + + // Create + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let Contract { addr: caller_addr, account_id: caller_account } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + let callee_addr = create2( + &caller_addr, + &callee_binary, + &[0, 1, 34, 51, 68, 85, 102, 119], // hard coded in binary + &[0u8; 32], + ); + let callee_account = ::AddressMapper::to_account_id(&callee_addr); + + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + callee_binary, + deposit_limit::(), + ) + .unwrap(); + + // Drop previous events + initialize_block(2); + + // Call BOB contract, which attempts to instantiate and call the callee contract and + // makes various assertions on the results from those calls. + assert_ok!(builder::call(caller_addr) + .data( + (callee_code_hash, code_load_weight.ref_time(), code_load_weight.proof_size()) + .encode() + ) + .build()); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: callee_account.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: callee_account.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: callee_account.clone(), + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: caller_account.clone(), + to: callee_account.clone(), + amount: 32768 // hardcoded in binary + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: caller_account.clone(), + to: callee_account.clone(), + amount: 32768, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: ALICE, + dest: callee_account.clone(), + transferred: 2156, + }), + topics: vec![], + }, + ] + ); + }); +} + +#[test] +fn delegate_call() { + let (caller_binary, _caller_code_hash) = compile_module("delegate_call").unwrap(); + let (callee_binary, _callee_code_hash) = compile_module("delegate_call_lib").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the 'caller' + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + // Instantiate the 'callee' + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((callee_addr, u64::MAX, u64::MAX).encode()) + .build()); + }); +} + +#[test] +fn delegate_call_non_existant_is_noop() { + let (caller_binary, _caller_code_hash) = compile_module("delegate_call_simple").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the 'caller' + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((BOB_ADDR, u64::MAX, u64::MAX).encode()) + .build()); + + assert_eq!(get_balance(&BOB_FALLBACK), 0); + }); +} + +#[test] +fn delegate_call_with_weight_limit() { + let (caller_binary, _caller_code_hash) = compile_module("delegate_call").unwrap(); + let (callee_binary, _callee_code_hash) = compile_module("delegate_call_lib").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the 'caller' + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + // Instantiate the 'callee' + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + // fails, not enough weight + assert_err!( + builder::bare_call(caller_addr) + .native_value(1337) + .data((callee_addr, 100u64, 100u64).encode()) + .build() + .result, + Error::::ContractTrapped, + ); + + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((callee_addr, 500_000_000u64, 100_000u64).encode()) + .build()); + }); +} + +#[test] +fn delegate_call_with_deposit_limit() { + let (caller_binary, _caller_code_hash) = compile_module("delegate_call_deposit_limit").unwrap(); + let (callee_binary, _callee_code_hash) = compile_module("delegate_call_lib").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the 'caller' + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + // Instantiate the 'callee' + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + // Delegate call will write 1 storage and deposit of 2 (1 item) + 32 (bytes) is required. + // + 32 + 16 for blake2_128concat + // Fails, not enough deposit + let ret = builder::bare_call(caller_addr) + .native_value(1337) + .data((callee_addr, 81u64).encode()) + .build_and_unwrap_result(); + assert_return_code!(ret, RuntimeReturnCode::OutOfResources); + + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((callee_addr, 82u64).encode()) + .build()); + }); +} + +#[test] +fn transfer_expendable_cannot_kill_account() { + let (binary, _code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the BOB contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(1_000) + .build_and_unwrap_contract(); + + // Check that the BOB contract has been instantiated. + get_contract(&addr); + + let account = ::AddressMapper::to_account_id(&addr); + let total_balance = ::Currency::total_balance(&account); + + assert_eq!( + get_balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account), + contract_base_deposit(&addr) + ); + + // Some or the total balance is held, so it can't be transferred. + assert_err!( + <::Currency as Mutate>::transfer( + &account, + &ALICE, + total_balance, + Preservation::Expendable, + ), + TokenError::FundsUnavailable, + ); + + assert_eq!(::Currency::total_balance(&account), total_balance); + }); +} + +#[test] +fn cannot_self_destruct_through_draining() { + let (binary, _code_hash) = compile_module("drain").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let value = 1_000; + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(value) + .build_and_unwrap_contract(); + let account = ::AddressMapper::to_account_id(&addr); + + // Check that the BOB contract has been instantiated. + get_contract(&addr); + + // Call BOB which makes it send all funds to the zero address + // The contract code asserts that the transfer fails with the correct error code + assert_ok!(builder::call(addr).build()); + + // Make sure the account wasn't remove by sending all free balance away. + assert_eq!( + ::Currency::total_balance(&account), + value + contract_base_deposit(&addr) + min_balance, + ); + }); +} + +#[test] +fn cannot_self_destruct_through_storage_refund_after_price_change() { + let (binary, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let contract = builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + let info_deposit = contract_base_deposit(&contract.addr); + + // Check that the contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&contract.addr).total_deposit(), info_deposit); + assert_eq!(get_contract(&contract.addr).extra_deposit(), 0); + assert_eq!( + ::Currency::total_balance(&contract.account_id), + info_deposit + min_balance + ); + + // Create 100 (16 + 32 bytes for key for blake128 concat) bytes of storage with a + // price of per byte and a single storage item of price 2 + assert_ok!(builder::call(contract.addr).data(100u32.to_le_bytes().to_vec()).build()); + assert_eq!(get_contract(&contract.addr).total_deposit(), info_deposit + 100 + 16 + 32 + 2); + + // Increase the byte price and trigger a refund. This should not have any influence + // because the removal is pro rata and exactly those 100 bytes should have been + // removed as we didn't delete the key. + DEPOSIT_PER_BYTE.with(|c| *c.borrow_mut() = 500); + assert_ok!(builder::call(contract.addr).data(0u32.to_le_bytes().to_vec()).build()); + + // Make sure the account wasn't removed by the refund + assert_eq!( + ::Currency::total_balance(&contract.account_id), + get_contract(&contract.addr).total_deposit() + min_balance, + ); + // + 1 because due to fixed point arithmetic we can sometimes refund + // one unit to little + assert_eq!(get_contract(&contract.addr).extra_deposit(), 16 + 32 + 2 + 1); + }); +} + +#[test] +fn can_self_destruct_while_live() { + let (binary, _code_hash) = compile_module("self_destruct_by_precompile").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the BOB contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + // Check that the BOB contract has been instantiated. + get_contract(&addr); + + // Call BOB with input that forces it to recurse and self-destruct. + // New behavior: termination while on the stack is allowed, so expect success. + assert_ok!(builder::call(addr).data(vec![0]).build()); + + assert!(get_contract_checked(&addr).is_none(), "contract should have been destroyed"); + }); +} + +#[test] +fn self_destruct_by_precompile_works() { + let (binary, code_hash) = compile_module("self_destruct_by_precompile").unwrap(); + ExtBuilder::default().existential_deposit(1_000).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&DJANGO_FALLBACK, 1_000_000); + let min_balance = Contracts::min_balance(); + + let initial_contract_balance = 100_000; + + // Instantiate the BOB contract. + let contract = builder::bare_instantiate(Code::Upload(binary)) + .native_value(initial_contract_balance) + .build_and_unwrap_contract(); + + let hold_balance = contract_base_deposit(&contract.addr); + let upload_deposit = get_code_deposit(&code_hash); + + // Check that the BOB contract has been instantiated. + let _ = get_contract(&contract.addr); + + // Drop all previous events + initialize_block(2); + + // Call BOB without input data which triggers termination. + assert_matches!(builder::call(contract.addr).build(), Ok(_)); + + // Check that the code is gone + assert!(PristineCode::::get(&code_hash).is_none()); + + // Check that account is gone + assert!(get_contract_checked(&contract.addr).is_none()); + assert_eq!(::Currency::total_balance(&contract.account_id), 0); + + // Check that the beneficiary (django) got remaining balance. + assert_eq!( + ::Currency::free_balance(DJANGO_FALLBACK), + 1_000_000 + initial_contract_balance + ); + + // Check that the Alice is missing Django's benefit. Within ALICE's total balance + // there's also the code upload deposit held. + assert_eq!( + ::Currency::total_balance(&ALICE), + 1_000_000 - initial_contract_balance, + ); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: contract.account_id.clone(), + to: DJANGO_FALLBACK, + amount: initial_contract_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferOnHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: contract.account_id.clone(), + dest: ALICE, + amount: hold_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::KilledAccount { + account: contract.account_id.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: contract.account_id.clone(), + to: ALICE, + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferOnHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::CodeUploadDepositReserve, + ), + source: Pallet::::account_id(), + dest: ALICE, + amount: upload_deposit, + }), + topics: vec![], + }, + ], + ); + }); +} + +#[test] +fn self_destruct_by_syscall_does_not_delete_code() { + // Test EIP-6780 behavior where self-destruct does not delete the code. + let (binary, code_hash) = compile_module("self_destruct_by_syscall").unwrap(); + ExtBuilder::default().existential_deposit(1_000).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&DJANGO_FALLBACK, 1_000_000); + let min_balance = Contracts::min_balance(); + + let initial_contract_balance = 100_000; + + // Instantiate the BOB contract. + let contract = builder::bare_instantiate(Code::Upload(binary)) + .native_value(initial_contract_balance) + .build_and_unwrap_contract(); + + let hold_balance = contract_base_deposit(&contract.addr); + + // Check that the BOB contract has been instantiated. + let _ = get_contract(&contract.addr); + + // Drop all previous events + initialize_block(2); + + let alice_balance_before_termination = ::Currency::total_balance(&ALICE); + + // Call BOB without input data which triggers termination. + assert_matches!(builder::call(contract.addr).build(), Ok(_)); + + // Check that the code still exists + assert!(PristineCode::::get(&code_hash).is_some()); + + // Check that account still exists + assert!(get_contract_checked(&contract.addr).is_some()); + assert_eq!( + ::Currency::total_balance(&contract.account_id), + min_balance + hold_balance + ); + + // Check that the beneficiary (django) got remaining balance. + assert_eq!( + ::Currency::free_balance(DJANGO_FALLBACK), + 1_000_000 + initial_contract_balance + ); + + // Check that the Alice did not get a refund. + assert_eq!( + ::Currency::total_balance(&ALICE), + alice_balance_before_termination + ); + + pretty_assertions::assert_eq!( + System::events(), + vec![EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: contract.account_id.clone(), + to: DJANGO_FALLBACK, + amount: initial_contract_balance, + }), + topics: vec![], + },], + ); + }); +} + +#[test] +fn self_destruct_by_syscall_works() { + let (factory_binary, factory_code_hash) = compile_module("self_destruct_factory").unwrap(); + let (selfdestruct_binary, selfdestruct_code_hash) = + compile_module("self_destruct_by_syscall").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, 1_000_000); + let min_balance = Contracts::min_balance(); + let initial_contract_balance = 100_000; + + // Upload both contracts + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(BOB), + selfdestruct_binary, + deposit_limit::(), + )); + + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(BOB), + factory_binary, + deposit_limit::(), + )); + + // Deploy factory + let factory = builder::bare_instantiate(Code::Existing(factory_code_hash)) + .origin(RuntimeOrigin::signed(BOB)) + .native_value(initial_contract_balance) + .build_and_unwrap_contract(); + + let mut input_data = Vec::new(); + input_data.extend_from_slice(selfdestruct_code_hash.as_bytes()); + + // Call factory + let result = builder::bare_call(factory.addr).data(input_data.clone()).build(); + assert!(result.result.is_ok()); + + let returned_data = result.result.unwrap().data; + assert!(returned_data.len() >= 20, "Returned data too short to contain address"); + let mut contract_addr_bytes = [0u8; 20]; + contract_addr_bytes.copy_from_slice(&returned_data[0..20]); + let contract_addr = H160::from(contract_addr_bytes); + + initialize_block(System::block_number() + 1); + + assert!(get_contract_checked(&contract_addr).is_none(), "Contract found"); + + // min balance is taken from origin to fund DJANGO_FALLBACK + assert_eq!( + ::Currency::total_balance(&DJANGO_FALLBACK), + initial_contract_balance + min_balance, + ); + assert_eq!(::Currency::total_balance(&ALICE), 1_000_000 - min_balance); + }); +} + +#[test] +fn can_self_destruct_in_constructor_by_syscall() { + let (binary, _) = compile_module("self_destructing_constructor_by_syscall").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Fail to instantiate the BOB because the constructor calls seal_terminate. + assert_ok!(builder::instantiate_with_code(binary).value(100_000).build(),); + }); +} + +#[test] +fn cannot_self_destruct_in_constructor_by_precompile() { + let (binary, _) = compile_module("self_destructing_constructor_by_precompile").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Fail to instantiate the BOB because the constructor calls seal_terminate. + // Error is ContractTrapped because precompile call fails. + assert_err_ignore_postinfo!( + builder::instantiate_with_code(binary).value(100_000).build(), + Error::::ContractTrapped, + ); + }); +} + +#[test] +fn crypto_hash_keccak_256() { + let (binary, _code_hash) = compile_module("crypto_hash_keccak_256").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the CRYPTO_HASH_KECCAK_256 contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + // Perform the call. + let input = b"_DEAD_BEEF"; + use sp_io::hashing::*; + // Wraps a hash function into a more dynamic form usable for testing. + macro_rules! dyn_hash_fn { + ($name:ident) => { + Box::new(|input| $name(input).as_ref().to_vec().into_boxed_slice()) + }; + } + // The hash function and its associated output byte lengths. + let hash_fn: Box Box<[u8]>> = dyn_hash_fn!(keccak_256); + let expected_size: usize = 32; + // Test the hash function for the input: "_DEAD_BEEF" + let result = builder::bare_call(addr).data(input.to_vec()).build_and_unwrap_result(); + assert!(!result.did_revert()); + let expected = hash_fn(input.as_ref()); + assert_eq!(&result.data[..expected_size], &*expected); + }) +} + +#[test] +fn transfer_return_code() { + let (binary, _code_hash) = compile_module("transfer_return_code").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let contract = builder::bare_instantiate(Code::Upload(binary)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + // Contract has only the minimal balance so any transfer will fail. + ::Currency::set_balance(&contract.account_id, min_balance); + let result = builder::bare_call(contract.addr).build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + }); +} + +#[test] +fn call_return_code() { + 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(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let _ = ::Currency::set_balance(&CHARLIE, 1000 * min_balance); + + let bob = builder::bare_instantiate(Code::Upload(caller_code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + // BOB cannot pay the ed which is needed to pull DJANGO into existence + // this does trap the caller instead of returning an error code + // reasoning is that this error state does not exist on eth where + // ed does not exist. We hide this fact from the contract. + let result = builder::bare_call(bob.addr) + .data((DJANGO_ADDR, u256_bytes(1)).encode()) + .origin(RuntimeOrigin::signed(BOB)) + .build(); + assert_err!(result.result, >::StorageDepositNotEnoughFunds); + + // 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 value = Pallet::::convert_native_to_evm(min_balance * 200); + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&value.to_little_endian()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Sending below the minimum balance should result in success. + // The ED is charged from the call origin. + let alice_before = get_balance(&ALICE_FALLBACK); + assert_eq!(get_balance(&DJANGO_FALLBACK), 0); + + let value = Pallet::::convert_native_to_evm(1u64); + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&DJANGO_ADDR) + .iter() + .chain(&value.to_little_endian()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::Success); + assert_eq!(get_balance(&DJANGO_FALLBACK), min_balance + 1); + assert_eq!(get_balance(&ALICE_FALLBACK), alice_before - min_balance); + + let django = builder::bare_instantiate(Code::Upload(callee_code)) + .origin(RuntimeOrigin::signed(CHARLIE)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + // Sending more than the contract has will make the transfer fail. + let value = Pallet::::convert_native_to_evm(min_balance * 300); + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&django.addr) + .iter() + .chain(&value.to_little_endian()) + .chain(&0u32.to_le_bytes()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Contract has enough balance but callee reverts because "1" is passed. + ::Currency::set_balance(&bob.account_id, min_balance + 1000); + let value = Pallet::::convert_native_to_evm(5u64); + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&django.addr) + .iter() + .chain(&value.to_little_endian()) + .chain(&1u32.to_le_bytes()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeReverted); + + // Contract has enough balance but callee traps because "2" is passed. + let result = builder::bare_call(bob.addr) + .data( + AsRef::<[u8]>::as_ref(&django.addr) + .iter() + .chain(&value.to_little_endian()) + .chain(&2u32.to_le_bytes()) + .cloned() + .collect(), + ) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeTrapped); + }); +} + +#[test] +fn instantiate_return_code() { + let (caller_code, _caller_hash) = compile_module("instantiate_return_code").unwrap(); + let (callee_code, callee_hash) = compile_module("ok_trap_revert").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let _ = ::Currency::set_balance(&CHARLIE, 1000 * min_balance); + let callee_hash = callee_hash.as_ref().to_vec(); + + assert_ok!(builder::instantiate_with_code(callee_code).value(min_balance * 100).build()); + + let contract = builder::bare_instantiate(Code::Upload(caller_code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + // bob cannot pay the ED to create the contract as he has no money + // this traps the caller rather than returning an error + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&0u32.to_le_bytes()).cloned().collect()) + .origin(RuntimeOrigin::signed(BOB)) + .build(); + assert_err!(result.result, >::StorageDepositNotEnoughFunds); + + // Contract has only the minimal balance so any transfer will fail. + ::Currency::set_balance(&contract.account_id, min_balance); + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&0u32.to_le_bytes()).cloned().collect()) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::TransferFailed); + + // Contract has enough balance but the passed code hash is invalid + ::Currency::set_balance(&contract.account_id, min_balance + 10_000); + let result = builder::bare_call(contract.addr).data(vec![0; 36]).build(); + assert_err!(result.result, >::CodeNotFound); + + // Contract has enough balance but callee reverts because "1" is passed. + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&1u32.to_le_bytes()).cloned().collect()) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeReverted); + + // Contract has enough balance but callee traps because "2" is passed. + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&2u32.to_le_bytes()).cloned().collect()) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::CalleeTrapped); + + // Contract instantiation succeeds + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&0u32.to_le_bytes()).cloned().collect()) + .build_and_unwrap_result(); + assert_return_code!(result, 0); + + // Contract instantiation fails because the same salt is being used again. + let result = builder::bare_call(contract.addr) + .data(callee_hash.iter().chain(&0u32.to_le_bytes()).cloned().collect()) + .build_and_unwrap_result(); + assert_return_code!(result, RuntimeReturnCode::DuplicateContractAddress); + }); +} + +#[test] +fn lazy_removal_works() { + let (code, _hash) = compile_module("self_destruct_by_precompile").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let contract = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&contract.addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract + assert_ok!(builder::call(contract.addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&contract.addr)); + + // But value should be still there as the lazy removal did not run, yet. + assert_matches!(child::get(trie, &[99]), Some(42)); + + // Run the lazy removal + Contracts::on_idle(System::block_number(), Weight::MAX); + + // Value should be gone now + assert_matches!(child::get::(trie, &[99]), None); + }); +} + +#[test] +fn lazy_batch_removal_works() { + let (code, _hash) = compile_module("self_destruct_by_precompile").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let mut tries: Vec = vec![]; + + for i in 0..3u8 { + let contract = builder::bare_instantiate(Code::Upload(code.clone())) + .native_value(min_balance * 100) + .salt(Some([i; 32])) + .build_and_unwrap_contract(); + + let info = get_contract(&contract.addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract. Contract info should be gone, but value should be still + // there as the lazy removal did not run, yet. + assert_ok!(builder::call(contract.addr).build()); + + assert!(!>::contains_key(&contract.addr)); + assert_matches!(child::get(trie, &[99]), Some(42)); + + tries.push(trie.clone()) + } + + // Run single lazy removal + Contracts::on_idle(System::block_number(), Weight::MAX); + + // The single lazy removal should have removed all queued tries + for trie in tries.iter() { + assert_matches!(child::get::(trie, &[99]), None); + } + }); +} + +#[test] +fn gas_left_api_works() { + let (code, _) = compile_module("gas_left").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract without hold + let received = builder::bare_call(addr).build_and_unwrap_result(); + assert_eq!(received.flags, ReturnFlags::empty()); + let gas_left = U256::from_little_endian(received.data.as_ref()); + let gas_left_max = ::FeeInfo::weight_to_fee(&WEIGHT_LIMIT) + 1_000_000; + assert!(gas_left > 0u32.into()); + assert!(gas_left < gas_left_max.into()); + + // Call the contract using the hold + let hold_initial = ::FeeInfo::weight_to_fee(&WEIGHT_LIMIT); + ::FeeInfo::deposit_txfee(::Currency::issue(hold_initial)); + let mut exec_config = ExecConfig::new_substrate_tx(); + exec_config.collect_deposit_from_hold = Some((0u32.into(), Default::default())); + let received = builder::bare_call(addr).exec_config(exec_config).build_and_unwrap_result(); + assert_eq!(received.flags, ReturnFlags::empty()); + let gas_left = U256::from_little_endian(received.data.as_ref()); + assert!(gas_left > 0u32.into()); + assert!(gas_left < hold_initial.into()); + }); +} + +#[test] +fn lazy_removal_partial_remove_works() { + let (code, _hash) = compile_module("self_destruct_by_precompile").unwrap(); + + // We create a contract with some extra keys above the weight limit + let extra_keys = 7u32; + let mut meter = WeightMeter::with_limit(Weight::from_parts(5_000_000_000, 100 * 1024)); + let (weight_per_key, max_keys) = ContractInfo::::deletion_budget(&meter); + let vals: Vec<_> = (0..max_keys + extra_keys) + .map(|i| (blake2_256(&i.encode()), (i as u32), (i as u32).encode())) + .collect(); + + let mut ext = ExtBuilder::default().existential_deposit(50).build(); + + let trie = ext.execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + + // Put value into the contracts child trie + for val in &vals { + info.write(&Key::Fix(val.0), Some(val.2.clone()), None, false).unwrap(); + } + AccountInfo::::insert_contract(&addr, info.clone()); + + // Terminate the contract + assert_ok!(builder::call(addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&addr)); + + let trie = info.child_trie_info(); + + // But value should be still there as the lazy removal did not run, yet. + for val in &vals { + assert_eq!(child::get::(&trie, &blake2_256(&val.0)), Some(val.1)); + } + + trie.clone() + }); + + // The lazy removal limit only applies to the backend but not to the overlay. + // This commits all keys from the overlay to the backend. + ext.commit_all().unwrap(); + + ext.execute_with(|| { + // Run the lazy removal + ContractInfo::::process_deletion_queue_batch(&mut meter); + + // Weight should be exhausted because we could not even delete all keys + assert!(!meter.can_consume(weight_per_key)); + + let mut num_deleted = 0u32; + let mut num_remaining = 0u32; + + for val in &vals { + match child::get::(&trie, &blake2_256(&val.0)) { + None => num_deleted += 1, + Some(x) if x == val.1 => num_remaining += 1, + Some(_) => panic!("Unexpected value in contract storage"), + } + } + + // All but one key is removed + assert_eq!(num_deleted + num_remaining, vals.len() as u32); + assert_eq!(num_deleted, max_keys); + assert_eq!(num_remaining, extra_keys); + }); +} + +#[test] +fn lazy_removal_does_no_run_on_low_remaining_weight() { + let (code, _hash) = compile_module("self_destruct_by_precompile").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract + assert_ok!(builder::call(addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&addr)); + + // But value should be still there as the lazy removal did not run, yet. + assert_matches!(child::get(trie, &[99]), Some(42)); + + // Assign a remaining weight which is too low for a successful deletion of the contract + let low_remaining_weight = + <::WeightInfo as WeightInfo>::on_process_deletion_queue_batch(); + + // Run the lazy removal + Contracts::on_idle(System::block_number(), low_remaining_weight); + + // Value should still be there, since remaining weight was too low for removal + assert_matches!(child::get::(trie, &[99]), Some(42)); + + // Run the lazy removal while deletion_queue is not full + Contracts::on_initialize(System::block_number()); + + // Value should still be there, since deletion_queue was not full + assert_matches!(child::get::(trie, &[99]), Some(42)); + + // Run on_idle with max remaining weight, this should remove the value + Contracts::on_idle(System::block_number(), Weight::MAX); + + // Value should be gone + assert_matches!(child::get::(trie, &[99]), None); + }); +} + +#[test] +fn lazy_removal_does_not_use_all_weight() { + let (code, _hash) = compile_module("self_destruct_by_precompile").unwrap(); + + let mut meter = WeightMeter::with_limit(Weight::from_parts(5_000_000_000, 100 * 1024)); + let mut ext = ExtBuilder::default().existential_deposit(50).build(); + + let (trie, vals, weight_per_key) = ext.execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + let (weight_per_key, max_keys) = ContractInfo::::deletion_budget(&meter); + assert!(max_keys > 0); + + // We create a contract with one less storage item than we can remove within the limit + let vals: Vec<_> = (0..max_keys - 1) + .map(|i| (blake2_256(&i.encode()), (i as u32), (i as u32).encode())) + .collect(); + + // Put value into the contracts child trie + for val in &vals { + info.write(&Key::Fix(val.0), Some(val.2.clone()), None, false).unwrap(); + } + AccountInfo::::insert_contract(&addr, info.clone()); + + // Terminate the contract + assert_ok!(builder::call(addr).build()); + + // Contract info should be gone + assert!(!>::contains_key(&addr)); + + let trie = info.child_trie_info(); + + // But value should be still there as the lazy removal did not run, yet. + for val in &vals { + assert_eq!(child::get::(&trie, &blake2_256(&val.0)), Some(val.1)); + } + + (trie, vals, weight_per_key) + }); + + // The lazy removal limit only applies to the backend but not to the overlay. + // This commits all keys from the overlay to the backend. + ext.commit_all().unwrap(); + + ext.execute_with(|| { + // Run the lazy removal + ContractInfo::::process_deletion_queue_batch(&mut meter); + let base_weight = + <::WeightInfo as WeightInfo>::on_process_deletion_queue_batch(); + assert_eq!(meter.consumed(), weight_per_key.mul(vals.len() as _) + base_weight); + + // All the keys are removed + for val in vals { + assert_eq!(child::get::(&trie, &blake2_256(&val.0)), None); + } + }); +} + +#[test] +fn deletion_queue_ring_buffer_overflow() { + let (code, _hash) = compile_module("self_destruct_by_precompile").unwrap(); + let mut ext = ExtBuilder::default().existential_deposit(50).build(); + + // setup the deletion queue with custom counters + ext.execute_with(|| { + let queue = DeletionQueueManager::from_test_values(u32::MAX - 1, u32::MAX - 1); + >::set(queue); + }); + + // commit the changes to the storage + ext.commit_all().unwrap(); + + ext.execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let mut tries: Vec = vec![]; + + // add 3 contracts to the deletion queue + for i in 0..3u8 { + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code.clone())) + .native_value(min_balance * 100) + .salt(Some([i; 32])) + .build_and_unwrap_contract(); + + let info = get_contract(&addr); + let trie = &info.child_trie_info(); + + // Put value into the contracts child trie + child::put(trie, &[99], &42); + + // Terminate the contract. Contract info should be gone, but value should be still + // there as the lazy removal did not run, yet. + assert_ok!(builder::call(addr).build()); + + assert!(!>::contains_key(&addr)); + assert_matches!(child::get(trie, &[99]), Some(42)); + + tries.push(trie.clone()) + } + + // Run single lazy removal + Contracts::on_idle(System::block_number(), Weight::MAX); + + // The single lazy removal should have removed all queued tries + for trie in tries.iter() { + assert_matches!(child::get::(trie, &[99]), None); + } + + // insert and delete counter values should go from u32::MAX - 1 to 1 + assert_eq!(>::get().as_test_tuple(), (1, 1)); + }) +} +#[test] +fn refcounter() { + let (binary, code_hash) = compile_module("self_destruct_by_precompile").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Create two contracts with the same code and check that they do in fact share it. + let Contract { addr: addr0, .. } = builder::bare_instantiate(Code::Upload(binary.clone())) + .native_value(min_balance * 100) + .salt(Some([0; 32])) + .build_and_unwrap_contract(); + let Contract { addr: addr1, .. } = builder::bare_instantiate(Code::Upload(binary.clone())) + .native_value(min_balance * 100) + .salt(Some([1; 32])) + .build_and_unwrap_contract(); + assert_refcount!(code_hash, 2); + + // Sharing should also work with the usual instantiate call + let Contract { addr: addr2, .. } = builder::bare_instantiate(Code::Existing(code_hash)) + .native_value(min_balance * 100) + .salt(Some([2; 32])) + .build_and_unwrap_contract(); + assert_refcount!(code_hash, 3); + + // Terminating one contract should decrement the refcount + assert_ok!(builder::call(addr0).build()); + assert_refcount!(code_hash, 2); + + // remove another one + assert_ok!(builder::call(addr1).build()); + assert_refcount!(code_hash, 1); + + // Pristine code should still be there + PristineCode::::get(code_hash).unwrap(); + + // remove the last contract + assert_ok!(builder::call(addr2).build()); + assert!(PristineCode::::get(&code_hash).is_none()); + }); +} + +#[test] +fn gas_estimation_for_subcalls() { + let (caller_code, _caller_hash) = compile_module("call_with_limit").unwrap(); + let (dummy_code, _callee_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 2_000 * min_balance); + + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(caller_code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + let Contract { addr: addr_dummy, .. } = builder::bare_instantiate(Code::Upload(dummy_code)) + .native_value(min_balance * 100) + .build_and_unwrap_contract(); + + // Run the test for all of those weight limits for the subcall + let weights = [ + Weight::MAX, + WEIGHT_LIMIT, + WEIGHT_LIMIT * 2, + WEIGHT_LIMIT / 5, + Weight::from_parts(u64::MAX, WEIGHT_LIMIT.proof_size()), + Weight::from_parts(WEIGHT_LIMIT.ref_time(), u64::MAX), + ]; + + let (sub_addr, sub_input) = (addr_dummy.as_ref(), vec![]); + + for weight in weights { + let input: Vec = sub_addr + .iter() + .cloned() + .chain(weight.ref_time().to_le_bytes()) + .chain(weight.proof_size().to_le_bytes()) + .chain(sub_input.clone()) + .collect(); + + // Call in order to determine the gas that is required for this call + let result_orig = builder::bare_call(addr_caller).data(input.clone()).build(); + assert_ok!(&result_orig.result); + assert_eq!(result_orig.weight_required, result_orig.weight_consumed); + + // Make the same call using the estimated gas. Should succeed. + let result = builder::bare_call(addr_caller) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: result_orig.weight_required, + deposit_limit: result_orig.storage_deposit.charge_or_zero().into(), + }) + .data(input.clone()) + .build(); + assert_ok!(&result.result); + + // Check that it fails with too little ref_time + let result = builder::bare_call(addr_caller) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: result_orig.weight_required.sub_ref_time(1), + deposit_limit: result_orig.storage_deposit.charge_or_zero().into(), + }) + .data(input.clone()) + .build(); + assert_err!(result.result, >::OutOfGas); + + // Check that it fails with too little proof_size + let result = builder::bare_call(addr_caller) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: result_orig.weight_required.sub_proof_size(1), + deposit_limit: result_orig.storage_deposit.charge_or_zero().into(), + }) + .data(input.clone()) + .build(); + assert_err!(result.result, >::OutOfGas); + } + }); +} + +#[test] +fn call_runtime_reentrancy_guarded() { + use crate::precompiles::Precompile; + use alloy_core::sol_types::SolInterface; + use precompiles::{INoInfo, NoInfo}; + + let precompile_addr = H160(NoInfo::::MATCHER.base_address()); + + let (callee_code, _callee_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1000 * min_balance); + let _ = ::Currency::set_balance(&CHARLIE, 1000 * min_balance); + + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(callee_code)) + .native_value(min_balance * 100) + .salt(Some([1; 32])) + .build_and_unwrap_contract(); + + // Call pallet_revive call() dispatchable + let call = RuntimeCall::Contracts(crate::Call::call { + dest: addr_callee, + value: 0, + weight_limit: WEIGHT_LIMIT / 3, + storage_deposit_limit: deposit_limit::(), + data: vec![], + }) + .encode(); + + // Call runtime to re-enter back to contracts engine by + // calling dummy contract + let result = builder::bare_call(precompile_addr) + .data( + INoInfo::INoInfoCalls::callRuntime(INoInfo::callRuntimeCall { call: call.into() }) + .abi_encode(), + ) + .build(); + // Call to runtime should fail because of the re-entrancy guard + assert_err!(result.result, >::ReenteredPallet); + }); +} + +#[test] +fn sr25519_verify() { + let (binary, _code_hash) = compile_module("sr25519_verify").unwrap(); + + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the sr25519_verify contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + let call_with = |message: &[u8; 11]| { + // Alice's signature for "hello world" + #[rustfmt::skip] + let signature: [u8; 64] = [ + 184, 49, 74, 238, 78, 165, 102, 252, 22, 92, 156, 176, 124, 118, 168, 116, 247, + 99, 0, 94, 2, 45, 9, 170, 73, 222, 182, 74, 60, 32, 75, 64, 98, 174, 69, 55, 83, + 85, 180, 98, 208, 75, 231, 57, 205, 62, 4, 105, 26, 136, 172, 17, 123, 99, 90, 255, + 228, 54, 115, 63, 30, 207, 205, 131, + ]; + + // Alice's public key + #[rustfmt::skip] + let public_key: [u8; 32] = [ + 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, + 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, + ]; + + let mut params = vec![]; + params.extend_from_slice(&signature); + params.extend_from_slice(&public_key); + params.extend_from_slice(message); + + builder::bare_call(addr).data(params).build_and_unwrap_result() + }; + + // verification should succeed for "hello world" + assert_return_code!(call_with(&b"hello world"), RuntimeReturnCode::Success); + + // verification should fail for other messages + assert_return_code!(call_with(&b"hello worlD"), RuntimeReturnCode::Sr25519VerifyFailed); + }); +} + +#[test] +fn upload_code_works() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert!(!PristineCode::::contains_key(&code_hash)); + assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), binary, 1_000,)); + // Ensure the contract was stored and get expected deposit amount to be reserved. + expected_deposit(ensure_stored(code_hash)); + }); +} + +#[test] +fn upload_code_limit_too_low() { + let (binary, _code_hash) = compile_module("dummy").unwrap(); + let deposit_expected = expected_deposit(binary.len()); + let deposit_insufficient = deposit_expected.saturating_sub(1); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::upload_code(RuntimeOrigin::signed(ALICE), binary, deposit_insufficient,), + >::StorageDepositLimitExhausted, + ); + + assert_eq!(System::events(), vec![]); + }); +} + +#[test] +fn upload_code_not_enough_balance() { + let (binary, _code_hash) = compile_module("dummy").unwrap(); + let deposit_expected = expected_deposit(binary.len()); + let deposit_insufficient = deposit_expected.saturating_sub(1); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, deposit_insufficient); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::upload_code(RuntimeOrigin::signed(ALICE), binary, 1_000,), + >::StorageDepositNotEnoughFunds, + ); + + assert_eq!(System::events(), vec![]); + }); +} + +#[test] +fn remove_code_works() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), binary, 1_000,)); + // Ensure the contract was stored and get expected deposit amount to be reserved. + expected_deposit(ensure_stored(code_hash)); + assert_ok!(Contracts::remove_code(RuntimeOrigin::signed(ALICE), code_hash)); + }); +} +#[test] +fn remove_code_wrong_origin() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_ok!(Contracts::upload_code(RuntimeOrigin::signed(ALICE), binary, 1_000,)); + // Ensure the contract was stored and get expected deposit amount to be reserved. + expected_deposit(ensure_stored(code_hash)); + + assert_noop!( + Contracts::remove_code(RuntimeOrigin::signed(BOB), code_hash), + sp_runtime::traits::BadOrigin, + ); + }); +} + +#[test] +fn remove_code_in_use() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + assert_ok!(builder::instantiate_with_code(binary).build()); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::remove_code(RuntimeOrigin::signed(ALICE), code_hash), + >::CodeInUse, + ); + + assert_eq!(System::events(), vec![]); + }); +} + +#[test] +fn remove_code_not_found() { + let (_binary, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Drop previous events + initialize_block(2); + + assert_noop!( + Contracts::remove_code(RuntimeOrigin::signed(ALICE), code_hash), + >::CodeNotFound, + ); + + assert_eq!(System::events(), vec![]); + }); +} + +#[test] +fn instantiate_with_zero_balance_works() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Drop previous events + initialize_block(2); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + // Ensure the contract was stored and get expected deposit amount to be reserved. + expected_deposit(ensure_stored(code_hash)); + + // Make sure the account exists even though no free balance was send + assert_eq!(::Currency::free_balance(&account_id), min_balance); + assert_eq!( + ::Currency::total_balance(&account_id), + min_balance + contract_base_deposit(&addr) + ); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + source: ALICE, + dest: Pallet::::account_id(), + transferred: 777, + reason: ::RuntimeHoldReason::Contracts( + HoldReason::CodeUploadDepositReserve, + ), + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: account_id.clone(), + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: account_id.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Instantiated { + deployer: ALICE_ADDR, + contract: addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: ALICE, + dest: account_id, + transferred: 337, + }), + topics: vec![], + }, + ] + ); + }); +} + +#[test] +fn instantiate_with_below_existential_deposit_works() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + let value = 50; + + // Drop previous events + initialize_block(2); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(value) + .build_and_unwrap_contract(); + + // Ensure the contract was stored and get expected deposit amount to be reserved. + expected_deposit(ensure_stored(code_hash)); + // Make sure the account exists even though not enough free balance was send + assert_eq!(::Currency::free_balance(&account_id), min_balance + value); + assert_eq!( + ::Currency::total_balance(&account_id), + min_balance + value + contract_base_deposit(&addr) + ); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + source: ALICE, + dest: Pallet::::account_id(), + transferred: 777, + reason: ::RuntimeHoldReason::Contracts( + HoldReason::CodeUploadDepositReserve, + ), + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::System(frame_system::Event::NewAccount { + account: account_id.clone() + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: account_id.clone(), + free_balance: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: min_balance, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: 50, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Contracts(crate::Event::Instantiated { + deployer: ALICE_ADDR, + contract: addr, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: ALICE, + dest: account_id.clone(), + transferred: 337, + }), + topics: vec![], + }, + ] + ); + }); +} + +#[test] +fn storage_deposit_works() { + let (binary, _code_hash) = compile_module("multi_store").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + let mut deposit = contract_base_deposit(&addr); + + // Drop previous events + initialize_block(2); + + // Create storage + assert_ok!(builder::call(addr).value(42).data((50u32, 20u32).encode()).build()); + // 4 is for creating 2 storage items + // 48 is for each of the keys + let charged0 = 4 + 50 + 20 + 48 + 48; + deposit += charged0; + assert_eq!(get_contract(&addr).total_deposit(), deposit); + + // Add more storage (but also remove some) + assert_ok!(builder::call(addr).data((100u32, 10u32).encode()).build()); + let charged1 = 50 - 10; + deposit += charged1; + assert_eq!(get_contract(&addr).total_deposit(), deposit); + + // Remove more storage (but also add some) + assert_ok!(builder::call(addr).data((10u32, 20u32).encode()).build()); + // -1 for numeric instability + let refunded0 = 90 - 10 - 1; + deposit -= refunded0; + assert_eq!(get_contract(&addr).total_deposit(), deposit); + + assert_eq!( + System::events(), + vec![ + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: ALICE, + to: account_id.clone(), + amount: 42, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: ALICE, + dest: account_id.clone(), + transferred: charged0, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferAndHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: ALICE, + dest: account_id.clone(), + transferred: charged1, + }), + topics: vec![], + }, + EventRecord { + phase: Phase::Initialization, + event: RuntimeEvent::Balances(pallet_balances::Event::TransferOnHold { + reason: ::RuntimeHoldReason::Contracts( + HoldReason::StorageDepositReserve, + ), + source: account_id.clone(), + dest: ALICE, + amount: refunded0, + }), + topics: vec![], + }, + ] + ); + }); +} + +#[test] +fn storage_deposit_callee_works() { + let (binary_caller, _code_hash_caller) = compile_module("call").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + assert_ok!(builder::call(addr_caller).data((100u32, &addr_callee).encode()).build()); + + let callee = get_contract(&addr_callee); + let deposit = DepositPerByte::get() * 100 + DepositPerItem::get() * 1 + 48; + + assert_eq!(Pallet::::evm_balance(&addr_caller), U256::zero()); + assert_eq!(callee.total_deposit(), deposit + contract_base_deposit(&addr_callee)); + }); +} + +#[test] +fn set_code_extrinsic() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + let (new_binary, new_code_hash) = compile_module("crypto_hash_keccak_256").unwrap(); + + assert_ne!(code_hash, new_code_hash); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + new_binary, + deposit_limit::(), + )); + + // Drop previous events + initialize_block(2); + + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + + // only root can execute this extrinsic + assert_noop!( + Contracts::set_code(RuntimeOrigin::signed(ALICE), addr, new_code_hash), + sp_runtime::traits::BadOrigin, + ); + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + assert_eq!(System::events(), vec![]); + + // contract must exist + assert_noop!( + Contracts::set_code(RuntimeOrigin::root(), BOB_ADDR, new_code_hash), + >::ContractNotFound, + ); + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + assert_eq!(System::events(), vec![]); + + // new code hash must exist + assert_noop!( + Contracts::set_code(RuntimeOrigin::root(), addr, Default::default()), + >::CodeNotFound, + ); + assert_eq!(get_contract(&addr).code_hash, code_hash); + assert_refcount!(&code_hash, 1); + assert_refcount!(&new_code_hash, 0); + assert_eq!(System::events(), vec![]); + + // successful call + assert_ok!(Contracts::set_code(RuntimeOrigin::root(), addr, new_code_hash)); + assert_eq!(get_contract(&addr).code_hash, new_code_hash); + assert!(PristineCode::::get(&code_hash).is_none()); + assert_refcount!(&new_code_hash, 1); + }); +} + +#[test] +fn slash_cannot_kill_account() { + let (binary, _code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let value = 700; + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + let Contract { addr, account_id } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(value) + .build_and_unwrap_contract(); + + // Drop previous events + initialize_block(2); + + let info_deposit = contract_base_deposit(&addr); + + assert_eq!( + get_balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account_id), + info_deposit + ); + + assert_eq!( + ::Currency::total_balance(&account_id), + info_deposit + value + min_balance + ); + + // Try to destroy the account of the contract by slashing the total balance. + // The account does not get destroyed because slashing only affects the balance held + // under certain `reason`. Slashing can for example happen if the contract takes part + // in staking. + let _ = ::Currency::slash( + &HoldReason::StorageDepositReserve.into(), + &account_id, + ::Currency::total_balance(&account_id), + ); + + // Slashing only removed the balance held. + assert_eq!(::Currency::total_balance(&account_id), value + min_balance); + }); +} + +#[test] +fn contract_reverted() { + let (binary, code_hash) = compile_module("return_with_data").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let flags = ReturnFlags::REVERT; + let buffer = [4u8, 8, 15, 16, 23, 42]; + let input = (flags.bits(), buffer).encode(); + + // We just upload the code for later use + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + binary.clone(), + deposit_limit::(), + )); + + // Calling extrinsic: revert leads to an error + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).data(input.clone()).build(), + >::ContractReverted, + ); + + // Calling extrinsic: revert leads to an error + assert_err_ignore_postinfo!( + builder::instantiate_with_code(binary).data(input.clone()).build(), + >::ContractReverted, + ); + + // Calling directly: revert leads to success but the flags indicate the error + // This is just a different way of transporting the error that allows the read out + // the `data` which is only there on success. Obviously, the contract isn't + // instantiated. + let result = builder::bare_instantiate(Code::Existing(code_hash)) + .data(input.clone()) + .build_and_unwrap_result(); + assert_eq!(result.result.flags, flags); + assert_eq!(result.result.data, buffer); + assert!(!>::contains_key(result.addr)); + + // Pass empty flags and therefore successfully instantiate the contract for later use. + let Contract { addr, .. } = builder::bare_instantiate(Code::Existing(code_hash)) + .data(ReturnFlags::empty().bits().encode()) + .build_and_unwrap_contract(); + + // Calling extrinsic: revert leads to an error + assert_err_ignore_postinfo!( + builder::call(addr).data(input.clone()).build(), + >::ContractReverted, + ); + + // Calling directly: revert leads to success but the flags indicate the error + let result = builder::bare_call(addr).data(input).build_and_unwrap_result(); + assert_eq!(result.flags, flags); + assert_eq!(result.data, buffer); + }); +} + +#[test] +fn set_code_hash() { + let (binary, _) = compile_module("set_code_hash").unwrap(); + let (new_binary, new_code_hash) = compile_module("new_set_code_hash_contract").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the 'caller' + let Contract { addr: contract_addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + // upload new code + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + new_binary.clone(), + deposit_limit::(), + )); + + System::reset_events(); + + // First call sets new code_hash and returns 1 + let result = builder::bare_call(contract_addr) + .data(new_code_hash.as_ref().to_vec()) + .build_and_unwrap_result(); + assert_return_code!(result, 1); + + // Second calls new contract code that returns 2 + let result = builder::bare_call(contract_addr).build_and_unwrap_result(); + assert_return_code!(result, 2); + }); +} + +#[test] +fn storage_deposit_limit_is_enforced() { + let (binary, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let min_balance = Contracts::min_balance(); + + // Setting insufficient storage_deposit should fail. + assert_err!( + builder::bare_instantiate(Code::Upload(binary.clone())) + // expected deposit is 2 * ed + 3 for the call + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: Default::default(), + deposit_limit: (2 * min_balance + 3 - 1).into() + }) + .build() + .result, + >::StorageDepositLimitExhausted, + ); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + let info_deposit = contract_base_deposit(&addr); + // Check that the BOB contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&addr).total_deposit(), info_deposit); + assert_eq!( + ::Currency::total_balance(&account_id), + info_deposit + min_balance + ); + + // Create 1 byte of storage with a price of per byte, + // setting insufficient deposit limit, as it requires 3 Balance: + // 2 for the item added + 1 (value) + 48 (key) + assert_err_ignore_postinfo!( + builder::call(addr) + .storage_deposit_limit(50) + .data(1u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // now with enough limit + assert_ok!(builder::call(addr) + .storage_deposit_limit(51) + .data(1u32.to_le_bytes().to_vec()) + .build()); + + // Use 4 more bytes of the storage for the same item, which requires 4 Balance. + // Should fail as DefaultDepositLimit is 3 and hence isn't enough. + assert_err_ignore_postinfo!( + builder::call(addr) + .storage_deposit_limit(3) + .data(5u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositLimitExhausted, + ); + }); +} + +#[test] +fn deposit_limit_in_nested_calls() { + let (binary_caller, _code_hash_caller) = compile_module("create_storage_and_call").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + // Create 100 bytes of storage with a price of per byte + // This is 100 Balance + 2 Balance for the item + // 48 for the key + assert_ok!(builder::call(addr_callee) + .storage_deposit_limit(102 + 48) + .data(100u32.to_le_bytes().to_vec()) + .build()); + + // We do not remove any storage but add a storage item of 12 bytes in the caller + // contract. This would cost 12 + 2 + 72 = 86 Balance. + // The nested call doesn't get a special limit, which is set by passing `u64::MAX` to it. + // This should fail as the specified parent's limit is less than the cost: 13 < + // 14. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(85) + .data((100u32, &addr_callee, U256::MAX).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // Now we specify the parent's limit high enough to cover the caller's storage + // additions. However, we use a single byte more in the callee, hence the storage + // deposit should be 87 Balance. + // The nested call doesn't get a special limit, which is set by passing `u64::MAX` to it. + // This should fail as the specified parent's limit is less than the cost: 86 < 87 + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(86) + .data((101u32, &addr_callee, &U256::MAX).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // The parents storage deposit limit doesn't matter as the sub calls limit + // is enforced eagerly. However, we set a special deposit limit of 1 Balance for the + // nested call. This should fail as callee adds up 2 bytes to the storage, meaning + // that the nested call should have a deposit limit of at least 2 Balance. The + // sub-call should be rolled back, which is covered by the next test case. + let ret = builder::bare_call(addr_caller) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: WEIGHT_LIMIT, + deposit_limit: u64::MAX, + }) + .data((102u32, &addr_callee, U256::from(1u64)).encode()) + .build_and_unwrap_result(); + assert_return_code!(ret, RuntimeReturnCode::OutOfResources); + + // Refund in the callee contract but not enough to cover the Balance required by the + // caller. Note that if previous sub-call wouldn't roll back, this call would pass + // making the test case fail. We don't set a special limit for the nested call here. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(0) + .data((87u32, &addr_callee, &U256::MAX.to_little_endian()).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + let _ = ::Currency::set_balance(&ALICE, 511); + + // Require more than the sender's balance. + // Limit the sub call to little balance so it should fail in there + let ret = builder::bare_call(addr_caller) + .data((416, &addr_callee, U256::from(1u64)).encode()) + .build_and_unwrap_result(); + assert_return_code!(ret, RuntimeReturnCode::OutOfResources); + + // Free up a bit of storage in the callee but not enough for the caller to + // create a new item + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .storage_deposit_limit(78) + .data((95u32, &addr_callee, U256::from(1u64)).encode()) + .build(), + >::StorageDepositLimitExhausted, + ); + + // Free up enough storage in the callee so that the caller can create a new item + // We set the special deposit limit of 1 Balance for the nested call, which isn't + // enforced as callee frees up storage. This should pass. + assert_ok!(builder::call(addr_caller) + .storage_deposit_limit(78) + .data((0u32, &addr_callee, U256::from(1u64)).encode()) + .build()); + }); +} + +#[test] +fn deposit_limit_in_nested_instantiate() { + let (binary_caller, _code_hash_caller) = + compile_module("create_storage_and_instantiate").unwrap(); + let (binary_callee, code_hash_callee) = compile_module("store_deploy").unwrap(); + const ED: u64 = 5; + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, 1_000_000); + // Create caller contract + let Contract { addr: addr_caller, account_id: caller_id } = + builder::bare_instantiate(Code::Upload(binary_caller)) + .native_value(10_000) // this balance is later passed to the deployed contract + .build_and_unwrap_contract(); + // Deploy a contract to get its occupied storage size + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary_callee)) + .data(vec![0, 0, 0, 0]) + .build_and_unwrap_contract(); + + // This is the deposit we expect to be charged just for instantiatiting the callee. + // + // - callee_info_len + 2 for storing the new contract info + // - the deposit for depending on a code hash + // - ED for deployed contract account + // - 2 for the storage item of 0 bytes being created in the callee constructor + // - 48 for the key + let callee_min_deposit = { + let callee_info_len = + AccountInfo::::load_contract(&addr).unwrap().encoded_size() as u64; + let code_deposit = lockup_deposit(&code_hash_callee); + callee_info_len + code_deposit + 2 + ED + 2 + 48 + }; + + // The parent just stores an item of the passed size so at least + // we need to pay for the item itself. + // stores 2 storage items: one before the subcall and one after + let caller_min_deposit = callee_min_deposit + 2 * (2 + 48); + + // Fail in callee. + // + // We still fail in the sub call because we enforce limits on return from a contract. + // Sub calls return first to they are checked first. + let ret = builder::bare_call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: WEIGHT_LIMIT, + deposit_limit: 155, + }) + .data((&code_hash_callee, 100u32, &U256::MAX.to_little_endian()).encode()) + .build_and_unwrap_result(); + assert_return_code!(ret, RuntimeReturnCode::OutOfResources); + // The charges made on instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Fail in the callee with bytes. + // + // Same as above but stores one byte in both caller and callee. + let ret = builder::bare_call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: WEIGHT_LIMIT, + deposit_limit: caller_min_deposit + 1, + }) + .data((&code_hash_callee, 1u32, U256::from(callee_min_deposit)).encode()) + .build_and_unwrap_result(); + assert_return_code!(ret, RuntimeReturnCode::OutOfResources); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Fail in the caller with bytes. + // + // Same as above but stores one byte in both caller and callee. + let ret = builder::bare_call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: WEIGHT_LIMIT, + deposit_limit: caller_min_deposit + 2, + }) + .data((&code_hash_callee, 1u32, U256::from(callee_min_deposit + 1)).encode()) + .build(); + assert_err!(ret.result, >::StorageDepositLimitExhausted); + // The charges made on the instantiation should be rolled back. + assert_eq!(::Currency::free_balance(&BOB), 1_000_000); + + // Set enough deposit limit for the child instantiate. This should succeed. + let result = builder::bare_call(addr_caller) + .origin(RuntimeOrigin::signed(BOB)) + .transaction_limits(TransactionLimits::WeightAndDeposit { + weight_limit: WEIGHT_LIMIT, + deposit_limit: (caller_min_deposit + 3).into(), + }) + .data((&code_hash_callee, 1u32, U256::from(callee_min_deposit + 1)).encode()) + .build(); + + let returned = result.result.unwrap(); + assert!(!returned.did_revert()); + + // All balance of the caller except ED has been transferred to the callee. + // No deposit has been taken from it. + assert_eq!(::Currency::free_balance(&caller_id), ED); + // Get address of the deployed contract. + let addr_callee = H160::from_slice(&returned.data[0..20]); + let callee_account_id = ::AddressMapper::to_account_id(&addr_callee); + // 10_000 should be sent to callee from the caller contract, plus ED to be sent from the + // origin. + assert_eq!(::Currency::free_balance(&callee_account_id), 10_000 + ED); + // The origin should be charged with what the outer call consumed + assert_eq!( + ::Currency::free_balance(&BOB), + 1_000_000 - (caller_min_deposit + 3), + ); + assert_eq!(result.storage_deposit.charge_or_zero(), (caller_min_deposit + 3)) + }); +} + +#[test] +fn deposit_limit_honors_liquidity_restrictions() { + let (binary, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let min_balance = Contracts::min_balance(); + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, min_balance); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + let info_deposit = contract_base_deposit(&addr); + // Check that the contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&addr).total_deposit(), info_deposit); + assert_eq!( + ::Currency::total_balance(&account_id), + info_deposit + min_balance + ); + + assert_err_ignore_postinfo!( + builder::call(addr) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(10_000) + .data(100u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositNotEnoughFunds, + ); + }); +} + +#[test] +fn deposit_limit_honors_existential_deposit() { + let (binary, _code_hash) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let _ = ::Currency::set_balance(&BOB, 300); + let min_balance = Contracts::min_balance(); + + // Instantiate the BOB contract. + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + let info_deposit = contract_base_deposit(&addr); + + // Check that the contract has been instantiated and has the minimum balance + assert_eq!(get_contract(&addr).total_deposit(), info_deposit); + assert_eq!( + ::Currency::total_balance(&account_id), + min_balance + info_deposit + ); + + // check that the deposit can't bring the account below the existential deposit + assert_err_ignore_postinfo!( + builder::call(addr) + .origin(RuntimeOrigin::signed(BOB)) + .storage_deposit_limit(10_000) + .data(100u32.to_le_bytes().to_vec()) + .build(), + >::StorageDepositNotEnoughFunds, + ); + assert_eq!(::Currency::free_balance(&BOB), 300); + }); +} + +#[test] +fn native_dependency_deposit_works() { + let (binary, code_hash) = compile_module("set_code_hash").unwrap(); + let (dummy_binary, dummy_code_hash) = compile_module("dummy").unwrap(); + + // Test with both existing and uploaded code + for code in [Code::Upload(binary.clone()), Code::Existing(code_hash)] { + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let lockup_deposit_percent = CodeHashLockupDepositPercent::get(); + + // Upload the dummy contract, + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + dummy_binary.clone(), + deposit_limit::(), + ) + .unwrap(); + + // Upload `set_code_hash` contracts if using Code::Existing. + let add_upload_deposit = match code { + Code::Existing(_) => { + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + binary.clone(), + deposit_limit::(), + ) + .unwrap(); + false + }, + Code::Upload(_) => true, + }; + + // Instantiate the set_code_hash contract. + let res = builder::bare_instantiate(code).build(); + + let addr = res.result.unwrap().addr; + let account_id = ::AddressMapper::to_account_id(&addr); + let base_deposit = contract_base_deposit(&addr); + let upload_deposit = get_code_deposit(&code_hash); + let extra_deposit = add_upload_deposit.then(|| upload_deposit).unwrap_or_default(); + + assert_eq!( + res.storage_deposit.charge_or_zero(), + extra_deposit + base_deposit + Contracts::min_balance() + ); + + // call set_code_hash + builder::bare_call(addr) + .data(dummy_code_hash.encode()) + .build_and_unwrap_result(); + + // Check updated storage_deposit due to code size changes + let deposit_diff = lockup_deposit_percent.mul_ceil(upload_deposit) - + lockup_deposit_percent.mul_ceil(get_code_deposit(&dummy_code_hash)); + let new_base_deposit = contract_base_deposit(&addr); + assert_ne!(deposit_diff, 0); + assert_eq!(base_deposit - new_base_deposit, deposit_diff); + + assert_eq!( + get_balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account_id), + new_base_deposit + ); + }); + } +} + +#[test] +fn block_hash_works() { + let (code, _) = compile_module("block_hash").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // The genesis config sets to the block number to 1 + + // Store block hash in pallet-revive BlockHash mapping + let block_hash = H256::from([1; 32]); + crate::BlockHash::::insert( + crate::BlockNumberFor::::from(0u32), + H256::from(block_hash), + ); + + assert_ok!(builder::call(addr) + .data((U256::zero(), H256::from(block_hash)).encode()) + .build()); + + // A block number out of range returns the zero value + assert_ok!(builder::call(addr).data((U256::from(1), H256::zero()).encode()).build()); + }); +} + +#[test] +fn block_author_works() { + let (code, _) = compile_module("block_author").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // The fixture asserts the input to match the find_author API method output. + assert_ok!(builder::call(addr).data(EVE_ADDR.encode()).build()); + }); +} + +#[test] +fn root_cannot_upload_code() { + let (binary, _) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Contracts::upload_code(RuntimeOrigin::root(), binary, deposit_limit::()), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn root_cannot_remove_code() { + let (_, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Contracts::remove_code(RuntimeOrigin::root(), code_hash), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn signed_cannot_set_code() { + let (_, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + Contracts::set_code(RuntimeOrigin::signed(ALICE), BOB_ADDR, code_hash), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn none_cannot_call_code() { + ExtBuilder::default().build().execute_with(|| { + assert_err_ignore_postinfo!( + builder::call(BOB_ADDR).origin(RuntimeOrigin::none()).build(), + DispatchError::BadOrigin, + ); + }); +} + +#[test] +fn root_can_call() { + let (binary, _) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + + // Call the contract. + assert_ok!(builder::call(addr).origin(RuntimeOrigin::root()).build()); + }); +} + +#[test] +fn root_cannot_instantiate_with_code() { + let (binary, _) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_err_ignore_postinfo!( + builder::instantiate_with_code(binary).origin(RuntimeOrigin::root()).build(), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn root_cannot_instantiate() { + let (_, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).origin(RuntimeOrigin::root()).build(), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn only_upload_origin_can_upload() { + let (binary, _) = compile_module("dummy").unwrap(); + UploadAccount::set(Some(ALICE)); + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let _ = Balances::set_balance(&BOB, 1_000_000); + + assert_err!( + Contracts::upload_code(RuntimeOrigin::root(), binary.clone(), deposit_limit::(),), + DispatchError::BadOrigin + ); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(BOB), + binary.clone(), + deposit_limit::(), + ), + DispatchError::BadOrigin + ); + + // Only alice is allowed to upload contract code. + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + binary.clone(), + deposit_limit::(), + )); + }); +} + +#[test] +fn only_instantiation_origin_can_instantiate() { + let (code, code_hash) = compile_module("dummy").unwrap(); + InstantiateAccount::set(Some(ALICE)); + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let _ = Balances::set_balance(&BOB, 1_000_000); + + assert_err_ignore_postinfo!( + builder::instantiate_with_code(code.clone()) + .origin(RuntimeOrigin::root()) + .build(), + DispatchError::BadOrigin + ); + + assert_err_ignore_postinfo!( + builder::instantiate_with_code(code.clone()) + .origin(RuntimeOrigin::signed(BOB)) + .build(), + DispatchError::BadOrigin + ); + + // Only Alice can instantiate + assert_ok!(builder::instantiate_with_code(code).build()); + + // Bob cannot instantiate with either `instantiate_with_code` or `instantiate`. + assert_err_ignore_postinfo!( + builder::instantiate(code_hash).origin(RuntimeOrigin::signed(BOB)).build(), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn balance_of_api() { + let (binary, _code_hash) = compile_module("balance_of").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + let _ = Balances::set_balance(&ALICE_FALLBACK, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(binary.to_vec())).build_and_unwrap_contract(); + + // The fixture asserts a non-zero returned free balance of the account; + // The ALICE_FALLBACK account is endowed; + // Hence we should not revert + assert_ok!(builder::call(addr).data(ALICE_ADDR.0.to_vec()).build()); + + // The fixture asserts a non-zero returned free balance of the account; + // The ETH_BOB account is not endowed; + // Hence we should revert + assert_err_ignore_postinfo!( + builder::call(addr).data(BOB_ADDR.0.to_vec()).build(), + >::ContractTrapped + ); + }); +} + +#[test] +fn balance_api_returns_free_balance() { + let (binary, _code_hash) = compile_module("balance").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Instantiate the BOB contract without any extra balance. + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(binary.to_vec())).build_and_unwrap_contract(); + + let value = 0; + // Call BOB which makes it call the balance runtime API. + // The contract code asserts that the returned balance is 0. + assert_ok!(builder::call(addr).value(value).build()); + + let value = 1; + // Calling with value will trap the contract. + assert_err_ignore_postinfo!( + builder::call(addr).value(value).build(), + >::ContractTrapped + ); + }); +} + +#[test] +fn call_depth_is_enforced() { + let (binary, _code_hash) = compile_module("recurse").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let extra_recursions = 1024; + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(binary.to_vec())).build_and_unwrap_contract(); + + // takes the number of recursions + // returns the number of left over recursions + assert_eq!( + u32::from_le_bytes( + builder::bare_call(addr) + .data((limits::CALL_STACK_DEPTH + extra_recursions).encode()) + .build_and_unwrap_result() + .data + .try_into() + .unwrap() + ), + // + 1 because when the call depth is reached the caller contract is trapped without + // the ability to return any data. hence the last call frame is untracked. + extra_recursions + 1, + ); + }); +} + +#[test] +fn weight_consumed_is_linear_for_nested_calls() { + let (code, _code_hash) = compile_module("recurse").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let [gas_0, gas_1, gas_2, gas_max] = { + [0u32, 1u32, 2u32, limits::CALL_STACK_DEPTH] + .iter() + .map(|i| { + let result = builder::bare_call(addr).data(i.encode()).build(); + assert_eq!( + u32::from_le_bytes(result.result.unwrap().data.try_into().unwrap()), + 0 + ); + result.weight_consumed + }) + .collect::>() + .try_into() + .unwrap() + }; + + let gas_per_recursion = gas_2.checked_sub(&gas_1).unwrap(); + assert_eq!(gas_max, gas_0 + gas_per_recursion * limits::CALL_STACK_DEPTH as u64); + }); +} + +#[test] +fn read_only_call_cannot_store() { + let (binary_caller, _code_hash_caller) = compile_module("read_only_call").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + // Read-only call fails when modifying storage. + assert_err_ignore_postinfo!( + builder::call(addr_caller).data((&addr_callee, 100u32).encode()).build(), + >::ContractTrapped + ); + }); +} + +#[test] +fn read_only_call_cannot_transfer() { + let (binary_caller, _code_hash_caller) = compile_module("call_with_flags_and_value").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + // Read-only call fails when a non-zero value is set. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data( + (addr_callee, pallet_revive_uapi::CallFlags::READ_ONLY.bits(), 100u64).encode() + ) + .build(), + >::StateChangeDenied + ); + }); +} + +#[test] +fn read_only_subsequent_call_cannot_store() { + let (binary_read_only_caller, _code_hash_caller) = compile_module("read_only_call").unwrap(); + let (binary_caller, _code_hash_caller) = compile_module("call_with_flags_and_value").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("store_call").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_read_only_caller)) + .build_and_unwrap_contract(); + let Contract { addr: addr_subsequent_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + // Subsequent call input. + let input = (&addr_callee, pallet_revive_uapi::CallFlags::empty().bits(), 0u64, 100u32); + + // Read-only call fails when modifying storage. + assert_err_ignore_postinfo!( + builder::call(addr_caller) + .data((&addr_subsequent_caller, input).encode()) + .build(), + >::ContractTrapped + ); + }); +} + +#[test] +fn read_only_call_works() { + let (binary_caller, _code_hash_caller) = compile_module("read_only_call").unwrap(); + let (binary_callee, _code_hash_callee) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create both contracts: Constructors do nothing. + let Contract { addr: addr_caller, .. } = + builder::bare_instantiate(Code::Upload(binary_caller)).build_and_unwrap_contract(); + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + assert_ok!(builder::call(addr_caller).data(addr_callee.encode()).build()); + }); +} + +#[test] +fn create1_with_value_works() { + let (code, code_hash) = compile_module("create1_with_value").unwrap(); + let value = 42; + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing. + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: Deploys itself using create1 and the expected value + assert_ok!(builder::call(addr).value(value).data(code_hash.encode()).build()); + + // We should see the expected balance at the expected account + let address = crate::address::create1(&addr, 1); + let account_id = ::AddressMapper::to_account_id(&address); + let usable_balance = ::Currency::usable_balance(&account_id); + assert_eq!(usable_balance, value); + }); +} + +#[test] +fn gas_price_api_works() { + let (code, _) = compile_module("gas_price").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: It echoes back the value returned by the gas price API. + let received = builder::bare_call(addr).build_and_unwrap_result(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!( + u64::from_le_bytes(received.data[..].try_into().unwrap()), + u64::try_from(>::evm_base_fee()).unwrap(), + ); + }); +} + +#[test] +fn base_fee_api_works() { + let (code, _) = compile_module("base_fee").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: It echoes back the value returned by the base fee API. + let received = builder::bare_call(addr).build_and_unwrap_result(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!( + U256::from_little_endian(received.data[..].try_into().unwrap()), + >::evm_base_fee(), + ); + }); +} + +#[test] +fn call_data_size_api_works() { + let (code, _) = compile_module("call_data_size").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: It echoes back the value returned by the call data size API. + let received = builder::bare_call(addr).build_and_unwrap_result(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!(u64::from_le_bytes(received.data.try_into().unwrap()), 0); + + let received = builder::bare_call(addr).data(vec![1; 256]).build_and_unwrap_result(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!(u64::from_le_bytes(received.data.try_into().unwrap()), 256); + }); +} + +#[test] +fn call_data_copy_api_works() { + let (code, _) = compile_module("call_data_copy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call fixture: Expects an input of [255; 32] and executes tests. + assert_ok!(builder::call(addr).data(vec![255; 32]).build()); + }); +} + +#[test] +fn static_data_limit_is_enforced() { + let (oom_rw_trailing, _) = compile_module("oom_rw_trailing").unwrap(); + let (oom_rw_included, _) = compile_module("oom_rw_included").unwrap(); + let (oom_ro, _) = compile_module("oom_ro").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + oom_rw_trailing, + deposit_limit::(), + ), + >::StaticMemoryTooLarge + ); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + oom_rw_included, + deposit_limit::(), + ), + >::BlobTooLarge + ); + + assert_err!( + Contracts::upload_code(RuntimeOrigin::signed(ALICE), oom_ro, deposit_limit::(),), + >::BlobTooLarge + ); + }); +} + +#[test] +fn call_diverging_out_len_works() { + let (code, _) = compile_module("call_diverging_out_len").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: It will issue calls and deploys, asserting on + // correct output if the supplied output length was smaller than + // than what the callee returned. + assert_ok!(builder::call(addr).build()); + }); +} + +#[test] +fn call_own_code_hash_works() { + let (code, code_hash) = compile_module("call_own_code_hash").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let ret = builder::bare_call(addr).build_and_unwrap_result(); + let ret_hash = FixedBytes::<32>::abi_decode(&ret.data).unwrap(); + assert_eq!(ret_hash, code_hash.0); + }); +} + +#[test] +fn call_caller_is_root() { + let (code, _) = compile_module("call_caller_is_root").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let ret = builder::bare_call(addr).origin(RuntimeOrigin::root()).build_and_unwrap_result(); + let is_root = Bool::abi_decode(&ret.data).expect("decoding failed"); + assert!(is_root); + }); +} + +#[test] +fn call_caller_is_root_from_non_root() { + let (code, _) = compile_module("call_caller_is_root").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let ret = builder::bare_call(addr).build_and_unwrap_result(); + let is_root = Bool::abi_decode(&ret.data).expect("decoding failed"); + assert!(!is_root); + }); +} + +#[test] +fn call_caller_is_origin() { + let (code, _) = compile_module("call_caller_is_origin").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create the contract: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let ret = builder::bare_call(addr).build_and_unwrap_result(); + let is_origin = Bool::abi_decode(&ret.data).expect("decoding failed"); + assert!(is_origin); + }); +} + +#[test] +fn chain_id_works() { + let (code, _) = compile_module("chain_id").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let chain_id = U256::from(::ChainId::get()); + let received = builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_result(); + assert_eq!(received.result.data, chain_id.encode()); + }); +} + +#[test] +fn call_data_load_api_works() { + let (code, _) = compile_module("call_data_load").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: It reads a byte for the offset and then returns + // what call data load returned using this byte as the offset. + let input = (3u8, U256::max_value(), U256::max_value()).encode(); + let received = builder::bare_call(addr).data(input).build().result.unwrap(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!(U256::from_little_endian(&received.data), U256::max_value()); + + // Edge case + let input = (2u8, U256::from(255).to_big_endian()).encode(); + let received = builder::bare_call(addr).data(input).build().result.unwrap(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!(U256::from_little_endian(&received.data), U256::from(65280)); + + // Edge case + let received = builder::bare_call(addr).data(vec![1]).build().result.unwrap(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!(U256::from_little_endian(&received.data), U256::zero()); + + // OOB case + let input = (42u8).encode(); + let received = builder::bare_call(addr).data(input).build().result.unwrap(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!(U256::from_little_endian(&received.data), U256::zero()); + + // No calldata should return the zero value + let received = builder::bare_call(addr).build().result.unwrap(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!(U256::from_little_endian(&received.data), U256::zero()); + }); +} + +#[test] +fn return_data_api_works() { + let (code_return_data_api, _) = compile_module("return_data_api").unwrap(); + let (code_return_with_data, hash_return_with_data) = + compile_module("return_with_data").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Upload the io echoing fixture for later use + assert_ok!(Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + code_return_with_data, + deposit_limit::(), + )); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code_return_data_api)) + .build_and_unwrap_contract(); + + // Call the contract: It will issue calls and deploys, asserting on + assert_ok!(builder::call(addr) + .value(10 * 1024) + .data(hash_return_with_data.encode()) + .build()); + }); +} + +#[test] +fn immutable_data_works() { + let (code, _) = compile_module("immutable_data").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let data = [0xfe; 8]; + + // Create fixture: Constructor sets the immtuable data + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .data(data.to_vec()) + .build_and_unwrap_contract(); + + let contract = get_contract(&addr); + let account = ::AddressMapper::to_account_id(&addr); + let actual_deposit = + get_balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account); + + assert_eq!(contract.immutable_data_len(), data.len() as u32); + + // Storing immmutable data charges storage deposit; verify it explicitly. + assert_eq!(actual_deposit, contract_base_deposit(&addr)); + + // make sure it is also recorded in the base deposit + assert_eq!( + get_balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account), + contract.storage_base_deposit(), + ); + + // Call the contract: Asserts the input to equal the immutable data + assert_ok!(builder::call(addr).data(data.to_vec()).build()); + }); +} + +#[test] +fn sbrk_cannot_be_linked() { + // The sbrk instruction is not available in the revive_v1 instruction set. + // This test verifies that the linker rejects it during the linking phase. + let result = pallet_revive_fixtures::try_compile_invalid_fixture("sbrk"); + + assert!(result.is_err(), "Expected linking to fail for sbrk fixture"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("sbrk") || err_msg.contains("not available"), + "Expected error message to mention 'sbrk' or 'not available', got: {}", + err_msg + ); +} + +#[test] +fn overweight_basic_block_cannot_be_deployed() { + let (code, _) = compile_module("basic_block").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = Balances::set_balance(&ALICE, 1_000_000); + + assert_err!( + Contracts::upload_code( + RuntimeOrigin::signed(ALICE), + code.clone(), + deposit_limit::(), + ), + >::BasicBlockTooLarge + ); + + assert_err!( + builder::bare_instantiate(Code::Upload(code)).build().result, + >::BasicBlockTooLarge + ); + }); +} + +#[test] +fn origin_api_works() { + let (code, _) = compile_module("origin").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: Asserts the origin API to work as expected + assert_ok!(builder::call(addr).build()); + }); +} + +#[test] +fn code_hash_works() { + use crate::precompiles::{Precompile, EVM_REVERT}; + use precompiles::NoInfo; + + let builtin_precompile = H160(NoInfo::::MATCHER.base_address()); + let primitive_precompile = H160::from_low_u64_be(1); + + let (code_hash_code, self_code_hash) = compile_module("code_hash").unwrap(); + let (dummy_code, code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code_hash_code)).build_and_unwrap_contract(); + let Contract { addr: dummy_addr, .. } = + builder::bare_instantiate(Code::Upload(dummy_code)).build_and_unwrap_contract(); + + // code hash of dummy contract + assert_ok!(builder::call(addr).data((dummy_addr, code_hash).encode()).build()); + // code hash of itself + assert_ok!(builder::call(addr).data((addr, self_code_hash).encode()).build()); + // code hash of primitive pre-compile (exist but have no bytecode) + assert_ok!(builder::call(addr) + .data((primitive_precompile, crate::exec::EMPTY_CODE_HASH).encode()) + .build()); + // code hash of normal pre-compile (do have a bytecode) + assert_ok!(builder::call(addr) + .data((builtin_precompile, sp_io::hashing::keccak_256(&EVM_REVERT)).encode()) + .build()); + + // EOA doesn't exists + assert_err!( + builder::bare_call(addr) + .data((BOB_ADDR, crate::exec::EMPTY_CODE_HASH).encode()) + .build() + .result, + Error::::ContractTrapped + ); + // non-existing will return zero + assert_ok!(builder::call(addr).data((BOB_ADDR, H256::zero()).encode()).build()); + + // create EOA + let _ = ::Currency::set_balance( + &::AddressMapper::to_account_id(&BOB_ADDR), + 1_000_000, + ); + + // EOA returns empty code hash + assert_ok!(builder::call(addr) + .data((BOB_ADDR, crate::exec::EMPTY_CODE_HASH).encode()) + .build()); + }); +} + +#[test] +fn code_size_works() { + let (tester_code, _) = compile_module("extcodesize").unwrap(); + let tester_code_len = tester_code.len() as u64; + + let (dummy_code, _) = compile_module("dummy").unwrap(); + let dummy_code_len = dummy_code.len() as u64; + + ExtBuilder::default().existential_deposit(1).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr: tester_addr, .. } = + builder::bare_instantiate(Code::Upload(tester_code)).build_and_unwrap_contract(); + let Contract { addr: dummy_addr, .. } = + builder::bare_instantiate(Code::Upload(dummy_code)).build_and_unwrap_contract(); + + // code size of another contract address + assert_ok!(builder::call(tester_addr).data((dummy_addr, dummy_code_len).encode()).build()); + + // code size of own contract address + assert_ok!(builder::call(tester_addr) + .data((tester_addr, tester_code_len).encode()) + .build()); + + // code size of non contract accounts + assert_ok!(builder::call(tester_addr).data(([8u8; 20], 0u64).encode()).build()); + }); +} + +#[test] +fn origin_must_be_mapped() { + let (code, hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + ::Currency::set_balance(&ALICE, 1_000_000); + ::Currency::set_balance(&EVE, 1_000_000); + + let eve = RuntimeOrigin::signed(EVE); + + // alice can instantiate as she doesn't need a mapping + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // without a mapping eve can neither call nor instantiate + assert_err!( + builder::bare_call(addr).origin(eve.clone()).build().result, + >::AccountUnmapped + ); + assert_err!( + builder::bare_instantiate(Code::Existing(hash)) + .origin(eve.clone()) + .build() + .result, + >::AccountUnmapped + ); + + // after mapping eve is usable as an origin + >::map_account(eve.clone()).unwrap(); + assert_ok!(builder::bare_call(addr).origin(eve.clone()).build().result); + assert_ok!(builder::bare_instantiate(Code::Existing(hash)).origin(eve).build().result); + }); +} + +#[test] +fn mapped_address_works() { + let (code, _) = compile_module("terminate_and_send_to_argument").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + ::Currency::set_balance(&ALICE, 1_000_000); + + // without a mapping everything will be send to the fallback account + // since EVE_FALLBACK does not exist it will be funded from the origin + let Contract { addr, account_id } = + builder::bare_instantiate(Code::Upload(code.clone())).build_and_unwrap_contract(); + ::Currency::set_balance(&account_id, 200); + assert_eq!(::Currency::total_balance(&EVE_FALLBACK), 0); + builder::bare_call(addr).data(EVE_ADDR.encode()).build_and_unwrap_result(); + assert_eq!(::Currency::total_balance(&EVE_FALLBACK), 200); + assert_eq!(::Currency::total_balance(&EVE), 0); + assert_eq!(::Currency::total_balance(&ALICE), 1_000_000 - 100); + + // after mapping it will be sent to the real eve account + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + ::Currency::set_balance(&account_id, 200); + ::AddressMapper::map_no_deposit(&EVE).unwrap(); + assert_eq!(::Currency::total_balance(&EVE), 0); + builder::bare_call(addr).data(EVE_ADDR.encode()).build_and_unwrap_result(); + assert_eq!(::Currency::total_balance(&EVE_FALLBACK), 200); + assert_eq!(::Currency::total_balance(&EVE), 200); + assert_eq!(::Currency::total_balance(&ALICE), 1_000_000 - 200); + }); +} + +#[test] +fn recovery_works() { + let (code, _) = compile_module("terminate_and_send_to_argument").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + ::Currency::set_balance(&ALICE, 1_000_000); + + let initial_contract_balance = 100_000; + + // eve puts her AccountId20 as argument to terminate but forgot to register + // her AccountId32 first so now the funds are trapped in her fallback account + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code.clone())) + .native_value(initial_contract_balance) + .build_and_unwrap_contract(); + assert_eq!(::Currency::total_balance(&EVE), 0); + assert_eq!(::Currency::total_balance(&EVE_FALLBACK), 0); + builder::bare_call(addr).data(EVE_ADDR.encode()).build_and_unwrap_result(); + assert_eq!( + ::Currency::total_balance(&EVE_FALLBACK), + initial_contract_balance + 100, + ); + assert_eq!(::Currency::total_balance(&EVE), 0); + + let call = RuntimeCall::Balances(pallet_balances::Call::transfer_all { + dest: EVE, + keep_alive: false, + }); + + // she now uses the recovery function to move all funds from the fallback + // account to her real account + >::dispatch_as_fallback_account(RuntimeOrigin::signed(EVE), Box::new(call)) + .unwrap(); + assert_eq!(::Currency::total_balance(&EVE_FALLBACK), 0); + assert_eq!(::Currency::total_balance(&EVE), initial_contract_balance + 100); + }); +} + +#[test] +fn gas_limit_api_works() { + let (code, _) = compile_module("gas_limit").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // Create fixture: Constructor does nothing + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + // Call the contract: It echoes back the value returned by the gas limit API. + let received = builder::bare_call(addr).build_and_unwrap_result(); + assert_eq!(received.flags, ReturnFlags::empty()); + assert_eq!( + u64::from_le_bytes(received.data[..].try_into().unwrap()), + >::evm_block_gas_limit().saturated_into::(), + ); + }); +} + +#[test] +fn unknown_syscall_rejected() { + let (code, _) = compile_module("unknown_syscall").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + ::Currency::set_balance(&ALICE, 1_000_000); + + assert_err!( + builder::bare_instantiate(Code::Upload(code)).build().result, + >::CodeRejected, + ) + }); +} + +#[test] +fn unstable_interface_rejected() { + let (code, _) = compile_module("unstable_interface").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + ::Currency::set_balance(&ALICE, 1_000_000); + + Test::set_unstable_interface(false); + assert_err!( + builder::bare_instantiate(Code::Upload(code.clone())).build().result, + >::CodeRejected, + ); + + Test::set_unstable_interface(true); + assert_ok!(builder::bare_instantiate(Code::Upload(code)).build().result); + }); +} + +#[test] +fn tracing_works_for_transfers() { + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000); + let mut tracer = CallTracer::new(Default::default()); + trace(&mut tracer, || { + builder::bare_call(BOB_ADDR).evm_value(10.into()).build_and_unwrap_result(); + }); + + let trace = tracer.collect_trace(); + assert_eq!( + trace, + Some(CallTrace { + from: ALICE_ADDR, + to: BOB_ADDR, + value: Some(U256::from(10)), + call_type: CallType::Call, + ..Default::default() + }) + ) + }); +} + +fn replace_actual_gas(expected: &mut CallTrace, actual: &CallTrace) { + expected.gas = actual.gas; + expected.gas_used = actual.gas_used; + expected + .calls + .iter_mut() + .zip(actual.calls.iter()) + .for_each(|(e, a)| replace_actual_gas(e, a)); +} + +#[test] +fn call_tracing_works() { + use crate::evm::*; + use CallType::*; + let (code, _code_hash) = compile_module("tracing").unwrap(); + let (binary_callee, _) = compile_module("tracing_callee").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000); + + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(binary_callee)).build_and_unwrap_contract(); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).evm_value(10_000_000.into()).build_and_unwrap_contract(); + + + let tracer_configs = vec![ + CallTracerConfig{ with_logs: false, only_top_call: false}, + CallTracerConfig{ with_logs: true, only_top_call: false}, + CallTracerConfig{ with_logs: false, only_top_call: true}, + CallTracerConfig{ with_logs: true, only_top_call: true}, + ]; + + // Verify that the first trace report the same weight reported by bare_call + let mut tracer = CallTracer::new(CallTracerConfig {with_logs: true, only_top_call: false}); + let gas_used = trace(&mut tracer, || { + let a = builder::bare_call(addr).data((3u32, addr_callee).encode()).build(); + a.gas_consumed + }); + let gas_trace = tracer.collect_trace().unwrap(); + assert_eq!(&gas_trace.gas_used, &gas_used.into()); + + for config in tracer_configs { + let logs = if config.with_logs { + vec![ + CallLog { + address: addr, + topics: Default::default(), + data: b"before".to_vec().into(), + position: 0, + }, + CallLog { + address: addr, + topics: Default::default(), + data: b"after".to_vec().into(), + position: 1, + }, + ] + } else { + vec![] + }; + + let calls = if config.only_top_call { + vec![] + } else { + vec![ + CallTrace { + from: addr, + to: addr_callee, + input: 2u32.encode().into(), + output: hex_literal::hex!( + "08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a546869732066756e6374696f6e20616c77617973206661696c73000000000000" + ).to_vec().into(), + revert_reason: Some("revert: This function always fails".to_string()), + error: Some("execution reverted".to_string()), + call_type: Call, + value: Some(U256::from(0)), + gas: 0.into(), + gas_used: 0.into(), + ..Default::default() + }, + CallTrace { + from: addr, + to: addr, + input: (2u32, addr_callee).encode().into(), + call_type: Call, + logs: logs.clone(), + value: Some(U256::from(0)), + gas: 0.into(), + gas_used: 0.into(), + calls: vec![ + CallTrace { + from: addr, + to: addr_callee, + input: 1u32.encode().into(), + output: Default::default(), + error: Some("ContractTrapped".to_string()), + call_type: Call, + value: Some(U256::from(0)), + gas: 0.into(), + gas_used: 0.into(), + ..Default::default() + }, + CallTrace { + from: addr, + to: addr, + input: (1u32, addr_callee).encode().into(), + call_type: Call, + logs: logs.clone(), + value: Some(U256::from(0)), + gas: 0.into(), + gas_used: 0.into(), + calls: vec![ + CallTrace { + from: addr, + to: addr_callee, + input: 0u32.encode().into(), + output: 0u32.to_le_bytes().to_vec().into(), + call_type: Call, + value: Some(U256::from(0)), + gas: 0.into(), + gas_used: 0.into(), + ..Default::default() + }, + CallTrace { + from: addr, + to: addr, + input: (0u32, addr_callee).encode().into(), + call_type: Call, + value: Some(U256::from(0)), + gas: 0.into(), + gas_used: 0.into(), + calls: vec![ + CallTrace { + from: addr, + to: BOB_ADDR, + value: Some(U256::from(100)), + call_type: CallType::Call, + ..Default::default() + } + ], + child_call_count: 1, + ..Default::default() + }, + ], + child_call_count: 2, + ..Default::default() + }, + ], + child_call_count: 2, + ..Default::default() + }, + ] + }; + + let mut tracer = CallTracer::new(config); + trace(&mut tracer, || { + builder::bare_call(addr).data((3u32, addr_callee).encode()).build() + }); + + let trace = tracer.collect_trace(); + let mut expected_trace = CallTrace { + from: ALICE_ADDR, + to: addr, + input: (3u32, addr_callee).encode().into(), + call_type: Call, + logs: logs.clone(), + value: Some(U256::from(0)), + calls: calls, + child_call_count: 2, + gas: 0.into(), + gas_used: 0.into(), + ..Default::default() + }; + + // No need to manually adjust expected gas every time the weights change + if let Some(actual) = &trace { + replace_actual_gas(&mut expected_trace, actual); + } + + assert_eq!( + trace, + expected_trace.into(), + ); + } + }); +} + +#[test] +fn create_call_tracing_works() { + use crate::evm::*; + let (code, code_hash) = compile_module("create2_with_value").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000); + + let mut tracer = CallTracer::new(Default::default()); + + let Contract { addr, .. } = trace(&mut tracer, || { + builder::bare_instantiate(Code::Upload(code.clone())) + .evm_value(100.into()) + .salt(None) + .build_and_unwrap_contract() + }); + + let call_trace = tracer.collect_trace().unwrap(); + assert_eq!( + call_trace, + CallTrace { + from: ALICE_ADDR, + to: addr, + value: Some(100.into()), + input: Bytes(code.clone()), + call_type: CallType::Create, + gas: call_trace.gas, + gas_used: call_trace.gas_used, + ..Default::default() + } + ); + + let mut tracer = CallTracer::new(Default::default()); + let data = b"garbage"; + let input = (code_hash, data).encode(); + trace(&mut tracer, || { + assert_ok!(builder::call(addr).data(input.clone()).build()); + }); + + let call_trace = tracer.collect_trace().unwrap(); + let child_addr = crate::address::create2(&addr, &code, data, &[1u8; 32]); + + assert_eq!( + call_trace, + CallTrace { + from: ALICE_ADDR, + to: addr, + value: Some(0.into()), + input: input.clone().into(), + gas: call_trace.gas, + gas_used: call_trace.gas_used, + calls: vec![CallTrace { + from: addr, + input: input.clone().into(), + to: child_addr, + value: Some(0.into()), + call_type: CallType::Create2, + gas: call_trace.calls[0].gas, + gas_used: call_trace.calls[0].gas_used, + ..Default::default() + },], + child_call_count: 1, + ..Default::default() + } + ); + }); +} + +#[test] +fn prestate_tracing_works() { + use crate::evm::*; + use alloc::collections::BTreeMap; + + let (dummy_code, _) = compile_module("dummy").unwrap(); + let (code, _) = compile_module("tracing").unwrap(); + let (callee_code, _) = compile_module("tracing_callee").unwrap(); + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000); + + let Contract { addr: addr_callee, .. } = + builder::bare_instantiate(Code::Upload(callee_code.clone())) + .build_and_unwrap_contract(); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code.clone())) + .native_value(10) + .build_and_unwrap_contract(); + + // redact balance so that tests are resilient to weight changes + let alice_redacted_balance = Some(U256::from(1)); + + struct TestCase { + description: &'static str, + exec_call: Box, + config: PrestateTracerConfig, + expected_trace: PrestateTrace, + } + + let test_cases: Vec = vec![ + TestCase { + description: "prestate mode with cross-contract call", + exec_call: Box::new({ + let addr = addr; + let addr_callee = addr_callee; + move || { + builder::bare_call(addr) + .data((3u32, addr_callee).encode()) + .build_and_unwrap_result(); + } + }), + config: PrestateTracerConfig { + diff_mode: false, + disable_storage: false, + disable_code: false, + }, + expected_trace: PrestateTrace::Prestate(BTreeMap::from([ + ( + ALICE_ADDR, + PrestateTraceInfo { + balance: alice_redacted_balance, + nonce: Some(2), + ..Default::default() + }, + ), + ( + BOB_ADDR, + PrestateTraceInfo { balance: Some(U256::from(0u64)), ..Default::default() }, + ), + ( + addr_callee, + PrestateTraceInfo { + balance: Some(U256::from(0u64)), + code: Some(Bytes(callee_code.clone())), + nonce: Some(1), + ..Default::default() + }, + ), + ( + addr, + PrestateTraceInfo { + balance: Some(U256::from(10_000_000u64)), + code: Some(Bytes(code.clone())), + nonce: Some(1), + ..Default::default() + }, + ), + ])), + }, + TestCase { + description: "diff mode with cross-contract call", + exec_call: Box::new({ + let addr = addr; + let addr_callee = addr_callee; + move || { + builder::bare_call(addr) + .data((3u32, addr_callee).encode()) + .build_and_unwrap_result(); + } + }), + config: PrestateTracerConfig { + diff_mode: true, + disable_storage: false, + disable_code: false, + }, + expected_trace: PrestateTrace::DiffMode { + pre: BTreeMap::from([ + ( + BOB_ADDR, + PrestateTraceInfo { + balance: Some(U256::from(100u64)), + ..Default::default() + }, + ), + ( + addr, + PrestateTraceInfo { + balance: Some(U256::from(9_999_900u64)), + code: Some(Bytes(code.clone())), + nonce: Some(1), + ..Default::default() + }, + ), + ]), + post: BTreeMap::from([ + ( + BOB_ADDR, + PrestateTraceInfo { + balance: Some(U256::from(200u64)), + ..Default::default() + }, + ), + ( + addr, + PrestateTraceInfo { + balance: Some(U256::from(9_999_800u64)), + ..Default::default() + }, + ), + ]), + }, + }, + TestCase { + description: "diff mode with contract instantiation", + exec_call: Box::new({ + let dummy_code = dummy_code.clone(); + move || { + builder::bare_instantiate(Code::Upload(dummy_code)) + .salt(None) + .build_and_unwrap_result(); + } + }), + config: PrestateTracerConfig { + diff_mode: true, + disable_storage: false, + disable_code: false, + }, + expected_trace: PrestateTrace::DiffMode { + pre: BTreeMap::from([( + ALICE_ADDR, + PrestateTraceInfo { + balance: alice_redacted_balance, + nonce: Some(2), + ..Default::default() + }, + )]), + post: BTreeMap::from([ + ( + ALICE_ADDR, + PrestateTraceInfo { + balance: alice_redacted_balance, + nonce: Some(3), + ..Default::default() + }, + ), + ( + create1(&ALICE_ADDR, 1), + PrestateTraceInfo { + code: Some(dummy_code.clone().into()), + balance: Some(U256::from(0)), + nonce: Some(1), + ..Default::default() + }, + ), + ]), + }, + }, + ]; + + for TestCase { description, exec_call, config, expected_trace } in test_cases.into_iter() { + let mut tracer = PrestateTracer::::new(config); + trace(&mut tracer, || { + exec_call(); + }); + + let mut trace = tracer.collect_trace(); + + // redact alice balance + match trace { + PrestateTrace::DiffMode { ref mut pre, ref mut post } => { + pre.get_mut(&ALICE_ADDR).map(|info| { + info.balance = alice_redacted_balance; + }); + post.get_mut(&ALICE_ADDR).map(|info| { + info.balance = alice_redacted_balance; + }); + }, + PrestateTrace::Prestate(ref mut pre) => { + pre.get_mut(&ALICE_ADDR).map(|info| { + info.balance = alice_redacted_balance; + }); + }, + } + + assert_eq!(trace, expected_trace, "Trace mismatch for: {description}"); + } + }); +} + +#[test] +fn unknown_precompiles_revert() { + let (code, _code_hash) = compile_module("read_only_call").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let cases: Vec<(H160, Box)> = vec![( + H160::from_low_u64_be(0x0a), + Box::new(|result| { + assert_err!(result, >::UnsupportedPrecompileAddress); + }), + )]; + + for (callee_addr, assert_result) in cases { + let result = + builder::bare_call(addr).data((callee_addr, [0u8; 0]).encode()).build().result; + assert_result(result); + } + }); +} + +#[test] +fn pure_precompile_works() { + use hex_literal::hex; + + let cases = vec![ + ( + "ECRecover", + H160::from_low_u64_be(1), + hex!("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549").to_vec(), + hex!("000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b").to_vec(), + ), + ( + "Sha256", + H160::from_low_u64_be(2), + hex!("ec07171c4f0f0e2b").to_vec(), + hex!("d0591ea667763c69a5f5a3bae657368ea63318b2c9c8349cccaf507e3cbd7c7a").to_vec(), + ), + ( + "Ripemd160", + H160::from_low_u64_be(3), + hex!("ec07171c4f0f0e2b").to_vec(), + hex!("000000000000000000000000a9c5ebaf7589fd8acfd542c3a008956de84fbeb7").to_vec(), + ), + ( + "Identity", + H160::from_low_u64_be(4), + [42u8; 128].to_vec(), + [42u8; 128].to_vec(), + ), + ( + "Modexp", + H160::from_low_u64_be(5), + hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002003fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2efffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f").to_vec(), + hex!("0000000000000000000000000000000000000000000000000000000000000001").to_vec(), + ), + ( + "Bn128Add", + H160::from_low_u64_be(6), + hex!("18b18acfb4c2c30276db5411368e7185b311dd124691610c5d3b74034e093dc9063c909c4720840cb5134cb9f59fa749755796819658d32efc0d288198f3726607c2b7f58a84bd6145f00c9c2bc0bb1a187f20ff2c92963a88019e7c6a014eed06614e20c147e940f2d70da3f74c9a17df361706a4485c742bd6788478fa17d7").to_vec(), + hex!("2243525c5efd4b9c3d3c45ac0ca3fe4dd85e830a4ce6b65fa1eeaee202839703301d1d33be6da8e509df21cc35964723180eed7532537db9ae5e7d48f195c915").to_vec(), + ), + ( + "Bn128Mul", + H160::from_low_u64_be(7), + hex!("2bd3e6d0f3b142924f5ca7b49ce5b9d54c4703d7ae5648e61d02268b1a0a9fb721611ce0a6af85915e2f1d70300909ce2e49dfad4a4619c8390cae66cefdb20400000000000000000000000000000000000000000000000011138ce750fa15c2").to_vec(), + hex!("070a8d6a982153cae4be29d434e8faef8a47b274a053f5a4ee2a6c9c13c31e5c031b8ce914eba3a9ffb989f9cdd5b0f01943074bf4f0f315690ec3cec6981afc").to_vec(), + ), + ( + "Bn128Pairing", + H160::from_low_u64_be(8), + hex!("1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f593034dd2920f673e204fee2811c678745fc819b55d3e9d294e45c9b03a76aef41209dd15ebff5d46c4bd888e51a93cf99a7329636c63514396b4a452003a35bf704bf11ca01483bfa8b34b43561848d28905960114c8ac04049af4b6315a416782bb8324af6cfc93537a2ad1a445cfd0ca2a71acd7ac41fadbf933c2a51be344d120a2a4cf30c1bf9845f20c6fe39e07ea2cce61f0c9bb048165fe5e4de877550111e129f1cf1097710d41c4ac70fcdfa5ba2023c6ff1cbeac322de49d1b6df7c2032c61a830e3c17286de9462bf242fca2883585b93870a73853face6a6bf411198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c21800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa").to_vec(), + hex!("0000000000000000000000000000000000000000000000000000000000000001").to_vec(), + ), + ( + "Blake2F", + H160::from_low_u64_be(9), + hex!("0000000048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e1319cde05b61626300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000001").to_vec(), + hex!("08c9bcf367e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d282e6ad7f520e511f6c3e2b8c68059b9442be0454267ce079217e1319cde05b").to_vec(), + ), + ]; + + for (description, precompile_addr, input, output) in cases { + let (code, _code_hash) = compile_module("call_and_return").unwrap(); + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(1_000) + .build_and_unwrap_contract(); + + let result = builder::bare_call(addr) + .data( + (&precompile_addr, 100u64) + .encode() + .into_iter() + .chain(input) + .collect::>(), + ) + .build_and_unwrap_result(); + + assert_eq!( + Pallet::::evm_balance(&precompile_addr), + U256::from(100), + "{description}: unexpected balance" + ); + assert_eq!( + alloy_core::hex::encode(result.data), + alloy_core::hex::encode(output), + "{description} Unexpected output for precompile: {precompile_addr:?}", + ); + assert_eq!(result.flags, ReturnFlags::empty()); + }); + } +} + +#[test] +fn precompiles_work() { + use crate::precompiles::Precompile; + use alloy_core::sol_types::{Panic, PanicKind, Revert, SolError, SolInterface, SolValue}; + use precompiles::{INoInfo, NoInfo}; + + let precompile_addr = H160(NoInfo::::MATCHER.base_address()); + + let cases = vec![ + ( + INoInfo::INoInfoCalls::identity(INoInfo::identityCall { number: 42u64.into() }) + .abi_encode(), + 42u64.abi_encode(), + RuntimeReturnCode::Success, + ), + ( + INoInfo::INoInfoCalls::reverts(INoInfo::revertsCall { error: "panic".to_string() }) + .abi_encode(), + Revert::from("panic").abi_encode(), + RuntimeReturnCode::CalleeReverted, + ), + ( + INoInfo::INoInfoCalls::panics(INoInfo::panicsCall {}).abi_encode(), + Panic::from(PanicKind::Assert).abi_encode(), + RuntimeReturnCode::CalleeReverted, + ), + ( + INoInfo::INoInfoCalls::errors(INoInfo::errorsCall {}).abi_encode(), + Vec::new(), + RuntimeReturnCode::CalleeTrapped, + ), + // passing non decodeable input reverts with solidity panic + ( + b"invalid".to_vec(), + Panic::from(PanicKind::ResourceError).abi_encode(), + RuntimeReturnCode::CalleeReverted, + ), + ( + INoInfo::INoInfoCalls::passData(INoInfo::passDataCall { + inputLen: limits::CALLDATA_BYTES, + }) + .abi_encode(), + Vec::new(), + RuntimeReturnCode::Success, + ), + ( + INoInfo::INoInfoCalls::passData(INoInfo::passDataCall { + inputLen: limits::CALLDATA_BYTES + 1, + }) + .abi_encode(), + Vec::new(), + RuntimeReturnCode::CalleeTrapped, + ), + ( + INoInfo::INoInfoCalls::returnData(INoInfo::returnDataCall { + returnLen: limits::CALLDATA_BYTES - 4, + }) + .abi_encode(), + vec![42u8; limits::CALLDATA_BYTES as usize - 4], + RuntimeReturnCode::Success, + ), + ( + INoInfo::INoInfoCalls::returnData(INoInfo::returnDataCall { + returnLen: limits::CALLDATA_BYTES + 1, + }) + .abi_encode(), + vec![], + RuntimeReturnCode::CalleeTrapped, + ), + ]; + + for (input, output, error_code) in cases { + let (code, _code_hash) = compile_module("call_and_returncode").unwrap(); + ExtBuilder::default().build().execute_with(|| { + let id = ::AddressMapper::to_account_id(&precompile_addr); + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(1000) + .build_and_unwrap_contract(); + + let result = builder::bare_call(addr) + .data( + (&precompile_addr, 0u64).encode().into_iter().chain(input).collect::>(), + ) + .build_and_unwrap_result(); + + // no account or contract info should be created for a NoInfo pre-compile + assert!(get_contract_checked(&precompile_addr).is_none()); + assert!(!System::account_exists(&id)); + assert_eq!(Pallet::::evm_balance(&precompile_addr), U256::zero()); + + assert_eq!(result.flags, ReturnFlags::empty()); + assert_eq!(u32::from_le_bytes(result.data[..4].try_into().unwrap()), error_code as u32); + assert_eq!( + &result.data[4..], + &output, + "Unexpected output for precompile: {precompile_addr:?}", + ); + }); + } +} + +#[test] +fn precompiles_with_info_creates_contract() { + use crate::precompiles::Precompile; + use alloy_core::sol_types::SolInterface; + use precompiles::{IWithInfo, WithInfo}; + + let precompile_addr = H160(WithInfo::::MATCHER.base_address()); + + let cases = vec![( + IWithInfo::IWithInfoCalls::dummy(IWithInfo::dummyCall {}).abi_encode(), + Vec::::new(), + RuntimeReturnCode::Success, + )]; + + for (input, output, error_code) in cases { + let (code, _code_hash) = compile_module("call_and_returncode").unwrap(); + ExtBuilder::default().build().execute_with(|| { + let id = ::AddressMapper::to_account_id(&precompile_addr); + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(1000) + .build_and_unwrap_contract(); + + let result = builder::bare_call(addr) + .data( + (&precompile_addr, 0u64).encode().into_iter().chain(input).collect::>(), + ) + .build_and_unwrap_result(); + + // a pre-compile with contract info should create an account on first call + assert!(get_contract_checked(&precompile_addr).is_some()); + assert!(System::account_exists(&id)); + assert_eq!(Pallet::::evm_balance(&precompile_addr), U256::from(0)); + + assert_eq!(result.flags, ReturnFlags::empty()); + assert_eq!(u32::from_le_bytes(result.data[..4].try_into().unwrap()), error_code as u32); + assert_eq!( + &result.data[4..], + &output, + "Unexpected output for precompile: {precompile_addr:?}", + ); + }); + } +} + +#[test] +fn bump_nonce_once_works() { + let (code, hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + frame_system::Account::::mutate(&ALICE, |account| account.nonce = 1); + + let do_not_bump = ExecConfig::new_substrate_tx_without_bump(); + + let _ = ::Currency::set_balance(&BOB, 1_000_000); + frame_system::Account::::mutate(&BOB, |account| account.nonce = 1); + + builder::bare_instantiate(Code::Upload(code.clone())) + .origin(RuntimeOrigin::signed(ALICE)) + .salt(None) + .build_and_unwrap_result(); + assert_eq!(System::account_nonce(&ALICE), 2); + + // instantiate again is ok + let result = builder::bare_instantiate(Code::Existing(hash)) + .origin(RuntimeOrigin::signed(ALICE)) + .salt(None) + .build() + .result; + assert!(result.is_ok()); + + builder::bare_instantiate(Code::Upload(code.clone())) + .origin(RuntimeOrigin::signed(BOB)) + .exec_config(ExecConfig::new_substrate_tx_without_bump()) + .salt(None) + .build_and_unwrap_result(); + assert_eq!(System::account_nonce(&BOB), 1); + + // instantiate again should fail + let err = builder::bare_instantiate(Code::Upload(code)) + .origin(RuntimeOrigin::signed(BOB)) + .exec_config(do_not_bump) + .salt(None) + .build() + .result + .unwrap_err(); + + assert_eq!(err, >::DuplicateContract.into()); + }); +} + +#[test] +fn code_size_for_precompiles_works() { + use crate::precompiles::Precompile; + use precompiles::NoInfo; + + let builtin_precompile = H160(NoInfo::::MATCHER.base_address()); + let primitive_precompile = H160::from_low_u64_be(1); + + let (code, _code_hash) = compile_module("extcodesize").unwrap(); + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .native_value(1000) + .build_and_unwrap_contract(); + + // the primitive pre-compiles return 0 code size on eth + builder::bare_call(addr) + .data((&primitive_precompile, 0u64).encode()) + .build_and_unwrap_result(); + + // other precompiles should return the minimal evm revert code + builder::bare_call(addr) + .data((&builtin_precompile, 5u64).encode()) + .build_and_unwrap_result(); + }); +} + +#[test] +fn call_data_limit_is_enforced_subcalls() { + let (code, _code_hash) = compile_module("call_with_input_size").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let cases: Vec<(u32, Box)> = vec![ + ( + 0_u32, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + 1_u32, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + limits::CALLDATA_BYTES, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + limits::CALLDATA_BYTES + 1, + Box::new(|result| { + assert_err!(result, >::CallDataTooLarge); + }), + ), + ]; + + for (callee_input_size, assert_result) in cases { + let result = builder::bare_call(addr).data(callee_input_size.encode()).build().result; + assert_result(result); + } + }); +} + +#[test] +fn call_data_limit_is_enforced_root_call() { + let (code, _code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let cases: Vec<(H160, u32, Box)> = vec![ + ( + addr, + 0_u32, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + addr, + 1_u32, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + addr, + limits::CALLDATA_BYTES, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + addr, + limits::CALLDATA_BYTES + 1, + Box::new(|result| { + assert_err!(result, >::CallDataTooLarge); + }), + ), + ( + // limit is not enforced when tx calls EOA + BOB_ADDR, + limits::CALLDATA_BYTES + 1, + Box::new(|result| { + assert_ok!(result); + }), + ), + ]; + + for (addr, callee_input_size, assert_result) in cases { + let result = builder::bare_call(addr) + .data(vec![42; callee_input_size as usize]) + .build() + .result; + assert_result(result); + } + }); +} + +#[test] +fn return_data_limit_is_enforced() { + let (code, _code_hash) = compile_module("return_sized").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let cases: Vec<(u32, Box)> = vec![ + ( + 1_u32, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + limits::CALLDATA_BYTES, + Box::new(|result| { + assert_ok!(result); + }), + ), + ( + limits::CALLDATA_BYTES + 1, + Box::new(|result| { + assert_err!(result, >::ReturnDataTooLarge); + }), + ), + ]; + + for (return_size, assert_result) in cases { + let result = builder::bare_call(addr).data(return_size.encode()).build().result; + assert_result(result); + } + }); +} + +#[test] +fn storage_deposit_from_hold_works() { + let ed = 200; + let (binary, code_hash) = compile_module("dummy").unwrap(); + ExtBuilder::default().existential_deposit(ed).build().execute_with(|| { + let hold_initial = 500_000; + ::Currency::set_balance(&ALICE, 1_000_000); + ::FeeInfo::deposit_txfee(::Currency::issue(hold_initial)); + let mut exec_config = ExecConfig::new_substrate_tx(); + exec_config.collect_deposit_from_hold = Some((0u32.into(), Default::default())); + + // Instantiate the BOB contract. + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary)) + .exec_config(exec_config) + .native_value(1_000) + .build_and_unwrap_contract(); + + // Check that the BOB contract has been instantiated. + get_contract(&addr); + + let account = ::AddressMapper::to_account_id(&addr); + let base_deposit = contract_base_deposit(&addr); + let code_deposit = get_code_deposit(&code_hash); + assert!(base_deposit > 0); + assert!(code_deposit > 0); + + assert_eq!( + get_balance_on_hold(&HoldReason::StorageDepositReserve.into(), &account), + base_deposit, + ); + assert_eq!( + ::FeeInfo::remaining_txfee(), + hold_initial - base_deposit - code_deposit - ed, + ); + }); +} + +#[test] +fn eip3607_reject_tx_from_contract_or_precompile() { + let (binary, _code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(200).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // the origins from which we try to call a dispatchable + let Contract { addr: contract_addr, .. } = + builder::bare_instantiate(Code::Upload(binary)).build_and_unwrap_contract(); + assert!(>::is_contract(&contract_addr)); + let blake2_addr = H160::from_low_u64_be(9); + let system_addr = H160::from_low_u64_be(0x900); + let addresses = [contract_addr, blake2_addr, system_addr]; + + // used to test `dispatch_as_fallback_account` + let call = Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_all { + dest: EVE, + keep_alive: false, + })); + + for address in addresses.iter() { + let origin = ::AddressMapper::to_fallback_account_id(address); + + let result = + builder::call(BOB_ADDR).origin(RuntimeOrigin::signed(origin.clone())).build(); + assert_err!(result, DispatchError::BadOrigin); + + let result = builder::eth_call(BOB_ADDR) + .origin(Origin::EthTransaction(origin.clone()).into()) + .build(); + assert_err!(result, DispatchError::BadOrigin); + + let result = builder::instantiate(Default::default()) + .origin(RuntimeOrigin::signed(origin.clone())) + .build(); + assert_err!(result, DispatchError::BadOrigin); + + let result = builder::eth_instantiate_with_code(Default::default()) + .origin(Origin::EthTransaction(origin.clone()).into()) + .build(); + assert_err!(result, DispatchError::BadOrigin); + + let result = builder::instantiate_with_code(Default::default()) + .origin(RuntimeOrigin::signed(origin.clone())) + .build(); + assert_err!(result, DispatchError::BadOrigin); + + let result = >::upload_code( + RuntimeOrigin::signed(origin.clone()), + Default::default(), + >::MAX, + ); + assert_err!(result, DispatchError::BadOrigin); + + let result = >::map_account(RuntimeOrigin::signed(origin.clone())); + assert_err!(result, DispatchError::BadOrigin); + + let result = >::dispatch_as_fallback_account( + RuntimeOrigin::signed(origin.clone()), + call.clone(), + ); + assert_err!(result, DispatchError::BadOrigin); + } + }); +} + +#[test] +fn eip3607_allow_tx_from_contract_or_precompile_if_debug_setting_configured() { + let (binary, code_hash) = compile_module("dummy").unwrap(); + + let genesis_config = GenesisConfig:: { + debug_settings: Some(DebugSettings::new(false, true, false)), + ..Default::default() + }; + + ExtBuilder::default() + .genesis_config(Some(genesis_config)) + .existential_deposit(200) + .build() + .execute_with(|| { + DebugFlag::set(true); + + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + // the origins from which we try to call a dispatchable + let Contract { addr: contract_addr, .. } = + builder::bare_instantiate(Code::Upload(binary.clone())).build_and_unwrap_contract(); + + assert!(>::is_contract(&contract_addr)); + + let blake2_addr = H160::from_low_u64_be(9); + let system_addr = H160::from_low_u64_be(0x900); + let addresses = [contract_addr, blake2_addr, system_addr]; + + for address in addresses { + let origin = ::AddressMapper::to_fallback_account_id(&address); + + let _ = ::Currency::set_balance(&origin, 10_000_000_000_000); + + let result = + builder::call(BOB_ADDR).origin(RuntimeOrigin::signed(origin.clone())).build(); + assert_ok!(result); + + let result = builder::eth_call(BOB_ADDR) + .origin(Origin::EthTransaction(origin.clone()).into()) + .build(); + assert_ok!(result); + + let result = builder::instantiate(code_hash) + .origin(RuntimeOrigin::signed(origin.clone())) + .build(); + assert_ok!(result); + + let result = builder::eth_instantiate_with_code(binary.clone()) + .origin(Origin::EthTransaction(origin.clone()).into()) + .build(); + assert_ok!(result); + + let result = >::dispatch_as_fallback_account( + RuntimeOrigin::signed(origin.clone()), + Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_all { + dest: EVE, + keep_alive: false, + })), + ); + assert_ok!(result); + + let result = >::upload_code( + RuntimeOrigin::signed(origin.clone()), + binary.clone(), + >::MAX, + ); + assert_ok!(result); + } + }); +} + +#[test] +fn get_set_storage_key_works() { + let (code, _code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let contract_key_to_test = [1; 32]; + // Checking non-existing keys gets created. + let storage_value = Pallet::::get_storage(addr, contract_key_to_test).unwrap(); + assert_eq!(storage_value, None); + + let value_to_write = Some(vec![1, 2, 3]); + let write_result = + Pallet::::set_storage(addr, contract_key_to_test, value_to_write.clone()) + .unwrap(); + assert_eq!(write_result, WriteOutcome::New); + let storage_value = Pallet::::get_storage(addr, contract_key_to_test).unwrap(); + assert_eq!(storage_value, value_to_write); + + // Check existing keys overwrite + + let new_value_to_write = Some(vec![5, 1, 2, 3]); + let write_result = + Pallet::::set_storage(addr, contract_key_to_test, new_value_to_write.clone()) + .unwrap(); + assert_eq!( + write_result, + WriteOutcome::Overwritten(value_to_write.map(|v| v.len()).unwrap_or_default() as u32) + ); + let storage_value = Pallet::::get_storage(addr, contract_key_to_test).unwrap(); + assert_eq!(storage_value, new_value_to_write); + }); +} + +#[test] +fn get_set_storage_var_key_works() { + let (code, _code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = + builder::bare_instantiate(Code::Upload(code)).build_and_unwrap_contract(); + + let contract_key_to_test = vec![1; 85]; + // Checking non-existing keys gets created. + let storage_value = + Pallet::::get_storage_var_key(addr, contract_key_to_test.clone()).unwrap(); + assert_eq!(storage_value, None); + + let value_to_write = Some(vec![1, 2, 3]); + let write_result = Pallet::::set_storage_var_key( + addr, + contract_key_to_test.clone(), + value_to_write.clone(), + ) + .unwrap(); + assert_eq!(write_result, WriteOutcome::New); + let storage_value = + Pallet::::get_storage_var_key(addr, contract_key_to_test.clone()).unwrap(); + assert_eq!(storage_value, value_to_write); + + // Check existing keys overwrite + + let new_value_to_write = Some(vec![5, 1, 2, 3]); + let write_result = Pallet::::set_storage_var_key( + addr, + contract_key_to_test.clone(), + new_value_to_write.clone(), + ) + .unwrap(); + assert_eq!( + write_result, + WriteOutcome::Overwritten(value_to_write.map(|v| v.len()).unwrap_or_default() as u32) + ); + let storage_value = + Pallet::::get_storage_var_key(addr, contract_key_to_test.clone()).unwrap(); + assert_eq!(storage_value, new_value_to_write); + }); +} + +#[test] +fn get_set_immutables_works() { + let (code, _code_hash) = compile_module("immutable_data").unwrap(); + + ExtBuilder::default().existential_deposit(100).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + let data = [0xfe; 8]; + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code)) + .data(data.to_vec()) + .build_and_unwrap_contract(); + + // Checking non-existing keys gets created. + let immutable_data = Pallet::::get_immutables(addr).unwrap(); + assert_eq!(immutable_data, data.to_vec()); + + let new_data = [0xdeu8; 8].to_vec(); + + Pallet::::set_immutables(addr, BoundedVec::truncate_from(new_data.clone())).unwrap(); + let immutable_data = Pallet::::get_immutables(addr).unwrap(); + assert_eq!(immutable_data, new_data); + }); +} + +#[test] +fn consume_all_gas_works() { + let (code, code_hash) = compile_module("consume_all_gas").unwrap(); + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000); + + assert_eq!( + builder::bare_instantiate(Code::Upload(code)).build().weight_consumed, + WEIGHT_LIMIT, + "callvalue == 0 should consume all gas in deploy" + ); + assert_ne!( + builder::bare_instantiate(Code::Existing(code_hash)) + .evm_value(1.into()) + .build() + .weight_consumed, + WEIGHT_LIMIT, + "callvalue == 1 should not consume all gas in deploy" + ); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Existing(code_hash)) + .evm_value(2.into()) + .build_and_unwrap_contract(); + + assert_eq!( + builder::bare_call(addr).build().weight_consumed, + WEIGHT_LIMIT, + "callvalue == 0 should consume all gas" + ); + assert_ne!( + builder::bare_call(addr).evm_value(1.into()).build().weight_consumed, + WEIGHT_LIMIT, + "callvalue == 1 should not consume all gas" + ); + }); +} + +#[test] +fn existential_deposit_shall_not_be_charged_twice() { + let (code, _) = compile_module("dummy").unwrap(); + + let salt = [0u8; 32]; + + ExtBuilder::default().build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 100_000_000_000_000); + let callee_addr = create2( + &ALICE_ADDR, + &code, + &[0u8; 0], // empty input + &salt, + ); + let callee_account = ::AddressMapper::to_account_id(&callee_addr); + + // first send funds to callee_addr + let _ = ::Currency::set_balance(&callee_account, Contracts::min_balance()); + assert_eq!(get_balance(&callee_account), Contracts::min_balance()); + + // then deploy contract to callee_addr using create2 + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(code.clone())) + .salt(Some(salt)) + .build_and_unwrap_contract(); + + assert_eq!(callee_addr, addr); + + // check we charged ed only 1 time + assert_eq!(get_balance(&callee_account), Contracts::min_balance()); + }); +} + +#[test] +fn self_destruct_by_syscall_tracing_works() { + use crate::{ + evm::{PrestateTrace, PrestateTracer, PrestateTracerConfig, Tracer}, + Trace, + }; + + let (binary, _code_hash) = compile_module("self_destruct_by_syscall").unwrap(); + + struct TestCase { + description: &'static str, + create_tracer: Box Tracer>, + expected_trace_fn: Box) -> Trace>, + modify_trace_fn: Option Trace>>, + } + + let test_cases = vec![ + TestCase { + description: "CallTracer", + create_tracer: Box::new(|| Tracer::CallTracer(CallTracer::new(Default::default()))), + expected_trace_fn: Box::new(|addr, _binary| { + Trace::Call(CallTrace { + from: ALICE_ADDR, + to: addr, + call_type: CallType::Call, + value: Some(U256::zero()), + gas: 0.into(), + gas_used: 0.into(), + calls: vec![CallTrace { + from: addr, + to: DJANGO_ADDR, + gas: 0.into(), + + call_type: CallType::Selfdestruct, + value: Some(Pallet::::convert_native_to_evm(100_000u64)), + ..Default::default() + }], + ..Default::default() + }) + }), + modify_trace_fn: Some(Box::new(|mut actual_trace| { + if let Trace::Call(trace) = &mut actual_trace { + trace.gas = 0.into(); + trace.gas_used = 0.into(); + trace.calls[0].gas = 0.into(); + } + actual_trace + })), + }, + TestCase { + description: "PrestateTracer (diff mode)", + create_tracer: Box::new(|| { + Tracer::PrestateTracer(PrestateTracer::new(PrestateTracerConfig { + diff_mode: true, + disable_storage: false, + disable_code: false, + })) + }), + expected_trace_fn: Box::new(|addr, binary| { + use alloy_core::hex; + + let json = r#"{ + "pre": { + "{{ALICE_ADDR}}": { + "balance": "{{ALICE_BALANCE_PRE}}", + "nonce": 1 + }, + "{{DJANGO_ADDR}}": { + "balance": "{{DJANGO_BALANCE}}" + }, + "{{CONTRACT_ADDR}}": { + "balance": "{{CONTRACT_BALANCE}}", + "nonce": 1, + "code": "{{CONTRACT_CODE}}" + } + }, + "post": { + "{{ALICE_ADDR}}": { + "balance": "{{ALICE_BALANCE_POST}}" + }, + "{{DJANGO_ADDR}}": { + "balance": "{{DJANGO_BALANCE_POST}}" + }, + "{{CONTRACT_ADDR}}": { + "balance": "0x0" + } + } + }"#; + + let alice_balance_pre = Pallet::::evm_balance(&ALICE_ADDR); + let alice_balance_post = alice_balance_pre - 50_000_000u64; + let django_balance = Pallet::::evm_balance(&DJANGO_ADDR); + let contract_balance = Pallet::::evm_balance(&addr); + let django_balance_post = contract_balance; + + let json = json + .replace("{{ALICE_ADDR}}", &format!("{:#x}", ALICE_ADDR)) + .replace("{{ALICE_BALANCE_PRE}}", &format!("{:#x}", alice_balance_pre)) + .replace("{{ALICE_BALANCE_POST}}", &format!("{:#x}", alice_balance_post)) + .replace("{{CONTRACT_ADDR}}", &format!("{:#x}", addr)) + .replace("{{DJANGO_ADDR}}", &format!("{:#x}", DJANGO_ADDR)) + .replace("{{CONTRACT_ADDR}}", &format!("{:#x}", addr)) + .replace("{{DJANGO_BALANCE}}", &format!("{:#x}", django_balance)) + .replace("{{CONTRACT_BALANCE}}", &format!("{:#x}", contract_balance)) + .replace("{{DJANGO_BALANCE_POST}}", &format!("{:#x}", django_balance_post)) + .replace("{{CONTRACT_CODE}}", &format!("0x{}", hex::encode(&binary))); + + let expected: PrestateTrace = serde_json::from_str(&json).unwrap(); + Trace::Prestate(expected) + }), + modify_trace_fn: None, + }, + TestCase { + description: "PrestateTracer (prestate mode)", + create_tracer: Box::new(|| { + Tracer::PrestateTracer(PrestateTracer::new(PrestateTracerConfig { + diff_mode: false, + disable_storage: false, + disable_code: false, + })) + }), + expected_trace_fn: Box::new(|addr, binary| { + use alloy_core::hex; + + let json = r#"{ + "{{ALICE_ADDR}}": { + "balance": "{{ALICE_BALANCE}}", + "nonce": 1 + }, + "{{CONTRACT_ADDR}}": { + "balance": "{{CONTRACT_BALANCE}}", + "nonce": 1, + "code": "{{CONTRACT_CODE}}" + }, + "{{DJANGO_ADDR}}": { + "balance": "{{DJANGO_BALANCE}}" + } + }"#; + + let alice_balance = Pallet::::evm_balance(&ALICE_ADDR); + let contract_balance = Pallet::::evm_balance(&addr); + let django_balance = Pallet::::evm_balance(&DJANGO_ADDR); + + let json = json + .replace("{{ALICE_ADDR}}", &format!("{:#x}", ALICE_ADDR)) + .replace("{{CONTRACT_ADDR}}", &format!("{:#x}", addr)) + .replace("{{DJANGO_ADDR}}", &format!("{:#x}", DJANGO_ADDR)) + .replace("{{ALICE_BALANCE}}", &format!("{:#x}", alice_balance)) + .replace("{{CONTRACT_BALANCE}}", &format!("{:#x}", contract_balance)) + .replace("{{DJANGO_BALANCE}}", &format!("{:#x}", django_balance)) + .replace("{{CONTRACT_CODE}}", &format!("0x{}", hex::encode(&binary))); + + let expected: PrestateTrace = serde_json::from_str(&json).unwrap(); + Trace::Prestate(expected) + }), + modify_trace_fn: None, + }, + ]; + + for TestCase { description, create_tracer, expected_trace_fn, modify_trace_fn } in test_cases { + ExtBuilder::default().existential_deposit(50).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr, .. } = builder::bare_instantiate(Code::Upload(binary.clone())) + .native_value(100_000) + .build_and_unwrap_contract(); + + get_contract(&addr); + + let expected_trace = expected_trace_fn(addr, binary.clone()); + let mut tracer = create_tracer(); + trace(tracer.as_tracing(), || { + builder::call(addr).build().unwrap(); + }); + + let mut trace = tracer.collect_trace().unwrap(); + + if let Some(modify_trace_fn) = modify_trace_fn { + trace = modify_trace_fn(trace); + } + let trace_wrapped = match trace { + crate::evm::Trace::Call(ct) => Trace::Call(ct), + crate::evm::Trace::Prestate(pt) => Trace::Prestate(pt), + }; + + assert_eq!(trace_wrapped, expected_trace, "Trace mismatch for: {}", description); + }); + } +} + +#[test] +fn delegate_call_with_gas_limit() { + let (caller_binary, _caller_code_hash) = compile_module("delegate_call_evm").unwrap(); + let (callee_binary, _callee_code_hash) = compile_module("delegate_call_lib").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + // fails, not enough gas + assert_err!( + builder::bare_call(caller_addr) + .native_value(1337) + .data((callee_addr, 100u64).encode()) + .build() + .result, + Error::::ContractTrapped, + ); + + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((callee_addr, 100_000_000_000u64).encode()) + .build()); + }); +} + +#[test] +fn call_with_gas_limit() { + let (caller_binary, _caller_code_hash) = compile_module("call_with_gas").unwrap(); + let (callee_binary, _callee_code_hash) = compile_module("dummy").unwrap(); + + ExtBuilder::default().existential_deposit(500).build().execute_with(|| { + let _ = ::Currency::set_balance(&ALICE, 1_000_000); + + let Contract { addr: caller_addr, .. } = + builder::bare_instantiate(Code::Upload(caller_binary)) + .native_value(300_000) + .build_and_unwrap_contract(); + + let Contract { addr: callee_addr, .. } = + builder::bare_instantiate(Code::Upload(callee_binary)) + .native_value(100_000) + .build_and_unwrap_contract(); + + // fails, not enough gas + assert_err!( + builder::bare_call(caller_addr) + .data((callee_addr, 1u64).encode()) + .build() + .result, + Error::::ContractTrapped, + ); + + // succeeds, not enough gas but call stipend will be added + assert_ok!(builder::call(caller_addr) + .value(1337) + .data((callee_addr, 100u64).encode()) + .build()); + + // succeeds, enough gas + assert_ok!(builder::call(caller_addr) + .data((callee_addr, 100_000_000_000u64).encode()) + .build()); + }); +} diff --git a/substrate/frame/revive/src/vm/pvm.rs b/substrate/frame/revive/src/vm/pvm.rs new file mode 100644 index 0000000000000..e582cc5c95475 --- /dev/null +++ b/substrate/frame/revive/src/vm/pvm.rs @@ -0,0 +1,865 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Environment definition of the vm smart-contract runtime. + +pub mod env; + +#[cfg(doc)] +pub use env::SyscallDoc; + +use crate::{ + exec::{CallResources, ExecError, ExecResult, Ext, Key}, + limits, + metering::ChargedAmount, + precompiles::{All as AllPrecompiles, Precompiles}, + primitives::ExecReturnValue, + Code, Config, Error, Pallet, ReentrancyProtection, RuntimeCosts, LOG_TARGET, SENTINEL, +}; +use alloc::{vec, vec::Vec}; +use codec::Encode; +use core::{fmt, marker::PhantomData, mem}; +use frame_support::{ensure, weights::Weight}; +use pallet_revive_uapi::{CallFlags, ReturnErrorCode, ReturnFlags, StorageFlags}; +use sp_core::{H160, H256, U256}; +use sp_runtime::{DispatchError, RuntimeDebug}; + +/// Extracts the code and data from a given program blob. +pub fn extract_code_and_data(data: &[u8]) -> Option<(Vec, Vec)> { + let blob_len = polkavm::ProgramBlob::blob_length(data)?; + let blob_len = blob_len.try_into().ok()?; + let (code, data) = data.split_at_checked(blob_len)?; + Some((code.to_vec(), data.to_vec())) +} + +/// Abstraction over the memory access within syscalls. +/// +/// The reason for this abstraction is that we run syscalls on the host machine when +/// benchmarking them. In that case we have direct access to the contract's memory. However, when +/// running within PolkaVM we need to resort to copying as we can't map the contracts memory into +/// the host (as of now). +pub trait Memory { + /// Read designated chunk from the sandbox memory into the supplied buffer. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - requested buffer is not within the bounds of the sandbox memory. + fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), DispatchError>; + + /// Write the given buffer to the designated location in the sandbox memory. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - designated area is not within the bounds of the sandbox memory. + fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError>; + + /// Zero the designated location in the sandbox memory. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - designated area is not within the bounds of the sandbox memory. + fn zero(&mut self, ptr: u32, len: u32) -> Result<(), DispatchError>; + + /// This will reset all compilation artifacts of the currently executing instance. + /// + /// This is used before we call into a new contract to free up some memory. Doing + /// so we make sure that we only ever have to hold one compilation cache at a time + /// independtently of of our call stack depth. + fn reset_interpreter_cache(&mut self); + + /// Read designated chunk from the sandbox memory. + /// + /// Returns `Err` if one of the following conditions occurs: + /// + /// - requested buffer is not within the bounds of the sandbox memory. + fn read(&self, ptr: u32, len: u32) -> Result, DispatchError> { + let mut buf = vec![0u8; len as usize]; + self.read_into_buf(ptr, buf.as_mut_slice())?; + Ok(buf) + } + + /// Same as `read` but reads into a fixed size buffer. + fn read_array(&self, ptr: u32) -> Result<[u8; N], DispatchError> { + let mut buf = [0u8; N]; + self.read_into_buf(ptr, &mut buf)?; + Ok(buf) + } + + /// Read a `u32` from the sandbox memory. + fn read_u32(&self, ptr: u32) -> Result { + let buf: [u8; 4] = self.read_array(ptr)?; + Ok(u32::from_le_bytes(buf)) + } + + /// Read a `U256` from the sandbox memory. + fn read_u256(&self, ptr: u32) -> Result { + let buf: [u8; 32] = self.read_array(ptr)?; + Ok(U256::from_little_endian(&buf)) + } + + /// Read a `H160` from the sandbox memory. + fn read_h160(&self, ptr: u32) -> Result { + let mut buf = H160::default(); + self.read_into_buf(ptr, buf.as_bytes_mut())?; + Ok(buf) + } + + /// Read a `H256` from the sandbox memory. + fn read_h256(&self, ptr: u32) -> Result { + let mut code_hash = H256::default(); + self.read_into_buf(ptr, code_hash.as_bytes_mut())?; + Ok(code_hash) + } +} + +/// Allows syscalls access to the PolkaVM instance they are executing in. +/// +/// In case a contract is executing within PolkaVM its `memory` argument will also implement +/// this trait. The benchmarking implementation of syscalls will only require `Memory` +/// to be implemented. +pub trait PolkaVmInstance: Memory { + fn gas(&self) -> polkavm::Gas; + fn set_gas(&mut self, gas: polkavm::Gas); + fn read_input_regs(&self) -> (u64, u64, u64, u64, u64, u64); + fn write_output(&mut self, output: u64); +} + +// Memory implementation used in benchmarking where guest memory is mapped into the host. +// +// Please note that we could optimize the `read_as_*` functions by decoding directly from +// memory without a copy. However, we don't do that because as it would change the behaviour +// of those functions: A `read_as` with a `len` larger than the actual type can succeed +// in the streaming implementation while it could fail with a segfault in the copy implementation. +#[cfg(feature = "runtime-benchmarks")] +impl Memory for [u8] { + fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), DispatchError> { + let ptr = ptr as usize; + let bound_checked = + self.get(ptr..ptr + buf.len()).ok_or_else(|| Error::::OutOfBounds)?; + buf.copy_from_slice(bound_checked); + Ok(()) + } + + fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError> { + let ptr = ptr as usize; + let bound_checked = + self.get_mut(ptr..ptr + buf.len()).ok_or_else(|| Error::::OutOfBounds)?; + bound_checked.copy_from_slice(buf); + Ok(()) + } + + fn zero(&mut self, ptr: u32, len: u32) -> Result<(), DispatchError> { + <[u8] as Memory>::write(self, ptr, &vec![0; len as usize]) + } + + fn reset_interpreter_cache(&mut self) {} +} + +impl Memory for polkavm::RawInstance { + fn read_into_buf(&self, ptr: u32, buf: &mut [u8]) -> Result<(), DispatchError> { + self.read_memory_into(ptr, buf) + .map(|_| ()) + .map_err(|_| Error::::OutOfBounds.into()) + } + + fn write(&mut self, ptr: u32, buf: &[u8]) -> Result<(), DispatchError> { + self.write_memory(ptr, buf).map_err(|_| Error::::OutOfBounds.into()) + } + + fn zero(&mut self, ptr: u32, len: u32) -> Result<(), DispatchError> { + self.zero_memory(ptr, len).map_err(|_| Error::::OutOfBounds.into()) + } + + fn reset_interpreter_cache(&mut self) { + self.reset_interpreter_cache(); + } +} + +impl PolkaVmInstance for polkavm::RawInstance { + fn gas(&self) -> polkavm::Gas { + self.gas() + } + + fn set_gas(&mut self, gas: polkavm::Gas) { + self.set_gas(gas) + } + + fn read_input_regs(&self) -> (u64, u64, u64, u64, u64, u64) { + ( + self.reg(polkavm::Reg::A0), + self.reg(polkavm::Reg::A1), + self.reg(polkavm::Reg::A2), + self.reg(polkavm::Reg::A3), + self.reg(polkavm::Reg::A4), + self.reg(polkavm::Reg::A5), + ) + } + + fn write_output(&mut self, output: u64) { + self.set_reg(polkavm::Reg::A0, output); + } +} + +impl From<&ExecReturnValue> for ReturnErrorCode { + fn from(from: &ExecReturnValue) -> Self { + if from.flags.contains(ReturnFlags::REVERT) { + Self::CalleeReverted + } else { + Self::Success + } + } +} + +/// The data passed through when a contract uses `seal_return`. +#[derive(RuntimeDebug)] +pub struct ReturnData { + /// The flags as passed through by the contract. They are still unchecked and + /// will later be parsed into a `ReturnFlags` bitflags struct. + flags: u32, + /// The output buffer passed by the contract as return data. + data: Vec, +} + +/// Enumerates all possible reasons why a trap was generated. +/// +/// This is either used to supply the caller with more information about why an error +/// occurred (the SupervisorError variant). +/// The other case is where the trap does not constitute an error but rather was invoked +/// as a quick way to terminate the application (all other variants). +#[derive(RuntimeDebug)] +pub enum TrapReason { + /// The supervisor trapped the contract because of an error condition occurred during + /// execution in privileged code. + SupervisorError(DispatchError), + /// Signals that trap was generated in response to call `seal_return` host function. + Return(ReturnData), + /// Signals that a trap was generated in response to a successful call to the + /// `seal_terminate` host function. + Termination, +} + +impl> From for TrapReason { + fn from(from: T) -> Self { + Self::SupervisorError(from.into()) + } +} + +impl fmt::Display for TrapReason { + fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + Ok(()) + } +} + +/// Same as [`Runtime::charge_gas`]. +/// +/// We need this access as a macro because sometimes hiding the lifetimes behind +/// a function won't work out. +macro_rules! charge_gas { + ($runtime:expr, $costs:expr) => {{ + $runtime.ext.frame_meter_mut().charge_weight_token($costs) + }}; +} + +/// The kind of call that should be performed. +enum CallType { + /// Execute another instantiated contract + Call { value_ptr: u32 }, + /// Execute another contract code in the context (storage, account ID, value) of the caller + /// contract + DelegateCall, +} + +impl CallType { + fn cost(&self) -> RuntimeCosts { + match self { + CallType::Call { .. } => RuntimeCosts::CallBase, + CallType::DelegateCall => RuntimeCosts::DelegateCallBase, + } + } +} + +/// This is only appropriate when writing out data of constant size that does not depend on user +/// input. In this case the costs for this copy was already charged as part of the token at +/// the beginning of the API entry point. +fn already_charged(_: u32) -> Option { + None +} + +/// Helper to extract two `u32` values from a given `u64` register. +fn extract_hi_lo(reg: u64) -> (u32, u32) { + ((reg >> 32) as u32, reg as u32) +} + +/// Provides storage variants to support standard and Etheruem compatible semantics. +enum StorageValue { + /// Indicates that the storage value should be read from a memory buffer. + /// - `ptr`: A pointer to the start of the data in sandbox memory. + /// - `len`: The length (in bytes) of the data. + Memory { ptr: u32, len: u32 }, + + /// Indicates that the storage value is provided inline as a fixed-size (256-bit) value. + /// This is used by set_storage_or_clear() to avoid double reads. + /// This variant is used to implement Ethereum SSTORE-like semantics. + Value(Vec), +} + +/// Controls the output behavior for storage reads, both when a key is found and when it is not. +enum StorageReadMode { + /// VariableOutput mode: if the key exists, the full stored value is returned + /// using the caller‑provided output length. + VariableOutput { output_len_ptr: u32 }, + /// Ethereum compatible(FixedOutput32) mode: always write a 32-byte value into the output + /// buffer. If the key is missing, write 32 bytes of zeros. + FixedOutput32, +} + +/// Can only be used for one call. +pub struct Runtime<'a, E: Ext, M: ?Sized> { + ext: &'a mut E, + input_data: Option>, + _phantom_data: PhantomData, +} + +impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { + pub fn new(ext: &'a mut E, input_data: Vec) -> Self { + Self { ext, input_data: Some(input_data), _phantom_data: Default::default() } + } + + /// Get a mutable reference to the inner `Ext`. + pub fn ext(&mut self) -> &mut E { + self.ext + } + + /// Charge the gas meter with the specified token. + /// + /// Returns `Err(HostError)` if there is not enough gas. + fn charge_gas(&mut self, costs: RuntimeCosts) -> Result { + charge_gas!(self, costs) + } + + /// Adjust a previously charged amount down to its actual amount. + /// + /// This is when a maximum a priori amount was charged and then should be partially + /// refunded to match the actual amount. + fn adjust_gas(&mut self, charged: ChargedAmount, actual_costs: RuntimeCosts) { + self.ext.frame_meter_mut().adjust_weight(charged, actual_costs); + } + + /// Write the given buffer and its length to the designated locations in sandbox memory and + /// charge gas according to the token returned by `create_token`. + /// + /// `out_ptr` is the location in sandbox memory where `buf` should be written to. + /// `out_len_ptr` is an in-out location in sandbox memory. It is read to determine the + /// length of the buffer located at `out_ptr`. If that buffer is smaller than the actual + /// `buf.len()`, only what fits into that buffer is written to `out_ptr`. + /// The actual amount of bytes copied to `out_ptr` is written to `out_len_ptr`. + /// + /// If `out_ptr` is set to the sentinel value of `SENTINEL` and `allow_skip` is true the + /// operation is skipped and `Ok` is returned. This is supposed to help callers to make copying + /// output optional. For example to skip copying back the output buffer of an `seal_call` + /// when the caller is not interested in the result. + /// + /// `create_token` can optionally instruct this function to charge the gas meter with the token + /// it returns. `create_token` receives the variable amount of bytes that are about to be copied + /// by this function. + /// + /// In addition to the error conditions of `Memory::write` this functions returns + /// `Err` if the size of the buffer located at `out_ptr` is too small to fit `buf`. + pub fn write_sandbox_output( + &mut self, + memory: &mut M, + out_ptr: u32, + out_len_ptr: u32, + buf: &[u8], + allow_skip: bool, + create_token: impl FnOnce(u32) -> Option, + ) -> Result<(), DispatchError> { + if allow_skip && out_ptr == SENTINEL { + return Ok(()); + } + + let len = memory.read_u32(out_len_ptr)?; + let buf_len = len.min(buf.len() as u32); + + if let Some(costs) = create_token(buf_len) { + self.charge_gas(costs)?; + } + + memory.write(out_ptr, &buf[..buf_len as usize])?; + memory.write(out_len_ptr, &buf_len.encode()) + } + + /// Same as `write_sandbox_output` but for static size output. + pub fn write_fixed_sandbox_output( + &mut self, + memory: &mut M, + out_ptr: u32, + buf: &[u8], + allow_skip: bool, + create_token: impl FnOnce(u32) -> Option, + ) -> Result<(), DispatchError> { + if buf.is_empty() || (allow_skip && out_ptr == SENTINEL) { + return Ok(()); + } + + let buf_len = buf.len() as u32; + if let Some(costs) = create_token(buf_len) { + self.charge_gas(costs)?; + } + + memory.write(out_ptr, buf) + } + + /// Computes the given hash function on the supplied input. + /// + /// Reads from the sandboxed input buffer into an intermediate buffer. + /// Returns the result directly to the output buffer of the sandboxed memory. + /// + /// It is the callers responsibility to provide an output buffer that + /// is large enough to hold the expected amount of bytes returned by the + /// chosen hash function. + /// + /// # Note + /// + /// The `input` and `output` buffers may overlap. + fn compute_hash_on_intermediate_buffer( + &self, + memory: &mut M, + hash_fn: F, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + ) -> Result<(), DispatchError> + where + F: FnOnce(&[u8]) -> R, + R: AsRef<[u8]>, + { + // Copy input into supervisor memory. + let input = memory.read(input_ptr, input_len)?; + // Compute the hash on the input buffer using the given hash function. + let hash = hash_fn(&input); + // Write the resulting hash back into the sandboxed output buffer. + memory.write(output_ptr, hash.as_ref())?; + Ok(()) + } + + fn decode_key(&self, memory: &M, key_ptr: u32, key_len: u32) -> Result { + let res = match key_len { + SENTINEL => { + let mut buffer = [0u8; 32]; + memory.read_into_buf(key_ptr, buffer.as_mut())?; + Ok(Key::from_fixed(buffer)) + }, + len => { + ensure!(len <= limits::STORAGE_KEY_BYTES, Error::::DecodingFailed); + let key = memory.read(key_ptr, len)?; + Key::try_from_var(key) + }, + }; + + res.map_err(|_| Error::::DecodingFailed.into()) + } + + fn is_transient(flags: u32) -> Result { + StorageFlags::from_bits(flags) + .ok_or_else(|| >::InvalidStorageFlags.into()) + .map(|flags| flags.contains(StorageFlags::TRANSIENT)) + } + + fn set_storage( + &mut self, + memory: &M, + flags: u32, + key_ptr: u32, + key_len: u32, + value: StorageValue, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |new_bytes: u32, old_bytes: u32| { + if transient { + RuntimeCosts::SetTransientStorage { new_bytes, old_bytes } + } else { + RuntimeCosts::SetStorage { new_bytes, old_bytes } + } + }; + + let value_len = match &value { + StorageValue::Memory { ptr: _, len } => *len, + StorageValue::Value(data) => data.len() as u32, + }; + + let max_size = limits::STORAGE_BYTES; + let charged = self.charge_gas(costs(value_len, max_size))?; + if value_len > max_size { + return Err(Error::::ValueTooLarge.into()); + } + + let key = self.decode_key(memory, key_ptr, key_len)?; + + let value = match value { + StorageValue::Memory { ptr, len } => Some(memory.read(ptr, len)?), + StorageValue::Value(data) => Some(data), + }; + + let write_outcome = if transient { + self.ext.set_transient_storage(&key, value, false)? + } else { + self.ext.set_storage(&key, value, false)? + }; + + self.adjust_gas(charged, costs(value_len, write_outcome.old_len())); + Ok(write_outcome.old_len_with_sentinel()) + } + + fn clear_storage( + &mut self, + memory: &M, + flags: u32, + key_ptr: u32, + key_len: u32, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |len| { + if transient { + RuntimeCosts::ClearTransientStorage(len) + } else { + RuntimeCosts::ClearStorage(len) + } + }; + let charged = self.charge_gas(costs(limits::STORAGE_BYTES))?; + let key = self.decode_key(memory, key_ptr, key_len)?; + let outcome = if transient { + self.ext.set_transient_storage(&key, None, false)? + } else { + self.ext.set_storage(&key, None, false)? + }; + self.adjust_gas(charged, costs(outcome.old_len())); + Ok(outcome.old_len_with_sentinel()) + } + + fn get_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + out_ptr: u32, + read_mode: StorageReadMode, + ) -> Result { + let transient = Self::is_transient(flags)?; + let costs = |len| { + if transient { + RuntimeCosts::GetTransientStorage(len) + } else { + RuntimeCosts::GetStorage(len) + } + }; + let charged = self.charge_gas(costs(limits::STORAGE_BYTES))?; + let key = self.decode_key(memory, key_ptr, key_len)?; + let outcome = if transient { + self.ext.get_transient_storage(&key) + } else { + self.ext.get_storage(&key) + }; + + if let Some(value) = outcome { + self.adjust_gas(charged, costs(value.len() as u32)); + + match read_mode { + StorageReadMode::FixedOutput32 => { + let mut fixed_output = [0u8; 32]; + let len = value.len().min(fixed_output.len()); + fixed_output[..len].copy_from_slice(&value[..len]); + + self.write_fixed_sandbox_output( + memory, + out_ptr, + &fixed_output, + false, + already_charged, + )?; + Ok(ReturnErrorCode::Success) + }, + StorageReadMode::VariableOutput { output_len_ptr: out_len_ptr } => { + self.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + &value, + false, + already_charged, + )?; + Ok(ReturnErrorCode::Success) + }, + } + } else { + self.adjust_gas(charged, costs(0)); + + match read_mode { + StorageReadMode::FixedOutput32 => { + self.write_fixed_sandbox_output( + memory, + out_ptr, + &[0u8; 32], + false, + already_charged, + )?; + Ok(ReturnErrorCode::Success) + }, + StorageReadMode::VariableOutput { .. } => Ok(ReturnErrorCode::KeyNotFound), + } + } + } + + fn call( + &mut self, + memory: &mut M, + flags: CallFlags, + call_type: CallType, + callee_ptr: u32, + resources: &CallResources, + input_data_ptr: u32, + input_data_len: u32, + output_ptr: u32, + output_len_ptr: u32, + ) -> Result { + let callee = memory.read_h160(callee_ptr)?; + let precompile = >::get::(&callee.as_fixed_bytes()); + match &precompile { + Some(precompile) if precompile.has_contract_info() => + self.charge_gas(RuntimeCosts::PrecompileWithInfoBase)?, + Some(_) => self.charge_gas(RuntimeCosts::PrecompileBase)?, + None => self.charge_gas(call_type.cost())?, + }; + + // we do check this in exec.rs but we want to error out early + if input_data_len > limits::CALLDATA_BYTES { + Err(>::CallDataTooLarge)?; + } + + let input_data = if flags.contains(CallFlags::CLONE_INPUT) { + let input = self.input_data.as_ref().ok_or(Error::::InputForwarded)?; + charge_gas!(self, RuntimeCosts::CallInputCloned(input.len() as u32))?; + input.clone() + } else if flags.contains(CallFlags::FORWARD_INPUT) { + self.input_data.take().ok_or(Error::::InputForwarded)? + } else { + if precompile.is_some() { + self.charge_gas(RuntimeCosts::PrecompileDecode(input_data_len))?; + } else { + self.charge_gas(RuntimeCosts::CopyFromContract(input_data_len))?; + } + memory.read(input_data_ptr, input_data_len)? + }; + + memory.reset_interpreter_cache(); + + let call_outcome = match call_type { + CallType::Call { value_ptr } => { + let read_only = flags.contains(CallFlags::READ_ONLY); + let value = memory.read_u256(value_ptr)?; + if value > 0u32.into() { + // If the call value is non-zero and state change is not allowed, issue an + // error. + if read_only || self.ext.is_read_only() { + return Err(Error::::StateChangeDenied.into()); + } + + self.charge_gas(RuntimeCosts::CallTransferSurcharge { + dust_transfer: Pallet::::has_dust(value), + })?; + } + + let reentrancy = if flags.contains(CallFlags::ALLOW_REENTRY) { + ReentrancyProtection::AllowReentry + } else { + ReentrancyProtection::Strict + }; + + self.ext.call(resources, &callee, value, input_data, reentrancy, read_only) + }, + CallType::DelegateCall => { + if flags.intersects(CallFlags::ALLOW_REENTRY | CallFlags::READ_ONLY) { + return Err(Error::::InvalidCallFlags.into()); + } + self.ext.delegate_call(resources, callee, input_data) + }, + }; + + match call_outcome { + // `TAIL_CALL` only matters on an `OK` result. Otherwise the call stack comes to + // a halt anyways without anymore code being executed. + Ok(_) if flags.contains(CallFlags::TAIL_CALL) => { + let output = mem::take(self.ext.last_frame_output_mut()); + return Err(TrapReason::Return(ReturnData { + flags: output.flags.bits(), + data: output.data, + })); + }, + Ok(_) => { + let output = mem::take(self.ext.last_frame_output_mut()); + let write_result = self.write_sandbox_output( + memory, + output_ptr, + output_len_ptr, + &output.data, + true, + |len| Some(RuntimeCosts::CopyToContract(len)), + ); + *self.ext.last_frame_output_mut() = output; + write_result?; + Ok(self.ext.last_frame_output().into()) + }, + Err(err) => { + let error_code = super::exec_error_into_return_code::(err)?; + memory.write(output_len_ptr, &0u32.to_le_bytes())?; + Ok(error_code) + }, + } + } + + fn instantiate( + &mut self, + memory: &mut M, + code_hash_ptr: u32, + weight: Weight, + deposit_ptr: u32, + value_ptr: u32, + input_data_ptr: u32, + input_data_len: u32, + address_ptr: u32, + output_ptr: u32, + output_len_ptr: u32, + salt_ptr: u32, + ) -> Result { + let value = match memory.read_u256(value_ptr) { + Ok(value) => { + self.charge_gas(RuntimeCosts::Instantiate { + input_data_len, + balance_transfer: Pallet::::has_balance(value), + dust_transfer: Pallet::::has_dust(value), + })?; + value + }, + Err(err) => { + self.charge_gas(RuntimeCosts::Instantiate { + input_data_len: 0, + balance_transfer: false, + dust_transfer: false, + })?; + return Err(err.into()); + }, + }; + let deposit_limit: U256 = memory.read_u256(deposit_ptr)?; + let code_hash = memory.read_h256(code_hash_ptr)?; + if input_data_len > limits::CALLDATA_BYTES { + Err(>::CallDataTooLarge)?; + } + let input_data = memory.read(input_data_ptr, input_data_len)?; + let salt = if salt_ptr == SENTINEL { + None + } else { + let salt: [u8; 32] = memory.read_array(salt_ptr)?; + Some(salt) + }; + + memory.reset_interpreter_cache(); + + match self.ext.instantiate( + &CallResources::from_weight_and_deposit(weight, deposit_limit), + Code::Existing(code_hash), + value, + input_data, + salt.as_ref(), + ) { + Ok(address) => { + if !self.ext.last_frame_output().flags.contains(ReturnFlags::REVERT) { + self.write_fixed_sandbox_output( + memory, + address_ptr, + &address.as_bytes(), + true, + already_charged, + )?; + } + let output = mem::take(self.ext.last_frame_output_mut()); + let write_result = self.write_sandbox_output( + memory, + output_ptr, + output_len_ptr, + &output.data, + true, + |len| Some(RuntimeCosts::CopyToContract(len)), + ); + *self.ext.last_frame_output_mut() = output; + write_result?; + Ok(self.ext.last_frame_output().into()) + }, + Err(err) => Ok(super::exec_error_into_return_code::(err)?), + } + } +} + +pub struct PreparedCall<'a, E: Ext> { + module: polkavm::Module, + instance: polkavm::RawInstance, + runtime: Runtime<'a, E, polkavm::RawInstance>, +} + +impl<'a, E: Ext> PreparedCall<'a, E> { + pub fn call(mut self) -> ExecResult { + let exec_result = loop { + let interrupt = self.instance.run(); + if let Some(exec_result) = + self.runtime.handle_interrupt(interrupt, &self.module, &mut self.instance) + { + break exec_result + } + }; + self.runtime.ext().frame_meter_mut().sync_from_executor(self.instance.gas())?; + exec_result + } + + /// The guest memory address at which the aux data is located. + #[cfg(feature = "runtime-benchmarks")] + pub fn aux_data_base(&self) -> u32 { + self.instance.module().memory_map().aux_data_address() + } + + /// Copies `data` to the aux data at address `offset`. + /// + /// It sets `a0` to the beginning of data inside the aux data. + /// It sets `a1` to the value passed. + /// + /// Only used in benchmarking so far. + #[cfg(feature = "runtime-benchmarks")] + pub fn setup_aux_data( + &mut self, + data: &[u8], + offset: u32, + a1: u64, + ) -> frame_support::dispatch::DispatchResult { + let a0 = self.aux_data_base().saturating_add(offset); + self.instance.write_memory(a0, data).map_err(|err| { + log::debug!(target: LOG_TARGET, "failed to write aux data: {err:?}"); + Error::::CodeRejected + })?; + self.instance.set_reg(polkavm::Reg::A0, a0.into()); + self.instance.set_reg(polkavm::Reg::A1, a1); + Ok(()) + } +} diff --git a/substrate/frame/revive/src/vm/pvm/env.rs b/substrate/frame/revive/src/vm/pvm/env.rs new file mode 100644 index 0000000000000..5232c45497160 --- /dev/null +++ b/substrate/frame/revive/src/vm/pvm/env.rs @@ -0,0 +1,1014 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use super::*; + +use crate::{ + address::AddressMapper, + debug::DebugSettings, + exec::Ext, + limits, + primitives::ExecReturnValue, + vm::{calculate_code_deposit, BytecodeType, ExportedFunction, RuntimeCosts}, + AccountIdOf, CodeInfo, Config, ContractBlob, Error, Weight, SENTINEL, +}; +use alloc::vec::Vec; +use core::mem; +use frame_support::traits::Get; +use pallet_revive_proc_macro::define_env; +use pallet_revive_uapi::{CallFlags, ReturnErrorCode, ReturnFlags}; +use sp_core::U256; +use sp_io::hashing::keccak_256; +use sp_runtime::{DispatchError, SaturatedConversion}; + +impl ContractBlob { + /// Compile and instantiate contract. + /// + /// `aux_data_size` is only used for runtime benchmarks. Real contracts + /// don't make use of this buffer. Hence this should not be set to anything + /// other than `0` when not used for benchmarking. + pub fn prepare_call>( + self, + mut runtime: Runtime, + entry_point: ExportedFunction, + aux_data_size: u32, + ) -> Result, ExecError> { + let mut config = polkavm::Config::default(); + // Log filtering by level with log::enabled! returns always true, + // passing all logs through impacting performance \ + // (more details: https://github.com/paritytech/polkadot-sdk/issues/8760#issuecomment-3499548774) + // By default, disable polkavm logging unless pvm_logs debug setting is enabled. + let pvm_logs_enabled = DebugSettings::is_pvm_logs_enabled::(); + config.set_imperfect_logger_filtering_workaround(!pvm_logs_enabled); + config.set_backend(Some(polkavm::BackendKind::Interpreter)); + config.set_cache_enabled(false); + #[cfg(feature = "std")] + if std::env::var_os("REVIVE_USE_COMPILER").is_some() { + log::warn!(target: LOG_TARGET, "Using PolkaVM compiler backend because env var REVIVE_USE_COMPILER is set"); + config.set_backend(Some(polkavm::BackendKind::Compiler)); + } + let engine = polkavm::Engine::new(&config).expect( + "on-chain (no_std) use of interpreter is hard coded. + interpreter is available on all platforms; qed", + ); + + let mut module_config = polkavm::ModuleConfig::new(); + module_config.set_page_size(limits::PAGE_SIZE); + module_config.set_gas_metering(Some(polkavm::GasMeteringKind::Sync)); + module_config.set_aux_data_size(aux_data_size); + let module = + polkavm::Module::new(&engine, &module_config, self.code.into()).map_err(|err| { + log::debug!(target: LOG_TARGET, "failed to create polkavm module: {err:?}"); + Error::::CodeRejected + })?; + + let entry_program_counter = module + .exports() + .find(|export| export.symbol().as_bytes() == entry_point.identifier().as_bytes()) + .ok_or_else(|| >::CodeRejected)? + .program_counter(); + + let gas_limit_polkavm: polkavm::Gas = runtime.ext().frame_meter_mut().sync_to_executor(); + + let mut instance = module.instantiate().map_err(|err| { + log::debug!(target: LOG_TARGET, "failed to instantiate polkavm module: {err:?}"); + Error::::CodeRejected + })?; + + instance.set_gas(gas_limit_polkavm); + instance + .set_interpreter_cache_size_limit(Some(polkavm::SetCacheSizeLimitArgs { + max_block_size: limits::code::BASIC_BLOCK_SIZE, + max_cache_size_bytes: limits::code::INTERPRETER_CACHE_BYTES + .try_into() + .map_err(|_| Error::::CodeRejected)?, + })) + .map_err(|_| Error::::CodeRejected)?; + instance.prepare_call_untyped(entry_program_counter, &[]); + + Ok(PreparedCall { module, instance, runtime }) + } +} + +impl ContractBlob { + /// We only check for size and nothing else when the code is uploaded. + pub fn from_pvm_code(code: Vec, owner: AccountIdOf) -> Result { + // We do validation only when new code is deployed. This allows us to increase + // the limits later without affecting already deployed code. + let available_syscalls = list_syscalls(T::UnsafeUnstableInterface::get()); + let code = limits::code::enforce::(code, available_syscalls)?; + + let code_len = code.len() as u32; + let deposit = calculate_code_deposit::(code_len); + + let code_info = CodeInfo { + owner, + deposit, + refcount: 0, + code_len, + code_type: BytecodeType::Pvm, + behaviour_version: Default::default(), + }; + let code_hash = H256(sp_io::hashing::keccak_256(&code)); + Ok(ContractBlob { code, code_info, code_hash }) + } +} + +impl<'a, E: Ext, M: PolkaVmInstance> Runtime<'a, E, M> { + pub fn handle_interrupt( + &mut self, + interrupt: Result, + module: &polkavm::Module, + instance: &mut M, + ) -> Option { + use polkavm::InterruptKind::*; + + match interrupt { + Err(error) => { + // in contrast to the other returns this "should" not happen: log level error + log::error!(target: LOG_TARGET, "polkavm execution error: {error}"); + Some(Err(Error::::ExecutionFailed.into())) + }, + Ok(Finished) => + Some(Ok(ExecReturnValue { flags: ReturnFlags::empty(), data: Vec::new() })), + Ok(Trap) => Some(Err(Error::::ContractTrapped.into())), + Ok(Segfault(_)) => Some(Err(Error::::ExecutionFailed.into())), + Ok(NotEnoughGas) => Some(Err(Error::::OutOfGas.into())), + Ok(Step) => None, + Ok(Ecalli(idx)) => { + // This is a special hard coded syscall index which is used by benchmarks + // to abort contract execution. It is used to terminate the execution without + // breaking up a basic block. The fixed index is used so that the benchmarks + // don't have to deal with import tables. + if cfg!(feature = "runtime-benchmarks") && idx == SENTINEL { + return Some(Ok(ExecReturnValue { + flags: ReturnFlags::empty(), + data: Vec::new(), + })) + } + let Some(syscall_symbol) = module.imports().get(idx) else { + return Some(Err(>::InvalidSyscall.into())); + }; + match self.handle_ecall(instance, syscall_symbol.as_bytes()) { + Ok(None) => None, + Ok(Some(return_value)) => { + instance.write_output(return_value); + None + }, + Err(TrapReason::Return(ReturnData { flags, data })) => + match ReturnFlags::from_bits(flags) { + None => Some(Err(Error::::InvalidCallFlags.into())), + Some(flags) => Some(Ok(ExecReturnValue { flags, data })), + }, + Err(TrapReason::Termination) => Some(Ok(Default::default())), + Err(TrapReason::SupervisorError(error)) => Some(Err(error.into())), + } + }, + } + } +} + +// This is the API exposed to contracts. +// +// # Note +// +// Any input that leads to a out of bound error (reading or writing) or failing to decode +// data passed to the supervisor will lead to a trap. This is not documented explicitly +// for every function. +#[define_env] +pub mod env { + /// Noop function used to benchmark the time it takes to execute an empty function. + /// + /// Marked as stable because it needs to be called from benchmarks even when the benchmarked + /// parachain has unstable functions disabled. + #[cfg(feature = "runtime-benchmarks")] + #[stable] + fn noop(&mut self, memory: &mut M) -> Result<(), TrapReason> { + Ok(()) + } + + /// Set the value at the given key in the contract storage. + /// See [`pallet_revive_uapi::HostFn::set_storage_v2`] + #[stable] + #[mutating] + fn set_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + value_ptr: u32, + value_len: u32, + ) -> Result { + self.set_storage( + memory, + flags, + key_ptr, + key_len, + StorageValue::Memory { ptr: value_ptr, len: value_len }, + ) + } + + /// Sets the storage at a fixed 256-bit key with a fixed 256-bit value. + /// See [`pallet_revive_uapi::HostFn::set_storage_or_clear`]. + #[stable] + #[mutating] + fn set_storage_or_clear( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + value_ptr: u32, + ) -> Result { + let value = memory.read(value_ptr, 32)?; + + if value.iter().all(|&b| b == 0) { + self.clear_storage(memory, flags, key_ptr, SENTINEL) + } else { + self.set_storage(memory, flags, key_ptr, SENTINEL, StorageValue::Value(value)) + } + } + + /// Retrieve the value under the given key from storage. + /// See [`pallet_revive_uapi::HostFn::get_storage`] + #[stable] + fn get_storage( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + key_len: u32, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result { + self.get_storage( + memory, + flags, + key_ptr, + key_len, + out_ptr, + StorageReadMode::VariableOutput { output_len_ptr: out_len_ptr }, + ) + } + + /// Reads the storage at a fixed 256-bit key and writes back a fixed 256-bit value. + /// See [`pallet_revive_uapi::HostFn::get_storage_or_zero`]. + #[stable] + fn get_storage_or_zero( + &mut self, + memory: &mut M, + flags: u32, + key_ptr: u32, + out_ptr: u32, + ) -> Result<(), TrapReason> { + let _ = self.get_storage( + memory, + flags, + key_ptr, + SENTINEL, + out_ptr, + StorageReadMode::FixedOutput32, + )?; + + Ok(()) + } + + /// Make a call to another contract. + /// See [`pallet_revive_uapi::HostFn::call`]. + #[stable] + fn call( + &mut self, + memory: &mut M, + flags_and_callee: u64, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_and_value: u64, + input_data: u64, + output_data: u64, + ) -> Result { + let (flags, callee_ptr) = extract_hi_lo(flags_and_callee); + let (deposit_ptr, value_ptr) = extract_hi_lo(deposit_and_value); + let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); + let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let weight = Weight::from_parts(ref_time_limit, proof_size_limit); + + self.charge_gas(RuntimeCosts::CopyFromContract(32))?; + let deposit_limit = memory.read_u256(deposit_ptr)?; + + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::Call { value_ptr }, + callee_ptr, + &CallResources::from_weight_and_deposit(weight, deposit_limit), + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Make a call to another contract. + /// See [`pallet_revive_uapi::HostFn::call_evm`]. + #[stable] + fn call_evm( + &mut self, + memory: &mut M, + flags: u32, + callee: u32, + value_ptr: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> Result { + let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); + let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let resources = if gas == u64::MAX { + CallResources::NoLimits + } else { + self.charge_gas(RuntimeCosts::CopyFromContract(32))?; + let value = memory.read_u256(value_ptr)?; + // We also need to detect the 2300: We need to add something scaled. + let add_stipend = !value.is_zero() || gas == revm::interpreter::gas::CALL_STIPEND; + CallResources::from_ethereum_gas(gas.into(), add_stipend) + }; + + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::Call { value_ptr }, + callee, + &resources, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Execute code in the context (storage, caller, value) of the current contract. + /// See [`pallet_revive_uapi::HostFn::delegate_call`]. + #[stable] + fn delegate_call( + &mut self, + memory: &mut M, + flags_and_callee: u64, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_ptr: u32, + input_data: u64, + output_data: u64, + ) -> Result { + let (flags, address_ptr) = extract_hi_lo(flags_and_callee); + let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); + let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let weight = Weight::from_parts(ref_time_limit, proof_size_limit); + + self.charge_gas(RuntimeCosts::CopyFromContract(32))?; + let deposit_limit = memory.read_u256(deposit_ptr)?; + + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::DelegateCall, + address_ptr, + &CallResources::from_weight_and_deposit(weight, deposit_limit), + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Same as `delegate_call` but with EVM gas. + /// See [`pallet_revive_uapi::HostFn::delegate_call_evm`]. + #[stable] + fn delegate_call_evm( + &mut self, + memory: &mut M, + flags: u32, + callee: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> Result { + let (input_data_len, input_data_ptr) = extract_hi_lo(input_data); + let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let resources = if gas == u64::MAX { + CallResources::NoLimits + } else { + CallResources::from_ethereum_gas(gas.into(), false) + }; + + self.call( + memory, + CallFlags::from_bits(flags).ok_or(Error::::InvalidCallFlags)?, + CallType::DelegateCall, + callee, + &resources, + input_data_ptr, + input_data_len, + output_ptr, + output_len_ptr, + ) + } + + /// Instantiate a contract with the specified code hash. + /// See [`pallet_revive_uapi::HostFn::instantiate`]. + #[stable] + #[mutating] + fn instantiate( + &mut self, + memory: &mut M, + ref_time_limit: u64, + proof_size_limit: u64, + deposit_and_value: u64, + input_data: u64, + output_data: u64, + address_and_salt: u64, + ) -> Result { + let (deposit_ptr, value_ptr) = extract_hi_lo(deposit_and_value); + let (input_data_len, code_hash_ptr) = extract_hi_lo(input_data); + let (output_len_ptr, output_ptr) = extract_hi_lo(output_data); + let (address_ptr, salt_ptr) = extract_hi_lo(address_and_salt); + let Some(input_data_ptr) = code_hash_ptr.checked_add(32) else { + return Err(Error::::OutOfBounds.into()); + }; + let Some(input_data_len) = input_data_len.checked_sub(32) else { + return Err(Error::::OutOfBounds.into()); + }; + + self.instantiate( + memory, + code_hash_ptr, + Weight::from_parts(ref_time_limit, proof_size_limit), + deposit_ptr, + value_ptr, + input_data_ptr, + input_data_len, + address_ptr, + output_ptr, + output_len_ptr, + salt_ptr, + ) + } + + /// Returns the total size of the contract call input data. + /// See [`pallet_revive_uapi::HostFn::call_data_size `]. + #[stable] + fn call_data_size(&mut self, memory: &mut M) -> Result { + self.charge_gas(RuntimeCosts::CallDataSize)?; + Ok(self + .input_data + .as_ref() + .map(|input| input.len().try_into().expect("usize fits into u64; qed")) + .unwrap_or_default()) + } + + /// Stores the input passed by the caller into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::call_data_copy`]. + #[stable] + fn call_data_copy( + &mut self, + memory: &mut M, + out_ptr: u32, + out_len: u32, + offset: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::CallDataCopy(out_len))?; + + let Some(input) = self.input_data.as_ref() else { + return Err(Error::::InputForwarded.into()); + }; + + let start = offset as usize; + if start >= input.len() { + memory.zero(out_ptr, out_len)?; + return Ok(()); + } + + let end = start.saturating_add(out_len as usize).min(input.len()); + memory.write(out_ptr, &input[start..end])?; + + let bytes_written = (end - start) as u32; + memory.zero(out_ptr.saturating_add(bytes_written), out_len - bytes_written)?; + + Ok(()) + } + + /// Stores the U256 value at given call input `offset` into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::call_data_load`]. + #[stable] + fn call_data_load( + &mut self, + memory: &mut M, + out_ptr: u32, + offset: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::CallDataLoad)?; + + let Some(input) = self.input_data.as_ref() else { + return Err(Error::::InputForwarded.into()); + }; + + let mut data = [0; 32]; + let start = offset as usize; + let data = if start >= input.len() { + data // Any index is valid to request; OOB offsets return zero. + } else { + let end = start.saturating_add(32).min(input.len()); + data[..end - start].copy_from_slice(&input[start..end]); + data.reverse(); + data // Solidity expects right-padded data + }; + + self.write_fixed_sandbox_output(memory, out_ptr, &data, false, already_charged)?; + + Ok(()) + } + + /// Cease contract execution and save a data buffer as a result of the execution. + /// See [`pallet_revive_uapi::HostFn::return_value`]. + #[stable] + fn seal_return( + &mut self, + memory: &mut M, + flags: u32, + data_ptr: u32, + data_len: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::CopyFromContract(data_len))?; + if data_len > limits::CALLDATA_BYTES { + Err(>::ReturnDataTooLarge)?; + } + Err(TrapReason::Return(ReturnData { flags, data: memory.read(data_ptr, data_len)? })) + } + + /// Stores the address of the caller into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::caller`]. + #[stable] + fn caller(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Caller)?; + let caller = ::AddressMapper::to_address(self.ext.caller().account_id()?); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + caller.as_bytes(), + false, + already_charged, + )?) + } + + /// Stores the address of the call stack origin into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::origin`]. + #[stable] + fn origin(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Origin)?; + let origin = ::AddressMapper::to_address(self.ext.origin().account_id()?); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + origin.as_bytes(), + false, + already_charged, + )?) + } + + /// Retrieve the code hash for a specified contract address. + /// See [`pallet_revive_uapi::HostFn::code_hash`]. + #[stable] + fn code_hash(&mut self, memory: &mut M, addr_ptr: u32, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::CodeHash)?; + let address = memory.read_h160(addr_ptr)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &self.ext.code_hash(&address).as_bytes(), + false, + already_charged, + )?) + } + + /// Retrieve the code size for a given contract address. + /// See [`pallet_revive_uapi::HostFn::code_size`]. + #[stable] + fn code_size(&mut self, memory: &mut M, addr_ptr: u32) -> Result { + self.charge_gas(RuntimeCosts::CodeSize)?; + let address = memory.read_h160(addr_ptr)?; + Ok(self.ext.code_size(&address)) + } + + /// Stores the address of the current contract into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::address`]. + #[stable] + fn address(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Address)?; + let address = self.ext.address(); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + address.as_bytes(), + false, + already_charged, + )?) + } + + /// Stores the immutable data into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::get_immutable_data`]. + #[stable] + fn get_immutable_data( + &mut self, + memory: &mut M, + out_ptr: u32, + out_len_ptr: u32, + ) -> Result<(), TrapReason> { + // quering the length is free as it is stored with the contract metadata + let len = self.ext.immutable_data_len(); + self.charge_gas(RuntimeCosts::GetImmutableData(len))?; + let data = self.ext.get_immutable_data()?; + self.write_sandbox_output(memory, out_ptr, out_len_ptr, &data, false, already_charged)?; + Ok(()) + } + + /// Attaches the supplied immutable data to the currently executing contract. + /// See [`pallet_revive_uapi::HostFn::set_immutable_data`]. + #[stable] + fn set_immutable_data(&mut self, memory: &mut M, ptr: u32, len: u32) -> Result<(), TrapReason> { + if len > limits::IMMUTABLE_BYTES { + return Err(Error::::OutOfBounds.into()); + } + self.charge_gas(RuntimeCosts::SetImmutableData(len))?; + let buf = memory.read(ptr, len)?; + let data = buf.try_into().expect("bailed out earlier; qed"); + self.ext.set_immutable_data(data)?; + Ok(()) + } + + /// Stores the *free* balance of the current account into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::balance`]. + #[stable] + fn balance(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Balance)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &self.ext.balance().to_little_endian(), + false, + already_charged, + )?) + } + + /// Stores the *free* balance of the supplied address into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::balance`]. + #[stable] + fn balance_of( + &mut self, + memory: &mut M, + addr_ptr: u32, + out_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::BalanceOf)?; + let address = memory.read_h160(addr_ptr)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &self.ext.balance_of(&address).to_little_endian(), + false, + already_charged, + )?) + } + + /// Returns the chain ID. + /// See [`pallet_revive_uapi::HostFn::chain_id`]. + #[stable] + fn chain_id(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &U256::from(::ChainId::get()).to_little_endian(), + false, + |_| Some(RuntimeCosts::CopyToContract(32)), + )?) + } + + /// Returns the block ref_time limit. + /// See [`pallet_revive_uapi::HostFn::gas_limit`]. + #[stable] + fn gas_limit(&mut self, memory: &mut M) -> Result { + self.charge_gas(RuntimeCosts::GasLimit)?; + Ok(self.ext.gas_limit()) + } + + /// Stores the value transferred along with this call/instantiate into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::value_transferred`]. + #[stable] + fn value_transferred(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::ValueTransferred)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &self.ext.value_transferred().to_little_endian(), + false, + already_charged, + )?) + } + + /// Returns the simulated ethereum `GASPRICE` value. + /// See [`pallet_revive_uapi::HostFn::gas_price`]. + #[stable] + fn gas_price(&mut self, memory: &mut M) -> Result { + self.charge_gas(RuntimeCosts::GasPrice)?; + Ok(self.ext.effective_gas_price().saturated_into()) + } + + /// Returns the simulated ethereum `BASEFEE` value. + /// See [`pallet_revive_uapi::HostFn::base_fee`]. + #[stable] + fn base_fee(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::BaseFee)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &Pallet::::evm_base_fee().to_little_endian(), + false, + already_charged, + )?) + } + + /// Load the latest block timestamp into the supplied buffer + /// See [`pallet_revive_uapi::HostFn::now`]. + #[stable] + fn now(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::Now)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &self.ext.now().to_little_endian(), + false, + already_charged, + )?) + } + + /// Deposit a contract event with the data buffer and optional list of topics. + /// See [pallet_revive_uapi::HostFn::deposit_event] + #[stable] + #[mutating] + fn deposit_event( + &mut self, + memory: &mut M, + topics_ptr: u32, + num_topic: u32, + data_ptr: u32, + data_len: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::DepositEvent { num_topic, len: data_len })?; + + if num_topic > limits::NUM_EVENT_TOPICS { + return Err(Error::::TooManyTopics.into()); + } + + if data_len > limits::EVENT_BYTES { + return Err(Error::::ValueTooLarge.into()); + } + + let topics: Vec = match num_topic { + 0 => Vec::new(), + _ => { + let mut v = Vec::with_capacity(num_topic as usize); + let topics_len = num_topic * H256::len_bytes() as u32; + let buf = memory.read(topics_ptr, topics_len)?; + for chunk in buf.chunks_exact(H256::len_bytes()) { + v.push(H256::from_slice(chunk)); + } + v + }, + }; + + let event_data = memory.read(data_ptr, data_len)?; + self.ext.deposit_event(topics, event_data); + Ok(()) + } + + /// Stores the current block number of the current contract into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::block_number`]. + #[stable] + fn block_number(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::BlockNumber)?; + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &self.ext.block_number().to_little_endian(), + false, + already_charged, + )?) + } + + /// Stores the block hash at given block height into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::block_hash`]. + #[stable] + fn block_hash( + &mut self, + memory: &mut M, + block_number_ptr: u32, + out_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::BlockHash)?; + let block_number = memory.read_u256(block_number_ptr)?; + let block_hash = self.ext.block_hash(block_number).unwrap_or(H256::zero()); + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &block_hash.as_bytes(), + false, + already_charged, + )?) + } + + /// Stores the current block author into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::block_author`]. + #[stable] + fn block_author(&mut self, memory: &mut M, out_ptr: u32) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::BlockAuthor)?; + let block_author = self.ext.block_author(); + + Ok(self.write_fixed_sandbox_output( + memory, + out_ptr, + &block_author.as_bytes(), + false, + already_charged, + )?) + } + + /// Computes the KECCAK 256-bit hash on the given input buffer. + /// See [`pallet_revive_uapi::HostFn::hash_keccak_256`]. + #[stable] + fn hash_keccak_256( + &mut self, + memory: &mut M, + input_ptr: u32, + input_len: u32, + output_ptr: u32, + ) -> Result<(), TrapReason> { + self.charge_gas(RuntimeCosts::HashKeccak256(input_len))?; + Ok(self.compute_hash_on_intermediate_buffer( + memory, keccak_256, input_ptr, input_len, output_ptr, + )?) + } + + /// Stores the length of the data returned by the last call into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::return_data_size`]. + #[stable] + fn return_data_size(&mut self, memory: &mut M) -> Result { + self.charge_gas(RuntimeCosts::ReturnDataSize)?; + Ok(self + .ext + .last_frame_output() + .data + .len() + .try_into() + .expect("usize fits into u64; qed")) + } + + /// Stores data returned by the last call, starting from `offset`, into the supplied buffer. + /// See [`pallet_revive_uapi::HostFn::return_data`]. + #[stable] + fn return_data_copy( + &mut self, + memory: &mut M, + out_ptr: u32, + out_len_ptr: u32, + offset: u32, + ) -> Result<(), TrapReason> { + let output = mem::take(self.ext.last_frame_output_mut()); + let result = if offset as usize > output.data.len() { + Err(Error::::OutOfBounds.into()) + } else { + self.write_sandbox_output( + memory, + out_ptr, + out_len_ptr, + &output.data[offset as usize..], + false, + |len| Some(RuntimeCosts::CopyToContract(len)), + ) + }; + *self.ext.last_frame_output_mut() = output; + Ok(result?) + } + + /// Returns the amount of evm gas left. + /// + /// The name is only for historical reasons as renaming functions + /// would be a breaking change. + /// + /// See [`pallet_revive_uapi::HostFn::gas_left`]. + #[stable] + fn ref_time_left(&mut self, memory: &mut M) -> Result { + self.charge_gas(RuntimeCosts::RefTimeLeft)?; + Ok(self.ext.gas_left()) + } + + /// Reverts the execution without data and cedes all remaining gas. + /// + /// See [`pallet_revive_uapi::HostFn::consume_all_gas`]. + #[stable] + fn consume_all_gas(&mut self, memory: &mut M) -> Result<(), TrapReason> { + self.ext.frame_meter_mut().consume_all_weight(); + Err(TrapReason::Return(ReturnData { + flags: ReturnFlags::REVERT.bits(), + data: Default::default(), + })) + } + + /// Calculates Ethereum address from the ECDSA compressed public key and stores + /// See [`pallet_revive_uapi::HostFn::ecdsa_to_eth_address`]. + fn ecdsa_to_eth_address( + &mut self, + memory: &mut M, + key_ptr: u32, + out_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::EcdsaToEthAddress)?; + let mut compressed_key: [u8; 33] = [0; 33]; + memory.read_into_buf(key_ptr, &mut compressed_key)?; + let result = self.ext.ecdsa_to_eth_address(&compressed_key); + match result { + Ok(eth_address) => { + memory.write(out_ptr, eth_address.as_ref())?; + Ok(ReturnErrorCode::Success) + }, + Err(_) => Ok(ReturnErrorCode::EcdsaRecoveryFailed), + } + } + + /// Replace the contract code at the specified address with new code. + /// See [`pallet_revive_uapi::HostFn::set_code_hash`]. + /// + /// Disabled until the internal implementation takes care of collecting + /// the immutable data of the new code hash. + #[mutating] + fn set_code_hash(&mut self, memory: &mut M, code_hash_ptr: u32) -> Result<(), TrapReason> { + let charged = self.charge_gas(RuntimeCosts::SetCodeHash { old_code_removed: true })?; + let code_hash: H256 = memory.read_h256(code_hash_ptr)?; + if matches!(self.ext.set_code_hash(code_hash)?, crate::CodeRemoved::No) { + self.adjust_gas(charged, RuntimeCosts::SetCodeHash { old_code_removed: false }); + } + Ok(()) + } + + /// Verify a sr25519 signature + /// See [`pallet_revive_uapi::HostFn::sr25519_verify`]. + fn sr25519_verify( + &mut self, + memory: &mut M, + signature_ptr: u32, + pub_key_ptr: u32, + message_len: u32, + message_ptr: u32, + ) -> Result { + self.charge_gas(RuntimeCosts::Sr25519Verify(message_len))?; + + let mut signature: [u8; 64] = [0; 64]; + memory.read_into_buf(signature_ptr, &mut signature)?; + + let mut pub_key: [u8; 32] = [0; 32]; + memory.read_into_buf(pub_key_ptr, &mut pub_key)?; + + let message: Vec = memory.read(message_ptr, message_len)?; + + if self.ext.sr25519_verify(&signature, &message, &pub_key) { + Ok(ReturnErrorCode::Success) + } else { + Ok(ReturnErrorCode::Sr25519VerifyFailed) + } + } + + /// Remove the calling account and transfer remaining balance: + /// **total** balance if code is deleted from storage, else **free** balance only. + /// See [`pallet_revive_uapi::HostFn::terminate`]. + #[mutating] + #[stable] + fn terminate(&mut self, memory: &mut M, beneficiary_ptr: u32) -> Result<(), TrapReason> { + let charged = self.charge_gas(RuntimeCosts::Terminate { code_removed: true })?; + let beneficiary = memory.read_h160(beneficiary_ptr)?; + if matches!(self.ext.terminate_if_same_tx(&beneficiary)?, crate::CodeRemoved::No) { + self.adjust_gas(charged, RuntimeCosts::Terminate { code_removed: false }); + } + Err(TrapReason::Termination) + } +} diff --git a/substrate/frame/revive/uapi/src/host.rs b/substrate/frame/revive/uapi/src/host.rs index f87fb1a54b887..e552938139703 100644 --- a/substrate/frame/revive/uapi/src/host.rs +++ b/substrate/frame/revive/uapi/src/host.rs @@ -119,6 +119,20 @@ pub trait HostFn: private::Sealed { output: Option<&mut &mut [u8]>, ) -> Result; + /// Same as [HostFn::call] but receives the one-dimensional EVM gas argument. + /// + /// Adds the EVM gas stipend for non-zero value calls. + /// + /// If gas is `u64::MAX`, the call will run with uncapped limits. + fn call_evm( + flags: CallFlags, + callee: &[u8; 20], + gas: u64, + value: &[u8; 32], + input_data: &[u8], + output: Option<&mut &mut [u8]>, + ) -> Result; + /// Stores the address of the caller into the supplied buffer. /// /// If this is a top-level call (i.e. initiated by an extrinsic) the origin address of the @@ -207,6 +221,17 @@ pub trait HostFn: private::Sealed { output: Option<&mut &mut [u8]>, ) -> Result; + /// Same as [HostFn::delegate_call] but receives the one-dimensional EVM gas argument. + /// + /// If gas is `u64::MAX`, the call will run with uncapped limits. + fn delegate_call_evm( + flags: CallFlags, + address: &[u8; 20], + gas: u64, + input_data: &[u8], + output: Option<&mut &mut [u8]>, + ) -> Result; + /// Deposit a contract event with the data buffer and optional list of topics. There is a limit /// on the maximum number of topics specified by `event_topics`. /// diff --git a/substrate/frame/revive/uapi/src/host/riscv64.rs b/substrate/frame/revive/uapi/src/host/riscv64.rs index adfd8f2789594..fbb1c5e16f9fe 100644 --- a/substrate/frame/revive/uapi/src/host/riscv64.rs +++ b/substrate/frame/revive/uapi/src/host/riscv64.rs @@ -73,6 +73,14 @@ mod sys { input_data: u64, output_data: u64, ) -> ReturnCode; + pub fn call_evm( + flags: u32, + callee: u32, + value_ptr: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> ReturnCode; pub fn delegate_call( flags_and_callee: u64, ref_time_limit: u64, @@ -89,6 +97,13 @@ mod sys { output_data: u64, address_and_salt: u64, ) -> ReturnCode; + pub fn delegate_call_evm( + flags: u32, + callee: u32, + gas: u64, + input_data: u64, + output_data: u64, + ) -> ReturnCode; pub fn terminate(beneficiary_ptr: *const u8); pub fn call_data_copy(out_ptr: *mut u8, out_len: u32, offset: u32); pub fn call_data_load(out_ptr: *mut u8, offset: u32); @@ -250,6 +265,36 @@ impl HostFn for HostFnImpl { ret_code.into() } + fn call_evm( + flags: CallFlags, + callee: &[u8; 20], + gas: u64, + value_ptr: &[u8; 32], + input: &[u8], + mut output: Option<&mut &mut [u8]>, + ) -> Result { + let input_data = pack_hi_lo(input.len() as _, input.as_ptr() as _); + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let output_data = pack_hi_lo(&mut output_len as *mut _ as _, output_ptr as _); + + let ret_code = unsafe { + sys::call_evm( + flags.bits(), + callee.as_ptr() as _, + value_ptr.as_ptr() as _, + gas, + input_data, + output_data, + ) + }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + + ret_code.into() + } + fn delegate_call( flags: CallFlags, address: &[u8; 20], @@ -284,6 +329,34 @@ impl HostFn for HostFnImpl { ret_code.into() } + fn delegate_call_evm( + flags: CallFlags, + address: &[u8; 20], + gas: u64, + input: &[u8], + mut output: Option<&mut &mut [u8]>, + ) -> Result { + let input_data = pack_hi_lo(input.len() as u32, input.as_ptr() as u32); + let (output_ptr, mut output_len) = ptr_len_or_sentinel(&mut output); + let output_data = pack_hi_lo(&mut output_len as *mut _ as u32, output_ptr as u32); + + let ret_code = unsafe { + sys::delegate_call_evm( + flags.bits(), + address.as_ptr() as _, + gas, + input_data, + output_data, + ) + }; + + if let Some(ref mut output) = output { + extract_from_slice(output, output_len as usize); + } + + ret_code.into() + } + fn deposit_event(topics: &[[u8; 32]], data: &[u8]) { unsafe { sys::deposit_event(