use alloc::string::String; use core::marker::PhantomData; use fp_evm::{ExitError, PrecompileFailure}; use frame_support::dispatch::{DispatchInfo, GetDispatchInfo, PostDispatchInfo}; use frame_support::traits::IsSubType; use frame_system::RawOrigin; use pallet_evm::AddressMapping; use pallet_evm::PrecompileHandle; use pallet_subtensor_proxy as pallet_proxy; use precompile_utils::prelude::Address; use precompile_utils::{EvmResult, solidity::Codec}; use sp_core::{ByteArray, H256}; use sp_runtime::traits::{AsSystemOriginSigner, Dispatchable, UniqueSaturatedInto}; use crate::{PrecompileExt, PrecompileHandleExt}; pub struct CrowdloanPrecompile(PhantomData); impl PrecompileExt for CrowdloanPrecompile where R: frame_system::Config + pallet_balances::Config + pallet_crowdloan::Config + pallet_evm::Config + pallet_proxy::Config + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, R::AccountId: From<[u8; 32]> + ByteArray, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + GetDispatchInfo + Dispatchable + IsSubType> + IsSubType> + IsSubType> + IsSubType>, ::AddressMapping: AddressMapping, { const INDEX: u64 = 2057; } #[precompile_utils::precompile] impl CrowdloanPrecompile where R: frame_system::Config + pallet_balances::Config + pallet_crowdloan::Config + pallet_evm::Config + pallet_proxy::Config + pallet_subtensor::Config + pallet_shield::Config + pallet_subtensor_proxy::Config + Send + Sync + scale_info::TypeInfo, R::AccountId: From<[u8; 32]> + ByteArray, ::RuntimeOrigin: AsSystemOriginSigner + Clone, ::RuntimeCall: From> + GetDispatchInfo + Dispatchable + IsSubType> + IsSubType> + IsSubType> + IsSubType>, ::RuntimeCall: From> + GetDispatchInfo + Dispatchable, ::AddressMapping: AddressMapping, { #[precompile::public("getCrowdloan(uint32)")] #[precompile::view] fn get_crowdloan( handle: &mut impl PrecompileHandle, crowdloan_id: u32, ) -> EvmResult { handle.record_db_reads::(1)?; let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id).ok_or( PrecompileFailure::Error { exit_status: ExitError::Other("Crowdloan not found".into()), }, )?; Ok(CrowdloanInfo { creator: H256::from_slice(crowdloan.creator.as_slice()), deposit: u64::from(crowdloan.deposit), min_contribution: u64::from(crowdloan.min_contribution), end: crowdloan.end.unique_saturated_into(), cap: u64::from(crowdloan.cap), funds_account: H256::from_slice(crowdloan.funds_account.as_slice()), raised: u64::from(crowdloan.raised), has_target_address: crowdloan.target_address.is_some(), target_address: crowdloan .target_address .map(|a| H256::from_slice(a.as_slice())) .unwrap_or_else(H256::zero), finalized: crowdloan.finalized, contributors_count: crowdloan.contributors_count, }) } #[precompile::public("getContribution(uint32,bytes32)")] #[precompile::view] fn get_contribution( handle: &mut impl PrecompileHandle, crowdloan_id: u32, coldkey: H256, ) -> EvmResult { handle.record_db_reads::(1)?; let coldkey = R::AccountId::from(coldkey.0); let contribution = pallet_crowdloan::Contributions::::get(crowdloan_id, coldkey).ok_or( PrecompileFailure::Error { exit_status: ExitError::Other("Crowdloan or contribution not found".into()), }, )?; Ok(u64::from(contribution)) } #[precompile::public("create(uint64,uint64,uint64,uint32,address)")] #[precompile::payable] fn create( handle: &mut impl PrecompileHandle, deposit: u64, min_contribution: u64, cap: u64, end: u32, target_address: Address, ) -> EvmResult<()> { let who = handle.caller_account_id::(); let target_address = R::AddressMapping::into_account_id(target_address.0); let call = pallet_crowdloan::Call::::create { deposit: deposit.into(), min_contribution: min_contribution.into(), cap: cap.into(), end: end.into(), call: None, target_address: Some(target_address), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(who)) } #[precompile::public("contribute(uint32,uint64)")] #[precompile::payable] fn contribute( handle: &mut impl PrecompileHandle, crowdloan_id: u32, amount: u64, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::contribute { crowdloan_id, amount: amount.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("withdraw(uint32)")] #[precompile::payable] fn withdraw(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::withdraw { crowdloan_id }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("finalize(uint32)")] #[precompile::payable] fn finalize(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::finalize { crowdloan_id }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("refund(uint32)")] #[precompile::payable] fn refund(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::refund { crowdloan_id }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("dissolve(uint32)")] #[precompile::payable] fn dissolve(handle: &mut impl PrecompileHandle, crowdloan_id: u32) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::dissolve { crowdloan_id }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("updateMinContribution(uint32,uint64)")] #[precompile::payable] fn update_min_contribution( handle: &mut impl PrecompileHandle, crowdloan_id: u32, new_min_contribution: u64, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::update_min_contribution { crowdloan_id, new_min_contribution: new_min_contribution.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("updateEnd(uint32,uint32)")] #[precompile::payable] fn update_end( handle: &mut impl PrecompileHandle, crowdloan_id: u32, new_end: u32, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::update_end { crowdloan_id, new_end: new_end.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } #[precompile::public("updateCap(uint32,uint64)")] #[precompile::payable] fn update_cap( handle: &mut impl PrecompileHandle, crowdloan_id: u32, new_cap: u64, ) -> EvmResult<()> { let account_id = handle.caller_account_id::(); let call = pallet_crowdloan::Call::::update_cap { crowdloan_id, new_cap: new_cap.into(), }; handle.try_dispatch_runtime_call::(call, RawOrigin::Signed(account_id)) } } #[derive(Codec)] struct CrowdloanInfo { creator: H256, deposit: u64, min_contribution: u64, end: u32, cap: u64, funds_account: H256, raised: u64, has_target_address: bool, target_address: H256, finalized: bool, contributors_count: u32, } #[cfg(test)] mod tests { #![allow(clippy::expect_used, clippy::arithmetic_side_effects)] use super::*; use crate::PrecompileExt; use crate::mock::{ AccountId, Runtime, RuntimeOrigin, System, addr_from_index, fund_account, mapped_account, new_test_ext, precompiles, selector_u32, }; use precompile_utils::solidity::{codec::Address, encode_return_value, encode_with_selector}; use precompile_utils::testing::PrecompileTesterExt; use sp_core::H160; const CREATOR_DEPOSIT: u64 = 50; const MIN_CONTRIBUTION: u64 = 10; const CAP: u64 = 300; const END: u32 = 50; const ACCOUNT_BALANCE: u64 = 1_000; fn get_crowdloan(caller: H160, crowdloan_id: u32, expected: CrowdloanInfo) { let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles::>() .prepare_test( caller, precompile_addr, encode_with_selector(selector_u32("getCrowdloan(uint32)"), (crowdloan_id,)), ) .with_static_call(true) .execute_returns_raw(encode_return_value(expected)); } fn expected_crowdloan_info(crowdloan_id: u32) -> CrowdloanInfo { let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); CrowdloanInfo { creator: H256::from_slice(crowdloan.creator.as_slice()), deposit: u64::from(crowdloan.deposit), min_contribution: u64::from(crowdloan.min_contribution), end: crowdloan.end as u32, cap: u64::from(crowdloan.cap), funds_account: H256::from_slice(crowdloan.funds_account.as_slice()), raised: u64::from(crowdloan.raised), has_target_address: crowdloan.target_address.is_some(), target_address: crowdloan .target_address .map(|account| H256::from_slice(account.as_slice())) .unwrap_or_else(H256::zero), finalized: crowdloan.finalized, contributors_count: crowdloan.contributors_count, } } fn create_crowdloan(caller: H160, target: H160) -> u32 { let crowdloan_id = pallet_crowdloan::NextCrowdloanId::::get(); let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles::>() .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("create(uint64,uint64,uint64,uint32,address)"), (CREATOR_DEPOSIT, MIN_CONTRIBUTION, CAP, END, Address(target)), ), ) .execute_returns(()); crowdloan_id } fn contribute(caller: H160, crowdloan_id: u32, amount: u64) { let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles::>() .prepare_test( caller, precompile_addr, encode_with_selector( selector_u32("contribute(uint32,uint64)"), (crowdloan_id, amount), ), ) .execute_returns(()); } fn withdraw(caller: H160, crowdloan_id: u32) { let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles::>() .prepare_test( caller, precompile_addr, encode_with_selector(selector_u32("withdraw(uint32)"), (crowdloan_id,)), ) .execute_returns(()); } fn finalize(caller: H160, crowdloan_id: u32) { let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles::>() .prepare_test( caller, precompile_addr, encode_with_selector(selector_u32("finalize(uint32)"), (crowdloan_id,)), ) .execute_returns(()); } #[test] fn crowdloan_precompile_reads_existing_pallet_crowdloan() { new_test_ext().execute_with(|| { let creator = AccountId::from([0x11; 32]); let target = AccountId::from([0x12; 32]); let caller = addr_from_index(0x7001); let crowdloan_id = pallet_crowdloan::NextCrowdloanId::::get(); fund_account(&creator, ACCOUNT_BALANCE); pallet_crowdloan::Pallet::::create( RuntimeOrigin::signed(creator), CREATOR_DEPOSIT.into(), MIN_CONTRIBUTION.into(), CAP.into(), END.into(), None, Some(target), ) .expect("direct crowdloan create should work"); get_crowdloan(caller, crowdloan_id, expected_crowdloan_info(crowdloan_id)); }); } #[test] fn crowdloan_precompile_creates_and_reads_crowdloan() { new_test_ext().execute_with(|| { let creator = addr_from_index(0x7002); let target = addr_from_index(0x7003); let creator_account = mapped_account(creator); let target_account = mapped_account(target); fund_account(&creator_account, ACCOUNT_BALANCE); let crowdloan_id = create_crowdloan(creator, target); let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); assert_eq!(crowdloan.creator, creator_account); assert_eq!(u64::from(crowdloan.deposit), CREATOR_DEPOSIT); assert_eq!(u64::from(crowdloan.min_contribution), MIN_CONTRIBUTION); assert_eq!(u64::from(crowdloan.cap), CAP); assert_eq!(crowdloan.end, END as u64); assert_eq!(u64::from(crowdloan.raised), CREATOR_DEPOSIT); assert_eq!(crowdloan.target_address, Some(target_account)); assert!(!crowdloan.finalized); assert_eq!(crowdloan.contributors_count, 1); get_crowdloan(creator, crowdloan_id, expected_crowdloan_info(crowdloan_id)); }); } #[test] fn crowdloan_precompile_contributes_and_withdraws() { new_test_ext().execute_with(|| { let creator = addr_from_index(0x7004); let contributor = addr_from_index(0x7005); let target = addr_from_index(0x7006); let creator_account = mapped_account(creator); let contributor_account = mapped_account(contributor); let contribution = 30_u64; fund_account(&creator_account, ACCOUNT_BALANCE); fund_account(&contributor_account, ACCOUNT_BALANCE); let crowdloan_id = create_crowdloan(creator, target); contribute(contributor, crowdloan_id, contribution); let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); assert_eq!(u64::from(crowdloan.raised), CREATOR_DEPOSIT + contribution); assert_eq!(crowdloan.contributors_count, 2); assert_eq!( pallet_crowdloan::Contributions::::get( crowdloan_id, &contributor_account, ), Some(contribution.into()), ); get_crowdloan(creator, crowdloan_id, expected_crowdloan_info(crowdloan_id)); withdraw(contributor, crowdloan_id); let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); assert_eq!(u64::from(crowdloan.raised), CREATOR_DEPOSIT); assert_eq!(crowdloan.contributors_count, 1); assert_eq!( pallet_crowdloan::Contributions::::get( crowdloan_id, &contributor_account, ), None, ); get_crowdloan(creator, crowdloan_id, expected_crowdloan_info(crowdloan_id)); }); } #[test] fn crowdloan_precompile_contributes_and_withdraws_from_pallet_crowdloan() { new_test_ext().execute_with(|| { let creator = AccountId::from([0x22; 32]); let target = AccountId::from([0x23; 32]); let contributor = addr_from_index(0x7016); let contributor_account = mapped_account(contributor); let crowdloan_id = pallet_crowdloan::NextCrowdloanId::::get(); let contribution = 30_u64; fund_account(&creator, ACCOUNT_BALANCE); fund_account(&contributor_account, ACCOUNT_BALANCE); pallet_crowdloan::Pallet::::create( RuntimeOrigin::signed(creator), CREATOR_DEPOSIT.into(), MIN_CONTRIBUTION.into(), CAP.into(), END.into(), None, Some(target), ) .expect("direct crowdloan create should work"); contribute(contributor, crowdloan_id, contribution); let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); assert_eq!(u64::from(crowdloan.raised), CREATOR_DEPOSIT + contribution); assert_eq!(crowdloan.contributors_count, 2); get_crowdloan(contributor, crowdloan_id, expected_crowdloan_info(crowdloan_id)); withdraw(contributor, crowdloan_id); let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); assert_eq!(u64::from(crowdloan.raised), CREATOR_DEPOSIT); assert_eq!(crowdloan.contributors_count, 1); assert_eq!( pallet_crowdloan::Contributions::::get( crowdloan_id, &contributor_account, ), None, ); get_crowdloan(contributor, crowdloan_id, expected_crowdloan_info(crowdloan_id)); }); } #[test] fn crowdloan_precompile_finalizes_capped_crowdloan() { new_test_ext().execute_with(|| { let creator = addr_from_index(0x7007); let contributor = addr_from_index(0x7008); let target = addr_from_index(0x7009); let creator_account = mapped_account(creator); let contributor_account = mapped_account(contributor); let target_account = mapped_account(target); fund_account(&creator_account, ACCOUNT_BALANCE); fund_account(&contributor_account, ACCOUNT_BALANCE); let crowdloan_id = create_crowdloan(creator, target); contribute(contributor, crowdloan_id, CAP - CREATOR_DEPOSIT); System::set_block_number(END.into()); finalize(creator, crowdloan_id); let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); assert!(crowdloan.finalized); assert_eq!( pallet_balances::Pallet::::free_balance(&target_account), CAP.into(), ); get_crowdloan(creator, crowdloan_id, expected_crowdloan_info(crowdloan_id)); }); } #[test] fn crowdloan_precompile_refunds_and_dissolves_crowdloan() { new_test_ext().execute_with(|| { let creator = addr_from_index(0x7010); let first = addr_from_index(0x7011); let second = addr_from_index(0x7012); let target = addr_from_index(0x7013); let creator_account = mapped_account(creator); let first_account = mapped_account(first); let second_account = mapped_account(second); let contribution = 30_u64; fund_account(&creator_account, ACCOUNT_BALANCE); fund_account(&first_account, ACCOUNT_BALANCE); fund_account(&second_account, ACCOUNT_BALANCE); let crowdloan_id = create_crowdloan(creator, target); contribute(first, crowdloan_id, contribution); contribute(second, crowdloan_id, contribution); System::set_block_number(END.into()); let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles::>() .prepare_test( creator, precompile_addr, encode_with_selector(selector_u32("refund(uint32)"), (crowdloan_id,)), ) .execute_returns(()); let crowdloan = pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist"); assert_eq!(u64::from(crowdloan.raised), CREATOR_DEPOSIT); assert_eq!(crowdloan.contributors_count, 1); get_crowdloan(creator, crowdloan_id, expected_crowdloan_info(crowdloan_id)); precompiles::>() .prepare_test( creator, precompile_addr, encode_with_selector(selector_u32("dissolve(uint32)"), (crowdloan_id,)), ) .execute_returns(()); assert!(pallet_crowdloan::Crowdloans::::get(crowdloan_id).is_none()); }); } #[test] fn crowdloan_precompile_updates_crowdloan_terms() { new_test_ext().execute_with(|| { let creator = addr_from_index(0x7014); let target = addr_from_index(0x7015); let creator_account = mapped_account(creator); let new_min_contribution = 20_u64; let new_end = 80_u32; let new_cap = 400_u64; fund_account(&creator_account, ACCOUNT_BALANCE); let crowdloan_id = create_crowdloan(creator, target); let precompiles = precompiles::>(); let precompile_addr = addr_from_index(CrowdloanPrecompile::::INDEX); precompiles .prepare_test( creator, precompile_addr, encode_with_selector( selector_u32("updateMinContribution(uint32,uint64)"), (crowdloan_id, new_min_contribution), ), ) .execute_returns(()); assert_eq!( u64::from( pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist") .min_contribution, ), new_min_contribution, ); precompiles .prepare_test( creator, precompile_addr, encode_with_selector( selector_u32("updateEnd(uint32,uint32)"), (crowdloan_id, new_end), ), ) .execute_returns(()); assert_eq!( pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist") .end, new_end as u64, ); precompiles .prepare_test( creator, precompile_addr, encode_with_selector( selector_u32("updateCap(uint32,uint64)"), (crowdloan_id, new_cap), ), ) .execute_returns(()); assert_eq!( u64::from( pallet_crowdloan::Crowdloans::::get(crowdloan_id) .expect("crowdloan should exist") .cap, ), new_cap, ); get_crowdloan(creator, crowdloan_id, expected_crowdloan_info(crowdloan_id)); }); } }