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

            
21
use evm::{ExitError, ExitReason};
22
use fp_evm::{Context, Log, PrecompileFailure, PrecompileHandle, Transfer};
23
use frame_support::traits::ConstU32;
24
use precompile_utils::{evm::costs::call_cost, prelude::*};
25
use sp_core::{H160, U256};
26
use sp_std::{iter::repeat, marker::PhantomData, vec, vec::Vec};
27

            
28
#[cfg(test)]
29
mod mock;
30
#[cfg(test)]
31
mod tests;
32

            
33
#[derive(Copy, Clone, Debug, PartialEq)]
34
pub 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

            
40
pub const LOG_SUBCALL_SUCCEEDED: [u8; 32] = keccak256!("SubcallSucceeded(uint256)");
41
pub const LOG_SUBCALL_FAILED: [u8; 32] = keccak256!("SubcallFailed(uint256)");
42
pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
43
pub const ARRAY_LIMIT: u32 = 2u32.pow(9);
44

            
45
type GetCallDataLimit = ConstU32<CALL_DATA_LIMIT>;
46
type GetArrayLimit = ConstU32<ARRAY_LIMIT>;
47

            
48
77
pub fn log_subcall_succeeded(address: impl Into<H160>, index: usize) -> Log {
49
77
	log1(
50
77
		address,
51
77
		LOG_SUBCALL_SUCCEEDED,
52
77
		solidity::encode_event_data(U256::from(index)),
53
77
	)
54
77
}
55

            
56
145
pub fn log_subcall_failed(address: impl Into<H160>, index: usize) -> Log {
57
145
	log1(
58
145
		address,
59
145
		LOG_SUBCALL_FAILED,
60
145
		solidity::encode_event_data(U256::from(index)),
61
145
	)
62
145
}
63

            
64
/// Batch precompile.
65
#[derive(Debug, Clone)]
66
pub 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
444
#[precompile_utils::precompile]
71
impl<Runtime> BatchPrecompile<Runtime>
72
where
73
	Runtime: pallet_evm::Config,
74
{
75
	#[precompile::public("batchSome(address[],uint256[],bytes[],uint64[])")]
76
10
	fn batch_some(
77
10
		handle: &mut impl PrecompileHandle,
78
10
		to: BoundedVec<Address, GetArrayLimit>,
79
10
		value: BoundedVec<U256, GetArrayLimit>,
80
10
		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
81
10
		gas_limit: BoundedVec<u64, GetArrayLimit>,
82
10
	) -> EvmResult {
83
10
		Self::inner_batch(Mode::BatchSome, handle, to, value, call_data, gas_limit)
84
10
	}
85

            
86
	#[precompile::public("batchSomeUntilFailure(address[],uint256[],bytes[],uint64[])")]
87
10
	fn batch_some_until_failure(
88
10
		handle: &mut impl PrecompileHandle,
89
10
		to: BoundedVec<Address, GetArrayLimit>,
90
10
		value: BoundedVec<U256, GetArrayLimit>,
91
10
		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
92
10
		gas_limit: BoundedVec<u64, GetArrayLimit>,
93
10
	) -> EvmResult {
94
10
		Self::inner_batch(
95
10
			Mode::BatchSomeUntilFailure,
96
10
			handle,
97
10
			to,
98
10
			value,
99
10
			call_data,
100
10
			gas_limit,
101
10
		)
102
10
	}
103

            
104
	#[precompile::public("batchAll(address[],uint256[],bytes[],uint64[])")]
105
56
	fn batch_all(
106
56
		handle: &mut impl PrecompileHandle,
107
56
		to: BoundedVec<Address, GetArrayLimit>,
108
56
		value: BoundedVec<U256, GetArrayLimit>,
109
56
		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
110
56
		gas_limit: BoundedVec<u64, GetArrayLimit>,
111
56
	) -> EvmResult {
112
56
		Self::inner_batch(Mode::BatchAll, handle, to, value, call_data, gas_limit)
113
56
	}
114

            
115
76
	fn inner_batch(
116
76
		mode: Mode,
117
76
		handle: &mut impl PrecompileHandle,
118
76
		to: BoundedVec<Address, GetArrayLimit>,
119
76
		value: BoundedVec<U256, GetArrayLimit>,
120
76
		call_data: BoundedVec<BoundedBytes<GetCallDataLimit>, GetArrayLimit>,
121
76
		gas_limit: BoundedVec<u64, GetArrayLimit>,
122
76
	) -> EvmResult {
123
76
		let addresses = Vec::from(to).into_iter().enumerate();
124
76
		let values = Vec::from(value)
125
76
			.into_iter()
126
107
			.map(|x| Some(x))
127
76
			.chain(repeat(None));
128
76
		let calls_data = Vec::from(call_data)
129
76
			.into_iter()
130
76
			.map(|x| Some(x.into()))
131
76
			.chain(repeat(None));
132
76
		let gas_limits = Vec::from(gas_limit).into_iter().map(|x|
133
			// x = 0 => forward all remaining gas
134
3
			if x == 0 {
135
				None
136
			} else {
137
3
				Some(x)
138
76
			}
139
76
		).chain(repeat(None));
140

            
141
		// Cost of batch log. (doesn't change when index changes)
142
76
		let log_cost = log_subcall_failed(handle.code_address(), 0)
143
76
			.compute_cost()
144
76
			.map_err(|_| revert("Failed to compute log cost"))?;
145

            
146
111
		for ((i, address), (value, (call_data, gas_limit))) in
147
76
			addresses.zip(values.zip(calls_data.zip(gas_limits)))
148
		{
149
111
			let address = address.0;
150
111
			let value = value.unwrap_or(U256::zero());
151
111
			let call_data = call_data.unwrap_or(vec![]);
152
111

            
153
111
			let sub_context = Context {
154
111
				caller: handle.context().caller,
155
111
				address: address.clone(),
156
111
				apparent_value: value,
157
111
			};
158

            
159
111
			let transfer = if value.is_zero() {
160
24
				None
161
			} else {
162
87
				Some(Transfer {
163
87
					source: handle.context().caller,
164
87
					target: address.clone(),
165
87
					value,
166
87
				})
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
111
			let remaining_gas = handle.remaining_gas();
172

            
173
111
			let forwarded_gas = match (remaining_gas.checked_sub(log_cost), mode) {
174
108
				(Some(remaining), _) => remaining,
175
				(None, Mode::BatchAll) => {
176
1
					return Err(PrecompileFailure::Error {
177
1
						exit_status: ExitError::OutOfGas,
178
1
					})
179
				}
180
				(None, _) => {
181
2
					return Ok(());
182
				}
183
			};
184

            
185
			// Cost of the call itself that the batch precompile must pay.
186
108
			let call_cost = call_cost(value, <Runtime as pallet_evm::Config>::config());
187

            
188
108
			let forwarded_gas = match forwarded_gas.checked_sub(call_cost) {
189
105
				Some(remaining) => remaining,
190
				None => {
191
3
					let log = log_subcall_failed(handle.code_address(), i);
192
3
					handle.record_log_costs(&[&log])?;
193
3
					log.record(handle)?;
194

            
195
3
					match mode {
196
						Mode::BatchAll => {
197
1
							return Err(PrecompileFailure::Error {
198
1
								exit_status: ExitError::OutOfGas,
199
1
							})
200
						}
201
1
						Mode::BatchSomeUntilFailure => return Ok(()),
202
1
						Mode::BatchSome => continue,
203
					}
204
				}
205
			};
206

            
207
			// If there is a provided gas limit we ensure there is enough gas remaining.
208
105
			let forwarded_gas = match gas_limit {
209
102
				None => forwarded_gas, // provide all gas if no gas limit,
210
3
				Some(limit) => {
211
3
					if limit > forwarded_gas {
212
3
						let log = log_subcall_failed(handle.code_address(), i);
213
3
						handle.record_log_costs(&[&log])?;
214
3
						log.record(handle)?;
215

            
216
3
						match mode {
217
							Mode::BatchAll => {
218
1
								return Err(PrecompileFailure::Error {
219
1
									exit_status: ExitError::OutOfGas,
220
1
								})
221
							}
222
1
							Mode::BatchSomeUntilFailure => return Ok(()),
223
1
							Mode::BatchSome => continue,
224
						}
225
					}
226
					limit
227
				}
228
			};
229

            
230
102
			let (reason, output) = handle.call(
231
102
				address,
232
102
				transfer,
233
102
				call_data,
234
102
				Some(forwarded_gas),
235
102
				false,
236
102
				&sub_context,
237
102
			);
238
102

            
239
102
			// Logs
240
102
			// We reserved enough gas so this should not OOG.
241
102
			match reason {
242
				ExitReason::Revert(_) | ExitReason::Error(_) => {
243
34
					let log = log_subcall_failed(handle.code_address(), i);
244
34
					handle.record_log_costs(&[&log])?;
245
34
					log.record(handle)?
246
				}
247
				ExitReason::Succeed(_) => {
248
68
					let log = log_subcall_succeeded(handle.code_address(), i);
249
68
					handle.record_log_costs(&[&log])?;
250
68
					log.record(handle)?
251
				}
252
				_ => (),
253
			}
254

            
255
			// How to proceed
256
102
			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
24
				(Mode::BatchAll, ExitReason::Revert(exit_status)) => {
264
24
					return Err(PrecompileFailure::Revert {
265
24
						exit_status,
266
24
						output,
267
24
					})
268
				}
269
2
				(Mode::BatchAll, ExitReason::Error(exit_status)) => {
270
2
					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
4
					return Ok(())
277
				}
278

            
279
				// Success or ignored revert/error.
280
72
				(_, _) => (),
281
			}
282
		}
283

            
284
39
		Ok(())
285
76
	}
286
}
287

            
288
// The enum is generated by the macro above.
289
// We add this method to simplify writing tests generic over the mode.
290
impl<Runtime> BatchPrecompileCall<Runtime>
291
where
292
	Runtime: pallet_evm::Config,
293
{
294
21
	pub fn batch_from_mode(
295
21
		mode: Mode,
296
21
		to: Vec<Address>,
297
21
		value: Vec<U256>,
298
21
		call_data: Vec<Vec<u8>>,
299
21
		gas_limit: Vec<u64>,
300
21
	) -> Self {
301
21
		// Convert Vecs into their bounded versions.
302
21
		// This is mainly a convenient function to write tests.
303
21
		// Bounds are only checked when parsing from call data.
304
21
		let to = to.into();
305
21
		let value = value.into();
306
23
		let call_data: Vec<_> = call_data.into_iter().map(|inner| inner.into()).collect();
307
21
		let call_data = call_data.into();
308
21
		let gas_limit = gas_limit.into();
309
21

            
310
21
		match mode {
311
6
			Mode::BatchSome => Self::batch_some {
312
6
				to,
313
6
				value,
314
6
				call_data,
315
6
				gas_limit,
316
6
			},
317
6
			Mode::BatchSomeUntilFailure => Self::batch_some_until_failure {
318
6
				to,
319
6
				value,
320
6
				call_data,
321
6
				gas_limit,
322
6
			},
323
9
			Mode::BatchAll => Self::batch_all {
324
9
				to,
325
9
				value,
326
9
				call_data,
327
9
				gas_limit,
328
9
			},
329
		}
330
21
	}
331
}