1#![cfg_attr(not(feature = "std"), no_std)]
18
19use fp_evm::PrecompileHandle;
20use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
21use frame_support::traits::{
22 schedule::DispatchTime, Bounded, Currency, Get, OriginTrait, VoteTally,
23};
24use pallet_evm::AddressMapping;
25use pallet_referenda::{
26 Call as ReferendaCall, DecidingCount, Deposit, Pallet as Referenda, ReferendumCount,
27 ReferendumInfo, ReferendumInfoFor, TracksInfo,
28};
29use parity_scale_codec::{Encode, MaxEncodedLen};
30use precompile_utils::prelude::*;
31use sp_core::{H160, H256, U256};
32use sp_runtime::str_array;
33use sp_runtime::traits::Dispatchable;
34use sp_std::str;
35use sp_std::{boxed::Box, marker::PhantomData, str::FromStr, vec::Vec};
36
37#[cfg(test)]
38mod mock;
39#[cfg(test)]
40mod tests;
41
42pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
43
44type BlockNumberFor<T> = pallet_referenda::BlockNumberFor<T, ()>;
45type BalanceOf<Runtime> = <<Runtime as pallet_referenda::Config>::Currency as Currency<
46 <Runtime as frame_system::Config>::AccountId,
47>>::Balance;
48type TrackIdOf<Runtime> = <<Runtime as pallet_referenda::Config>::Tracks as TracksInfo<
49 BalanceOf<Runtime>,
50 BlockNumberFor<Runtime>,
51>>::Id;
52type BoundedCallOf<Runtime> = Bounded<
53 <Runtime as pallet_referenda::Config>::RuntimeCall,
54 <Runtime as frame_system::Config>::Hashing,
55>;
56
57type OriginOf<Runtime> =
58 <<Runtime as frame_system::Config>::RuntimeOrigin as OriginTrait>::PalletsOrigin;
59
60pub(crate) const SELECTOR_LOG_SUBMITTED_AT: [u8; 32] =
61 keccak256!("SubmittedAt(uint16,uint32,bytes32)");
62
63pub(crate) const SELECTOR_LOG_SUBMITTED_AFTER: [u8; 32] =
64 keccak256!("SubmittedAfter(uint16,uint32,bytes32)");
65
66pub(crate) const SELECTOR_LOG_DECISION_DEPOSIT_PLACED: [u8; 32] =
67 keccak256!("DecisionDepositPlaced(uint32,address,uint256)");
68
69pub(crate) const SELECTOR_LOG_DECISION_DEPOSIT_REFUNDED: [u8; 32] =
70 keccak256!("DecisionDepositRefunded(uint32,address,uint256)");
71
72pub(crate) const SELECTOR_LOG_SUBMISSION_DEPOSIT_REFUNDED: [u8; 32] =
73 keccak256!("SubmissionDepositRefunded(uint32,address,uint256)");
74
75#[derive(solidity::Codec)]
76pub struct TrackInfo {
77 name: UnboundedBytes,
78 max_deciding: U256,
79 decision_deposit: U256,
80 prepare_period: U256,
81 decision_period: U256,
82 confirm_period: U256,
83 min_enactment_period: U256,
84 min_approval: UnboundedBytes,
85 min_support: UnboundedBytes,
86}
87
88#[derive(solidity::Codec)]
89pub struct OngoingReferendumInfo {
90 track_id: u16,
92 origin: UnboundedBytes,
94 proposal: UnboundedBytes,
96 enactment_type: bool,
98 enactment_time: U256,
100 submission_time: U256,
103 submission_depositor: Address,
104 submission_deposit: U256,
105 decision_depositor: Address,
106 decision_deposit: U256,
107 deciding_since: U256,
110 deciding_confirming_end: U256,
113 ayes: U256,
115 support: u32,
117 approval: u32,
119 in_queue: bool,
121 alarm_time: U256,
123 alarm_task_address: UnboundedBytes,
124}
125
126#[derive(solidity::Codec)]
127pub struct ClosedReferendumInfo {
128 status: u8,
129 end: U256,
130 submission_depositor: Address,
131 submission_deposit: U256,
132 decision_depositor: Address,
133 decision_deposit: U256,
134}
135
136pub struct ReferendaPrecompile<Runtime, GovOrigin>(PhantomData<(Runtime, GovOrigin)>);
138
139#[precompile_utils::precompile]
140impl<Runtime, GovOrigin> ReferendaPrecompile<Runtime, GovOrigin>
141where
142 Runtime: pallet_referenda::Config + pallet_evm::Config + frame_system::Config,
143 OriginOf<Runtime>: From<GovOrigin>,
144 Runtime::AccountId: Into<H160>,
145 <Runtime as frame_system::Config>::RuntimeCall:
146 Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
147 <<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
148 From<Option<Runtime::AccountId>>,
149 <Runtime as frame_system::Config>::RuntimeCall: From<ReferendaCall<Runtime>>,
150 <Runtime as frame_system::Config>::Hash: Into<H256>,
151 BlockNumberFor<Runtime>: Into<U256>,
152 Runtime::AccountId: Into<H160>,
153 TrackIdOf<Runtime>: TryFrom<u16> + TryInto<u16>,
154 BalanceOf<Runtime>: Into<U256>,
155 Runtime::Votes: Into<U256>,
156 GovOrigin: FromStr,
157 H256: From<<Runtime as frame_system::Config>::Hash>
158 + Into<<Runtime as frame_system::Config>::Hash>,
159 <Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
160{
161 #[precompile::public("referendumCount()")]
163 #[precompile::view]
164 fn referendum_count(handle: &mut impl PrecompileHandle) -> EvmResult<u32> {
165 handle.record_db_read::<Runtime>(4)?;
167 let ref_count = ReferendumCount::<Runtime>::get();
168 log::trace!(target: "referendum-precompile", "Referendum count is {:?}", ref_count);
169
170 Ok(ref_count)
171 }
172
173 #[precompile::public("submissionDeposit()")]
174 #[precompile::view]
175 fn submission_deposit(_handle: &mut impl PrecompileHandle) -> EvmResult<U256> {
176 let submission_deposit = Runtime::SubmissionDeposit::get();
177 log::trace!(target: "referendum-precompile", "Submission deposit is {:?}", submission_deposit);
178
179 Ok(submission_deposit.into())
180 }
181
182 #[precompile::public("decidingCount(uint16)")]
183 #[precompile::view]
184 fn deciding_count(handle: &mut impl PrecompileHandle, track_id: u16) -> EvmResult<U256> {
185 handle.record_db_read::<Runtime>(14)?;
188 let track_id: TrackIdOf<Runtime> = track_id
189 .try_into()
190 .map_err(|_| RevertReason::value_is_too_large("Track id type").into())
191 .in_field("trackId")?;
192 let deciding_count = DecidingCount::<Runtime>::get(track_id);
193 log::trace!(
194 target: "referendum-precompile", "Track {:?} deciding count is {:?}",
195 track_id,
196 deciding_count
197 );
198
199 Ok(deciding_count.into())
200 }
201
202 #[precompile::public("trackIds()")]
203 #[precompile::view]
204 fn track_ids(_handle: &mut impl PrecompileHandle) -> EvmResult<Vec<u16>> {
205 let track_ids: Vec<u16> = Runtime::Tracks::tracks()
206 .into_iter()
207 .filter_map(|track| {
208 if let Ok(track_id) = track.id.try_into() {
209 Some(track_id)
210 } else {
211 None
212 }
213 })
214 .collect();
215
216 Ok(track_ids)
217 }
218
219 #[precompile::public("trackInfo(uint16)")]
220 #[precompile::view]
221 fn track_info(_handle: &mut impl PrecompileHandle, track_id: u16) -> EvmResult<TrackInfo> {
222 let track_id: TrackIdOf<Runtime> = track_id
223 .try_into()
224 .map_err(|_| RevertReason::value_is_too_large("Track id type").into())
225 .in_field("trackId")?;
226 let track = Runtime::Tracks::tracks()
227 .find(|track| track.id == track_id)
228 .ok_or(RevertReason::custom("No such track").in_field("trackId"))?;
229 let track_info = &track.info;
230
231 let track_name_trimmed: &[u8] = track_info
234 .name
235 .iter()
236 .rposition(|&b| b != 0)
237 .map_or(&[], |pos| &track_info.name[..=pos]);
238
239 Ok(TrackInfo {
240 name: track_name_trimmed.into(),
241 max_deciding: track_info.max_deciding.into(),
242 decision_deposit: track_info.decision_deposit.into(),
243 prepare_period: track_info.prepare_period.into(),
244 decision_period: track_info.decision_period.into(),
245 confirm_period: track_info.confirm_period.into(),
246 min_enactment_period: track_info.min_enactment_period.into(),
247 min_approval: track_info.min_approval.encode().into(),
248 min_support: track_info.min_support.encode().into(),
249 })
250 }
251
252 fn track_id_to_origin(track_id: TrackIdOf<Runtime>) -> EvmResult<Box<OriginOf<Runtime>>> {
254 let track = Runtime::Tracks::tracks()
255 .find(|track| track.id == track_id)
256 .ok_or(RevertReason::custom("No such track").in_field("trackId"))?;
257 let track_info = &track.info;
258 let origin = if track_info.name == str_array("root") {
259 frame_system::RawOrigin::Root.into()
260 } else {
261 GovOrigin::from_str(str::from_utf8(&track_info.name).map_err(|_| {
262 RevertReason::custom("Track name is not valid UTF-8").in_field("trackId")
263 })?)
264 .map_err(|_| {
265 RevertReason::custom("Custom origin does not exist for {track_info.name}")
266 .in_field("trackId")
267 })?
268 .into()
269 };
270 Ok(Box::new(origin))
271 }
272
273 fn submit(
275 handle: &mut impl PrecompileHandle,
276 track_id: u16,
277 proposal: BoundedCallOf<Runtime>,
278 enactment_moment: DispatchTime<BlockNumberFor<Runtime>>,
279 ) -> EvmResult<u32> {
280 log::trace!(
281 target: "referendum-precompile",
282 "Submitting proposal {:?} [len: {:?}] to track {}",
283 proposal.hash(),
284 proposal.len(),
285 track_id
286 );
287 handle.record_db_read::<Runtime>(4)?;
289 let referendum_index = ReferendumCount::<Runtime>::get();
290
291 let proposal_origin = Self::track_id_to_origin(
292 track_id
293 .try_into()
294 .map_err(|_| RevertReason::value_is_too_large("Track id type").into())
295 .in_field("trackId")?,
296 )?;
297 let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
298
299 let call = ReferendaCall::<Runtime>::submit {
300 proposal_origin,
301 proposal,
302 enactment_moment,
303 }
304 .into();
305
306 <RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
307
308 Ok(referendum_index)
309 }
310
311 #[precompile::public("referendumStatus(uint32)")]
312 #[precompile::view]
313 fn referendum_status(
314 handle: &mut impl PrecompileHandle,
315 referendum_index: u32,
316 ) -> EvmResult<u8> {
317 handle.record_db_read::<Runtime>(
319 20 + pallet_referenda::ReferendumInfoOf::<Runtime, ()>::max_encoded_len(),
320 )?;
321
322 let status = match ReferendumInfoFor::<Runtime>::get(referendum_index).ok_or(
323 RevertReason::custom("Referendum does not exist for index")
324 .in_field("referendum_index"),
325 )? {
326 ReferendumInfo::Ongoing(..) => 0,
327 ReferendumInfo::Approved(..) => 1,
328 ReferendumInfo::Rejected(..) => 2,
329 ReferendumInfo::Cancelled(..) => 3,
330 ReferendumInfo::TimedOut(..) => 4,
331 ReferendumInfo::Killed(..) => 5,
332 };
333
334 Ok(status)
335 }
336
337 #[precompile::public("ongoingReferendumInfo(uint32)")]
338 #[precompile::view]
339 fn ongoing_referendum_info(
340 handle: &mut impl PrecompileHandle,
341 referendum_index: u32,
342 ) -> EvmResult<OngoingReferendumInfo> {
343 handle.record_db_read::<Runtime>(
345 20 + pallet_referenda::ReferendumInfoOf::<Runtime, ()>::max_encoded_len(),
346 )?;
347
348 match ReferendumInfoFor::<Runtime>::get(referendum_index).ok_or(
349 RevertReason::custom("Referendum does not exist for index")
350 .in_field("referendum_index"),
351 )? {
352 ReferendumInfo::Ongoing(info) => {
353 let track_id = info
354 .track
355 .try_into()
356 .map_err(|_| RevertReason::value_is_too_large("Track id type not u16"))?;
357 let (enactment_type, enactment_time) = match info.enactment {
358 DispatchTime::At(x) => (true, x.into()),
359 DispatchTime::After(x) => (false, x.into()),
360 };
361 let (decision_depositor, decision_deposit) =
362 if let Some(deposit) = info.decision_deposit {
363 (Address(deposit.who.into()), deposit.amount.into())
364 } else {
365 (Address(H160::zero()), U256::zero())
366 };
367 let (deciding_since, deciding_confirming_end) =
368 if let Some(deciding_status) = info.deciding {
369 (
370 deciding_status.since.into(),
371 deciding_status.confirming.unwrap_or_default().into(),
372 )
373 } else {
374 (U256::zero(), U256::zero())
375 };
376 let (alarm_time, alarm_task_address) =
377 if let Some((time, task_address)) = info.alarm {
378 (time.into(), task_address.encode().into())
379 } else {
380 (U256::zero(), UnboundedBytes::from(&[]))
381 };
382
383 Ok(OngoingReferendumInfo {
384 track_id,
385 origin: info.origin.encode().into(),
386 proposal: info.proposal.encode().into(),
387 enactment_type,
388 enactment_time,
389 submission_time: info.submitted.into(),
390 submission_depositor: Address(info.submission_deposit.who.into()),
391 submission_deposit: info.submission_deposit.amount.into(),
392 decision_depositor,
393 decision_deposit,
394 deciding_since,
395 deciding_confirming_end,
396 ayes: info.tally.ayes(info.track).into(),
397 support: info.tally.support(info.track).deconstruct(),
398 approval: info.tally.approval(info.track).deconstruct(),
399 in_queue: info.in_queue,
400 alarm_time,
401 alarm_task_address,
402 })
403 }
404 _ => Err(RevertReason::custom("Referendum not ongoing").into()),
405 }
406 }
407
408 #[precompile::public("closedReferendumInfo(uint32)")]
409 #[precompile::view]
410 fn closed_referendum_info(
411 handle: &mut impl PrecompileHandle,
412 referendum_index: u32,
413 ) -> EvmResult<ClosedReferendumInfo> {
414 handle.record_db_read::<Runtime>(
416 20 + pallet_referenda::ReferendumInfoOf::<Runtime, ()>::max_encoded_len(),
417 )?;
418
419 let get_closed_ref_info =
420 |status,
421 moment: BlockNumberFor<Runtime>,
422 submission_deposit: Option<Deposit<Runtime::AccountId, BalanceOf<Runtime>>>,
423 decision_deposit: Option<Deposit<Runtime::AccountId, BalanceOf<Runtime>>>|
424 -> ClosedReferendumInfo {
425 let (submission_depositor, submission_deposit_amount): (Address, U256) =
426 if let Some(Deposit { who, amount }) = submission_deposit {
427 (Address(who.into()), amount.into())
428 } else {
429 (Address(H160::zero()), U256::zero())
430 };
431 let (decision_depositor, decision_deposit_amount) =
432 if let Some(Deposit { who, amount }) = decision_deposit {
433 (Address(who.into()), amount.into())
434 } else {
435 (Address(H160::zero()), U256::zero())
436 };
437 ClosedReferendumInfo {
438 status,
439 end: moment.into(),
440 submission_depositor,
441 submission_deposit: submission_deposit_amount,
442 decision_depositor,
443 decision_deposit: decision_deposit_amount,
444 }
445 };
446
447 match ReferendumInfoFor::<Runtime>::get(referendum_index).ok_or(
448 RevertReason::custom("Referendum does not exist for index")
449 .in_field("referendum_index"),
450 )? {
451 ReferendumInfo::Approved(moment, submission_deposit, decision_deposit) => Ok(
452 get_closed_ref_info(1, moment, submission_deposit, decision_deposit),
453 ),
454 ReferendumInfo::Rejected(moment, submission_deposit, decision_deposit) => Ok(
455 get_closed_ref_info(2, moment, submission_deposit, decision_deposit),
456 ),
457 ReferendumInfo::Cancelled(moment, submission_deposit, decision_deposit) => Ok(
458 get_closed_ref_info(3, moment, submission_deposit, decision_deposit),
459 ),
460 ReferendumInfo::TimedOut(moment, submission_deposit, decision_deposit) => Ok(
461 get_closed_ref_info(4, moment, submission_deposit, decision_deposit),
462 ),
463 _ => Err(RevertReason::custom("Referendum not closed").into()),
464 }
465 }
466
467 #[precompile::public("killedReferendumBlock(uint32)")]
468 #[precompile::view]
469 fn killed_referendum_block(
470 handle: &mut impl PrecompileHandle,
471 referendum_index: u32,
472 ) -> EvmResult<U256> {
473 handle.record_db_read::<Runtime>(
475 20 + pallet_referenda::ReferendumInfoOf::<Runtime, ()>::max_encoded_len(),
476 )?;
477
478 let block = match ReferendumInfoFor::<Runtime>::get(referendum_index).ok_or(
479 RevertReason::custom("Referendum does not exist for index")
480 .in_field("referendum_index"),
481 )? {
482 ReferendumInfo::Killed(b) => b,
483 _ => return Err(RevertReason::custom("Referendum not killed").into()),
484 };
485
486 Ok(block.into())
487 }
488
489 #[precompile::public("submitAt(uint16,bytes32,uint32,uint32)")]
497 fn submit_at(
498 handle: &mut impl PrecompileHandle,
499 track_id: u16,
500 proposal_hash: H256,
501 proposal_len: u32,
502 block_number: u32,
503 ) -> EvmResult<u32> {
504 let proposal: BoundedCallOf<Runtime> = Bounded::Lookup {
505 hash: proposal_hash.into(),
506 len: proposal_len,
507 };
508 handle.record_log_costs_manual(2, 32 * 2)?;
509
510 let referendum_index = Self::submit(
511 handle,
512 track_id,
513 proposal,
514 DispatchTime::At(block_number.into()),
515 )?;
516 let event = log2(
517 handle.context().address,
518 SELECTOR_LOG_SUBMITTED_AT,
519 H256::from_low_u64_be(track_id as u64),
520 solidity::encode_event_data((referendum_index, proposal_hash)),
521 );
522 event.record(handle)?;
523
524 Ok(referendum_index)
525 }
526
527 #[precompile::public("submitAfter(uint16,bytes32,uint32,uint32)")]
535 fn submit_after(
536 handle: &mut impl PrecompileHandle,
537 track_id: u16,
538 proposal_hash: H256,
539 proposal_len: u32,
540 block_number: u32,
541 ) -> EvmResult<u32> {
542 let proposal: BoundedCallOf<Runtime> = Bounded::Lookup {
543 hash: proposal_hash.into(),
544 len: proposal_len,
545 };
546 handle.record_log_costs_manual(2, 32 * 2)?;
547
548 let referendum_index = Self::submit(
549 handle,
550 track_id,
551 proposal,
552 DispatchTime::After(block_number.into()),
553 )?;
554 let event = log2(
555 handle.context().address,
556 SELECTOR_LOG_SUBMITTED_AFTER,
557 H256::from_low_u64_be(track_id as u64),
558 solidity::encode_event_data((referendum_index, proposal_hash)),
559 );
560
561 event.record(handle)?;
562
563 Ok(referendum_index)
564 }
565
566 #[precompile::public("placeDecisionDeposit(uint32)")]
571 fn place_decision_deposit(handle: &mut impl PrecompileHandle, index: u32) -> EvmResult {
572 handle.record_db_read::<Runtime>(
574 20 + pallet_referenda::ReferendumInfoOf::<Runtime, ()>::max_encoded_len(),
575 )?;
576 handle.record_log_costs_manual(1, 32 * 3)?;
577
578 let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
579
580 let call = ReferendaCall::<Runtime>::place_decision_deposit { index }.into();
581
582 <RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
583
584 let ongoing_referendum = Referenda::<Runtime>::ensure_ongoing(index).map_err(|_| {
586 RevertReason::custom("Provided index is not an ongoing referendum").in_field("index")
587 })?;
588 let decision_deposit: U256 =
589 if let Some(decision_deposit) = ongoing_referendum.decision_deposit {
590 decision_deposit.amount.into()
591 } else {
592 U256::zero()
593 };
594 let event = log1(
595 handle.context().address,
596 SELECTOR_LOG_DECISION_DEPOSIT_PLACED,
597 solidity::encode_event_data((
598 index,
599 Address(handle.context().caller),
600 decision_deposit,
601 )),
602 );
603
604 event.record(handle)?;
605 Ok(())
606 }
607
608 #[precompile::public("refundDecisionDeposit(uint32)")]
613 fn refund_decision_deposit(handle: &mut impl PrecompileHandle, index: u32) -> EvmResult {
614 handle.record_log_costs_manual(1, 32 * 3)?;
615 handle.record_db_read::<Runtime>(
617 20 + pallet_referenda::ReferendumInfoOf::<Runtime, ()>::max_encoded_len(),
618 )?;
619 let (who, refunded_deposit): (H160, U256) = match ReferendumInfoFor::<Runtime>::get(index)
620 .ok_or(
621 RevertReason::custom("Referendum index does not exist").in_field("index"),
622 )? {
623 ReferendumInfo::Approved(_, _, Some(d))
624 | ReferendumInfo::Rejected(_, _, Some(d))
625 | ReferendumInfo::TimedOut(_, _, Some(d))
626 | ReferendumInfo::Cancelled(_, _, Some(d)) => (d.who.into(), d.amount.into()),
627 _ => (H160::default(), U256::zero()),
629 };
630
631 let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
632
633 let call = ReferendaCall::<Runtime>::refund_decision_deposit { index }.into();
634
635 <RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
636 let event = log1(
637 handle.context().address,
638 SELECTOR_LOG_DECISION_DEPOSIT_REFUNDED,
639 solidity::encode_event_data((index, Address(who), refunded_deposit)),
640 );
641
642 event.record(handle)?;
643 Ok(())
644 }
645
646 #[precompile::public("refundSubmissionDeposit(uint32)")]
651 fn refund_submission_deposit(handle: &mut impl PrecompileHandle, index: u32) -> EvmResult {
652 handle.record_log_costs_manual(1, 32 * 3)?;
653 handle.record_db_read::<Runtime>(
655 20 + pallet_referenda::ReferendumInfoOf::<Runtime, ()>::max_encoded_len(),
656 )?;
657 let (who, refunded_deposit): (H160, U256) =
658 match ReferendumInfoFor::<Runtime>::get(index)
659 .ok_or(RevertReason::custom("Referendum index does not exist").in_field("index"))?
660 {
661 ReferendumInfo::Approved(_, Some(s), _)
662 | ReferendumInfo::Cancelled(_, Some(s), _) => (s.who.into(), s.amount.into()),
663 _ => (H160::default(), U256::zero()),
665 };
666
667 let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
668
669 let call = ReferendaCall::<Runtime>::refund_submission_deposit { index }.into();
670
671 <RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
672
673 let event = log1(
674 handle.context().address,
675 SELECTOR_LOG_SUBMISSION_DEPOSIT_REFUNDED,
676 solidity::encode_event_data((index, Address(who), refunded_deposit)),
677 );
678
679 event.record(handle)?;
680
681 Ok(())
682 }
683}