Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions substrate/frame/revive/fixtures/contracts/call_with_gas.rs
Original file line number Diff line number Diff line change
@@ -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: &[u8; 32],
);

let mut value = [0; 32];
api::value_transferred(&mut value);

api::call_evm(CallFlags::empty(), callee_addr, gas, &value, &[], None).unwrap();
}
51 changes: 51 additions & 0 deletions substrate/frame/revive/fixtures/contracts/delegate_call_evm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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: &[u8; 32],
);

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);
}
70 changes: 70 additions & 0 deletions substrate/frame/revive/src/tests/pvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5513,3 +5513,73 @@ fn self_destruct_by_syscall_tracing_works() {
});
}
}

#[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 _ = <Test as Config>::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, U256::from(100).to_little_endian()).encode())
.build()
.result,
Error::<Test>::ContractTrapped,
);

assert_ok!(builder::call(caller_addr)
.value(1337)
.data((callee_addr, U256::from(100_000_000_000).to_little_endian()).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 _ = <Test as Config>::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, U256::from(100).to_little_endian()).encode())
.build()
.result,
Error::<Test>::ContractTrapped,
);

assert_ok!(builder::call(caller_addr)
.value(1337)
.data((callee_addr, U256::from(100_000_000_000).to_little_endian()).encode())
.build());
});
}
20 changes: 3 additions & 17 deletions substrate/frame/revive/src/vm/pvm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -631,8 +631,7 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
flags: CallFlags,
call_type: CallType,
callee_ptr: u32,
deposit_ptr: u32,
weight: Weight,
resources: &CallResources<E::T>,
input_data_ptr: u32,
input_data_len: u32,
output_ptr: u32,
Expand All @@ -647,8 +646,6 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> 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(<Error<E::T>>::CallDataTooLarge)?;
Expand Down Expand Up @@ -693,24 +690,13 @@ impl<'a, E: Ext, M: ?Sized + Memory<E::T>> Runtime<'a, E, M> {
ReentrancyProtection::Strict
};

self.ext.call(
&CallResources::from_weight_and_deposit(weight, deposit_limit),
&callee,
value,
input_data,
reentrancy,
read_only,
)
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::<E::T>::InvalidCallFlags.into());
}
self.ext.delegate_call(
&CallResources::from_weight_and_deposit(weight, deposit_limit),
callee,
input_data,
)
self.ext.delegate_call(resources, callee, input_data)
},
};

Expand Down
77 changes: 73 additions & 4 deletions substrate/frame/revive/src/vm/pvm/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,14 +298,49 @@ 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::<E::T>::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_ptr: u32,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't we pass gas as a u64?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why u64? It would move the overflow check into contract code.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not thinking of solidity

the uapi would be nicer if it was just

  pub fn call_evm(
    flags: u32,
    callee: u32,
    value: u32,
    gas: u64,
    input_data: u64,
    output_data: u64,
  ) -> ReturnCode;

since gas is just a u64.

but then I guess since in revive you translate call instructions where the gas is a uint256 that would force you to do the overflow check in the contract

Copy link
Member Author

@xermicus xermicus Nov 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the gas argument is an u256 (since it's the only int type in Yul).

However, actually considering it, an u64 probably wouldn't even be that bad. In many cases, the supplied gas is just what was returned from gas() or some constant value (address.transfer) - both cases don't need overflow checks (this is an optimization).

Such optimizations aren't planned for the initial resolc 1.0 release but easy to implement in the future. So I think it'd actually be helpful when this matches what gas_left returns, which is u64. Done right, I suspect that this will lead to LLVM itself to recognize and eliminate the overflow check - I'll test this to be sure (because then it's definitively favorable).

Thinking it further, we could also use a "no gas" API method for when the gas to use is what gas() returned. This would just use CallResources::NoLimits. Which could just be even be expressed by a sentinel pointer value. Probably fine to just us u64::max too, since this would mean uncapped resources anyways? Need to think about this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All that make sense.

when I wrote the comment I was mostly thinking of the use case of someone writing a contract in Rust with the low-level API where there I think it make sense to use a u64.

Thinking it further, we could also use a "no gas" API method for when the gas to use is what gas() returned. This would just use CallResources::NoLimits. Which could just be even be expressed by a sentinel pointer value. Probably fine to just us u64::max too, since this would mean uncapped resources anyways? Need to think about this.

Yeah maybe in that case you call the regular call method with W(u64::max, u64::max) as it result in less math ops to compute the gas_left (@TorstenStueber might be better advisor here)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the new syscalls are specifically for resolc - with Rust contracts people are not bound to EVM gas and can use the existing syscalls using Weight and Deposit.

Regarding the uncapped calls. I think this depends if we implement the "63/64" rule. Even if we don't implement it exactly like on Ethereum: The contract can't just supply the absolute maximum, instead it needs to be able to express that the call should get whatever is left.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds all good.

Yeah maybe in that case you call the regular call method with W(u64::max, u64::max) as it result in less math ops to compute the gas_left (@TorstenStueber might be better advisor here)

CallResources::NoLimits is always the least overhead. If the transaction as a whole uses Ethereum execution mode (limited by gas), then any call to a subsequent frame won't be able to switch that mode to Substrate execution mode even if using W(u64::max, u64::max).

Regarding the uncapped calls. I think this depends if we implement the "63/64" rule. Even if we don't implement it exactly like on Ethereum: The contract can't just supply the absolute maximum, instead it needs to be able to express that the call should get whatever is left.

There is currently no such implementation itself. We can add a 63/64 later but the current logic is already complicated enough for the first iteration.

Copy link
Contributor

@pgherveou pgherveou Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the new syscalls are specifically for resolc - with Rust contracts people are not bound to EVM gas and can use the existing syscalls using Weight and Deposit.

However, actually considering it, an u64 probably wouldn't even be that bad. In many cases, the supplied gas is just what was returned from gas() or some constant value (address.transfer) - both cases don't need overflow checks (this is an optimization).

It's your call but If future compiler optimisation can optimize that away, picking the host call that use u64 might be interesting as well.

If we revisit the whole pallet-transaction-payment, and gas mapping integration to make gas the main metering unit (instead of it being derived from being fees / gas_price) then this could also become a nice api for Rust contract as well

input_data: u64,
output_data: u64,
) -> Result<ReturnErrorCode, TrapReason> {
let (input_data_len, input_data_ptr) = extract_hi_lo(input_data);
let (output_len_ptr, output_ptr) = extract_hi_lo(output_data);

self.charge_gas(RuntimeCosts::CopyFromContract(32))?;
let gas = memory.read_u256(gas_ptr)?;

self.call(
memory,
CallFlags::from_bits(flags).ok_or(Error::<E::T>::InvalidCallFlags)?,
CallType::Call { value_ptr },
callee,
&CallResources::from_ethereum_gas(gas, true),
input_data_ptr,
input_data_len,
output_ptr,
Expand All @@ -329,14 +364,48 @@ 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::<E::T>::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_ptr: u32,
input_data: u64,
output_data: u64,
) -> Result<ReturnErrorCode, TrapReason> {
let (input_data_len, input_data_ptr) = extract_hi_lo(input_data);
let (output_len_ptr, output_ptr) = extract_hi_lo(output_data);

self.charge_gas(RuntimeCosts::CopyFromContract(32))?;
let gas = memory.read_u256(gas_ptr)?;

self.call(
memory,
CallFlags::from_bits(flags).ok_or(Error::<E::T>::InvalidCallFlags)?,
CallType::DelegateCall,
callee,
&CallResources::from_ethereum_gas(gas, true),
input_data_ptr,
input_data_len,
output_ptr,
Expand Down
19 changes: 19 additions & 0 deletions substrate/frame/revive/uapi/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ pub trait HostFn: private::Sealed {
output: Option<&mut &mut [u8]>,
) -> Result;

/// Same as [HostFn::call] but receives the one-dimensional EVM gas argument.
fn call_evm(
flags: CallFlags,
callee: &[u8; 20],
gas: &[u8; 32],
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
Expand Down Expand Up @@ -207,6 +217,15 @@ pub trait HostFn: private::Sealed {
output: Option<&mut &mut [u8]>,
) -> Result;

/// Same as [HostFn::delegate_call] but receives the one-dimensional EVM gas argument.
fn delegate_call_evm(
flags: CallFlags,
address: &[u8; 20],
gas: &[u8; 32],
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`.
///
Expand Down
Loading
Loading