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

            
19
use core::marker::PhantomData;
20
use evm::ExitReason;
21
use fp_evm::{Context, ExitRevert, PrecompileFailure, PrecompileHandle, Transfer};
22
use frame_support::{
23
	ensure,
24
	storage::types::{StorageMap, ValueQuery},
25
	traits::{ConstU32, Get, StorageInstance, Time},
26
	Blake2_128Concat,
27
};
28
use precompile_utils::{evm::costs::call_cost, prelude::*};
29
use sp_core::{H160, H256, U256};
30
use sp_io::hashing::keccak_256;
31
use sp_runtime::traits::UniqueSaturatedInto;
32
use sp_std::vec::Vec;
33

            
34
#[cfg(test)]
35
mod mock;
36
#[cfg(test)]
37
mod tests;
38

            
39
/// Storage prefix for nonces.
40
pub struct Nonces;
41

            
42
impl StorageInstance for Nonces {
43
	const STORAGE_PREFIX: &'static str = "Nonces";
44

            
45
13
	fn pallet_prefix() -> &'static str {
46
13
		"PrecompileCallPermit"
47
13
	}
48
}
49

            
50
/// Storage type used to store EIP2612 nonces.
51
pub type NoncesStorage = StorageMap<
52
	Nonces,
53
	// From
54
	Blake2_128Concat,
55
	H160,
56
	// Nonce
57
	U256,
58
	ValueQuery,
59
>;
60

            
61
/// EIP712 permit typehash.
62
pub 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.
68
const PERMIT_DOMAIN: [u8; 32] = keccak256!(
69
	"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
70
);
71

            
72
pub 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.
77
pub struct CallPermitPrecompile<Runtime>(PhantomData<Runtime>);
78

            
79
8
#[precompile_utils::precompile]
80
impl<Runtime> CallPermitPrecompile<Runtime>
81
where
82
	Runtime: pallet_evm::Config,
83
{
84
11
	fn compute_domain_separator(address: H160) -> [u8; 32] {
85
11
		let name: H256 = keccak_256(b"Call Permit Precompile").into();
86
11
		let version: H256 = keccak256!("1").into();
87
11
		let chain_id: U256 = Runtime::ChainId::get().into();
88

            
89
11
		let domain_separator_inner = solidity::encode_arguments((
90
11
			H256::from(PERMIT_DOMAIN),
91
11
			name,
92
11
			version,
93
11
			chain_id,
94
11
			Address(address),
95
11
		));
96

            
97
11
		keccak_256(&domain_separator_inner).into()
98
11
	}
99

            
100
10
	pub fn generate_permit(
101
10
		address: H160,
102
10
		from: H160,
103
10
		to: H160,
104
10
		value: U256,
105
10
		data: Vec<u8>,
106
10
		gaslimit: u64,
107
10
		nonce: U256,
108
10
		deadline: U256,
109
10
	) -> [u8; 32] {
110
10
		let domain_separator = Self::compute_domain_separator(address);
111

            
112
10
		let permit_content = solidity::encode_arguments((
113
10
			H256::from(PERMIT_TYPEHASH),
114
10
			Address(from),
115
10
			Address(to),
116
10
			value,
117
10
			// bytes are encoded as the keccak_256 of the content
118
10
			H256::from(keccak_256(&data)),
119
10
			gaslimit,
120
10
			nonce,
121
10
			deadline,
122
10
		));
123
10
		let permit_content = keccak_256(&permit_content);
124
10
		let mut pre_digest = Vec::with_capacity(2 + 32 + 32);
125
10
		pre_digest.extend_from_slice(b"\x19\x01");
126
10
		pre_digest.extend_from_slice(&domain_separator);
127
10
		pre_digest.extend_from_slice(&permit_content);
128
10
		keccak_256(&pre_digest)
129
10
	}
130

            
131
19
	pub fn dispatch_inherent_cost() -> u64 {
132
19
		3_000 // cost of ECRecover precompile for reference
133
19
			+ RuntimeHelper::<Runtime>::db_write_gas_cost() // we write nonce
134
19
	}
135

            
136
	#[precompile::public(
137
		"dispatch(address,address,uint256,bytes,uint64,uint256,uint8,bytes32,bytes32)"
138
	)]
139
7
	fn dispatch(
140
7
		handle: &mut impl PrecompileHandle,
141
7
		from: Address,
142
7
		to: Address,
143
7
		value: U256,
144
7
		data: BoundedBytes<ConstU32<CALL_DATA_LIMIT>>,
145
7
		gas_limit: u64,
146
7
		deadline: U256,
147
7
		v: u8,
148
7
		r: H256,
149
7
		s: H256,
150
7
	) -> EvmResult<UnboundedBytes> {
151
		// Now: 8
152
7
		handle.record_db_read::<Runtime>(8)?;
153
		// NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32)
154
7
		handle.record_db_read::<Runtime>(104)?;
155

            
156
7
		handle.record_cost(Self::dispatch_inherent_cost())?;
157

            
158
7
		let from: H160 = from.into();
159
7
		let to: H160 = to.into();
160
7
		let data: Vec<u8> = data.into();
161

            
162
		// ENSURE GASLIMIT IS SUFFICIENT
163
7
		let call_cost = call_cost(value, <Runtime as pallet_evm::Config>::config());
164

            
165
7
		let total_cost = gas_limit
166
7
			.checked_add(call_cost)
167
7
			.ok_or_else(|| revert("Call require too much gas (uint64 overflow)"))?;
168

            
169
6
		if total_cost > handle.remaining_gas() {
170
1
			return Err(revert("Gaslimit is too low to dispatch provided call"));
171
5
		}
172

            
173
		// VERIFY PERMIT
174

            
175
		// Blockchain time is in ms while Ethereum use second timestamps.
176
5
		let timestamp: u128 =
177
5
			<Runtime as pallet_evm::Config>::Timestamp::now().unique_saturated_into();
178
5
		let timestamp: U256 = U256::from(timestamp);
179

            
180
5
		let deadline_ms: U256 = deadline
181
5
			.checked_mul(U256::from(1000))
182
5
			.ok_or_else(|| revert("Deadline overflow"))?;
183

            
184
5
		ensure!(deadline_ms >= timestamp, revert("Permit expired"));
185

            
186
4
		let nonce = NoncesStorage::get(from);
187

            
188
4
		let permit = Self::generate_permit(
189
4
			handle.context().address,
190
4
			from,
191
4
			to,
192
4
			value,
193
4
			data.clone(),
194
4
			gas_limit,
195
4
			nonce,
196
4
			deadline,
197
		);
198

            
199
4
		let mut sig = [0u8; 65];
200
4
		sig[0..32].copy_from_slice(&r.as_bytes());
201
4
		sig[32..64].copy_from_slice(&s.as_bytes());
202
4
		sig[64] = v;
203

            
204
4
		let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit)
205
4
			.map_err(|_| revert("Invalid permit"))?;
206
4
		let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice()));
207

            
208
4
		ensure!(
209
4
			signer != H160::zero() && signer == from,
210
1
			revert("Invalid permit")
211
		);
212

            
213
3
		NoncesStorage::insert(from, nonce + U256::one());
214

            
215
		// DISPATCH CALL
216
3
		let sub_context = Context {
217
3
			caller: from,
218
3
			address: to.clone(),
219
3
			apparent_value: value,
220
3
		};
221

            
222
3
		let transfer = if value.is_zero() {
223
			None
224
		} else {
225
3
			Some(Transfer {
226
3
				source: from,
227
3
				target: to.clone(),
228
3
				value,
229
3
			})
230
		};
231

            
232
3
		let (reason, output) =
233
3
			handle.call(to, transfer, data, Some(gas_limit), false, &sub_context);
234
3
		match reason {
235
			ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }),
236
			ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }),
237
1
			ExitReason::Revert(_) => Err(PrecompileFailure::Revert {
238
1
				exit_status: ExitRevert::Reverted,
239
1
				output,
240
1
			}),
241
2
			ExitReason::Succeed(_) => Ok(output.into()),
242
		}
243
7
	}
244

            
245
	#[precompile::public("nonces(address)")]
246
	#[precompile::view]
247
6
	fn nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult<U256> {
248
		// NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32)
249
6
		handle.record_db_read::<Runtime>(104)?;
250

            
251
6
		let owner: H160 = owner.into();
252

            
253
6
		let nonce = NoncesStorage::get(owner);
254

            
255
6
		Ok(nonce)
256
6
	}
257

            
258
	#[precompile::public("DOMAIN_SEPARATOR()")]
259
	#[precompile::view]
260
1
	fn domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult<H256> {
261
		// ChainId
262
1
		handle.record_db_read::<Runtime>(8)?;
263

            
264
1
		let domain_separator: H256 =
265
1
			Self::compute_domain_separator(handle.context().address).into();
266

            
267
1
		Ok(domain_separator)
268
1
	}
269
}