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 encode relay staking calls via the EVM
18

            
19
#![cfg_attr(not(feature = "std"), no_std)]
20

            
21
use cumulus_primitives_core::relay_chain;
22

            
23
use fp_evm::PrecompileHandle;
24
use frame_support::{
25
	dispatch::{GetDispatchInfo, PostDispatchInfo},
26
	ensure,
27
	traits::ConstU32,
28
};
29
use pallet_staking::RewardDestination;
30
use precompile_utils::prelude::*;
31
use sp_core::{H256, U256};
32
use sp_runtime::{traits::Dispatchable, AccountId32, Perbill};
33
use sp_std::vec::Vec;
34
use sp_std::{convert::TryInto, marker::PhantomData};
35
use xcm_primitives::{AvailableStakeCalls, HrmpAvailableCalls, HrmpEncodeCall, StakeEncodeCall};
36

            
37
#[cfg(test)]
38
mod mock;
39
#[cfg(test)]
40
mod test_relay_runtime;
41
#[cfg(test)]
42
mod tests;
43

            
44
pub const REWARD_DESTINATION_SIZE_LIMIT: u32 = 2u32.pow(16);
45
pub const ARRAY_LIMIT: u32 = 512;
46
type GetArrayLimit = ConstU32<ARRAY_LIMIT>;
47
type GetRewardDestinationSizeLimit = ConstU32<REWARD_DESTINATION_SIZE_LIMIT>;
48

            
49
/// A precompile to provide relay stake calls encoding through evm
50
pub struct RelayEncoderPrecompile<Runtime>(PhantomData<Runtime>);
51

            
52
250
#[precompile_utils::precompile]
53
impl<Runtime> RelayEncoderPrecompile<Runtime>
54
where
55
	Runtime: pallet_evm::Config + pallet_xcm_transactor::Config,
56
	Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
57
{
58
	#[precompile::public("encodeBond(uint256,bytes)")]
59
	#[precompile::public("encode_bond(uint256,bytes)")]
60
	#[precompile::view]
61
1
	fn encode_bond(
62
1
		handle: &mut impl PrecompileHandle,
63
1
		amount: U256,
64
1
		reward_destination: RewardDestinationWrapper,
65
1
	) -> EvmResult<UnboundedBytes> {
66
1
		// No DB access but lot of logical stuff
67
1
		// To prevent spam, we charge an arbitrary amount of gas
68
1
		handle.record_cost(1000)?;
69

            
70
1
		let relay_amount = u256_to_relay_amount(amount)?;
71
1
		let reward_destination = reward_destination.into();
72
1

            
73
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
74
1
			AvailableStakeCalls::Bond(relay_amount, reward_destination),
75
1
		)
76
1
		.as_slice()
77
1
		.into();
78
1

            
79
1
		Ok(encoded)
80
1
	}
81

            
82
	#[precompile::public("encodeBondExtra(uint256)")]
83
	#[precompile::public("encode_bond_extra(uint256)")]
84
	#[precompile::view]
85
1
	fn encode_bond_extra(
86
1
		handle: &mut impl PrecompileHandle,
87
1
		amount: U256,
88
1
	) -> EvmResult<UnboundedBytes> {
89
1
		// No DB access but lot of logical stuff
90
1
		// To prevent spam, we charge an arbitrary amount of gas
91
1
		handle.record_cost(1000)?;
92

            
93
1
		let relay_amount = u256_to_relay_amount(amount)?;
94
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
95
1
			AvailableStakeCalls::BondExtra(relay_amount),
96
1
		)
97
1
		.as_slice()
98
1
		.into();
99
1

            
100
1
		Ok(encoded)
101
1
	}
102

            
103
	#[precompile::public("encodeUnbond(uint256)")]
104
	#[precompile::public("encode_unbond(uint256)")]
105
	#[precompile::view]
106
1
	fn encode_unbond(
107
1
		handle: &mut impl PrecompileHandle,
108
1
		amount: U256,
109
1
	) -> EvmResult<UnboundedBytes> {
110
1
		// No DB access but lot of logical stuff
111
1
		// To prevent spam, we charge an arbitrary amount of gas
112
1
		handle.record_cost(1000)?;
113

            
114
1
		let relay_amount = u256_to_relay_amount(amount)?;
115

            
116
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
117
1
			AvailableStakeCalls::Unbond(relay_amount),
118
1
		)
119
1
		.as_slice()
120
1
		.into();
121
1

            
122
1
		Ok(encoded)
123
1
	}
124

            
125
	#[precompile::public("encodeWithdrawUnbonded(uint32)")]
126
	#[precompile::public("encode_withdraw_unbonded(uint32)")]
127
	#[precompile::view]
128
1
	fn encode_withdraw_unbonded(
129
1
		handle: &mut impl PrecompileHandle,
130
1
		slashes: u32,
131
1
	) -> EvmResult<UnboundedBytes> {
132
1
		// No DB access but lot of logical stuff
133
1
		// To prevent spam, we charge an arbitrary amount of gas
134
1
		handle.record_cost(1000)?;
135

            
136
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
137
1
			AvailableStakeCalls::WithdrawUnbonded(slashes),
138
1
		)
139
1
		.as_slice()
140
1
		.into();
141
1

            
142
1
		Ok(encoded)
143
1
	}
144

            
145
	#[precompile::public("encodeValidate(uint256,bool)")]
146
	#[precompile::public("encode_validate(uint256,bool)")]
147
	#[precompile::view]
148
1
	fn encode_validate(
149
1
		handle: &mut impl PrecompileHandle,
150
1
		commission: Convert<U256, u32>,
151
1
		blocked: bool,
152
1
	) -> EvmResult<UnboundedBytes> {
153
1
		// No DB access but lot of logical stuff
154
1
		// To prevent spam, we charge an arbitrary amount of gas
155
1
		handle.record_cost(1000)?;
156

            
157
1
		let fraction = Perbill::from_parts(commission.converted());
158
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
159
1
			AvailableStakeCalls::Validate(pallet_staking::ValidatorPrefs {
160
1
				commission: fraction,
161
1
				blocked: blocked,
162
1
			}),
163
1
		)
164
1
		.as_slice()
165
1
		.into();
166
1

            
167
1
		Ok(encoded)
168
1
	}
169

            
170
	#[precompile::public("encodeNominate(bytes32[])")]
171
	#[precompile::public("encode_nominate(bytes32[])")]
172
	#[precompile::view]
173
1
	fn encode_nominate(
174
1
		handle: &mut impl PrecompileHandle,
175
1
		nominees: BoundedVec<H256, GetArrayLimit>,
176
1
	) -> EvmResult<UnboundedBytes> {
177
1
		// No DB access but lot of logical stuff
178
1
		// To prevent spam, we charge an arbitrary amount of gas
179
1
		handle.record_cost(1000)?;
180

            
181
1
		let nominees: Vec<_> = nominees.into();
182
1
		let nominated: Vec<AccountId32> = nominees
183
1
			.iter()
184
2
			.map(|&add| {
185
2
				let as_bytes: [u8; 32] = add.into();
186
2
				as_bytes.into()
187
2
			})
188
1
			.collect();
189
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
190
1
			AvailableStakeCalls::Nominate(nominated),
191
1
		)
192
1
		.as_slice()
193
1
		.into();
194
1

            
195
1
		Ok(encoded)
196
1
	}
197

            
198
	#[precompile::public("encodeChill()")]
199
	#[precompile::public("encode_chill()")]
200
	#[precompile::view]
201
3
	fn encode_chill(handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
202
3
		// No DB access but lot of logical stuff
203
3
		// To prevent spam, we charge an arbitrary amount of gas
204
3
		handle.record_cost(1000)?;
205

            
206
3
		let encoded =
207
3
			pallet_xcm_transactor::Pallet::<Runtime>::encode_call(AvailableStakeCalls::Chill)
208
3
				.as_slice()
209
3
				.into();
210
3

            
211
3
		Ok(encoded)
212
3
	}
213

            
214
	#[precompile::public("encodeSetPayee(bytes)")]
215
	#[precompile::public("encode_set_payee(bytes)")]
216
	#[precompile::view]
217
1
	fn encode_set_payee(
218
1
		handle: &mut impl PrecompileHandle,
219
1
		reward_destination: RewardDestinationWrapper,
220
1
	) -> EvmResult<UnboundedBytes> {
221
1
		// No DB access but lot of logical stuff
222
1
		// To prevent spam, we charge an arbitrary amount of gas
223
1
		handle.record_cost(1000)?;
224

            
225
1
		let reward_destination = reward_destination.into();
226
1

            
227
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
228
1
			AvailableStakeCalls::SetPayee(reward_destination),
229
1
		)
230
1
		.as_slice()
231
1
		.into();
232
1

            
233
1
		Ok(encoded)
234
1
	}
235

            
236
	#[precompile::public("encodeSetController()")]
237
	#[precompile::public("encode_set_controller()")]
238
	#[precompile::view]
239
3
	fn encode_set_controller(handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
240
3
		// No DB access but lot of logical stuff
241
3
		// To prevent spam, we charge an arbitrary amount of gas
242
3
		handle.record_cost(1000)?;
243

            
244
3
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
245
3
			AvailableStakeCalls::SetController,
246
3
		)
247
3
		.as_slice()
248
3
		.into();
249
3

            
250
3
		Ok(encoded)
251
3
	}
252

            
253
	#[precompile::public("encodeRebond(uint256)")]
254
	#[precompile::public("encode_rebond(uint256)")]
255
	#[precompile::view]
256
1
	fn encode_rebond(
257
1
		handle: &mut impl PrecompileHandle,
258
1
		amount: U256,
259
1
	) -> EvmResult<UnboundedBytes> {
260
1
		// No DB access but lot of logical stuff
261
1
		// To prevent spam, we charge an arbitrary amount of gas
262
1
		handle.record_cost(1000)?;
263

            
264
1
		let relay_amount = u256_to_relay_amount(amount)?;
265
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
266
1
			AvailableStakeCalls::Rebond(relay_amount),
267
1
		)
268
1
		.as_slice()
269
1
		.into();
270
1

            
271
1
		Ok(encoded)
272
1
	}
273
	#[precompile::public("encodeHrmpInitOpenChannel(uint32,uint32,uint32)")]
274
	#[precompile::public("encode_hrmp_init_open_channel(uint32,uint32,uint32)")]
275
	#[precompile::view]
276
	fn encode_hrmp_init_open_channel(
277
		handle: &mut impl PrecompileHandle,
278
		recipient: u32,
279
		max_capacity: u32,
280
		max_message_size: u32,
281
	) -> EvmResult<UnboundedBytes> {
282
		// No DB access but lot of logical stuff
283
		// To prevent spam, we charge an arbitrary amount of gas
284
		handle.record_cost(1000)?;
285

            
286
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
287
			HrmpAvailableCalls::InitOpenChannel(recipient.into(), max_capacity, max_message_size),
288
		)
289
		.map_err(|_| {
290
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
291
				.in_field("transactor")
292
		})?
293
		.as_slice()
294
		.into();
295
		Ok(encoded)
296
	}
297

            
298
	#[precompile::public("encodeHrmpAcceptOpenChannel(uint32)")]
299
	#[precompile::public("encode_hrmp_accept_open_channel(uint32)")]
300
	#[precompile::view]
301
	fn encode_hrmp_accept_open_channel(
302
		handle: &mut impl PrecompileHandle,
303
		sender: u32,
304
	) -> EvmResult<UnboundedBytes> {
305
		// No DB access but lot of logical stuff
306
		// To prevent spam, we charge an arbitrary amount of gas
307
		handle.record_cost(1000)?;
308

            
309
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
310
			HrmpAvailableCalls::AcceptOpenChannel(sender.into()),
311
		)
312
		.map_err(|_| {
313
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
314
				.in_field("transactor")
315
		})?
316
		.as_slice()
317
		.into();
318
		Ok(encoded)
319
	}
320

            
321
	#[precompile::public("encodeHrmpCloseChannel(uint32,uint32)")]
322
	#[precompile::public("encode_hrmp_close_channel(uint32,uint32)")]
323
	#[precompile::view]
324
	fn encode_hrmp_close_channel(
325
		handle: &mut impl PrecompileHandle,
326
		sender: u32,
327
		recipient: u32,
328
	) -> EvmResult<UnboundedBytes> {
329
		// No DB access but lot of logical stuff
330
		// To prevent spam, we charge an arbitrary amount of gas
331
		handle.record_cost(1000)?;
332

            
333
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
334
			HrmpAvailableCalls::CloseChannel(relay_chain::HrmpChannelId {
335
				sender: sender.into(),
336
				recipient: recipient.into(),
337
			}),
338
		)
339
		.map_err(|_| {
340
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
341
				.in_field("transactor")
342
		})?
343
		.as_slice()
344
		.into();
345
		Ok(encoded)
346
	}
347

            
348
	#[precompile::public("encodeHrmpCancelOpenRequest(uint32,uint32,uint32)")]
349
	#[precompile::public("encode_hrmp_cancel_open_request(uint32,uint32,uint32)")]
350
	#[precompile::view]
351
	fn encode_hrmp_cancel_open_request(
352
		handle: &mut impl PrecompileHandle,
353
		sender: u32,
354
		recipient: u32,
355
		open_requests: u32,
356
	) -> EvmResult<UnboundedBytes> {
357
		// No DB access but lot of logical stuff
358
		// To prevent spam, we charge an arbitrary amount of gas
359
		handle.record_cost(1000)?;
360

            
361
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
362
			HrmpAvailableCalls::CancelOpenRequest(
363
				relay_chain::HrmpChannelId {
364
					sender: sender.into(),
365
					recipient: recipient.into(),
366
				},
367
				open_requests,
368
			),
369
		)
370
		.map_err(|_| {
371
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
372
				.in_field("transactor")
373
		})?
374
		.as_slice()
375
		.into();
376
		Ok(encoded)
377
	}
378
}
379

            
380
4
pub fn u256_to_relay_amount(value: U256) -> EvmResult<relay_chain::Balance> {
381
4
	value
382
4
		.try_into()
383
4
		.map_err(|_| revert("amount is too large for provided balance type"))
384
4
}
385

            
386
// A wrapper to be able to implement here the solidity::Codec reader
387
#[derive(Clone, Eq, PartialEq)]
388
pub struct RewardDestinationWrapper(RewardDestination<AccountId32>);
389

            
390
impl From<RewardDestination<AccountId32>> for RewardDestinationWrapper {
391
	fn from(reward_dest: RewardDestination<AccountId32>) -> Self {
392
		RewardDestinationWrapper(reward_dest)
393
	}
394
}
395

            
396
impl Into<RewardDestination<AccountId32>> for RewardDestinationWrapper {
397
2
	fn into(self) -> RewardDestination<AccountId32> {
398
2
		self.0
399
2
	}
400
}
401

            
402
impl solidity::Codec for RewardDestinationWrapper {
403
2
	fn read(reader: &mut solidity::codec::Reader) -> MayRevert<Self> {
404
2
		let reward_destination = reader.read::<BoundedBytes<GetRewardDestinationSizeLimit>>()?;
405
2
		let reward_destination_bytes: Vec<_> = reward_destination.into();
406
2
		ensure!(
407
2
			reward_destination_bytes.len() > 0,
408
			RevertReason::custom("Reward destinations cannot be empty")
409
		);
410
		// For simplicity we use an EvmReader here
411
2
		let mut encoded_reward_destination =
412
2
			solidity::codec::Reader::new(&reward_destination_bytes);
413

            
414
		// We take the first byte
415
2
		let enum_selector = encoded_reward_destination.read_raw_bytes(1)?;
416
		// The firs byte selects the enum variant
417
2
		match enum_selector[0] {
418
			0u8 => Ok(RewardDestinationWrapper(RewardDestination::Staked)),
419
			1u8 => Ok(RewardDestinationWrapper(RewardDestination::Stash)),
420
			// Deprecated in https://github.com/paritytech/polkadot-sdk/pull/2380
421
			#[allow(deprecated)]
422
			2u8 => Ok(RewardDestinationWrapper(RewardDestination::Controller)),
423
			3u8 => {
424
2
				let address = encoded_reward_destination.read::<H256>()?;
425
2
				Ok(RewardDestinationWrapper(RewardDestination::Account(
426
2
					address.as_fixed_bytes().clone().into(),
427
2
				)))
428
			}
429
			4u8 => Ok(RewardDestinationWrapper(RewardDestination::None)),
430
			_ => Err(RevertReason::custom("Unknown reward destination").into()),
431
		}
432
2
	}
433

            
434
2
	fn write(writer: &mut solidity::codec::Writer, value: Self) {
435
2
		let mut encoded: Vec<u8> = Vec::new();
436
2
		let encoded_bytes: UnboundedBytes = match value.0 {
437
			RewardDestination::Staked => {
438
				encoded.push(0);
439
				encoded.as_slice().into()
440
			}
441
			RewardDestination::Stash => {
442
				encoded.push(1);
443
				encoded.as_slice().into()
444
			}
445
			// Deprecated in https://github.com/paritytech/polkadot-sdk/pull/2380
446
			#[allow(deprecated)]
447
			RewardDestination::Controller => {
448
				encoded.push(2);
449
				encoded.as_slice().into()
450
			}
451
2
			RewardDestination::Account(address) => {
452
2
				encoded.push(3);
453
2
				let address_bytes: [u8; 32] = address.into();
454
2
				encoded.append(&mut address_bytes.to_vec());
455
2
				encoded.as_slice().into()
456
			}
457
			RewardDestination::None => {
458
				encoded.push(4);
459
				encoded.as_slice().into()
460
			}
461
		};
462
2
		solidity::Codec::write(writer, encoded_bytes);
463
2
	}
464

            
465
	fn has_static_size() -> bool {
466
		false
467
	}
468

            
469
2
	fn signature() -> String {
470
2
		UnboundedBytes::signature()
471
2
	}
472
}