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, StakingTransactor>(
51
	PhantomData<(Runtime, StakingTransactor)>,
52
);
53

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

            
73
1
		let relay_amount = u256_to_relay_amount(amount)?;
74
1
		let reward_destination = reward_destination.into();
75
1

            
76
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
77
1
			StakingTransactor::get(),
78
1
			AvailableStakeCalls::Bond(relay_amount, reward_destination),
79
1
		)
80
1
		.as_slice()
81
1
		.into();
82
1

            
83
1
		Ok(encoded)
84
1
	}
85

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

            
97
1
		let relay_amount = u256_to_relay_amount(amount)?;
98
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
99
1
			StakingTransactor::get(),
100
1
			AvailableStakeCalls::BondExtra(relay_amount),
101
1
		)
102
1
		.as_slice()
103
1
		.into();
104
1

            
105
1
		Ok(encoded)
106
1
	}
107

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

            
119
1
		let relay_amount = u256_to_relay_amount(amount)?;
120

            
121
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
122
1
			StakingTransactor::get(),
123
1
			AvailableStakeCalls::Unbond(relay_amount),
124
1
		)
125
1
		.as_slice()
126
1
		.into();
127
1

            
128
1
		Ok(encoded)
129
1
	}
130

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

            
142
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
143
1
			StakingTransactor::get(),
144
1
			AvailableStakeCalls::WithdrawUnbonded(slashes),
145
1
		)
146
1
		.as_slice()
147
1
		.into();
148
1

            
149
1
		Ok(encoded)
150
1
	}
151

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

            
164
1
		let fraction = Perbill::from_parts(commission.converted());
165
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
166
1
			StakingTransactor::get(),
167
1
			AvailableStakeCalls::Validate(pallet_staking::ValidatorPrefs {
168
1
				commission: fraction,
169
1
				blocked: blocked,
170
1
			}),
171
1
		)
172
1
		.as_slice()
173
1
		.into();
174
1

            
175
1
		Ok(encoded)
176
1
	}
177

            
178
	#[precompile::public("encodeNominate(bytes32[])")]
179
	#[precompile::public("encode_nominate(bytes32[])")]
180
	#[precompile::view]
181
1
	fn encode_nominate(
182
1
		handle: &mut impl PrecompileHandle,
183
1
		nominees: BoundedVec<H256, GetArrayLimit>,
184
1
	) -> EvmResult<UnboundedBytes> {
185
1
		// No DB access but lot of logical stuff
186
1
		// To prevent spam, we charge an arbitrary amount of gas
187
1
		handle.record_cost(1000)?;
188

            
189
1
		let nominees: Vec<_> = nominees.into();
190
1
		let nominated: Vec<AccountId32> = nominees
191
1
			.iter()
192
2
			.map(|&add| {
193
2
				let as_bytes: [u8; 32] = add.into();
194
2
				as_bytes.into()
195
2
			})
196
1
			.collect();
197
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
198
1
			StakingTransactor::get(),
199
1
			AvailableStakeCalls::Nominate(nominated),
200
1
		)
201
1
		.as_slice()
202
1
		.into();
203
1

            
204
1
		Ok(encoded)
205
1
	}
206

            
207
	#[precompile::public("encodeChill()")]
208
	#[precompile::public("encode_chill()")]
209
	#[precompile::view]
210
3
	fn encode_chill(handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
211
3
		// No DB access but lot of logical stuff
212
3
		// To prevent spam, we charge an arbitrary amount of gas
213
3
		handle.record_cost(1000)?;
214

            
215
3
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
216
3
			StakingTransactor::get(),
217
3
			AvailableStakeCalls::Chill,
218
3
		)
219
3
		.as_slice()
220
3
		.into();
221
3

            
222
3
		Ok(encoded)
223
3
	}
224

            
225
	#[precompile::public("encodeSetPayee(bytes)")]
226
	#[precompile::public("encode_set_payee(bytes)")]
227
	#[precompile::view]
228
1
	fn encode_set_payee(
229
1
		handle: &mut impl PrecompileHandle,
230
1
		reward_destination: RewardDestinationWrapper,
231
1
	) -> EvmResult<UnboundedBytes> {
232
1
		// No DB access but lot of logical stuff
233
1
		// To prevent spam, we charge an arbitrary amount of gas
234
1
		handle.record_cost(1000)?;
235

            
236
1
		let reward_destination = reward_destination.into();
237
1

            
238
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
239
1
			StakingTransactor::get(),
240
1
			AvailableStakeCalls::SetPayee(reward_destination),
241
1
		)
242
1
		.as_slice()
243
1
		.into();
244
1

            
245
1
		Ok(encoded)
246
1
	}
247

            
248
	#[precompile::public("encodeSetController()")]
249
	#[precompile::public("encode_set_controller()")]
250
	#[precompile::view]
251
3
	fn encode_set_controller(handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
252
3
		// No DB access but lot of logical stuff
253
3
		// To prevent spam, we charge an arbitrary amount of gas
254
3
		handle.record_cost(1000)?;
255

            
256
3
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
257
3
			StakingTransactor::get(),
258
3
			AvailableStakeCalls::SetController,
259
3
		)
260
3
		.as_slice()
261
3
		.into();
262
3

            
263
3
		Ok(encoded)
264
3
	}
265

            
266
	#[precompile::public("encodeRebond(uint256)")]
267
	#[precompile::public("encode_rebond(uint256)")]
268
	#[precompile::view]
269
1
	fn encode_rebond(
270
1
		handle: &mut impl PrecompileHandle,
271
1
		amount: U256,
272
1
	) -> EvmResult<UnboundedBytes> {
273
1
		// No DB access but lot of logical stuff
274
1
		// To prevent spam, we charge an arbitrary amount of gas
275
1
		handle.record_cost(1000)?;
276

            
277
1
		let relay_amount = u256_to_relay_amount(amount)?;
278
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
279
1
			StakingTransactor::get(),
280
1
			AvailableStakeCalls::Rebond(relay_amount),
281
1
		)
282
1
		.as_slice()
283
1
		.into();
284
1

            
285
1
		Ok(encoded)
286
1
	}
287
	#[precompile::public("encodeHrmpInitOpenChannel(uint32,uint32,uint32)")]
288
	#[precompile::public("encode_hrmp_init_open_channel(uint32,uint32,uint32)")]
289
	#[precompile::view]
290
	fn encode_hrmp_init_open_channel(
291
		handle: &mut impl PrecompileHandle,
292
		recipient: u32,
293
		max_capacity: u32,
294
		max_message_size: u32,
295
	) -> EvmResult<UnboundedBytes> {
296
		// No DB access but lot of logical stuff
297
		// To prevent spam, we charge an arbitrary amount of gas
298
		handle.record_cost(1000)?;
299

            
300
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
301
			HrmpAvailableCalls::InitOpenChannel(recipient.into(), max_capacity, max_message_size),
302
		)
303
		.map_err(|_| {
304
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
305
				.in_field("transactor")
306
		})?
307
		.as_slice()
308
		.into();
309
		Ok(encoded)
310
	}
311

            
312
	#[precompile::public("encodeHrmpAcceptOpenChannel(uint32)")]
313
	#[precompile::public("encode_hrmp_accept_open_channel(uint32)")]
314
	#[precompile::view]
315
	fn encode_hrmp_accept_open_channel(
316
		handle: &mut impl PrecompileHandle,
317
		sender: u32,
318
	) -> EvmResult<UnboundedBytes> {
319
		// No DB access but lot of logical stuff
320
		// To prevent spam, we charge an arbitrary amount of gas
321
		handle.record_cost(1000)?;
322

            
323
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
324
			HrmpAvailableCalls::AcceptOpenChannel(sender.into()),
325
		)
326
		.map_err(|_| {
327
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
328
				.in_field("transactor")
329
		})?
330
		.as_slice()
331
		.into();
332
		Ok(encoded)
333
	}
334

            
335
	#[precompile::public("encodeHrmpCloseChannel(uint32,uint32)")]
336
	#[precompile::public("encode_hrmp_close_channel(uint32,uint32)")]
337
	#[precompile::view]
338
	fn encode_hrmp_close_channel(
339
		handle: &mut impl PrecompileHandle,
340
		sender: u32,
341
		recipient: u32,
342
	) -> EvmResult<UnboundedBytes> {
343
		// No DB access but lot of logical stuff
344
		// To prevent spam, we charge an arbitrary amount of gas
345
		handle.record_cost(1000)?;
346

            
347
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
348
			HrmpAvailableCalls::CloseChannel(relay_chain::HrmpChannelId {
349
				sender: sender.into(),
350
				recipient: recipient.into(),
351
			}),
352
		)
353
		.map_err(|_| {
354
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
355
				.in_field("transactor")
356
		})?
357
		.as_slice()
358
		.into();
359
		Ok(encoded)
360
	}
361

            
362
	#[precompile::public("encodeHrmpCancelOpenRequest(uint32,uint32,uint32)")]
363
	#[precompile::public("encode_hrmp_cancel_open_request(uint32,uint32,uint32)")]
364
	#[precompile::view]
365
	fn encode_hrmp_cancel_open_request(
366
		handle: &mut impl PrecompileHandle,
367
		sender: u32,
368
		recipient: u32,
369
		open_requests: u32,
370
	) -> EvmResult<UnboundedBytes> {
371
		// No DB access but lot of logical stuff
372
		// To prevent spam, we charge an arbitrary amount of gas
373
		handle.record_cost(1000)?;
374

            
375
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::hrmp_encode_call(
376
			HrmpAvailableCalls::CancelOpenRequest(
377
				relay_chain::HrmpChannelId {
378
					sender: sender.into(),
379
					recipient: recipient.into(),
380
				},
381
				open_requests,
382
			),
383
		)
384
		.map_err(|_| {
385
			RevertReason::custom("Non-implemented hrmp encoding for transactor")
386
				.in_field("transactor")
387
		})?
388
		.as_slice()
389
		.into();
390
		Ok(encoded)
391
	}
392
}
393

            
394
4
pub fn u256_to_relay_amount(value: U256) -> EvmResult<relay_chain::Balance> {
395
4
	value
396
4
		.try_into()
397
4
		.map_err(|_| revert("amount is too large for provided balance type"))
398
4
}
399

            
400
// A wrapper to be able to implement here the solidity::Codec reader
401
#[derive(Clone, Eq, PartialEq)]
402
pub struct RewardDestinationWrapper(RewardDestination<AccountId32>);
403

            
404
impl From<RewardDestination<AccountId32>> for RewardDestinationWrapper {
405
	fn from(reward_dest: RewardDestination<AccountId32>) -> Self {
406
		RewardDestinationWrapper(reward_dest)
407
	}
408
}
409

            
410
impl Into<RewardDestination<AccountId32>> for RewardDestinationWrapper {
411
2
	fn into(self) -> RewardDestination<AccountId32> {
412
2
		self.0
413
2
	}
414
}
415

            
416
impl solidity::Codec for RewardDestinationWrapper {
417
2
	fn read(reader: &mut solidity::codec::Reader) -> MayRevert<Self> {
418
2
		let reward_destination = reader.read::<BoundedBytes<GetRewardDestinationSizeLimit>>()?;
419
2
		let reward_destination_bytes: Vec<_> = reward_destination.into();
420
2
		ensure!(
421
2
			reward_destination_bytes.len() > 0,
422
			RevertReason::custom("Reward destinations cannot be empty")
423
		);
424
		// For simplicity we use an EvmReader here
425
2
		let mut encoded_reward_destination =
426
2
			solidity::codec::Reader::new(&reward_destination_bytes);
427

            
428
		// We take the first byte
429
2
		let enum_selector = encoded_reward_destination.read_raw_bytes(1)?;
430
		// The firs byte selects the enum variant
431
2
		match enum_selector[0] {
432
			0u8 => Ok(RewardDestinationWrapper(RewardDestination::Staked)),
433
			1u8 => Ok(RewardDestinationWrapper(RewardDestination::Stash)),
434
			// Deprecated in https://github.com/paritytech/polkadot-sdk/pull/2380
435
			#[allow(deprecated)]
436
			2u8 => Ok(RewardDestinationWrapper(RewardDestination::Controller)),
437
			3u8 => {
438
2
				let address = encoded_reward_destination.read::<H256>()?;
439
2
				Ok(RewardDestinationWrapper(RewardDestination::Account(
440
2
					address.as_fixed_bytes().clone().into(),
441
2
				)))
442
			}
443
			4u8 => Ok(RewardDestinationWrapper(RewardDestination::None)),
444
			_ => Err(RevertReason::custom("Unknown reward destination").into()),
445
		}
446
2
	}
447

            
448
2
	fn write(writer: &mut solidity::codec::Writer, value: Self) {
449
2
		let mut encoded: Vec<u8> = Vec::new();
450
2
		let encoded_bytes: UnboundedBytes = match value.0 {
451
			RewardDestination::Staked => {
452
				encoded.push(0);
453
				encoded.as_slice().into()
454
			}
455
			RewardDestination::Stash => {
456
				encoded.push(1);
457
				encoded.as_slice().into()
458
			}
459
			// Deprecated in https://github.com/paritytech/polkadot-sdk/pull/2380
460
			#[allow(deprecated)]
461
			RewardDestination::Controller => {
462
				encoded.push(2);
463
				encoded.as_slice().into()
464
			}
465
2
			RewardDestination::Account(address) => {
466
2
				encoded.push(3);
467
2
				let address_bytes: [u8; 32] = address.into();
468
2
				encoded.append(&mut address_bytes.to_vec());
469
2
				encoded.as_slice().into()
470
			}
471
			RewardDestination::None => {
472
				encoded.push(4);
473
				encoded.as_slice().into()
474
			}
475
		};
476
2
		solidity::Codec::write(writer, encoded_bytes);
477
2
	}
478

            
479
	fn has_static_size() -> bool {
480
		false
481
	}
482

            
483
2
	fn signature() -> String {
484
2
		UnboundedBytes::signature()
485
2
	}
486
}