1
// Copyright 2025 Moonbeam foundation
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 AssetHub 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
	traits::ConstU32,
27
};
28
use pallet_staking::RewardDestination;
29
use parity_scale_codec::{Decode, Encode};
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::{AssetHubTransactor, AvailableStakeCalls, StakeEncodeCall};
36

            
37
pub const REWARD_DESTINATION_SIZE_LIMIT: u32 = 2u32.pow(16);
38
pub const ARRAY_LIMIT: u32 = 512;
39
type GetArrayLimit = ConstU32<ARRAY_LIMIT>;
40
type GetRewardDestinationSizeLimit = ConstU32<REWARD_DESTINATION_SIZE_LIMIT>;
41

            
42
/// A precompile to provide AssetHub stake calls encoding through evm
43
pub struct AssetHubEncoderPrecompile<Runtime>(PhantomData<Runtime>);
44

            
45
85
#[precompile_utils::precompile]
46
impl<Runtime> AssetHubEncoderPrecompile<Runtime>
47
where
48
	Runtime: pallet_evm::Config + pallet_xcm_transactor::Config,
49
	Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
50
	<Runtime as pallet_xcm_transactor::Config>::Transactor: AssetHubTransactor,
51
{
52
	#[precompile::public("encodeBond(uint256,bytes)")]
53
	#[precompile::public("encode_bond(uint256,bytes)")]
54
	#[precompile::view]
55
1
	fn encode_bond(
56
1
		handle: &mut impl PrecompileHandle,
57
1
		amount: U256,
58
1
		reward_destination: RewardDestinationWrapper,
59
1
	) -> EvmResult<UnboundedBytes> {
60
1
		// No DB access but lot of logical stuff
61
1
		// To prevent spam, we charge an arbitrary amount of gas
62
1
		handle.record_cost(1000)?;
63

            
64
1
		let relay_amount = u256_to_relay_amount(amount)?;
65
1
		let reward_destination = reward_destination.into();
66
1

            
67
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
68
1
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
69
1
			AvailableStakeCalls::Bond(relay_amount, reward_destination),
70
1
		)
71
1
		.as_slice()
72
1
		.into();
73
1

            
74
1
		Ok(encoded)
75
1
	}
76

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

            
88
1
		let relay_amount = u256_to_relay_amount(amount)?;
89
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
90
1
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
91
1
			AvailableStakeCalls::BondExtra(relay_amount),
92
1
		)
93
1
		.as_slice()
94
1
		.into();
95
1

            
96
1
		Ok(encoded)
97
1
	}
98

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

            
110
1
		let relay_amount = u256_to_relay_amount(amount)?;
111
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
112
1
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
113
1
			AvailableStakeCalls::Unbond(relay_amount),
114
1
		)
115
1
		.as_slice()
116
1
		.into();
117
1

            
118
1
		Ok(encoded)
119
1
	}
120

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

            
132
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
133
1
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
134
1
			AvailableStakeCalls::WithdrawUnbonded(num_slashing_spans),
135
1
		)
136
1
		.as_slice()
137
1
		.into();
138
1

            
139
1
		Ok(encoded)
140
1
	}
141

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

            
154
1
		let fraction = Perbill::from_parts(commission.converted());
155
1

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

            
166
1
		Ok(encoded)
167
1
	}
168

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

            
180
1
		let nominees: Vec<_> = nominees.into();
181
1
		let nominees_as_account_ids: Vec<AccountId32> = nominees
182
1
			.iter()
183
2
			.map(|&add| {
184
2
				let as_bytes: [u8; 32] = add.into();
185
2
				as_bytes.into()
186
2
			})
187
1
			.collect();
188
1

            
189
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
190
1
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
191
1
			AvailableStakeCalls::Nominate(nominees_as_account_ids),
192
1
		)
193
1
		.as_slice()
194
1
		.into();
195
1

            
196
1
		Ok(encoded)
197
1
	}
198

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

            
207
3
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
208
3
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
209
3
			AvailableStakeCalls::Chill,
210
3
		)
211
3
		.as_slice()
212
3
		.into();
213
3

            
214
3
		Ok(encoded)
215
3
	}
216

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

            
228
1
		let reward_destination = reward_destination.into();
229
1

            
230
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
231
1
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
232
1
			AvailableStakeCalls::SetPayee(reward_destination),
233
1
		)
234
1
		.as_slice()
235
1
		.into();
236
1

            
237
1
		Ok(encoded)
238
1
	}
239

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

            
248
3
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
249
3
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
250
3
			AvailableStakeCalls::SetController,
251
3
		)
252
3
		.as_slice()
253
3
		.into();
254
3

            
255
3
		Ok(encoded)
256
3
	}
257

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

            
269
1
		let relay_amount = u256_to_relay_amount(amount)?;
270
1
		let encoded = pallet_xcm_transactor::Pallet::<Runtime>::encode_call(
271
1
			<Runtime as pallet_xcm_transactor::Config>::Transactor::asset_hub(),
272
1
			AvailableStakeCalls::Rebond(relay_amount),
273
1
		)
274
1
		.as_slice()
275
1
		.into();
276
1

            
277
1
		Ok(encoded)
278
1
	}
279
}
280

            
281
4
pub fn u256_to_relay_amount(value: U256) -> EvmResult<relay_chain::Balance> {
282
4
	value
283
4
		.try_into()
284
4
		.map_err(|_| revert("amount is too large for provided balance type"))
285
4
}
286

            
287
// A wrapper to be able to implement here the solidity::Codec reader
288
#[derive(Clone, Eq, PartialEq)]
289
pub struct RewardDestinationWrapper(RewardDestination<AccountId32>);
290

            
291
impl From<RewardDestination<AccountId32>> for RewardDestinationWrapper {
292
2
	fn from(reward_dest: RewardDestination<AccountId32>) -> Self {
293
2
		RewardDestinationWrapper(reward_dest)
294
2
	}
295
}
296

            
297
impl Into<RewardDestination<AccountId32>> for RewardDestinationWrapper {
298
2
	fn into(self) -> RewardDestination<AccountId32> {
299
2
		self.0
300
2
	}
301
}
302

            
303
impl solidity::Codec for RewardDestinationWrapper {
304
2
	fn read(reader: &mut solidity::codec::Reader) -> MayRevert<Self> {
305
2
		let reward_destination = reader.read::<BoundedBytes<GetRewardDestinationSizeLimit>>()?;
306
2
		let reward_destination_bytes: Vec<_> = reward_destination.into();
307
2
		if reward_destination_bytes.is_empty() {
308
			return Err(RevertReason::custom(
309
				"Error while decoding reward destination: input too short",
310
			)
311
			.into());
312
2
		}
313
2
		let reward_destination =
314
2
			RewardDestination::<AccountId32>::decode(&mut reward_destination_bytes.as_slice())
315
2
				.map_err(|_| RevertReason::custom("Error while decoding reward destination"))?;
316

            
317
2
		Ok(reward_destination.into())
318
2
	}
319

            
320
2
	fn write(writer: &mut solidity::codec::Writer, value: Self) {
321
2
		let encoded = value.0.encode();
322
2
		BoundedBytes::<GetRewardDestinationSizeLimit>::write(writer, encoded.as_slice().into());
323
2
	}
324

            
325
	fn has_static_size() -> bool {
326
		false
327
	}
328

            
329
2
	fn signature() -> String {
330
2
		BoundedBytes::<GetRewardDestinationSizeLimit>::signature()
331
2
	}
332
}
333

            
334
#[cfg(test)]
335
mod mock;
336
#[cfg(test)]
337
mod test_assethub_runtime;
338
#[cfg(test)]
339
mod tests;