pallet_evm_precompile_relay_encoder/
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 encode relay staking calls via the EVM
18
19#![cfg_attr(not(feature = "std"), no_std)]
20
21use cumulus_primitives_core::relay_chain;
22
23use fp_evm::PrecompileHandle;
24use frame_support::{
25	dispatch::{GetDispatchInfo, PostDispatchInfo},
26	ensure,
27	traits::ConstU32,
28};
29use pallet_staking::RewardDestination;
30use precompile_utils::prelude::*;
31use sp_core::{H256, U256};
32use sp_runtime::{traits::Dispatchable, AccountId32, Perbill};
33use sp_std::vec::Vec;
34use sp_std::{convert::TryInto, marker::PhantomData};
35use xcm_primitives::{AvailableStakeCalls, HrmpAvailableCalls, HrmpEncodeCall, StakeEncodeCall};
36
37#[cfg(test)]
38mod mock;
39#[cfg(test)]
40mod test_relay_runtime;
41#[cfg(test)]
42mod tests;
43
44pub const REWARD_DESTINATION_SIZE_LIMIT: u32 = 2u32.pow(16);
45pub const ARRAY_LIMIT: u32 = 512;
46type GetArrayLimit = ConstU32<ARRAY_LIMIT>;
47type GetRewardDestinationSizeLimit = ConstU32<REWARD_DESTINATION_SIZE_LIMIT>;
48
49/// A precompile to provide relay stake calls encoding through evm
50pub struct RelayEncoderPrecompile<Runtime, StakingTransactor>(
51	PhantomData<(Runtime, StakingTransactor)>,
52);
53
54#[precompile_utils::precompile]
55impl<Runtime, StakingTransactor> RelayEncoderPrecompile<Runtime, StakingTransactor>
56where
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	fn encode_bond(
65		handle: &mut impl PrecompileHandle,
66		amount: U256,
67		reward_destination: RewardDestinationWrapper,
68	) -> EvmResult<UnboundedBytes> {
69		// No DB access but lot of logical stuff
70		// To prevent spam, we charge an arbitrary amount of gas
71		handle.record_cost(1000)?;
72
73		let relay_amount = u256_to_relay_amount(amount)?;
74		let reward_destination = reward_destination.into();
75
76		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
77			StakingTransactor::get(),
78			AvailableStakeCalls::Bond(relay_amount, reward_destination),
79		)
80		.as_slice()
81		.into();
82
83		Ok(encoded)
84	}
85
86	#[precompile::public("encodeBondExtra(uint256)")]
87	#[precompile::public("encode_bond_extra(uint256)")]
88	#[precompile::view]
89	fn encode_bond_extra(
90		handle: &mut impl PrecompileHandle,
91		amount: U256,
92	) -> EvmResult<UnboundedBytes> {
93		// No DB access but lot of logical stuff
94		// To prevent spam, we charge an arbitrary amount of gas
95		handle.record_cost(1000)?;
96
97		let relay_amount = u256_to_relay_amount(amount)?;
98		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
99			StakingTransactor::get(),
100			AvailableStakeCalls::BondExtra(relay_amount),
101		)
102		.as_slice()
103		.into();
104
105		Ok(encoded)
106	}
107
108	#[precompile::public("encodeUnbond(uint256)")]
109	#[precompile::public("encode_unbond(uint256)")]
110	#[precompile::view]
111	fn encode_unbond(
112		handle: &mut impl PrecompileHandle,
113		amount: U256,
114	) -> EvmResult<UnboundedBytes> {
115		// No DB access but lot of logical stuff
116		// To prevent spam, we charge an arbitrary amount of gas
117		handle.record_cost(1000)?;
118
119		let relay_amount = u256_to_relay_amount(amount)?;
120
121		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
122			StakingTransactor::get(),
123			AvailableStakeCalls::Unbond(relay_amount),
124		)
125		.as_slice()
126		.into();
127
128		Ok(encoded)
129	}
130
131	#[precompile::public("encodeWithdrawUnbonded(uint32)")]
132	#[precompile::public("encode_withdraw_unbonded(uint32)")]
133	#[precompile::view]
134	fn encode_withdraw_unbonded(
135		handle: &mut impl PrecompileHandle,
136		slashes: u32,
137	) -> EvmResult<UnboundedBytes> {
138		// No DB access but lot of logical stuff
139		// To prevent spam, we charge an arbitrary amount of gas
140		handle.record_cost(1000)?;
141
142		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
143			StakingTransactor::get(),
144			AvailableStakeCalls::WithdrawUnbonded(slashes),
145		)
146		.as_slice()
147		.into();
148
149		Ok(encoded)
150	}
151
152	#[precompile::public("encodeValidate(uint256,bool)")]
153	#[precompile::public("encode_validate(uint256,bool)")]
154	#[precompile::view]
155	fn encode_validate(
156		handle: &mut impl PrecompileHandle,
157		commission: Convert<U256, u32>,
158		blocked: bool,
159	) -> EvmResult<UnboundedBytes> {
160		// No DB access but lot of logical stuff
161		// To prevent spam, we charge an arbitrary amount of gas
162		handle.record_cost(1000)?;
163
164		let fraction = Perbill::from_parts(commission.converted());
165		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
166			StakingTransactor::get(),
167			AvailableStakeCalls::Validate(pallet_staking::ValidatorPrefs {
168				commission: fraction,
169				blocked: blocked,
170			}),
171		)
172		.as_slice()
173		.into();
174
175		Ok(encoded)
176	}
177
178	#[precompile::public("encodeNominate(bytes32[])")]
179	#[precompile::public("encode_nominate(bytes32[])")]
180	#[precompile::view]
181	fn encode_nominate(
182		handle: &mut impl PrecompileHandle,
183		nominees: BoundedVec<H256, GetArrayLimit>,
184	) -> EvmResult<UnboundedBytes> {
185		// No DB access but lot of logical stuff
186		// To prevent spam, we charge an arbitrary amount of gas
187		handle.record_cost(1000)?;
188
189		let nominees: Vec<_> = nominees.into();
190		let nominated: Vec<AccountId32> = nominees
191			.iter()
192			.map(|&add| {
193				let as_bytes: [u8; 32] = add.into();
194				as_bytes.into()
195			})
196			.collect();
197		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
198			StakingTransactor::get(),
199			AvailableStakeCalls::Nominate(nominated),
200		)
201		.as_slice()
202		.into();
203
204		Ok(encoded)
205	}
206
207	#[precompile::public("encodeChill()")]
208	#[precompile::public("encode_chill()")]
209	#[precompile::view]
210	fn encode_chill(handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
211		// No DB access but lot of logical stuff
212		// To prevent spam, we charge an arbitrary amount of gas
213		handle.record_cost(1000)?;
214
215		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
216			StakingTransactor::get(),
217			AvailableStakeCalls::Chill,
218		)
219		.as_slice()
220		.into();
221
222		Ok(encoded)
223	}
224
225	#[precompile::public("encodeSetPayee(bytes)")]
226	#[precompile::public("encode_set_payee(bytes)")]
227	#[precompile::view]
228	fn encode_set_payee(
229		handle: &mut impl PrecompileHandle,
230		reward_destination: RewardDestinationWrapper,
231	) -> EvmResult<UnboundedBytes> {
232		// No DB access but lot of logical stuff
233		// To prevent spam, we charge an arbitrary amount of gas
234		handle.record_cost(1000)?;
235
236		let reward_destination = reward_destination.into();
237
238		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
239			StakingTransactor::get(),
240			AvailableStakeCalls::SetPayee(reward_destination),
241		)
242		.as_slice()
243		.into();
244
245		Ok(encoded)
246	}
247
248	#[precompile::public("encodeSetController()")]
249	#[precompile::public("encode_set_controller()")]
250	#[precompile::view]
251	fn encode_set_controller(handle: &mut impl PrecompileHandle) -> EvmResult<UnboundedBytes> {
252		// No DB access but lot of logical stuff
253		// To prevent spam, we charge an arbitrary amount of gas
254		handle.record_cost(1000)?;
255
256		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
257			StakingTransactor::get(),
258			AvailableStakeCalls::SetController,
259		)
260		.as_slice()
261		.into();
262
263		Ok(encoded)
264	}
265
266	#[precompile::public("encodeRebond(uint256)")]
267	#[precompile::public("encode_rebond(uint256)")]
268	#[precompile::view]
269	fn encode_rebond(
270		handle: &mut impl PrecompileHandle,
271		amount: U256,
272	) -> EvmResult<UnboundedBytes> {
273		// No DB access but lot of logical stuff
274		// To prevent spam, we charge an arbitrary amount of gas
275		handle.record_cost(1000)?;
276
277		let relay_amount = u256_to_relay_amount(amount)?;
278		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
279			StakingTransactor::get(),
280			AvailableStakeCalls::Rebond(relay_amount),
281		)
282		.as_slice()
283		.into();
284
285		Ok(encoded)
286	}
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
394pub fn u256_to_relay_amount(value: U256) -> EvmResult<relay_chain::Balance> {
395	value
396		.try_into()
397		.map_err(|_| revert("amount is too large for provided balance type"))
398}
399
400// A wrapper to be able to implement here the solidity::Codec reader
401#[derive(Clone, Eq, PartialEq)]
402pub struct RewardDestinationWrapper(RewardDestination<AccountId32>);
403
404impl From<RewardDestination<AccountId32>> for RewardDestinationWrapper {
405	fn from(reward_dest: RewardDestination<AccountId32>) -> Self {
406		RewardDestinationWrapper(reward_dest)
407	}
408}
409
410impl Into<RewardDestination<AccountId32>> for RewardDestinationWrapper {
411	fn into(self) -> RewardDestination<AccountId32> {
412		self.0
413	}
414}
415
416impl solidity::Codec for RewardDestinationWrapper {
417	fn read(reader: &mut solidity::codec::Reader) -> MayRevert<Self> {
418		let reward_destination = reader.read::<BoundedBytes<GetRewardDestinationSizeLimit>>()?;
419		let reward_destination_bytes: Vec<_> = reward_destination.into();
420		ensure!(
421			reward_destination_bytes.len() > 0,
422			RevertReason::custom("Reward destinations cannot be empty")
423		);
424		// For simplicity we use an EvmReader here
425		let mut encoded_reward_destination =
426			solidity::codec::Reader::new(&reward_destination_bytes);
427
428		// We take the first byte
429		let enum_selector = encoded_reward_destination.read_raw_bytes(1)?;
430		// The firs byte selects the enum variant
431		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				let address = encoded_reward_destination.read::<H256>()?;
439				Ok(RewardDestinationWrapper(RewardDestination::Account(
440					address.as_fixed_bytes().clone().into(),
441				)))
442			}
443			4u8 => Ok(RewardDestinationWrapper(RewardDestination::None)),
444			_ => Err(RevertReason::custom("Unknown reward destination").into()),
445		}
446	}
447
448	fn write(writer: &mut solidity::codec::Writer, value: Self) {
449		let mut encoded: Vec<u8> = Vec::new();
450		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			RewardDestination::Account(address) => {
466				encoded.push(3);
467				let address_bytes: [u8; 32] = address.into();
468				encoded.append(&mut address_bytes.to_vec());
469				encoded.as_slice().into()
470			}
471			RewardDestination::None => {
472				encoded.push(4);
473				encoded.as_slice().into()
474			}
475		};
476		solidity::Codec::write(writer, encoded_bytes);
477	}
478
479	fn has_static_size() -> bool {
480		false
481	}
482
483	fn signature() -> String {
484		UnboundedBytes::signature()
485	}
486}