pallet_evm_precompile_batch/
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//! Precompile to interact with pallet_balances instances using the ERC20 interface standard.
18
19#![cfg_attr(not(feature = "std"), no_std)]
20
21use evm::{ExitError, ExitReason};
22use fp_evm::{Context, Log, PrecompileFailure, PrecompileHandle, Transfer};
23use frame_support::traits::ConstU32;
24use precompile_utils::{evm::costs::call_cost, prelude::*};
25use sp_core::{H160, U256};
26use sp_std::{iter::repeat, marker::PhantomData, vec, vec::Vec};
27
28#[cfg(test)]
29mod mock;
30#[cfg(test)]
31mod tests;
32
33#[derive(Copy, Clone, Debug, PartialEq)]
34pub enum Mode {
35	BatchSome,             // = "batchSome(address[],uint256[],bytes[],uint64[])",
36	BatchSomeUntilFailure, // = "batchSomeUntilFailure(address[],uint256[],bytes[],uint64[])",
37	BatchAll,              // = "batchAll(address[],uint256[],bytes[],uint64[])",
38}
39
40pub const LOG_SUBCALL_SUCCEEDED: [u8; 32] = keccak256!("SubcallSucceeded(uint256)");
41pub const LOG_SUBCALL_FAILED: [u8; 32] = keccak256!("SubcallFailed(uint256)");
42pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
43pub const ARRAY_LIMIT: u32 = 2u32.pow(9);
44
45type GetCallDataLimit = ConstU32<CALL_DATA_LIMIT>;
46type GetArrayLimit = ConstU32<ARRAY_LIMIT>;
47
48pub fn log_subcall_succeeded(address: impl Into<H160>, index: usize) -> Log {
49	log1(
50		address,
51		LOG_SUBCALL_SUCCEEDED,
52		solidity::encode_event_data(U256::from(index)),
53	)
54}
55
56pub fn log_subcall_failed(address: impl Into<H160>, index: usize) -> Log {
57	log1(
58		address,
59		LOG_SUBCALL_FAILED,
60		solidity::encode_event_data(U256::from(index)),
61	)
62}
63
64/// Batch precompile.
65#[derive(Debug, Clone)]
66pub struct BatchPrecompile<Runtime>(PhantomData<Runtime>);
67
68// No funds are transfered to the precompile address.
69// Transfers will directly be made on the behalf of the user by the precompile.
70#[precompile_utils::precompile]
71impl<Runtime> BatchPrecompile<Runtime>
72where
73	Runtime: pallet_evm::Config,
74{
75	#[precompile::public("batchSome(address[],uint256[],bytes[],uint64[])")]
76	fn batch_some(
77		handle: &mut impl PrecompileHandle,
78		to: BoundedVec<Address, GetArrayLimit>,
79		value: BoundedVec<U256, GetArrayLimit>,
80		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
81		gas_limit: BoundedVec<u64, GetArrayLimit>,
82	) -> EvmResult {
83		Self::inner_batch(Mode::BatchSome, handle, to, value, call_data, gas_limit)
84	}
85
86	#[precompile::public("batchSomeUntilFailure(address[],uint256[],bytes[],uint64[])")]
87	fn batch_some_until_failure(
88		handle: &mut impl PrecompileHandle,
89		to: BoundedVec<Address, GetArrayLimit>,
90		value: BoundedVec<U256, GetArrayLimit>,
91		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
92		gas_limit: BoundedVec<u64, GetArrayLimit>,
93	) -> EvmResult {
94		Self::inner_batch(
95			Mode::BatchSomeUntilFailure,
96			handle,
97			to,
98			value,
99			call_data,
100			gas_limit,
101		)
102	}
103
104	#[precompile::public("batchAll(address[],uint256[],bytes[],uint64[])")]
105	fn batch_all(
106		handle: &mut impl PrecompileHandle,
107		to: BoundedVec<Address, GetArrayLimit>,
108		value: BoundedVec<U256, GetArrayLimit>,
109		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
110		gas_limit: BoundedVec<u64, GetArrayLimit>,
111	) -> EvmResult {
112		Self::inner_batch(Mode::BatchAll, handle, to, value, call_data, gas_limit)
113	}
114
115	fn inner_batch(
116		mode: Mode,
117		handle: &mut impl PrecompileHandle,
118		to: BoundedVec<Address, GetArrayLimit>,
119		value: BoundedVec<U256, GetArrayLimit>,
120		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
121		gas_limit: BoundedVec<u64, GetArrayLimit>,
122	) -> EvmResult {
123		let addresses = Vec::from(to).into_iter().enumerate();
124		let values = Vec::from(value)
125			.into_iter()
126			.map(|x| Some(x))
127			.chain(repeat(None));
128		let calls_data = Vec::from(call_data)
129			.into_iter()
130			.map(|x| Some(x.into()))
131			.chain(repeat(None));
132		let gas_limits = Vec::from(gas_limit).into_iter().map(|x|
133			// x = 0 => forward all remaining gas
134			if x == 0 {
135				None
136			} else {
137				Some(x)
138			}
139		).chain(repeat(None));
140
141		// Cost of batch log. (doesn't change when index changes)
142		let log_cost = log_subcall_failed(handle.code_address(), 0)
143			.compute_cost()
144			.map_err(|_| revert("Failed to compute log cost"))?;
145
146		for ((i, address), (value, (call_data, gas_limit))) in
147			addresses.zip(values.zip(calls_data.zip(gas_limits)))
148		{
149			let address = address.0;
150			let value = value.unwrap_or(U256::zero());
151			let call_data = call_data.unwrap_or(vec![]);
152
153			let sub_context = Context {
154				caller: handle.context().caller,
155				address: address.clone(),
156				apparent_value: value,
157			};
158
159			let transfer = if value.is_zero() {
160				None
161			} else {
162				Some(Transfer {
163					source: handle.context().caller,
164					target: address.clone(),
165					value,
166				})
167			};
168
169			// We reserve enough gas to emit a final log and perform the subcall itself.
170			// If not enough gas we stop there according to Mode strategy.
171			let remaining_gas = handle.remaining_gas();
172
173			let forwarded_gas = match (remaining_gas.checked_sub(log_cost), mode) {
174				(Some(remaining), _) => remaining,
175				(None, Mode::BatchAll) => {
176					return Err(PrecompileFailure::Error {
177						exit_status: ExitError::OutOfGas,
178					})
179				}
180				(None, _) => {
181					return Ok(());
182				}
183			};
184
185			// Cost of the call itself that the batch precompile must pay.
186			let call_cost = call_cost(value, <Runtime as pallet_evm::Config>::config());
187
188			let forwarded_gas = match forwarded_gas.checked_sub(call_cost) {
189				Some(remaining) => remaining,
190				None => {
191					let log = log_subcall_failed(handle.code_address(), i);
192					handle.record_log_costs(&[&log])?;
193					log.record(handle)?;
194
195					match mode {
196						Mode::BatchAll => {
197							return Err(PrecompileFailure::Error {
198								exit_status: ExitError::OutOfGas,
199							})
200						}
201						Mode::BatchSomeUntilFailure => return Ok(()),
202						Mode::BatchSome => continue,
203					}
204				}
205			};
206
207			// If there is a provided gas limit we ensure there is enough gas remaining.
208			let forwarded_gas = match gas_limit {
209				None => forwarded_gas, // provide all gas if no gas limit,
210				Some(limit) => {
211					if limit > forwarded_gas {
212						let log = log_subcall_failed(handle.code_address(), i);
213						handle.record_log_costs(&[&log])?;
214						log.record(handle)?;
215
216						match mode {
217							Mode::BatchAll => {
218								return Err(PrecompileFailure::Error {
219									exit_status: ExitError::OutOfGas,
220								})
221							}
222							Mode::BatchSomeUntilFailure => return Ok(()),
223							Mode::BatchSome => continue,
224						}
225					}
226					limit
227				}
228			};
229
230			let (reason, output) = handle.call(
231				address,
232				transfer,
233				call_data,
234				Some(forwarded_gas),
235				false,
236				&sub_context,
237			);
238
239			// Logs
240			// We reserved enough gas so this should not OOG.
241			match reason {
242				ExitReason::Revert(_) | ExitReason::Error(_) => {
243					let log = log_subcall_failed(handle.code_address(), i);
244					handle.record_log_costs(&[&log])?;
245					log.record(handle)?
246				}
247				ExitReason::Succeed(_) => {
248					let log = log_subcall_succeeded(handle.code_address(), i);
249					handle.record_log_costs(&[&log])?;
250					log.record(handle)?
251				}
252				_ => (),
253			}
254
255			// How to proceed
256			match (mode, reason) {
257				// _: Fatal is always fatal
258				(_, ExitReason::Fatal(exit_status)) => {
259					return Err(PrecompileFailure::Fatal { exit_status })
260				}
261
262				// BatchAll : Reverts and errors are immediatly forwarded.
263				(Mode::BatchAll, ExitReason::Revert(exit_status)) => {
264					return Err(PrecompileFailure::Revert {
265						exit_status,
266						output,
267					})
268				}
269				(Mode::BatchAll, ExitReason::Error(exit_status)) => {
270					return Err(PrecompileFailure::Error { exit_status })
271				}
272
273				// BatchSomeUntilFailure : Reverts and errors prevent subsequent subcalls to
274				// be executed but the precompile still succeed.
275				(Mode::BatchSomeUntilFailure, ExitReason::Revert(_) | ExitReason::Error(_)) => {
276					return Ok(())
277				}
278
279				// Success or ignored revert/error.
280				(_, _) => (),
281			}
282		}
283
284		Ok(())
285	}
286}
287
288// The enum is generated by the macro above.
289// We add this method to simplify writing tests generic over the mode.
290impl<Runtime> BatchPrecompileCall<Runtime>
291where
292	Runtime: pallet_evm::Config,
293{
294	pub fn batch_from_mode(
295		mode: Mode,
296		to: Vec<Address>,
297		value: Vec<U256>,
298		call_data: Vec<Vec<u8>>,
299		gas_limit: Vec<u64>,
300	) -> Self {
301		// Convert Vecs into their bounded versions.
302		// This is mainly a convenient function to write tests.
303		// Bounds are only checked when parsing from call data.
304		let to = to.into();
305		let value = value.into();
306		let call_data: Vec<_> = call_data.into_iter().map(|inner| inner.into()).collect();
307		let call_data = call_data.into();
308		let gas_limit = gas_limit.into();
309
310		match mode {
311			Mode::BatchSome => Self::batch_some {
312				to,
313				value,
314				call_data,
315				gas_limit,
316			},
317			Mode::BatchSomeUntilFailure => Self::batch_some_until_failure {
318				to,
319				value,
320				call_data,
321				gas_limit,
322			},
323			Mode::BatchAll => Self::batch_all {
324				to,
325				value,
326				call_data,
327				gas_limit,
328			},
329		}
330	}
331}