From 3af5d0700cfda28034fdbd81eb76f013d4c22650 Mon Sep 17 00:00:00 2001 From: xermicus Date: Sat, 6 Dec 2025 00:19:59 +0100 Subject: [PATCH] BACKPORT-CONFLICT --- prdoc/pr_10554.prdoc | 14 + .../fixtures/contracts/call_with_gas.rs | 42 ++ .../fixtures/contracts/delegate_call_evm.rs | 52 ++ substrate/frame/revive/src/tests/pvm.rs | 622 ++++++++++++++++++ substrate/frame/revive/src/vm/pvm.rs | 20 +- substrate/frame/revive/src/vm/pvm/env.rs | 85 ++- substrate/frame/revive/uapi/src/host.rs | 25 + .../frame/revive/uapi/src/host/riscv64.rs | 73 ++ 8 files changed, 925 insertions(+), 8 deletions(-) create mode 100644 prdoc/pr_10554.prdoc create mode 100644 substrate/frame/revive/fixtures/contracts/call_with_gas.rs create mode 100644 substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs 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 index c21dfe3ad02a1..c780492055c52 100644 --- a/substrate/frame/revive/src/tests/pvm.rs +++ b/substrate/frame/revive/src/tests/pvm.rs @@ -4898,3 +4898,625 @@ fn return_data_limit_is_enforced() { } }); } +<<<<<<< HEAD +======= + +#[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()); + }); +} +>>>>>>> 5a1128b9 ([pallet-revive] add EVM gas call syscalls (#10554)) diff --git a/substrate/frame/revive/src/vm/pvm.rs b/substrate/frame/revive/src/vm/pvm.rs index d9229eb00c0c1..b299c422d5a76 100644 --- a/substrate/frame/revive/src/vm/pvm.rs +++ b/substrate/frame/revive/src/vm/pvm.rs @@ -725,8 +725,7 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { flags: CallFlags, call_type: CallType, callee_ptr: u32, - deposit_ptr: u32, - weight: Weight, + resources: &CallResources, input_data_ptr: u32, input_data_len: u32, output_ptr: u32, @@ -741,8 +740,6 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { None => self.charge_gas(call_type.cost())?, }; - let deposit_limit = memory.read_u256(deposit_ptr)?; - // we do check this in exec.rs but we want to error out early if input_data_len > limits::CALLDATA_BYTES { Err(>::CallDataTooLarge)?; @@ -780,6 +777,7 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { dust_transfer: Pallet::::has_dust(value), })?; } +<<<<<<< HEAD self.ext.call( weight, deposit_limit, @@ -789,12 +787,26 @@ impl<'a, E: Ext, M: ?Sized + Memory> Runtime<'a, E, M> { flags.contains(CallFlags::ALLOW_REENTRY), read_only, ) +======= + + let reentrancy = if flags.contains(CallFlags::ALLOW_REENTRY) { + ReentrancyProtection::AllowReentry + } else { + ReentrancyProtection::Strict + }; + + self.ext.call(resources, &callee, value, input_data, reentrancy, read_only) +>>>>>>> 5a1128b9 ([pallet-revive] add EVM gas call syscalls (#10554)) }, CallType::DelegateCall => { if flags.intersects(CallFlags::ALLOW_REENTRY | CallFlags::READ_ONLY) { return Err(Error::::InvalidCallFlags.into()); } +<<<<<<< HEAD self.ext.delegate_call(weight, deposit_limit, callee, input_data) +======= + self.ext.delegate_call(resources, callee, input_data) +>>>>>>> 5a1128b9 ([pallet-revive] add EVM gas call syscalls (#10554)) }, }; diff --git a/substrate/frame/revive/src/vm/pvm/env.rs b/substrate/frame/revive/src/vm/pvm/env.rs index fe0572126856f..6dd4bc409d58d 100644 --- a/substrate/frame/revive/src/vm/pvm/env.rs +++ b/substrate/frame/revive/src/vm/pvm/env.rs @@ -302,14 +302,55 @@ pub mod env { 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, - deposit_ptr, - Weight::from_parts(ref_time_limit, proof_size_limit), + &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, @@ -333,14 +374,50 @@ pub mod env { 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, - deposit_ptr, - Weight::from_parts(ref_time_limit, proof_size_limit), + &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, diff --git a/substrate/frame/revive/uapi/src/host.rs b/substrate/frame/revive/uapi/src/host.rs index c7684bd8cece4..5368ef969da39 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 b0cc4987e2cb7..cc9b6373dae17 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); @@ -248,6 +263,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], @@ -282,6 +327,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(