pallet_evm_precompile_collective/
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 interact with pallet_collective instances.
18
19#![cfg_attr(not(feature = "std"), no_std)]
20
21use account::{AccountId20, SYSTEM_ACCOUNT_SIZE};
22use core::marker::PhantomData;
23use fp_evm::Log;
24use frame_support::{
25	dispatch::{GetDispatchInfo, Pays, PostDispatchInfo},
26	sp_runtime::traits::Hash,
27	traits::ConstU32,
28	weights::Weight,
29};
30use pallet_evm::AddressMapping;
31use parity_scale_codec::{DecodeLimit as _, MaxEncodedLen};
32use precompile_utils::prelude::*;
33use sp_core::{Decode, Get, H160, H256};
34use sp_runtime::traits::Dispatchable;
35use sp_std::{boxed::Box, vec::Vec};
36
37#[cfg(test)]
38mod mock;
39#[cfg(test)]
40mod tests;
41
42/// Solidity selector of the Executed log.
43pub const SELECTOR_LOG_EXECUTED: [u8; 32] = keccak256!("Executed(bytes32)");
44
45/// Solidity selector of the Proposed log.
46pub const SELECTOR_LOG_PROPOSED: [u8; 32] = keccak256!("Proposed(address,uint32,bytes32,uint32)");
47
48/// Solidity selector of the Voted log.
49pub const SELECTOR_LOG_VOTED: [u8; 32] = keccak256!("Voted(address,bytes32,bool)");
50
51/// Solidity selector of the Closed log.
52pub const SELECTOR_LOG_CLOSED: [u8; 32] = keccak256!("Closed(bytes32)");
53
54pub fn log_executed(address: impl Into<H160>, hash: H256) -> Log {
55	log2(address.into(), SELECTOR_LOG_EXECUTED, hash, Vec::new())
56}
57
58pub fn log_proposed(
59	address: impl Into<H160>,
60	who: impl Into<H160>,
61	index: u32,
62	hash: H256,
63	threshold: u32,
64) -> Log {
65	log4(
66		address.into(),
67		SELECTOR_LOG_PROPOSED,
68		who.into(),
69		H256::from_slice(&solidity::encode_arguments(index)),
70		hash,
71		solidity::encode_arguments(threshold),
72	)
73}
74
75pub fn log_voted(address: impl Into<H160>, who: impl Into<H160>, hash: H256, voted: bool) -> Log {
76	log3(
77		address.into(),
78		SELECTOR_LOG_VOTED,
79		who.into(),
80		hash,
81		solidity::encode_arguments(voted),
82	)
83}
84
85pub fn log_closed(address: impl Into<H160>, hash: H256) -> Log {
86	log2(address.into(), SELECTOR_LOG_CLOSED, hash, Vec::new())
87}
88
89type GetProposalLimit = ConstU32<{ 2u32.pow(16) }>;
90type DecodeLimit = ConstU32<8>;
91
92pub struct CollectivePrecompile<Runtime, Instance: 'static>(PhantomData<(Runtime, Instance)>);
93
94#[precompile_utils::precompile]
95impl<Runtime, Instance> CollectivePrecompile<Runtime, Instance>
96where
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::AccountId: Into<H160>,
103	H256: From<<Runtime as frame_system::Config>::Hash>
104		+ Into<<Runtime as frame_system::Config>::Hash>,
105	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
106{
107	#[precompile::public("execute(bytes)")]
108	fn execute(
109		handle: &mut impl PrecompileHandle,
110		proposal: BoundedBytes<GetProposalLimit>,
111	) -> EvmResult {
112		let proposal: Vec<_> = proposal.into();
113		let proposal_hash: H256 = hash::<Runtime>(&proposal);
114
115		let log = log_executed(handle.context().address, proposal_hash);
116		handle.record_log_costs(&[&log])?;
117
118		let proposal_length: u32 = proposal.len().try_into().map_err(|_| {
119			RevertReason::value_is_too_large("uint32")
120				.in_field("length")
121				.in_field("proposal")
122		})?;
123
124		let proposal =
125			Runtime::RuntimeCall::decode_with_depth_limit(DecodeLimit::get(), &mut &*proposal)
126				.map_err(|_| {
127					RevertReason::custom("Failed to decode proposal").in_field("proposal")
128				})?
129				.into();
130		let proposal = Box::new(proposal);
131
132		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
133		RuntimeHelper::<Runtime>::try_dispatch(
134			handle,
135			frame_system::RawOrigin::Signed(origin).into(),
136			pallet_collective::Call::<Runtime, Instance>::execute {
137				proposal,
138				length_bound: proposal_length,
139			},
140			SYSTEM_ACCOUNT_SIZE,
141		)?;
142
143		log.record(handle)?;
144
145		Ok(())
146	}
147
148	#[precompile::public("propose(uint32,bytes)")]
149	fn propose(
150		handle: &mut impl PrecompileHandle,
151		threshold: u32,
152		proposal: BoundedBytes<GetProposalLimit>,
153	) -> EvmResult<u32> {
154		// ProposalCount
155		handle.record_db_read::<Runtime>(4)?;
156
157		let proposal: Vec<_> = proposal.into();
158		let proposal_length: u32 = proposal.len().try_into().map_err(|_| {
159			RevertReason::value_is_too_large("uint32")
160				.in_field("length")
161				.in_field("proposal")
162		})?;
163
164		let proposal_index = pallet_collective::ProposalCount::<Runtime, Instance>::get();
165		let proposal_hash: H256 = hash::<Runtime>(&proposal);
166
167		// In pallet_collective a threshold < 2 means the proposal has been
168		// executed directly.
169		let log = if threshold < 2 {
170			log_executed(handle.context().address, proposal_hash)
171		} else {
172			log_proposed(
173				handle.context().address,
174				handle.context().caller,
175				proposal_index,
176				proposal_hash,
177				threshold,
178			)
179		};
180
181		handle.record_log_costs(&[&log])?;
182
183		let proposal =
184			Runtime::RuntimeCall::decode_with_depth_limit(DecodeLimit::get(), &mut &*proposal)
185				.map_err(|_| {
186					RevertReason::custom("Failed to decode proposal").in_field("proposal")
187				})?
188				.into();
189		let proposal = Box::new(proposal);
190
191		{
192			let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
193			RuntimeHelper::<Runtime>::try_dispatch(
194				handle,
195				frame_system::RawOrigin::Signed(origin).into(),
196				pallet_collective::Call::<Runtime, Instance>::propose {
197					threshold,
198					proposal,
199					length_bound: proposal_length,
200				},
201				SYSTEM_ACCOUNT_SIZE,
202			)?;
203		}
204
205		log.record(handle)?;
206
207		Ok(proposal_index)
208	}
209
210	#[precompile::public("vote(bytes32,uint32,bool)")]
211	fn vote(
212		handle: &mut impl PrecompileHandle,
213		proposal_hash: H256,
214		proposal_index: u32,
215		approve: bool,
216	) -> EvmResult {
217		// TODO: Since we cannot access ayes/nays of a proposal we cannot
218		// include it in the EVM events to mirror Substrate events.
219		let log = log_voted(
220			handle.context().address,
221			handle.context().caller,
222			proposal_hash,
223			approve,
224		);
225		handle.record_log_costs(&[&log])?;
226
227		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
228		RuntimeHelper::<Runtime>::try_dispatch(
229			handle,
230			frame_system::RawOrigin::Signed(origin).into(),
231			pallet_collective::Call::<Runtime, Instance>::vote {
232				proposal: proposal_hash.into(),
233				index: proposal_index,
234				approve,
235			},
236			SYSTEM_ACCOUNT_SIZE,
237		)?;
238
239		log.record(handle)?;
240
241		Ok(())
242	}
243
244	#[precompile::public("close(bytes32,uint32,uint64,uint32)")]
245	fn close(
246		handle: &mut impl PrecompileHandle,
247		proposal_hash: H256,
248		proposal_index: u32,
249		proposal_weight_bound: u64,
250		length_bound: u32,
251	) -> EvmResult<bool> {
252		// Because the actual log cannot be built before dispatch, we manually
253		// record it first (`executed` and `closed` have the same cost).
254		handle.record_log_costs_manual(2, 0)?;
255
256		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
257		let post_dispatch_info = RuntimeHelper::<Runtime>::try_dispatch(
258			handle,
259			frame_system::RawOrigin::Signed(origin).into(),
260			pallet_collective::Call::<Runtime, Instance>::close {
261				proposal_hash: proposal_hash.into(),
262				index: proposal_index,
263				proposal_weight_bound: Weight::from_parts(
264					proposal_weight_bound,
265					xcm_primitives::DEFAULT_PROOF_SIZE,
266				),
267				length_bound,
268			},
269			SYSTEM_ACCOUNT_SIZE,
270		)?;
271
272		// We can know if the proposal was executed or not based on the `pays_fee` in
273		// `PostDispatchInfo`.
274		let (executed, log) = match post_dispatch_info.pays_fee {
275			Pays::Yes => (true, log_executed(handle.context().address, proposal_hash)),
276			Pays::No => (false, log_closed(handle.context().address, proposal_hash)),
277		};
278		log.record(handle)?;
279
280		Ok(executed)
281	}
282
283	#[precompile::public("proposalHash(bytes)")]
284	#[precompile::view]
285	fn proposal_hash(
286		_handle: &mut impl PrecompileHandle,
287		proposal: BoundedBytes<GetProposalLimit>,
288	) -> EvmResult<H256> {
289		let proposal: Vec<_> = proposal.into();
290		let hash = hash::<Runtime>(&proposal);
291
292		Ok(hash)
293	}
294
295	#[precompile::public("proposals()")]
296	#[precompile::view]
297	fn proposals(handle: &mut impl PrecompileHandle) -> EvmResult<Vec<H256>> {
298		// Proposals: BoundedVec(32 * MaxProposals)
299		handle.record_db_read::<Runtime>(
300			32 * (<Runtime as pallet_collective::Config<Instance>>::MaxProposals::get() as usize),
301		)?;
302
303		let proposals = pallet_collective::Proposals::<Runtime, Instance>::get();
304		let proposals: Vec<_> = proposals.into_iter().map(|hash| hash.into()).collect();
305
306		Ok(proposals)
307	}
308
309	#[precompile::public("members()")]
310	#[precompile::view]
311	fn members(handle: &mut impl PrecompileHandle) -> EvmResult<Vec<Address>> {
312		// Record cost of reading the Members storage item, which contains up to MaxMembers accounts
313		// Cost: AccountId20 size × MaxMembers
314		handle.record_db_read::<Runtime>(
315			AccountId20::max_encoded_len()
316				* (<Runtime as pallet_collective::Config<Instance>>::MaxMembers::get() as usize),
317		)?;
318
319		let members = pallet_collective::Members::<Runtime, Instance>::get();
320		let members: Vec<_> = members.into_iter().map(|id| Address(id.into())).collect();
321
322		Ok(members)
323	}
324
325	#[precompile::public("isMember(address)")]
326	#[precompile::view]
327	fn is_member(handle: &mut impl PrecompileHandle, account: Address) -> EvmResult<bool> {
328		// Record cost of reading the Members storage item, which contains up to MaxMembers accounts
329		// Cost: AccountId20 size × MaxMembers
330		handle.record_db_read::<Runtime>(
331			AccountId20::max_encoded_len()
332				* (<Runtime as pallet_collective::Config<Instance>>::MaxMembers::get() as usize),
333		)?;
334
335		let account = Runtime::AddressMapping::into_account_id(account.into());
336
337		let is_member = pallet_collective::Pallet::<Runtime, Instance>::is_member(&account);
338
339		Ok(is_member)
340	}
341
342	#[precompile::public("prime()")]
343	#[precompile::view]
344	fn prime(handle: &mut impl PrecompileHandle) -> EvmResult<Address> {
345		// Prime
346		handle.record_db_read::<Runtime>(20)?;
347
348		let prime = pallet_collective::Prime::<Runtime, Instance>::get()
349			.map(|prime| prime.into())
350			.unwrap_or(H160::zero());
351
352		Ok(Address(prime))
353	}
354}
355
356pub fn hash<Runtime>(data: &[u8]) -> H256
357where
358	Runtime: frame_system::Config,
359	H256: From<<Runtime as frame_system::Config>::Hash>,
360{
361	<Runtime as frame_system::Config>::Hashing::hash(data).into()
362}