pallet_evm_precompile_call_permit/
lib.rs

1// Copyright 2019-2025 PureStake Inc.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17#![cfg_attr(not(feature = "std"), no_std)]
18
19use core::marker::PhantomData;
20use evm::ExitReason;
21use fp_evm::{Context, ExitRevert, PrecompileFailure, PrecompileHandle, Transfer};
22use frame_support::{
23	ensure,
24	storage::types::{StorageMap, ValueQuery},
25	traits::{ConstU32, Get, StorageInstance, Time},
26	Blake2_128Concat,
27};
28use precompile_utils::{evm::costs::call_cost, prelude::*};
29use sp_core::{H160, H256, U256};
30use sp_io::hashing::keccak_256;
31use sp_runtime::traits::UniqueSaturatedInto;
32use sp_std::vec::Vec;
33
34#[cfg(test)]
35mod mock;
36#[cfg(test)]
37mod tests;
38
39/// Storage prefix for nonces.
40pub struct Nonces;
41
42impl StorageInstance for Nonces {
43	const STORAGE_PREFIX: &'static str = "Nonces";
44
45	fn pallet_prefix() -> &'static str {
46		"PrecompileCallPermit"
47	}
48}
49
50/// Storage type used to store EIP2612 nonces.
51pub type NoncesStorage = StorageMap<
52	Nonces,
53	// From
54	Blake2_128Concat,
55	H160,
56	// Nonce
57	U256,
58	ValueQuery,
59>;
60
61/// EIP712 permit typehash.
62pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!(
63	"CallPermit(address from,address to,uint256 value,bytes data,uint64 gaslimit\
64,uint256 nonce,uint256 deadline)"
65);
66
67/// EIP712 permit domain used to compute an individualized domain separator.
68const PERMIT_DOMAIN: [u8; 32] = keccak256!(
69	"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
70);
71
72pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
73
74/// Precompile allowing to issue and dispatch call permits for gasless transactions.
75/// A user can sign a permit for a call that can be dispatched and paid by another user or
76/// smart contract.
77pub struct CallPermitPrecompile<Runtime>(PhantomData<Runtime>);
78
79#[precompile_utils::precompile]
80impl<Runtime> CallPermitPrecompile<Runtime>
81where
82	Runtime: pallet_evm::Config,
83{
84	fn compute_domain_separator(address: H160) -> [u8; 32] {
85		let name: H256 = keccak_256(b"Call Permit Precompile").into();
86		let version: H256 = keccak256!("1").into();
87		let chain_id: U256 = Runtime::ChainId::get().into();
88
89		let domain_separator_inner = solidity::encode_arguments((
90			H256::from(PERMIT_DOMAIN),
91			name,
92			version,
93			chain_id,
94			Address(address),
95		));
96
97		keccak_256(&domain_separator_inner).into()
98	}
99
100	pub fn generate_permit(
101		address: H160,
102		from: H160,
103		to: H160,
104		value: U256,
105		data: Vec<u8>,
106		gaslimit: u64,
107		nonce: U256,
108		deadline: U256,
109	) -> [u8; 32] {
110		let domain_separator = Self::compute_domain_separator(address);
111
112		let permit_content = solidity::encode_arguments((
113			H256::from(PERMIT_TYPEHASH),
114			Address(from),
115			Address(to),
116			value,
117			// bytes are encoded as the keccak_256 of the content
118			H256::from(keccak_256(&data)),
119			gaslimit,
120			nonce,
121			deadline,
122		));
123		let permit_content = keccak_256(&permit_content);
124		let mut pre_digest = Vec::with_capacity(2 + 32 + 32);
125		pre_digest.extend_from_slice(b"\x19\x01");
126		pre_digest.extend_from_slice(&domain_separator);
127		pre_digest.extend_from_slice(&permit_content);
128		keccak_256(&pre_digest)
129	}
130
131	pub fn dispatch_inherent_cost() -> u64 {
132		3_000 // cost of ECRecover precompile for reference
133			+ RuntimeHelper::<Runtime>::db_write_gas_cost() // we write nonce
134	}
135
136	#[precompile::public(
137		"dispatch(address,address,uint256,bytes,uint64,uint256,uint8,bytes32,bytes32)"
138	)]
139	fn dispatch(
140		handle: &mut impl PrecompileHandle,
141		from: Address,
142		to: Address,
143		value: U256,
144		data: BoundedBytes<ConstU32<CALL_DATA_LIMIT>>,
145		gas_limit: u64,
146		deadline: U256,
147		v: u8,
148		r: H256,
149		s: H256,
150	) -> EvmResult<UnboundedBytes> {
151		// Now: 8
152		handle.record_db_read::<Runtime>(8)?;
153		// NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32)
154		handle.record_db_read::<Runtime>(104)?;
155
156		handle.record_cost(Self::dispatch_inherent_cost())?;
157
158		let from: H160 = from.into();
159		let to: H160 = to.into();
160		let data: Vec<u8> = data.into();
161
162		// ENSURE GASLIMIT IS SUFFICIENT
163		let call_cost = call_cost(value, <Runtime as pallet_evm::Config>::config());
164
165		let total_cost = gas_limit
166			.checked_add(call_cost)
167			.ok_or_else(|| revert("Call require too much gas (uint64 overflow)"))?;
168
169		if total_cost > handle.remaining_gas() {
170			return Err(revert("Gaslimit is too low to dispatch provided call"));
171		}
172
173		// VERIFY PERMIT
174
175		// Blockchain time is in ms while Ethereum use second timestamps.
176		let timestamp: u128 =
177			<Runtime as pallet_evm::Config>::Timestamp::now().unique_saturated_into();
178		let timestamp: U256 = U256::from(timestamp / 1000);
179
180		ensure!(deadline >= timestamp, revert("Permit expired"));
181
182		let nonce = NoncesStorage::get(from);
183
184		let permit = Self::generate_permit(
185			handle.context().address,
186			from,
187			to,
188			value,
189			data.clone(),
190			gas_limit,
191			nonce,
192			deadline,
193		);
194
195		let mut sig = [0u8; 65];
196		sig[0..32].copy_from_slice(&r.as_bytes());
197		sig[32..64].copy_from_slice(&s.as_bytes());
198		sig[64] = v;
199
200		let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit)
201			.map_err(|_| revert("Invalid permit"))?;
202		let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice()));
203
204		ensure!(
205			signer != H160::zero() && signer == from,
206			revert("Invalid permit")
207		);
208
209		NoncesStorage::insert(from, nonce + U256::one());
210
211		// DISPATCH CALL
212		let sub_context = Context {
213			caller: from,
214			address: to.clone(),
215			apparent_value: value,
216		};
217
218		let transfer = if value.is_zero() {
219			None
220		} else {
221			Some(Transfer {
222				source: from,
223				target: to.clone(),
224				value,
225			})
226		};
227
228		let (reason, output) =
229			handle.call(to, transfer, data, Some(gas_limit), false, &sub_context);
230		match reason {
231			ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }),
232			ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }),
233			ExitReason::Revert(_) => Err(PrecompileFailure::Revert {
234				exit_status: ExitRevert::Reverted,
235				output,
236			}),
237			ExitReason::Succeed(_) => Ok(output.into()),
238		}
239	}
240
241	#[precompile::public("nonces(address)")]
242	#[precompile::view]
243	fn nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult<U256> {
244		// NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32)
245		handle.record_db_read::<Runtime>(104)?;
246
247		let owner: H160 = owner.into();
248
249		let nonce = NoncesStorage::get(owner);
250
251		Ok(nonce)
252	}
253
254	#[precompile::public("DOMAIN_SEPARATOR()")]
255	#[precompile::view]
256	fn domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult<H256> {
257		// ChainId
258		handle.record_db_read::<Runtime>(8)?;
259
260		let domain_separator: H256 =
261			Self::compute_domain_separator(handle.context().address).into();
262
263		Ok(domain_separator)
264	}
265}