Skip to content

Commit 3ca26f3

Browse files
committed
evm: Fix gas estimation issue in call delegation
1 parent 05975b5 commit 3ca26f3

File tree

4 files changed

+214
-86
lines changed

4 files changed

+214
-86
lines changed

Cargo.lock

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
2+
use pallet_evm::{
3+
IsPrecompileResult, Precompile, PrecompileHandle, PrecompileResult, PrecompileSet,
4+
};
5+
use sp_core::H160;
6+
use sp_runtime::traits::Dispatchable;
7+
use sp_std::marker::PhantomData;
8+
9+
use pallet_evm_precompile_modexp::Modexp;
10+
use pallet_evm_precompile_sha3fips::Sha3FIPS256;
11+
use pallet_evm_precompile_simple::{ECRecover, ECRecoverPublicKey, Identity, Ripemd160, Sha256};
12+
13+
pub struct FrontierPrecompiles<R>(PhantomData<R>);
14+
impl<R> Default for FrontierPrecompiles<R> {
15+
fn default() -> Self {
16+
Self(PhantomData)
17+
}
18+
}
19+
20+
impl<R> FrontierPrecompiles<R>
21+
where
22+
R: pallet_evm::Config,
23+
{
24+
pub fn used_addresses() -> [H160; 8] {
25+
[
26+
hash(1),
27+
hash(2),
28+
hash(3),
29+
hash(4),
30+
hash(5),
31+
hash(1024),
32+
hash(1025),
33+
hash(2048),
34+
]
35+
}
36+
}
37+
impl<R> PrecompileSet for FrontierPrecompiles<R>
38+
where
39+
R: pallet_evm::Config,
40+
R::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
41+
<R::RuntimeCall as Dispatchable>::RuntimeOrigin: From<Option<R::AccountId>>,
42+
{
43+
fn execute(&self, handle: &mut impl PrecompileHandle) -> Option<PrecompileResult> {
44+
match handle.code_address() {
45+
// Ethereum precompiles :
46+
a if a == hash(1) => Some(ECRecover::execute(handle)),
47+
a if a == hash(2) => Some(Sha256::execute(handle)),
48+
a if a == hash(3) => Some(Ripemd160::execute(handle)),
49+
a if a == hash(4) => Some(Identity::execute(handle)),
50+
a if a == hash(5) => Some(Modexp::execute(handle)),
51+
// Non-Frontier specific nor Ethereum precompiles :
52+
a if a == hash(1024) => Some(Sha3FIPS256::execute(handle)),
53+
a if a == hash(1025) => Some(ECRecoverPublicKey::execute(handle)),
54+
a if a == hash(2048) => Some(substrate_call::Dispatch::<R>::execute(handle)),
55+
_ => None,
56+
}
57+
}
58+
59+
fn is_precompile(&self, address: H160, _gas: u64) -> IsPrecompileResult {
60+
IsPrecompileResult::Answer {
61+
is_precompile: Self::used_addresses().contains(&address),
62+
extra_cost: 0,
63+
}
64+
}
65+
}
66+
67+
fn hash(a: u64) -> H160 {
68+
H160::from_low_u64_be(a)
69+
}
70+
71+
mod substrate_call {
72+
use alloc::format;
73+
use core::marker::PhantomData;
74+
75+
use codec::{Decode, DecodeLimit};
76+
// Substrate
77+
use frame_support::{
78+
dispatch::{DispatchClass, GetDispatchInfo, Pays, PostDispatchInfo},
79+
traits::{ConstU32, Get},
80+
};
81+
use sp_runtime::traits::Dispatchable;
82+
// Frontier
83+
use fp_evm::{
84+
ExitError, ExitSucceed, Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput,
85+
PrecompileResult,
86+
};
87+
use pallet_evm::{AddressMapping, GasWeightMapping};
88+
89+
// `DecodeLimit` specifies the max depth a call can use when decoding, as unbounded depth
90+
// can be used to overflow the stack.
91+
// Default value is 8, which is the same as in XCM call decoding.
92+
pub struct Dispatch<T, DispatchValidator = (), DecodeLimit = ConstU32<8>> {
93+
_marker: PhantomData<(T, DispatchValidator, DecodeLimit)>,
94+
}
95+
96+
impl<T, DispatchValidator, DecodeLimit> Precompile for Dispatch<T, DispatchValidator, DecodeLimit>
97+
where
98+
T: pallet_evm::Config,
99+
T::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo + Decode,
100+
<T::RuntimeCall as Dispatchable>::RuntimeOrigin: From<Option<T::AccountId>>,
101+
DispatchValidator: DispatchValidateT<T::AccountId, T::RuntimeCall>,
102+
DecodeLimit: Get<u32>,
103+
{
104+
fn execute(handle: &mut impl PrecompileHandle) -> PrecompileResult {
105+
let input = handle.input();
106+
let target_gas = handle.gas_limit();
107+
let context = handle.context();
108+
109+
let call = T::RuntimeCall::decode_with_depth_limit(DecodeLimit::get(), &mut &*input)
110+
.map_err(|_| PrecompileFailure::Error {
111+
exit_status: ExitError::Other("decode failed".into()),
112+
})?;
113+
let info = call.get_dispatch_info();
114+
115+
if let Some(gas) = target_gas {
116+
let valid_weight = T::GasWeightMapping::weight_to_gas(info.weight) <= gas;
117+
if !valid_weight {
118+
return Err(PrecompileFailure::Error {
119+
exit_status: ExitError::OutOfGas,
120+
});
121+
}
122+
}
123+
124+
let origin = T::AddressMapping::into_account_id(context.caller);
125+
126+
if let Some(err) = DispatchValidator::validate_before_dispatch(&origin, &call) {
127+
return Err(err);
128+
}
129+
130+
handle.record_external_cost(
131+
Some(info.weight.ref_time()),
132+
Some(info.weight.proof_size()),
133+
None,
134+
)?;
135+
match call.dispatch(Some(origin).into()) {
136+
Ok(post_info) => {
137+
if post_info.pays_fee(&info) == Pays::Yes {
138+
let actual_weight = post_info.actual_weight.unwrap_or(info.weight);
139+
let cost = T::GasWeightMapping::weight_to_gas(actual_weight);
140+
handle.refund_external_cost(
141+
Some(
142+
info.weight
143+
.ref_time()
144+
.saturating_sub(actual_weight.ref_time()),
145+
),
146+
Some(
147+
info.weight
148+
.proof_size()
149+
.saturating_sub(actual_weight.proof_size()),
150+
),
151+
);
152+
handle.record_cost(cost)?;
153+
}
154+
155+
Ok(PrecompileOutput {
156+
exit_status: ExitSucceed::Stopped,
157+
output: Default::default(),
158+
})
159+
}
160+
Err(e) => Err(PrecompileFailure::Error {
161+
exit_status: ExitError::Other(
162+
format!("dispatch execution failed: {}", <&'static str>::from(e)).into(),
163+
),
164+
}),
165+
}
166+
}
167+
}
168+
169+
/// Dispatch validation trait.
170+
pub trait DispatchValidateT<AccountId, RuntimeCall> {
171+
fn validate_before_dispatch(
172+
origin: &AccountId,
173+
call: &RuntimeCall,
174+
) -> Option<PrecompileFailure>;
175+
}
176+
177+
/// The default implementation of `DispatchValidateT`.
178+
impl<AccountId, RuntimeCall> DispatchValidateT<AccountId, RuntimeCall> for ()
179+
where
180+
RuntimeCall: GetDispatchInfo,
181+
{
182+
fn validate_before_dispatch(
183+
_origin: &AccountId,
184+
call: &RuntimeCall,
185+
) -> Option<PrecompileFailure> {
186+
let info = call.get_dispatch_info();
187+
if !(info.pays_fee == Pays::Yes && info.class == DispatchClass::Normal) {
188+
return Some(PrecompileFailure::Error {
189+
exit_status: ExitError::Other("invalid call".into()),
190+
});
191+
}
192+
None
193+
}
194+
}
195+
}

standalone/runtime/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323
#![recursion_limit = "1024"]
2424
#![allow(clippy::identity_op)]
2525

26+
#[macro_use]
27+
extern crate alloc;
28+
2629
mod msg_routing;
2730

2831
use codec::{Decode, Encode, MaxEncodedLen};
@@ -128,8 +131,8 @@ use pallet_ethereum::{
128131
TransactionData,
129132
};
130133
use pallet_evm::{Account as EVMAccount, FeeCalculator, Runner};
131-
use precompiles::FrontierPrecompiles;
132-
mod precompiles;
134+
use evm_precompiles::FrontierPrecompiles;
135+
mod evm_precompiles;
133136

134137
// Make the WASM binary available.
135138
#[cfg(all(feature = "std", feature = "include-wasm"))]

0 commit comments

Comments
 (0)