pallet_evm_precompile_balances_erc20/
eip2612.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
17use super::*;
18use frame_support::{
19	ensure,
20	traits::{Get, Time},
21};
22use sp_core::H256;
23use sp_io::hashing::keccak_256;
24use sp_runtime::traits::UniqueSaturatedInto;
25use sp_std::vec::Vec;
26
27/// EIP2612 permit typehash.
28pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!(
29	"Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"
30);
31
32/// EIP2612 permit domain used to compute an individualized domain separator.
33const PERMIT_DOMAIN: [u8; 32] = keccak256!(
34	"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
35);
36
37pub struct Eip2612<Runtime, Metadata, Instance = ()>(PhantomData<(Runtime, Metadata, Instance)>);
38
39impl<Runtime, Metadata, Instance> Eip2612<Runtime, Metadata, Instance>
40where
41	Runtime: pallet_balances::Config<Instance> + pallet_evm::Config,
42	Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
43	Runtime::RuntimeCall: From<pallet_balances::Call<Runtime, Instance>>,
44	BalanceOf<Runtime, Instance>: TryFrom<U256> + Into<U256>,
45	Metadata: Erc20Metadata,
46	Instance: InstanceToPrefix + 'static,
47	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
48{
49	pub fn compute_domain_separator(address: H160) -> [u8; 32] {
50		let name: H256 = keccak_256(Metadata::name().as_bytes()).into();
51		let version: H256 = keccak256!("1").into();
52		let chain_id: U256 = Runtime::ChainId::get().into();
53
54		let domain_separator_inner = solidity::encode_arguments((
55			H256::from(PERMIT_DOMAIN),
56			name,
57			version,
58			chain_id,
59			Address(address),
60		));
61
62		keccak_256(&domain_separator_inner)
63	}
64
65	pub fn generate_permit(
66		address: H160,
67		owner: H160,
68		spender: H160,
69		value: U256,
70		nonce: U256,
71		deadline: U256,
72	) -> [u8; 32] {
73		let domain_separator = Self::compute_domain_separator(address);
74
75		let permit_content = solidity::encode_arguments((
76			H256::from(PERMIT_TYPEHASH),
77			Address(owner),
78			Address(spender),
79			value,
80			nonce,
81			deadline,
82		));
83		let permit_content = keccak_256(&permit_content);
84
85		let mut pre_digest = Vec::with_capacity(2 + 32 + 32);
86		pre_digest.extend_from_slice(b"\x19\x01");
87		pre_digest.extend_from_slice(&domain_separator);
88		pre_digest.extend_from_slice(&permit_content);
89		keccak_256(&pre_digest)
90	}
91
92	// Translated from
93	// https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol#L81
94	#[allow(clippy::too_many_arguments)]
95	pub(crate) fn permit(
96		handle: &mut impl PrecompileHandle,
97		owner: Address,
98		spender: Address,
99		value: U256,
100		deadline: U256,
101		v: u8,
102		r: H256,
103		s: H256,
104	) -> EvmResult {
105		// NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32)
106		handle.record_db_read::<Runtime>(104)?;
107
108		let owner: H160 = owner.into();
109		let spender: H160 = spender.into();
110
111		// Blockchain time is in ms while Ethereum use second timestamps.
112		let timestamp: u128 =
113			<Runtime as pallet_evm::Config>::Timestamp::now().unique_saturated_into();
114		let timestamp: U256 = U256::from(timestamp / 1000);
115
116		ensure!(deadline >= timestamp, revert("Permit expired"));
117
118		let nonce = NoncesStorage::<Instance>::get(owner);
119
120		let permit = Self::generate_permit(
121			handle.context().address,
122			owner,
123			spender,
124			value,
125			nonce,
126			deadline,
127		);
128
129		let mut sig = [0u8; 65];
130		sig[0..32].copy_from_slice(r.as_bytes());
131		sig[32..64].copy_from_slice(s.as_bytes());
132		sig[64] = v;
133
134		let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit)
135			.map_err(|_| revert("Invalid permit"))?;
136		let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice()));
137
138		ensure!(
139			signer != H160::zero() && signer == owner,
140			revert("Invalid permit")
141		);
142
143		NoncesStorage::<Instance>::insert(owner, nonce + U256::one());
144
145		{
146			let amount =
147				Erc20BalancesPrecompile::<Runtime, Metadata, Instance>::u256_to_amount(value)
148					.unwrap_or_else(|_| Bounded::max_value());
149
150			let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner);
151			let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender);
152			ApprovesStorage::<Runtime, Instance>::insert(owner, spender, amount);
153		}
154
155		log3(
156			handle.context().address,
157			SELECTOR_LOG_APPROVAL,
158			owner,
159			spender,
160			solidity::encode_event_data(value),
161		)
162		.record(handle)?;
163
164		Ok(())
165	}
166
167	pub(crate) fn nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult<U256> {
168		// NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32)
169		handle.record_db_read::<Runtime>(104)?;
170
171		let owner: H160 = owner.into();
172
173		Ok(NoncesStorage::<Instance>::get(owner))
174	}
175
176	pub(crate) fn domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult<H256> {
177		// ChainId
178		handle.record_db_read::<Runtime>(8)?;
179
180		Ok(Self::compute_domain_separator(handle.context().address).into())
181	}
182}