From a2c2a4637ac5cdd81d2166e28f120199f21877d5 Mon Sep 17 00:00:00 2001 From: tgmichel Date: Tue, 14 Feb 2023 17:06:12 +0100 Subject: [PATCH] Add OpenGov precompile logs (#2071) * Add vote_yes/no logs * Comment * Wip more conviction voting logs * wip convition voting * wip preimage * conviction voting record costs * Add tests preimage * cleanup * Use Hashing assoc type instead Blake explicitly * Add referenda logs * Wip referenda log tests * referenda log tests * past tense convention for naming events * updated test * record log costs first * fmt * Merge branch 'master' into tgm-opengov-precompile-logs # Conflicts: # precompiles/preimage/src/lib.rs # precompiles/preimage/src/mock.rs # precompiles/preimage/src/tests.rs * Fix version * Cleanup after rebase --- .../conviction-voting/ConvictionVoting.sol | 70 +++++ precompiles/conviction-voting/src/lib.rs | 129 +++++++- precompiles/conviction-voting/src/mock.rs | 45 ++- precompiles/conviction-voting/src/tests.rs | 282 +++++++++++++++++- precompiles/preimage/Preimage.sol | 14 + precompiles/preimage/src/lib.rs | 25 +- precompiles/preimage/src/mock.rs | 7 + precompiles/preimage/src/tests.rs | 75 ++++- precompiles/referenda/Referenda.sol | 36 +++ precompiles/referenda/src/lib.rs | 88 +++++- precompiles/referenda/src/mock.rs | 10 +- precompiles/referenda/src/tests.rs | 173 ++++++++++- 12 files changed, 925 insertions(+), 29 deletions(-) diff --git a/precompiles/conviction-voting/ConvictionVoting.sol b/precompiles/conviction-voting/ConvictionVoting.sol index a35670b31d..78d6c3ac49 100644 --- a/precompiles/conviction-voting/ConvictionVoting.sol +++ b/precompiles/conviction-voting/ConvictionVoting.sol @@ -94,4 +94,74 @@ interface ConvictionVoting { /// @param trackId The trackId /// @param target The target address function unlock(uint16 trackId, address target) external; + + /// @dev An account made a vote in a poll. + /// @custom:selector 3839f7832b2a6263aa1fd5040f37d10fd4f9e9c4a9ef07ec384cb1cef9fb4c0e + /// @param pollIndex uint32 Index of the poll. + /// @param voter address Address of the voter. + /// @param aye bool Is it a vote for or against the poll. + /// @param voteAmount uint256 Amount used to vote. + /// @param conviction uint8 Conviction of the vote. + event Voted( + uint32 indexed pollIndex, + address voter, + bool aye, + uint256 voteAmount, + uint8 conviction + ); + + /// @dev An account removed its vote from an ongoing poll. + /// @custom:selector 49fc1dd929f126e1d88cbb9c135625e30c2deba291adeea4740e446098b9957b + /// @param pollIndex uint32 Index of the poll. + /// @param voter address Address of the voter. + event VoteRemoved( + uint32 indexed pollIndex, + address voter + ); + + /// @dev An account removed a vote from a poll. + /// @custom:selector c1d068675720ab00d0c8792a0cbc7e198c0d2202111f0280f039f2c09c50491b + /// @param pollIndex uint32 Index of the poll. + /// @param caller address Address of the origin caller. + /// @param target address Address of the address which's vote is being removed. + /// @param trackId uint16 The trackId. + event VoteRemovedOther( + uint32 indexed pollIndex, + address caller, + address target, + uint16 trackId + ); + + /// @dev An account delegated for the given trackId. + /// @custom:selector 6cc151d547592e227b1e85a264ac3699c6f1014112b08bb3832de1f23b9c66db + /// @param trackId uint16 The trackId. + /// @param from address Address of the caller. + /// @param to address Address of the representative. + /// @param delegatedAmount uint256 Amount being delegated. + /// @param conviction uint8 Conviction being delegated. + event Delegated( + uint16 indexed trackId, + address from, + address to, + uint256 delegatedAmount, + uint8 conviction + ); + + /// @dev An account undelegated for the given trackId. + /// @custom:selector 1053303328f6db14014ccced6297bcad2b3897157ce46070711ab995a05dfa14 + /// @param trackId uint16 The trackId. + /// @param caller address Address of the caller. + event Undelegated( + uint16 indexed trackId, + address caller + ); + + /// @dev An account called to unlock tokens for the given trackId. + /// @custom:selector dcf72fa65ca7fb720b9ccc8ee28e0188edc3d943115124cdd4086c49f836a128 + /// @param trackId uint16 The trackId. + /// @param caller address Address of the caller. + event Unlocked( + uint16 indexed trackId, + address caller + ); } diff --git a/precompiles/conviction-voting/src/lib.rs b/precompiles/conviction-voting/src/lib.rs index 90bedd252c..5027c1d972 100644 --- a/precompiles/conviction-voting/src/lib.rs +++ b/precompiles/conviction-voting/src/lib.rs @@ -24,7 +24,7 @@ use pallet_conviction_voting::Call as ConvictionVotingCall; use pallet_conviction_voting::{AccountVote, Conviction, Tally, Vote}; use pallet_evm::AddressMapping; use precompile_utils::prelude::*; -use sp_core::{H160, U256}; +use sp_core::{H160, H256, U256}; use sp_runtime::traits::StaticLookup; use sp_std::marker::PhantomData; @@ -53,8 +53,29 @@ type ClassOf = <::Polls as >, >>::Class; +/// Solidity selector of the Vote log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_VOTED: [u8; 32] = + keccak256!("Voted(uint32,address,bool,uint256,uint8)"); + +/// Solidity selector of the VoteRemove log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_VOTE_REMOVED: [u8; 32] = keccak256!("VoteRemoved(uint32,address)"); + +/// Solidity selector of the VoteRemoveOther log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_VOTE_REMOVED_OTHER: [u8; 32] = + keccak256!("VoteRemovedOther(uint32,address,address,uint16)"); + +/// Solidity selector of the Delegate log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_DELEGATED: [u8; 32] = + keccak256!("Delegated(uint16,address,address,uint256,uint8)"); + +/// Solidity selector of the Undelegate log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_UNDELEGATED: [u8; 32] = keccak256!("Undelegated(uint16,address)"); + +/// Solidity selector of the Unlock log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_UNLOCKED: [u8; 32] = keccak256!("Unlocked(uint16,address)"); + /// Direction of vote -enum VoteDirection { +pub(crate) enum VoteDirection { Yes, No, #[allow(unused)] @@ -85,15 +106,28 @@ where vote_amount: U256, conviction: u8, ) -> EvmResult { - let poll_index = Self::u32_to_index(poll_index).in_field("pollIndex")?; - let vote_amount = Self::u256_to_amount(vote_amount).in_field("voteAmount")?; - let conviction = Self::u8_to_conviction(conviction).in_field("conviction")?; - + let caller = handle.context().caller; let aye = match vote { VoteDirection::Yes => true, VoteDirection::No => false, _ => return Err(RevertReason::custom("Abstain not supported").into()), }; + let event = log2( + handle.context().address, + SELECTOR_LOG_VOTED, + H256::from_low_u64_be(poll_index as u64), // poll index, + EvmDataWriter::new() + .write::
(Address(caller)) + .write::(aye) + .write::(vote_amount) + .write::(conviction) + .build(), + ); + handle.record_log_costs(&[&event])?; + + let poll_index = Self::u32_to_index(poll_index).in_field("pollIndex")?; + let vote_amount = Self::u256_to_amount(vote_amount).in_field("voteAmount")?; + let conviction = Self::u8_to_conviction(conviction).in_field("conviction")?; let vote = AccountVote::Standard { vote: Vote { aye, conviction }, @@ -105,11 +139,13 @@ where aye, poll_index, conviction ); - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let origin = Runtime::AddressMapping::into_account_id(caller); let call = ConvictionVotingCall::::vote { poll_index, vote }.into(); >::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; + Ok(()) } @@ -159,6 +195,17 @@ where #[precompile::public("removeVote(uint32)")] fn remove_vote(handle: &mut impl PrecompileHandle, poll_index: u32) -> EvmResult { + let caller = handle.context().caller; + let event = log2( + handle.context().address, + SELECTOR_LOG_VOTE_REMOVED, + H256::from_low_u64_be(poll_index as u64), // poll index, + EvmDataWriter::new() + .write::
(Address(caller)) + .build(), + ); + handle.record_log_costs(&[&event])?; + let index = Self::u32_to_index(poll_index).in_field("pollIndex")?; log::trace!( @@ -167,11 +214,13 @@ where index ); - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let origin = Runtime::AddressMapping::into_account_id(caller); let call = ConvictionVotingCall::::remove_vote { class: None, index }; RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; + Ok(()) } @@ -182,6 +231,20 @@ where track_id: u16, poll_index: u32, ) -> EvmResult { + let caller = handle.context().caller; + + let event = log2( + handle.context().address, + SELECTOR_LOG_VOTE_REMOVED_OTHER, + H256::from_low_u64_be(poll_index as u64), // poll index, + EvmDataWriter::new() + .write::
(Address(caller)) + .write::
(target) + .write::(track_id) + .build(), + ); + handle.record_log_costs(&[&event])?; + let class = Self::u16_to_track_id(track_id).in_field("trackId")?; let index = Self::u32_to_index(poll_index).in_field("pollIndex")?; @@ -195,7 +258,7 @@ where index ); - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let origin = Runtime::AddressMapping::into_account_id(caller); let call = ConvictionVotingCall::::remove_other_vote { target, class, @@ -204,6 +267,8 @@ where RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; + Ok(()) } @@ -215,6 +280,21 @@ where conviction: u8, amount: U256, ) -> EvmResult { + let caller = handle.context().caller; + + let event = log2( + handle.context().address, + SELECTOR_LOG_DELEGATED, + H256::from_low_u64_be(track_id as u64), // track id, + EvmDataWriter::new() + .write::
(Address(caller)) + .write::
(representative) + .write::(amount) + .write::(conviction) + .build(), + ); + handle.record_log_costs(&[&event])?; + let class = Self::u16_to_track_id(track_id).in_field("trackId")?; let amount = Self::u256_to_amount(amount).in_field("amount")?; let conviction = Self::u8_to_conviction(conviction).in_field("conviction")?; @@ -227,7 +307,7 @@ where let representative = Runtime::AddressMapping::into_account_id(representative.into()); let to: ::Source = Runtime::Lookup::unlookup(representative.clone()); - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let origin = Runtime::AddressMapping::into_account_id(caller); let call = ConvictionVotingCall::::delegate { class, to, @@ -237,21 +317,46 @@ where RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; + Ok(()) } #[precompile::public("undelegate(uint16)")] fn undelegate(handle: &mut impl PrecompileHandle, track_id: u16) -> EvmResult { + let caller = handle.context().caller; + + let event = log2( + handle.context().address, + SELECTOR_LOG_UNDELEGATED, + H256::from_low_u64_be(track_id as u64), // track id, + EvmDataWriter::new() + .write::
(Address(caller)) + .build(), + ); + handle.record_log_costs(&[&event])?; + let class = Self::u16_to_track_id(track_id).in_field("trackId")?; - let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let origin = Runtime::AddressMapping::into_account_id(caller); let call = ConvictionVotingCall::::undelegate { class }; RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; + Ok(()) } #[precompile::public("unlock(uint16,address)")] fn unlock(handle: &mut impl PrecompileHandle, track_id: u16, target: Address) -> EvmResult { let class = Self::u16_to_track_id(track_id).in_field("trackId")?; + + let event = log2( + handle.context().address, + SELECTOR_LOG_UNLOCKED, + H256::from_low_u64_be(track_id as u64), // track id, + EvmDataWriter::new().write::
(target).build(), + ); + handle.record_log_costs(&[&event])?; + let target: H160 = target.into(); let target = Runtime::AddressMapping::into_account_id(target); let target: ::Source = @@ -267,6 +372,8 @@ where RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; + Ok(()) } fn u8_to_conviction(conviction: u8) -> MayRevert { diff --git a/precompiles/conviction-voting/src/mock.rs b/precompiles/conviction-voting/src/mock.rs index fbbc8b86ff..435a9fa385 100644 --- a/precompiles/conviction-voting/src/mock.rs +++ b/precompiles/conviction-voting/src/mock.rs @@ -246,4 +246,47 @@ impl pallet_conviction_voting::Config for Runtime { type Polls = TestPolls; } -// genesis builder TODO +pub(crate) struct ExtBuilder { + /// Endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + /// Fund some accounts before starting the test + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + /// Build the test externalities for use in tests + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances.clone(), + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext + } +} + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .collect::>() +} diff --git a/precompiles/conviction-voting/src/tests.rs b/precompiles/conviction-voting/src/tests.rs index e0bfa7b747..b244936d0a 100644 --- a/precompiles/conviction-voting/src/tests.rs +++ b/precompiles/conviction-voting/src/tests.rs @@ -13,8 +13,38 @@ // You should have received a copy of the GNU General Public License // along with Moonbeam. If not, see . -use crate::mock::*; -use precompile_utils::testing::*; +use crate::{ + mock::*, VoteDirection, SELECTOR_LOG_DELEGATED, SELECTOR_LOG_UNDELEGATED, + SELECTOR_LOG_UNLOCKED, SELECTOR_LOG_VOTED, SELECTOR_LOG_VOTE_REMOVED, + SELECTOR_LOG_VOTE_REMOVED_OTHER, +}; +use precompile_utils::{prelude::*, testing::*, EvmDataWriter}; + +use frame_support::{ + assert_ok, + dispatch::{Dispatchable, Pays, PostDispatchInfo}, +}; +use pallet_evm::{Call as EvmCall, Event as EvmEvent}; +use sp_core::{H160, H256, U256}; +use sp_runtime::{ + traits::PostDispatchInfoOf, DispatchError, DispatchErrorWithPostInfo, DispatchResultWithInfo, +}; + +const ONGOING_POLL_INDEX: u32 = 3; + +fn evm_call(input: Vec) -> EvmCall { + EvmCall::call { + source: Alice.into(), + target: Precompile1.into(), + input, + value: U256::zero(), + gas_limit: u64::max_value(), + max_fee_per_gas: 0.into(), + max_priority_fee_per_gas: Some(U256::zero()), + nonce: None, + access_list: Vec::new(), + } +} #[test] fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { @@ -40,3 +70,251 @@ fn test_solidity_interface_has_all_function_selectors_documented_and_implemented } } } + +fn vote( + direction: VoteDirection, + vote_amount: U256, + conviction: u8, +) -> DispatchResultWithInfo> { + let input = match direction { + // Vote Yes + VoteDirection::Yes => PCall::vote_yes { + poll_index: ONGOING_POLL_INDEX, + vote_amount, + conviction, + } + .into(), + // Vote No + VoteDirection::No => PCall::vote_no { + poll_index: ONGOING_POLL_INDEX, + vote_amount, + conviction, + } + .into(), + // Unsupported + _ => { + return Err(DispatchErrorWithPostInfo { + post_info: PostDispatchInfo { + actual_weight: None, + pays_fee: Pays::No, + }, + error: DispatchError::Other("Vote direction not supported"), + }) + } + }; + RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root()) +} + +#[test] +fn vote_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + // Vote Yes + assert_ok!(vote(VoteDirection::Yes, 100_000.into(), 0.into())); + + // Vote No + assert_ok!(vote(VoteDirection::No, 99_000.into(), 1.into())); + + // Assert vote events are emitted. + assert!(vec![ + EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_VOTED, + H256::from_low_u64_be(ONGOING_POLL_INDEX as u64), + EvmDataWriter::new() + .write::
(H160::from(Alice).into()) // caller + .write::(true) // vote + .write::(100_000.into()) // amount + .write::(0) // conviction + .build(), + ), + } + .into(), + EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_VOTED, + H256::from_low_u64_be(ONGOING_POLL_INDEX as u64), + EvmDataWriter::new() + .write::
(H160::from(Alice).into()) // caller + .write::(false) // vote + .write::(99_000.into()) // amount + .write::(1) // conviction + .build(), + ), + } + .into() + ] + .iter() + .all(|log| events().contains(log))); + }) +} + +#[test] +fn remove_vote_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + // Vote.. + assert_ok!(vote(VoteDirection::Yes, 100_000.into(), 0.into())); + + // ..and remove + let input = PCall::remove_vote { + poll_index: ONGOING_POLL_INDEX, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert remove vote event is emitted. + assert!(events().contains( + &EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_VOTE_REMOVED, + H256::from_low_u64_be(ONGOING_POLL_INDEX as u64), + EvmDataWriter::new() + .write::
(H160::from(Alice).into()) // caller + .build(), + ), + } + .into() + )); + }) +} + +#[test] +fn remove_other_vote_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + // Vote.. + assert_ok!(vote(VoteDirection::Yes, 100_000.into(), 0.into())); + + // ..and remove other + let input = PCall::remove_other_vote { + target: H160::from(Alice).into(), + track_id: 0u16, + poll_index: ONGOING_POLL_INDEX, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert remove other vote event is emitted. + assert!(events().contains( + &EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_VOTE_REMOVED_OTHER, + H256::from_low_u64_be(ONGOING_POLL_INDEX as u64), + EvmDataWriter::new() + .write::
(H160::from(Alice).into()) // caller + .write::
(H160::from(Alice).into()) // target + .write::(0u16) // track id + .build(), + ), + } + .into() + )); + }) +} + +#[test] +fn delegate_undelegate_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + // Delegate + let input = PCall::delegate { + track_id: 0u16, + representative: H160::from(Bob).into(), + conviction: 0.into(), + amount: 100_000.into(), + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert delegate event is emitted. + assert!(events().contains( + &EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_DELEGATED, + H256::from_low_u64_be(0 as u64), // track id + EvmDataWriter::new() + .write::
(H160::from(Alice).into()) // from + .write::
(H160::from(Bob).into()) // to + .write::(100_000.into()) // amount + .write::(0u8) // conviction + .build(), + ), + } + .into() + )); + + // Undelegate + let input = PCall::undelegate { track_id: 0u16 }.into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert undelegate event is emitted. + assert!(events().contains( + &EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_UNDELEGATED, + H256::from_low_u64_be(0 as u64), // track id + EvmDataWriter::new() + .write::
(H160::from(Alice).into()) // caller + .build(), + ), + } + .into() + )); + }) +} + +#[test] +fn unlock_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + // Vote + assert_ok!(vote(VoteDirection::Yes, 100_000.into(), 0.into())); + + // Remove + let input = PCall::remove_vote { + poll_index: ONGOING_POLL_INDEX, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Unlock + let input = PCall::unlock { + track_id: 0u16, + target: H160::from(Alice).into(), + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert unlock event is emitted. + assert!(events().contains( + &EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_UNLOCKED, + H256::from_low_u64_be(0 as u64), // track id + EvmDataWriter::new() + .write::
(H160::from(Alice).into()) // caller + .build(), + ), + } + .into() + )); + }) +} diff --git a/precompiles/preimage/Preimage.sol b/precompiles/preimage/Preimage.sol index 5ad9931458..7f4fe3dc4a 100644 --- a/precompiles/preimage/Preimage.sol +++ b/precompiles/preimage/Preimage.sol @@ -24,4 +24,18 @@ interface Preimage { /// @custom:selector 02e71b45 /// @param hash The preimage to be cleared from storage function unnotePreimage(bytes32 hash) external; + + /// @dev A Preimage was registered on-chain. + /// @custom:selector 8cb56a8ebdafbb14e25ec706da62a7dde761968dbf1fb45be207d1b15c88c187 + /// @param hash bytes32 The computed hash. + event PreimageNoted( + bytes32 hash + ); + + /// @dev A Preimage was un-registered on-chain. + /// @custom:selector be6cb9502cce812b6de50cc08f2481900ff6c7c6466df7d39c9f27a5f2b9c572 + /// @param hash bytes32 The target preimage hash. + event PreimageUnnoted( + bytes32 hash + ); } diff --git a/precompiles/preimage/src/lib.rs b/precompiles/preimage/src/lib.rs index affc895348..d0bea8c8dc 100644 --- a/precompiles/preimage/src/lib.rs +++ b/precompiles/preimage/src/lib.rs @@ -34,6 +34,12 @@ mod tests; pub const ENCODED_PROPOSAL_SIZE_LIMIT: u32 = 2u32.pow(16); type GetEncodedProposalSizeLimit = ConstU32; +/// Solidity selector of the PreimageNoted log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_PREIMAGE_NOTED: [u8; 32] = keccak256!("PreimageNoted(bytes32)"); + +/// Solidity selector of the PreimageUnnoted log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_PREIMAGE_UNNOTED: [u8; 32] = keccak256!("PreimageUnnoted(bytes32)"); + /// A precompile to wrap the functionality from pallet-preimage. pub struct PreimagePrecompile(PhantomData); @@ -41,7 +47,7 @@ pub struct PreimagePrecompile(PhantomData); impl PreimagePrecompile where Runtime: pallet_preimage::Config + pallet_evm::Config + frame_system::Config, - ::Hash: TryFrom, + ::Hash: TryFrom + Into, ::RuntimeCall: Dispatchable + GetDispatchInfo, <::RuntimeCall as Dispatchable>::RuntimeOrigin: @@ -60,12 +66,20 @@ where ) -> EvmResult { let bytes: Vec = encoded_proposal.into(); let hash: H256 = Runtime::Hashing::hash(&bytes).into(); + + let event = log1( + handle.context().address, + SELECTOR_LOG_PREIMAGE_NOTED, + EvmDataWriter::new().write::(hash.into()).build(), + ); + handle.record_log_costs(&[&event])?; let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let call = PreimageCall::::note_preimage { bytes }.into(); >::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; Ok(hash) } @@ -75,6 +89,13 @@ where /// * hash: The preimage cleared from storage #[precompile::public("unnotePreimage(bytes32)")] fn unnote_preimage(handle: &mut impl PrecompileHandle, hash: H256) -> EvmResult { + let event = log1( + handle.context().address, + SELECTOR_LOG_PREIMAGE_UNNOTED, + EvmDataWriter::new().write::(hash).build(), + ); + handle.record_log_costs(&[&event])?; + let hash: Runtime::Hash = hash .try_into() .map_err(|_| RevertReason::custom("H256 is Runtime::Hash").in_field("hash"))?; @@ -84,6 +105,8 @@ where >::try_dispatch(handle, Some(origin).into(), call)?; + event.record(handle)?; + Ok(()) } } diff --git a/precompiles/preimage/src/mock.rs b/precompiles/preimage/src/mock.rs index 501301027b..59b9977139 100644 --- a/precompiles/preimage/src/mock.rs +++ b/precompiles/preimage/src/mock.rs @@ -178,3 +178,10 @@ impl ExtBuilder { ext } } + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .collect::>() +} diff --git a/precompiles/preimage/src/tests.rs b/precompiles/preimage/src/tests.rs index c687356189..6d4cc14c8a 100644 --- a/precompiles/preimage/src/tests.rs +++ b/precompiles/preimage/src/tests.rs @@ -15,7 +15,26 @@ // along with Moonbeam. If not, see . use crate::mock::*; use crate::*; -use precompile_utils::testing::*; +use precompile_utils::{testing::*, EvmDataWriter}; + +use frame_support::{assert_ok, dispatch::Dispatchable}; +use pallet_evm::{Call as EvmCall, Event as EvmEvent}; + +use sp_core::{Hasher, H256, U256}; + +fn evm_call(input: Vec) -> EvmCall { + EvmCall::call { + source: Alice.into(), + target: Precompile1.into(), + input, + value: U256::zero(), + gas_limit: u64::max_value(), + max_fee_per_gas: 0.into(), + max_priority_fee_per_gas: Some(U256::zero()), + nonce: None, + access_list: Vec::new(), + } +} fn precompiles() -> Precompiles { PrecompilesValue::get() @@ -46,6 +65,60 @@ fn test_solidity_interface_has_all_function_selectors_documented_and_implemented } } +#[test] +fn note_unnote_preimage_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + let bytes = vec![1, 2, 3]; + let expected_hash = sp_runtime::traits::BlakeTwo256::hash(&bytes); + + // Note preimage + let input = PCall::note_preimage { + encoded_proposal: bytes.into(), + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert note preimage event is emited and matching frame event preimage hash. + assert!(vec![ + EvmEvent::Log { + log: log1( + Precompile1, + SELECTOR_LOG_PREIMAGE_NOTED, + EvmDataWriter::new().write::(expected_hash).build(), + ), + } + .into(), + RuntimeEvent::Preimage(pallet_preimage::pallet::Event::Noted { + hash: expected_hash + }) + ] + .iter() + .all(|log| events().contains(log))); + + // Unnote preimage + let input = PCall::unnote_preimage { + hash: expected_hash, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert unnote preimage is emited + assert!(events().contains( + &EvmEvent::Log { + log: log1( + Precompile1, + SELECTOR_LOG_PREIMAGE_UNNOTED, + EvmDataWriter::new().write::(expected_hash).build(), + ), + } + .into() + )); + }) +} + #[test] fn note_preimage_returns_preimage_hash() { ExtBuilder::default() diff --git a/precompiles/referenda/Referenda.sol b/precompiles/referenda/Referenda.sol index c90e4ab0e8..686653261f 100644 --- a/precompiles/referenda/Referenda.sol +++ b/precompiles/referenda/Referenda.sol @@ -82,4 +82,40 @@ interface Referenda { /// @custom:selector 1325d528 /// @param index The index of a closed referendum with decision deposit still locked function refundDecisionDeposit(uint32 index) external; + + /// @dev A referenda has been submitted at a given block + /// @custom:selector e02a819ecfc92874b5016c6a0e26f56a5cb08771f32ab818bf548d84ca3ae94d + /// @param trackId uint16 The trackId + /// @param blockNumber uint32 Block number at which it was set to be submitted + /// @param hash bytes32 The hash of the proposal preimage + event SubmittedAt( + uint16 indexed trackId, + uint32 blockNumber, + bytes32 hash + ); + + /// @dev A referenda has been submitted after a given block + /// @custom:selector a5117efbf0f4aa9e08dd135e69aa8ee4978f99fca86fc5154b5bd1b363eafdcf + /// @param trackId uint16 The trackId + /// @param blockNumber uint32 Block number after which it was set to be submitted + /// @param hash bytes32 The hash of the proposal preimage + event SubmittedAfter( + uint16 indexed trackId, + uint32 blockNumber, + bytes32 hash + ); + + /// @dev Decision Deposit for a referendum has been placed + /// @custom:selector 87e691fb2e6a679435f578d43cd67e1af825294e56064a9de0522b312b8e9a60 + /// @param index uint32 The index of the ongoing referendum that is not yet deciding + event DecisionDepositPlaced( + uint32 index + ); + + /// @dev Decision Deposit for a closed referendum has been refunded + /// @custom:selector 61f241739b215680a33261f1dee7646d0e840d5e498c1142c1a534987d9b8ed8 + /// @param index uint32 The index of the closed referendum + event DecisionDepositRefunded( + uint32 index + ); } diff --git a/precompiles/referenda/src/lib.rs b/precompiles/referenda/src/lib.rs index ce7e93d185..20a90562d6 100644 --- a/precompiles/referenda/src/lib.rs +++ b/precompiles/referenda/src/lib.rs @@ -26,7 +26,7 @@ use pallet_evm::AddressMapping; use pallet_referenda::{Call as ReferendaCall, DecidingCount, ReferendumCount, TracksInfo}; use parity_scale_codec::Encode; use precompile_utils::{data::String, prelude::*}; -use sp_core::U256; +use sp_core::{Hasher, H256, U256}; use sp_std::{boxed::Box, marker::PhantomData, vec::Vec}; #[cfg(test)] @@ -49,6 +49,22 @@ type BoundedCallOf = Bounded<::Run type OriginOf = <::RuntimeOrigin as OriginTrait>::PalletsOrigin; +/// Solidity selector of the SubmittedAt log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_SUBMITTED_AT: [u8; 32] = + keccak256!("SubmittedAt(uint16,uint32,bytes32)"); + +/// Solidity selector of the SubmittedAfter log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_SUBMITTED_AFTER: [u8; 32] = + keccak256!("SubmittedAfter(uint16,uint32,bytes32)"); + +/// Solidity selector of the DecisionDepositPlaced log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_DECISION_DEPOSIT_PLACED: [u8; 32] = + keccak256!("DecisionDepositPlaced(uint32)"); + +/// Solidity selector of the DecisionDepositRefunded log, which is the Keccak of the Log signature. +pub(crate) const SELECTOR_LOG_DECISION_DEPOSIT_REFUNDED: [u8; 32] = + keccak256!("DecisionDepositRefunded(uint32)"); + pub struct TrackInfo { name: UnboundedBytes, max_deciding: U256, @@ -146,6 +162,7 @@ where <::RuntimeCall as Dispatchable>::RuntimeOrigin: From>, ::RuntimeCall: From>, + ::Hash: Into, Runtime::BlockNumber: Into, TrackIdOf: TryFrom + TryInto, BalanceOf: Into, @@ -244,7 +261,7 @@ where fn submit( handle: &mut impl PrecompileHandle, track_id: u16, - proposal: BoundedBytes, + proposal: Vec, enactment_moment: DispatchTime, ) -> EvmResult { // for read of referendumCount to get the referendum index @@ -254,11 +271,10 @@ where RevertReason::custom("Origin does not exist for TrackId").in_field("trackId") })?; let proposal_origin: Box> = Box::new(proposal_origin.into()); - let proposal: BoundedCallOf = Bounded::Inline( - frame_support::BoundedVec::try_from(proposal.as_bytes().to_vec()).map_err(|_| { + let proposal: BoundedCallOf = + Bounded::Inline(frame_support::BoundedVec::try_from(proposal).map_err(|_| { RevertReason::custom("Proposal input is not a runtime call").in_field("proposal") - })?, - ); + })?); let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); @@ -287,12 +303,29 @@ where proposal: BoundedBytes, block_number: u32, ) -> EvmResult { - Self::submit( + let proposal: sp_std::vec::Vec = proposal.into(); + let hash = ::Hashing::hash(&proposal); + let event = log2( + handle.context().address, + SELECTOR_LOG_SUBMITTED_AT, + H256::from_low_u64_be(track_id as u64), + EvmDataWriter::new() + .write::(block_number) + .write::(hash.into()) + .build(), + ); + handle.record_log_costs(&[&event])?; + + let submit_result = Self::submit( handle, track_id, proposal, DispatchTime::At(block_number.into()), - ) + )?; + + event.record(handle)?; + + Ok(submit_result) } /// Propose a referendum on a privileged action. @@ -308,12 +341,29 @@ where proposal: BoundedBytes, block_number: u32, ) -> EvmResult { - Self::submit( + let proposal: sp_std::vec::Vec = proposal.into(); + let hash = ::Hashing::hash(&proposal); + let event = log2( + handle.context().address, + SELECTOR_LOG_SUBMITTED_AFTER, + H256::from_low_u64_be(track_id as u64), + EvmDataWriter::new() + .write::(block_number) + .write::(hash.into()) + .build(), + ); + handle.record_log_costs(&[&event])?; + + let submit_result = Self::submit( handle, track_id, proposal, DispatchTime::After(block_number.into()), - ) + )?; + + event.record(handle)?; + + Ok(submit_result) } /// Post the Decision Deposit for a referendum. @@ -322,11 +372,20 @@ where /// * index: The index of the submitted referendum whose Decision Deposit is yet to be posted. #[precompile::public("placeDecisionDeposit(uint32)")] fn place_decision_deposit(handle: &mut impl PrecompileHandle, index: u32) -> EvmResult { + let event = log1( + handle.context().address, + SELECTOR_LOG_DECISION_DEPOSIT_PLACED, + EvmDataWriter::new().write::(index).build(), + ); + handle.record_log_costs(&[&event])?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let call = ReferendaCall::::place_decision_deposit { index }.into(); >::try_dispatch(handle, Some(origin).into(), call)?; + + event.record(handle)?; Ok(()) } @@ -336,11 +395,20 @@ where /// * index: The index of a closed referendum whose Decision Deposit has not yet been refunded. #[precompile::public("refundDecisionDeposit(uint32)")] fn refund_decision_deposit(handle: &mut impl PrecompileHandle, index: u32) -> EvmResult { + let event = log1( + handle.context().address, + SELECTOR_LOG_DECISION_DEPOSIT_REFUNDED, + EvmDataWriter::new().write::(index).build(), + ); + handle.record_log_costs(&[&event])?; + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let call = ReferendaCall::::refund_decision_deposit { index }.into(); >::try_dispatch(handle, Some(origin).into(), call)?; + + event.record(handle)?; Ok(()) } } diff --git a/precompiles/referenda/src/mock.rs b/precompiles/referenda/src/mock.rs index 195cfc06ee..3c75fdb8af 100644 --- a/precompiles/referenda/src/mock.rs +++ b/precompiles/referenda/src/mock.rs @@ -118,6 +118,7 @@ pub type TestPrecompiles = PrecompileSetBuilder< pub type PCall = ReferendaPrecompileCall; parameter_types! { + pub BlockGasLimit: U256 = U256::max_value(); pub PrecompilesValue: TestPrecompiles = TestPrecompiles::new(); pub const WeightPerGas: Weight = Weight::from_ref_time(1); } @@ -136,7 +137,7 @@ impl pallet_evm::Config for Runtime { type PrecompilesValue = PrecompilesValue; type ChainId = (); type OnChargeTransaction = (); - type BlockGasLimit = (); + type BlockGasLimit = BlockGasLimit; type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; type FindAuthor = (); } @@ -379,3 +380,10 @@ impl ExtBuilder { ext } } + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .collect::>() +} diff --git a/precompiles/referenda/src/tests.rs b/precompiles/referenda/src/tests.rs index 6c7a3070e5..c827c3a661 100644 --- a/precompiles/referenda/src/tests.rs +++ b/precompiles/referenda/src/tests.rs @@ -13,8 +13,31 @@ // You should have received a copy of the GNU General Public License // along with Moonbeam. If not, see . -use crate::mock::*; -use precompile_utils::testing::*; +use crate::{ + mock::*, SELECTOR_LOG_DECISION_DEPOSIT_PLACED, SELECTOR_LOG_DECISION_DEPOSIT_REFUNDED, + SELECTOR_LOG_SUBMITTED_AFTER, SELECTOR_LOG_SUBMITTED_AT, +}; +use precompile_utils::{prelude::*, testing::*, EvmDataWriter}; + +use frame_support::{assert_ok, dispatch::Dispatchable}; +use pallet_evm::{Call as EvmCall, Event as EvmEvent}; +use pallet_referenda::Call as ReferendaCall; + +use sp_core::{Hasher, H256, U256}; + +fn evm_call(input: Vec) -> EvmCall { + EvmCall::call { + source: Alice.into(), + target: Precompile1.into(), + input, + value: U256::zero(), + gas_limit: u64::max_value(), + max_fee_per_gas: 0.into(), + max_priority_fee_per_gas: Some(U256::zero()), + nonce: None, + access_list: Vec::new(), + } +} #[test] fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { @@ -40,3 +63,149 @@ fn test_solidity_interface_has_all_function_selectors_documented_and_implemented } } } + +#[test] +fn submitted_at_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + let proposal = vec![1, 2, 3]; + let expected_hash = sp_runtime::traits::BlakeTwo256::hash(&proposal); + + let input = PCall::submit_at { + track_id: 0u16, + proposal: proposal.into(), + block_number: 0u32, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + assert!(events().contains( + &EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_SUBMITTED_AT, + H256::from_low_u64_be(0u64), + EvmDataWriter::new() + .write::(0u32) + .write::(expected_hash.into()) + .build(), + ), + } + .into() + )); + }); +} + +#[test] +fn submitted_after_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + let proposal = vec![1, 2, 3]; + let expected_hash = sp_runtime::traits::BlakeTwo256::hash(&proposal); + + let input = PCall::submit_after { + track_id: 0u16, + proposal: proposal.into(), + block_number: 0u32, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + assert!(events().contains( + &EvmEvent::Log { + log: log2( + Precompile1, + SELECTOR_LOG_SUBMITTED_AFTER, + H256::from_low_u64_be(0u64), + EvmDataWriter::new() + .write::(0u32) + .write::(expected_hash.into()) + .build(), + ), + } + .into() + )); + }); +} + +#[test] +fn place_and_refund_decision_deposit_logs_work() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 100_000)]) + .build() + .execute_with(|| { + let proposal = vec![1, 2, 3]; + let referendum_index = 0u32; + + // Create referendum + let input = PCall::submit_at { + track_id: 0u16, + proposal: proposal.into(), + block_number: 0u32, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Place referendum decision deposit + let input = PCall::place_decision_deposit { + index: referendum_index, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert all place events are emitted + assert!(vec![ + RuntimeEvent::Referenda(pallet_referenda::pallet::Event::DecisionDepositPlaced { + index: referendum_index, + who: Alice.into(), + amount: 10 + }), + EvmEvent::Log { + log: log1( + Precompile1, + SELECTOR_LOG_DECISION_DEPOSIT_PLACED, + EvmDataWriter::new().write::(referendum_index).build(), + ) + } + .into() + ] + .iter() + .all(|log| events().contains(log))); + + // Cancel referendum so we can refund + assert_ok!(RuntimeCall::Referenda(ReferendaCall::cancel { + index: referendum_index, + }) + .dispatch(RuntimeOrigin::signed(Alice.into()))); + + // Refund referendum decision deposit + let input = PCall::refund_decision_deposit { + index: referendum_index, + } + .into(); + assert_ok!(RuntimeCall::Evm(evm_call(input)).dispatch(RuntimeOrigin::root())); + + // Assert all refund events are emitted + assert!(vec![ + RuntimeEvent::Referenda(pallet_referenda::pallet::Event::DecisionDepositRefunded { + index: referendum_index, + who: Alice.into(), + amount: 10 + }), + EvmEvent::Log { + log: log1( + Precompile1, + SELECTOR_LOG_DECISION_DEPOSIT_REFUNDED, + EvmDataWriter::new().write::(referendum_index).build(), + ) + } + .into() + ] + .iter() + .all(|log| events().contains(log))); + }); +}