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_collective instances.
18

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

            
21
use account::SYSTEM_ACCOUNT_SIZE;
22
use core::marker::PhantomData;
23
use fp_evm::Log;
24
use frame_support::{
25
	dispatch::{GetDispatchInfo, Pays, PostDispatchInfo},
26
	sp_runtime::traits::Hash,
27
	traits::ConstU32,
28
	weights::Weight,
29
};
30
use pallet_evm::AddressMapping;
31
use parity_scale_codec::DecodeLimit as _;
32
use precompile_utils::prelude::*;
33
use sp_core::{Decode, Get, H160, H256};
34
use sp_runtime::traits::Dispatchable;
35
use sp_std::{boxed::Box, vec::Vec};
36

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

            
42
/// Solidity selector of the Executed log.
43
pub const SELECTOR_LOG_EXECUTED: [u8; 32] = keccak256!("Executed(bytes32)");
44

            
45
/// Solidity selector of the Proposed log.
46
pub const SELECTOR_LOG_PROPOSED: [u8; 32] = keccak256!("Proposed(address,uint32,bytes32,uint32)");
47

            
48
/// Solidity selector of the Voted log.
49
pub const SELECTOR_LOG_VOTED: [u8; 32] = keccak256!("Voted(address,bytes32,bool)");
50

            
51
/// Solidity selector of the Closed log.
52
pub const SELECTOR_LOG_CLOSED: [u8; 32] = keccak256!("Closed(bytes32)");
53

            
54
10
pub fn log_executed(address: impl Into<H160>, hash: H256) -> Log {
55
10
	log2(address.into(), SELECTOR_LOG_EXECUTED, hash, Vec::new())
56
10
}
57

            
58
14
pub fn log_proposed(
59
14
	address: impl Into<H160>,
60
14
	who: impl Into<H160>,
61
14
	index: u32,
62
14
	hash: H256,
63
14
	threshold: u32,
64
14
) -> Log {
65
14
	log4(
66
14
		address.into(),
67
14
		SELECTOR_LOG_PROPOSED,
68
14
		who.into(),
69
14
		H256::from_slice(&solidity::encode_arguments(index)),
70
14
		hash,
71
14
		solidity::encode_arguments(threshold),
72
14
	)
73
14
}
74

            
75
12
pub fn log_voted(address: impl Into<H160>, who: impl Into<H160>, hash: H256, voted: bool) -> Log {
76
12
	log3(
77
12
		address.into(),
78
12
		SELECTOR_LOG_VOTED,
79
12
		who.into(),
80
12
		hash,
81
12
		solidity::encode_arguments(voted),
82
12
	)
83
12
}
84

            
85
2
pub fn log_closed(address: impl Into<H160>, hash: H256) -> Log {
86
2
	log2(address.into(), SELECTOR_LOG_CLOSED, hash, Vec::new())
87
2
}
88

            
89
type GetProposalLimit = ConstU32<{ 2u32.pow(16) }>;
90
type DecodeLimit = ConstU32<8>;
91

            
92
pub struct CollectivePrecompile<Runtime, Instance: 'static>(PhantomData<(Runtime, Instance)>);
93

            
94
256
#[precompile_utils::precompile]
95
impl<Runtime, Instance> CollectivePrecompile<Runtime, Instance>
96
where
97
	Instance: 'static,
98
	Runtime: pallet_collective::Config<Instance> + pallet_evm::Config,
99
	Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo + Decode,
100
	Runtime::RuntimeCall: From<pallet_collective::Call<Runtime, Instance>>,
101
	<Runtime as pallet_collective::Config<Instance>>::Proposal: From<Runtime::RuntimeCall>,
102
	<Runtime::RuntimeCall as Dispatchable>::RuntimeOrigin: From<Option<Runtime::AccountId>>,
103
	Runtime::AccountId: Into<H160>,
104
	H256: From<<Runtime as frame_system::Config>::Hash>
105
		+ Into<<Runtime as frame_system::Config>::Hash>,
106
	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
107
{
108
	#[precompile::public("execute(bytes)")]
109
3
	fn execute(
110
3
		handle: &mut impl PrecompileHandle,
111
3
		proposal: BoundedBytes<GetProposalLimit>,
112
3
	) -> EvmResult {
113
3
		let proposal: Vec<_> = proposal.into();
114
3
		let proposal_hash: H256 = hash::<Runtime>(&proposal);
115
3

            
116
3
		let log = log_executed(handle.context().address, proposal_hash);
117
3
		handle.record_log_costs(&[&log])?;
118

            
119
3
		let proposal_length: u32 = proposal.len().try_into().map_err(|_| {
120
			RevertReason::value_is_too_large("uint32")
121
				.in_field("length")
122
				.in_field("proposal")
123
3
		})?;
124

            
125
2
		let proposal =
126
3
			Runtime::RuntimeCall::decode_with_depth_limit(DecodeLimit::get(), &mut &*proposal)
127
3
				.map_err(|_| {
128
1
					RevertReason::custom("Failed to decode proposal").in_field("proposal")
129
3
				})?
130
2
				.into();
131
2
		let proposal = Box::new(proposal);
132
2

            
133
2
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
134
2
		RuntimeHelper::<Runtime>::try_dispatch(
135
2
			handle,
136
2
			Some(origin).into(),
137
2
			pallet_collective::Call::<Runtime, Instance>::execute {
138
2
				proposal,
139
2
				length_bound: proposal_length,
140
2
			},
141
2
			SYSTEM_ACCOUNT_SIZE,
142
2
		)?;
143

            
144
		log.record(handle)?;
145

            
146
		Ok(())
147
3
	}
148

            
149
	#[precompile::public("propose(uint32,bytes)")]
150
11
	fn propose(
151
11
		handle: &mut impl PrecompileHandle,
152
11
		threshold: u32,
153
11
		proposal: BoundedBytes<GetProposalLimit>,
154
11
	) -> EvmResult<u32> {
155
11
		// ProposalCount
156
11
		handle.record_db_read::<Runtime>(4)?;
157

            
158
11
		let proposal: Vec<_> = proposal.into();
159
11
		let proposal_length: u32 = proposal.len().try_into().map_err(|_| {
160
			RevertReason::value_is_too_large("uint32")
161
				.in_field("length")
162
				.in_field("proposal")
163
11
		})?;
164

            
165
11
		let proposal_index = pallet_collective::ProposalCount::<Runtime, Instance>::get();
166
11
		let proposal_hash: H256 = hash::<Runtime>(&proposal);
167

            
168
		// In pallet_collective a threshold < 2 means the proposal has been
169
		// executed directly.
170
11
		let log = if threshold < 2 {
171
4
			log_executed(handle.context().address, proposal_hash)
172
		} else {
173
7
			log_proposed(
174
7
				handle.context().address,
175
7
				handle.context().caller,
176
7
				proposal_index,
177
7
				proposal_hash,
178
7
				threshold,
179
7
			)
180
		};
181

            
182
11
		handle.record_log_costs(&[&log])?;
183

            
184
10
		let proposal =
185
11
			Runtime::RuntimeCall::decode_with_depth_limit(DecodeLimit::get(), &mut &*proposal)
186
11
				.map_err(|_| {
187
1
					RevertReason::custom("Failed to decode proposal").in_field("proposal")
188
11
				})?
189
10
				.into();
190
10
		let proposal = Box::new(proposal);
191
10

            
192
10
		{
193
10
			let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
194
10
			RuntimeHelper::<Runtime>::try_dispatch(
195
10
				handle,
196
10
				Some(origin).into(),
197
10
				pallet_collective::Call::<Runtime, Instance>::propose {
198
10
					threshold,
199
10
					proposal,
200
10
					length_bound: proposal_length,
201
10
				},
202
10
				SYSTEM_ACCOUNT_SIZE,
203
10
			)?;
204
		}
205

            
206
8
		log.record(handle)?;
207

            
208
8
		Ok(proposal_index)
209
11
	}
210

            
211
	#[precompile::public("vote(bytes32,uint32,bool)")]
212
7
	fn vote(
213
7
		handle: &mut impl PrecompileHandle,
214
7
		proposal_hash: H256,
215
7
		proposal_index: u32,
216
7
		approve: bool,
217
7
	) -> EvmResult {
218
7
		// TODO: Since we cannot access ayes/nays of a proposal we cannot
219
7
		// include it in the EVM events to mirror Substrate events.
220
7
		let log = log_voted(
221
7
			handle.context().address,
222
7
			handle.context().caller,
223
7
			proposal_hash,
224
7
			approve,
225
7
		);
226
7
		handle.record_log_costs(&[&log])?;
227

            
228
7
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
229
7
		RuntimeHelper::<Runtime>::try_dispatch(
230
7
			handle,
231
7
			Some(origin).into(),
232
7
			pallet_collective::Call::<Runtime, Instance>::vote {
233
7
				proposal: proposal_hash.into(),
234
7
				index: proposal_index,
235
7
				approve,
236
7
			},
237
7
			SYSTEM_ACCOUNT_SIZE,
238
7
		)?;
239

            
240
5
		log.record(handle)?;
241

            
242
5
		Ok(())
243
7
	}
244

            
245
	#[precompile::public("close(bytes32,uint32,uint64,uint32)")]
246
4
	fn close(
247
4
		handle: &mut impl PrecompileHandle,
248
4
		proposal_hash: H256,
249
4
		proposal_index: u32,
250
4
		proposal_weight_bound: u64,
251
4
		length_bound: u32,
252
4
	) -> EvmResult<bool> {
253
4
		// Because the actual log cannot be built before dispatch, we manually
254
4
		// record it first (`executed` and `closed` have the same cost).
255
4
		handle.record_log_costs_manual(2, 0)?;
256

            
257
4
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
258
4
		let post_dispatch_info = RuntimeHelper::<Runtime>::try_dispatch(
259
4
			handle,
260
4
			Some(origin).into(),
261
4
			pallet_collective::Call::<Runtime, Instance>::close {
262
4
				proposal_hash: proposal_hash.into(),
263
4
				index: proposal_index,
264
4
				proposal_weight_bound: Weight::from_parts(
265
4
					proposal_weight_bound,
266
4
					xcm_primitives::DEFAULT_PROOF_SIZE,
267
4
				),
268
4
				length_bound,
269
4
			},
270
4
			SYSTEM_ACCOUNT_SIZE,
271
4
		)?;
272

            
273
		// We can know if the proposal was executed or not based on the `pays_fee` in
274
		// `PostDispatchInfo`.
275
2
		let (executed, log) = match post_dispatch_info.pays_fee {
276
1
			Pays::Yes => (true, log_executed(handle.context().address, proposal_hash)),
277
1
			Pays::No => (false, log_closed(handle.context().address, proposal_hash)),
278
		};
279
2
		log.record(handle)?;
280

            
281
2
		Ok(executed)
282
4
	}
283

            
284
	#[precompile::public("proposalHash(bytes)")]
285
	#[precompile::view]
286
	fn proposal_hash(
287
		_handle: &mut impl PrecompileHandle,
288
		proposal: BoundedBytes<GetProposalLimit>,
289
	) -> EvmResult<H256> {
290
		let proposal: Vec<_> = proposal.into();
291
		let hash = hash::<Runtime>(&proposal);
292

            
293
		Ok(hash)
294
	}
295

            
296
	#[precompile::public("proposals()")]
297
	#[precompile::view]
298
1
	fn proposals(handle: &mut impl PrecompileHandle) -> EvmResult<Vec<H256>> {
299
1
		// Proposals: BoundedVec(32 * MaxProposals)
300
1
		handle.record_db_read::<Runtime>(
301
1
			32 * (<Runtime as pallet_collective::Config<Instance>>::MaxProposals::get() as usize),
302
1
		)?;
303

            
304
1
		let proposals = pallet_collective::Proposals::<Runtime, Instance>::get();
305
1
		let proposals: Vec<_> = proposals.into_iter().map(|hash| hash.into()).collect();
306
1

            
307
1
		Ok(proposals)
308
1
	}
309

            
310
	#[precompile::public("members()")]
311
	#[precompile::view]
312
2
	fn members(handle: &mut impl PrecompileHandle) -> EvmResult<Vec<Address>> {
313
2
		// Members: Vec(20 * MaxMembers)
314
2
		handle.record_db_read::<Runtime>(
315
2
			20 * (<Runtime as pallet_collective::Config<Instance>>::MaxProposals::get() as usize),
316
2
		)?;
317

            
318
2
		let members = pallet_collective::Members::<Runtime, Instance>::get();
319
4
		let members: Vec<_> = members.into_iter().map(|id| Address(id.into())).collect();
320
2

            
321
2
		Ok(members)
322
2
	}
323

            
324
	#[precompile::public("isMember(address)")]
325
	#[precompile::view]
326
2
	fn is_member(handle: &mut impl PrecompileHandle, account: Address) -> EvmResult<bool> {
327
2
		// Members: Vec(20 * MaxMembers)
328
2
		handle.record_db_read::<Runtime>(
329
2
			20 * (<Runtime as pallet_collective::Config<Instance>>::MaxProposals::get() as usize),
330
2
		)?;
331

            
332
2
		let account = Runtime::AddressMapping::into_account_id(account.into());
333
2

            
334
2
		let is_member = pallet_collective::Pallet::<Runtime, Instance>::is_member(&account);
335
2

            
336
2
		Ok(is_member)
337
2
	}
338

            
339
	#[precompile::public("prime()")]
340
	#[precompile::view]
341
3
	fn prime(handle: &mut impl PrecompileHandle) -> EvmResult<Address> {
342
3
		// Prime
343
3
		handle.record_db_read::<Runtime>(20)?;
344

            
345
3
		let prime = pallet_collective::Prime::<Runtime, Instance>::get()
346
3
			.map(|prime| prime.into())
347
3
			.unwrap_or(H160::zero());
348
3

            
349
3
		Ok(Address(prime))
350
3
	}
351
}
352

            
353
22
pub fn hash<Runtime>(data: &[u8]) -> H256
354
22
where
355
22
	Runtime: frame_system::Config,
356
22
	H256: From<<Runtime as frame_system::Config>::Hash>,
357
22
{
358
22
	<Runtime as frame_system::Config>::Hashing::hash(data).into()
359
22
}