From 5654180b2e694efd3f7f5dc0a25707b02ffb8748 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Mon, 14 Oct 2024 14:54:48 +0200 Subject: [PATCH] Ethereum compatibility --- Cargo.lock | 9 +- Cargo.toml | 4 +- integration-tests/Cargo.toml | 2 + integration-tests/src/lib.rs | 1 + integration-tests/src/tests/defaults.rs | 5 +- integration-tests/src/tests/e2e.rs | 1 + .../src/tests/ethereum_support.rs | 29 +++++ integration-tests/src/tests/mod.rs | 1 + pallets/funding/Cargo.toml | 7 +- pallets/funding/src/benchmarking.rs | 2 + pallets/funding/src/functions/2_evaluation.rs | 7 + pallets/funding/src/functions/3_auction.rs | 20 ++- .../funding/src/functions/4_contribution.rs | 8 ++ pallets/funding/src/functions/6_settlement.rs | 12 +- pallets/funding/src/functions/misc.rs | 78 ++++++++++++ pallets/funding/src/functions/mod.rs | 2 +- .../src/instantiator/chain_interactions.rs | 15 ++- pallets/funding/src/instantiator/tests.rs | 2 + pallets/funding/src/lib.rs | 120 +++++++++++++++++- pallets/funding/src/mock.rs | 34 +++-- pallets/funding/src/runtime_api.rs | 16 ++- pallets/funding/src/tests/1_application.rs | 1 + pallets/funding/src/tests/2_evaluation.rs | 8 ++ pallets/funding/src/tests/3_auction.rs | 34 +++++ pallets/funding/src/tests/4_contribution.rs | 4 + pallets/funding/src/tests/misc.rs | 63 +++++++++ pallets/funding/src/tests/mod.rs | 4 +- pallets/funding/src/tests/runtime_api.rs | 20 ++- pallets/funding/src/types.rs | 32 ++++- runtimes/polimec/src/lib.rs | 15 ++- 30 files changed, 514 insertions(+), 42 deletions(-) create mode 100644 integration-tests/src/tests/ethereum_support.rs diff --git a/Cargo.lock b/Cargo.lock index 75ba85237..b3ab2d074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4592,6 +4592,8 @@ dependencies = [ "frame-metadata-hash-extension", "frame-support", "frame-system", + "hex", + "hex-literal", "itertools 0.11.0", "macros", "orml-oracle", @@ -4906,9 +4908,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -7274,7 +7276,10 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "hex", + "hex-literal", "itertools 0.11.0", + "k256", "log", "on-slash-vesting", "pallet-assets", diff --git a/Cargo.toml b/Cargo.toml index cb065e758..fc22a3638 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ scale-info = { version = "2.11.3", default-features = false, features = [ ] } jsonrpsee = { version = "0.22.5", features = ["server"] } hex-literal = "0.4.1" +hex = { version = "0.4.3", default-features = false, features = ["alloc"] } serde = { version = "1.0.204", default-features = false } serde_json = "1.0.120" smallvec = "1.13.2" @@ -103,6 +104,7 @@ array-bytes = { version = "6.2.3", default-features = false } serde-json-core = { version = '0.5.1', default-features = false } heapless = { version = "0.8", default-features = false } color-print = "0.3.6" +k256 = { version = "0.13.4", default-features = false, features = ["ecdsa"] } # Emulations xcm-emulator = { version = "0.12.0", default-features = false } @@ -125,7 +127,7 @@ sp-staking = { version = "33.0.0", default-features = false } # sp-runtime v38.0.1 takes out sp_std from runtime_string file and calls "alloc" directly which errors out sp-runtime = { version = "=38.0.0", default-features = false } sp-arithmetic = { version = "26.0.0", default-features = false } -sp-core = { version = "34.0.0", default-features = false } +sp-core = { version = "34.0.0", default-features = false, features = ["serde"] } sp-io = { version = "37.0.0", default-features = false } sp-blockchain = { version = "35.0.0", default-features = false } sp-consensus-aura = { version = "0.39.0", default-features = false } diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 258f285bb..7856bb96a 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -82,6 +82,8 @@ pallet-session.workspace = true pallet-proxy-bonding.workspace = true pallet-skip-feeless-payment.workspace = true xcm-fee-payment-runtime-api.workspace = true +hex-literal.workspace = true +hex.workspace = true # Runtimes polkadot-runtime.workspace = true diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index a81cc818e..55c825b80 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -21,6 +21,7 @@ mod tests; pub use constants::{accounts::*, asset_hub, penpal, polimec, polkadot}; pub use frame_support::{assert_noop, assert_ok, pallet_prelude::Weight, parameter_types, traits::Hooks}; +use macros::generate_accounts; pub use parachains_common::{AccountId, AssetHubPolkadotAuraId, AuraId, Balance, BlockNumber}; pub use sp_core::{sr25519, storage::Storage, Encode, Get}; pub use xcm::prelude::*; diff --git a/integration-tests/src/tests/defaults.rs b/integration-tests/src/tests/defaults.rs index 550e805f0..d58df5e42 100644 --- a/integration-tests/src/tests/defaults.rs +++ b/integration-tests/src/tests/defaults.rs @@ -17,8 +17,8 @@ use crate::PolimecRuntime; use frame_support::BoundedVec; pub use pallet_funding::instantiator::{BidParams, ContributionParams, UserToUSDBalance}; use pallet_funding::{ - AcceptedFundingAsset, BiddingTicketSizes, ContributingTicketSizes, CurrencyMetadata, ParticipationMode, - PriceProviderOf, ProjectMetadata, ProjectMetadataOf, TicketSize, + AcceptedFundingAsset, BiddingTicketSizes, ContributingTicketSizes, CurrencyMetadata, ParticipantsAccountType, + ParticipationMode, PriceProviderOf, ProjectMetadata, ProjectMetadataOf, TicketSize, }; use sp_arithmetic::{FixedPointNumber, Percent}; @@ -88,6 +88,7 @@ pub fn default_project_metadata(issuer: AccountId) -> ProjectMetadataOf Vec> { diff --git a/integration-tests/src/tests/e2e.rs b/integration-tests/src/tests/e2e.rs index ebf7a82ca..9b8d6111e 100644 --- a/integration-tests/src/tests/e2e.rs +++ b/integration-tests/src/tests/e2e.rs @@ -96,6 +96,7 @@ pub fn project_metadata() -> ProjectMetadataOf { .unwrap(), funding_destination_account: ISSUER.into(), policy_ipfs_cid: Some(metadata_hash), + participants_account_type: ParticipantsAccountType::Polkadot, } } diff --git a/integration-tests/src/tests/ethereum_support.rs b/integration-tests/src/tests/ethereum_support.rs new file mode 100644 index 000000000..c484a7003 --- /dev/null +++ b/integration-tests/src/tests/ethereum_support.rs @@ -0,0 +1,29 @@ +// use macros::generate_accounts; +// use crate::PolimecNet; +// use crate::tests::defaults::IntegrationInstantiator; +use crate::*; +use hex_literal::hex; +use sp_runtime::traits::Convert; + +generate_accounts!(ETH_BUYER); + +#[test] +fn test_hardcoded_signatures() { + let polimec_account: PolimecAccountId = ETH_BUYER.into(); + let project_id = 0; + + // Values generated with `https://github.com/lrazovic/ethsigner` + let polimec_account_ss58 = polimec_runtime::SS58Converter::convert(polimec_account.clone()); + dbg!(polimec_account_ss58); + let ethereum_account: [u8; 20] = hex!("FCAd0B19bB29D4674531d6f115237E16AfCE377c"); + let signature: [u8; 65] = hex!("4fa35369a2d654112d3fb419e24dc0d7d61b7e3f23936d6d4df0ac8608fa4530795971d4d1967da60853aa974ad57252a521f97bcd5a68ddea5f8959a5c60b471c"); + + PolimecNet::execute_with(|| { + assert_ok!(PolimecFunding::verify_receiving_account_signature( + &polimec_account, + project_id, + &Junction::AccountKey20 { network: Some(NetworkId::Ethereum { chain_id: 1 }), key: ethereum_account }, + signature, + )); + }); +} diff --git a/integration-tests/src/tests/mod.rs b/integration-tests/src/tests/mod.rs index c0e1a51e1..fc2e2050e 100644 --- a/integration-tests/src/tests/mod.rs +++ b/integration-tests/src/tests/mod.rs @@ -18,6 +18,7 @@ mod credentials; mod ct_migration; mod defaults; mod e2e; +mod ethereum_support; mod evaluator_slash_sideffects; mod governance; mod oracle; diff --git a/pallets/funding/Cargo.toml b/pallets/funding/Cargo.toml index 01cd1110e..97169261f 100644 --- a/pallets/funding/Cargo.toml +++ b/pallets/funding/Cargo.toml @@ -45,6 +45,9 @@ polkadot-parachain-primitives.workspace = true sp-api.workspace = true polimec-common-test-utils = { workspace = true, optional = true } frame-benchmarking = { workspace = true, optional = true } +hex-literal.workspace = true +k256.workspace = true +hex.workspace = true # Used in the instantiator. itertools.workspace = true @@ -59,7 +62,7 @@ xcm-builder.workspace = true xcm-executor.workspace = true [features] -default = [ "std" ] +default = [ "std", "sp-core/serde" ] std = [ "frame-benchmarking?/std", "frame-support/std", @@ -90,6 +93,8 @@ std = [ "xcm-builder/std", "xcm-executor/std", "xcm/std", + "k256/std", + "hex/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/pallets/funding/src/benchmarking.rs b/pallets/funding/src/benchmarking.rs index fb2c1f34a..38801cbaf 100644 --- a/pallets/funding/src/benchmarking.rs +++ b/pallets/funding/src/benchmarking.rs @@ -81,6 +81,7 @@ where participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), funding_destination_account: issuer, policy_ipfs_cid: Some(metadata_hash.into()), + participants_account_type: ParticipantsAccountType::Polkadot, } } @@ -410,6 +411,7 @@ mod benchmarks { participation_currencies: vec![AcceptedFundingAsset::USDT, AcceptedFundingAsset::USDC].try_into().unwrap(), funding_destination_account: issuer_funding.clone().clone(), policy_ipfs_cid: Some(BoundedVec::try_from(IPFS_CID.as_bytes().to_vec()).unwrap()), + participants_account_type: ParticipantsAccountType::Ethereum, }; let jwt = get_mock_jwt_with_cid( diff --git a/pallets/funding/src/functions/2_evaluation.rs b/pallets/funding/src/functions/2_evaluation.rs index d577ada0c..e6f02027c 100644 --- a/pallets/funding/src/functions/2_evaluation.rs +++ b/pallets/funding/src/functions/2_evaluation.rs @@ -73,6 +73,7 @@ impl Pallet { usd_amount: Balance, did: Did, whitelisted_policy: Cid, + receiving_account: Junction, ) -> DispatchResultWithPostInfo { // * Get variables * let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; @@ -88,6 +89,7 @@ impl Pallet { let total_evaluations_count = EvaluationCounts::::get(project_id); let user_evaluations_count = Evaluations::::iter_prefix((project_id, evaluator)).count() as u32; let project_policy = project_metadata.policy_ipfs_cid.ok_or(Error::::ImpossibleState)?; + let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; // * Validity Checks * ensure!(project_policy == whitelisted_policy, Error::::PolicyMismatch); @@ -100,6 +102,10 @@ impl Pallet { ); ensure!(total_evaluations_count < T::MaxEvaluationsPerProject::get(), Error::::TooManyProjectParticipations); ensure!(user_evaluations_count < T::MaxEvaluationsPerUser::get(), Error::::TooManyUserParticipations); + ensure!( + project_metadata.participants_account_type.junction_is_supported(&receiving_account), + Error::::UnsupportedReceiverAccountJunction + ); let plmc_bond = plmc_usd_price .reciprocal() @@ -129,6 +135,7 @@ impl Pallet { early_usd_amount, late_usd_amount, when: now, + receiving_account, }; T::NativeCurrency::hold(&HoldReason::Evaluation.into(), evaluator, plmc_bond)?; diff --git a/pallets/funding/src/functions/3_auction.rs b/pallets/funding/src/functions/3_auction.rs index dae7bea08..bf9ea1e2e 100644 --- a/pallets/funding/src/functions/3_auction.rs +++ b/pallets/funding/src/functions/3_auction.rs @@ -46,8 +46,17 @@ impl Pallet { #[transactional] pub fn do_bid(params: DoBidParams) -> DispatchResultWithPostInfo { // * Get variables * - let DoBidParams { bidder, project_id, ct_amount, mode, funding_asset, investor_type, did, whitelisted_policy } = - params; + let DoBidParams { + bidder, + project_id, + ct_amount, + mode, + funding_asset, + investor_type, + did, + whitelisted_policy, + receiving_account, + } = params; let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; let project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; @@ -110,6 +119,10 @@ impl Pallet { Error::::TooHigh ); ensure!(existing_bids.len() < T::MaxBidsPerUser::get() as usize, Error::::TooManyUserParticipations); + ensure!( + project_metadata.participants_account_type.junction_is_supported(&receiving_account), + Error::::UnsupportedReceiverAccountJunction + ); // While there's a remaining amount to bid for while !amount_to_bid.is_zero() { @@ -135,6 +148,7 @@ impl Pallet { metadata_ticket_size_bounds, total_bids_by_bidder: existing_bids_amount.saturating_add(perform_bid_calls), total_bids_for_project: total_bids_for_project.saturating_add(perform_bid_calls), + receiving_account, }; Self::do_perform_bid(perform_params)?; @@ -169,6 +183,7 @@ impl Pallet { metadata_ticket_size_bounds, total_bids_by_bidder, total_bids_for_project, + receiving_account, } = do_perform_bid_params; let ticket_size = ct_usd_price.checked_mul_int(ct_amount).ok_or(Error::::BadMath)?; @@ -200,6 +215,7 @@ impl Pallet { mode, plmc_bond, when: now, + receiving_account, }; Self::bond_plmc_with_mode(&bidder, project_id, plmc_bond, mode, funding_asset)?; diff --git a/pallets/funding/src/functions/4_contribution.rs b/pallets/funding/src/functions/4_contribution.rs index 5826fcf91..8b312ca46 100644 --- a/pallets/funding/src/functions/4_contribution.rs +++ b/pallets/funding/src/functions/4_contribution.rs @@ -13,6 +13,7 @@ impl Pallet { investor_type, did, whitelisted_policy, + receiving_account, } = params; let mut project_details = ProjectsDetails::::get(project_id).ok_or(Error::::ProjectDetailsNotFound)?; let did_has_winning_bid = DidWithWinningBids::::get(project_id, did.clone()); @@ -46,6 +47,7 @@ impl Pallet { investor_type, did, whitelisted_policy, + receiving_account, }; Self::do_perform_contribution(perform_params) @@ -63,6 +65,7 @@ impl Pallet { investor_type, did, whitelisted_policy, + receiving_account, } = params; let project_metadata = ProjectsMetadata::::get(project_id).ok_or(Error::::ProjectMetadataNotFound)?; @@ -107,6 +110,10 @@ impl Pallet { contributor_ticket_size.usd_ticket_below_maximum_per_did(total_usd_bought_by_did + ticket_size), Error::::TooHigh ); + ensure!( + project_metadata.participants_account_type.junction_is_supported(&receiving_account), + Error::::UnsupportedReceiverAccountJunction + ); let plmc_bond = Self::calculate_plmc_bond(ticket_size, multiplier)?; let funding_asset_amount = Self::calculate_funding_asset_amount(ticket_size, funding_asset)?; @@ -124,6 +131,7 @@ impl Pallet { funding_asset_amount, plmc_bond, when: now, + receiving_account, }; // Try adding the new contribution to the system diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index 32bacb598..9bb399bd9 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -17,10 +17,7 @@ use polimec_common::{ migration_types::{MigrationInfo, MigrationOrigin, MigrationStatus, ParticipationType}, ReleaseSchedule, }; -use sp_runtime::{ - traits::{Convert, Zero}, - Perquintill, -}; +use sp_runtime::{traits::Zero, Perquintill}; impl Pallet { #[transactional] @@ -140,6 +137,7 @@ impl Pallet { ParticipationType::Evaluation, ct_rewarded, duration, + evaluation.receiving_account, )?; } Evaluations::::remove((project_id, evaluation.evaluator.clone(), evaluation.id)); @@ -204,6 +202,7 @@ impl Pallet { ParticipationType::Bid, final_ct_amount, ct_vesting_duration, + bid.receiving_account, )?; Self::release_funding_asset( @@ -310,6 +309,7 @@ impl Pallet { ParticipationType::Contribution, contribution.ct_amount, ct_vesting_duration, + contribution.receiving_account, )?; final_ct_amount = contribution.ct_amount; @@ -484,10 +484,10 @@ impl Pallet { participation_type: ParticipationType, ct_amount: Balance, vesting_time: BlockNumberFor, + receiving_account: Junction, ) -> DispatchResult { UserMigrations::::try_mutate((project_id, origin), |maybe_migrations| -> DispatchResult { - let location_user = - Location::new(0, AccountId32 { network: None, id: T::AccountId32Conversion::convert(origin.clone()) }); + let location_user = Location::new(0, receiving_account); let migration_origin = MigrationOrigin { user: location_user, id, participation_type }; let vesting_time: u64 = vesting_time.try_into().map_err(|_| Error::::BadMath)?; let migration_info: MigrationInfo = (ct_amount, vesting_time).into(); diff --git a/pallets/funding/src/functions/misc.rs b/pallets/funding/src/functions/misc.rs index d9c1eac9f..772c07c84 100644 --- a/pallets/funding/src/functions/misc.rs +++ b/pallets/funding/src/functions/misc.rs @@ -1,6 +1,14 @@ #[allow(clippy::wildcard_imports)] use super::*; +use alloc::string::{String, ToString}; use polimec_common::ProvideAssetPrice; +use sp_core::{ + ecdsa::{Public as EcdsaPublic, Signature as EcdsaSignature}, + keccak_256, + sr25519::{Public as SrPublic, Signature as SrSignature}, + ByteArray, +}; +use sp_runtime::traits::Verify; // Helper functions // ATTENTION: if this is called directly, it will not be transactional @@ -413,6 +421,76 @@ impl Pallet { Ok(()) } + pub fn get_message_to_sign(polimec_account: AccountIdOf, project_id: ProjectId) -> Option { + let mut message = String::new(); + + let polimec_account_ss58_string = T::SS58Conversion::convert(polimec_account.clone()); + let project_id_string = project_id.to_string(); + let nonce_string = frame_system::Pallet::::account_nonce(polimec_account).to_string(); + + use alloc::fmt::Write; + write!( + &mut message, + "polimec account: {} - project id: {} - nonce: {}", + polimec_account_ss58_string, project_id_string, nonce_string + ) + .ok()?; + dbg!(&message); + Some(message) + } + + pub fn verify_receiving_account_signature( + polimec_account: &AccountIdOf, + project_id: ProjectId, + receiver_account: &Junction, + mut signature_bytes: [u8; 65], + ) -> DispatchResult { + let message_to_sign = Self::get_message_to_sign(polimec_account.clone(), project_id) + .ok_or(Error::::BadReceiverAccountSignature)?; + let message_bytes = message_to_sign.into_bytes(); + match receiver_account { + Junction::AccountId32 { network, id } => + if *network == None { + // If a user specifies an AccountId32, we assume they used the SR25519 crypto, so the signature is 64 bytes. + + let signature = SrSignature::from_slice(&signature_bytes[..64]) + .map_err(|_| Error::::BadReceiverAccountSignature)?; + let public = SrPublic::from_slice(id).map_err(|_| Error::::BadReceiverAccountSignature)?; + ensure!( + signature.verify(message_bytes.as_slice(), &public), + Error::::BadReceiverAccountSignature + ); + }, + Junction::AccountKey20 { network, key } if *network == Some(NetworkId::Ethereum { chain_id: 1 }) => { + let message_length = message_bytes.len().to_string().into_bytes(); + let message_prefix = b"\x19Ethereum Signed Message:\n".to_vec(); + let full_message = [&message_prefix[..], &message_length[..], &message_bytes[..]].concat(); + let hashed_message = keccak_256(full_message.as_slice()); + + match signature_bytes[64] { + 27 => signature_bytes[64] = 0x00, + 28 => signature_bytes[64] = 0x01, + _v => return Err(Error::::BadReceiverAccountSignature.into()), + } + + // If a user specifies an AccountKey20, we assume they used the ECDSA crypto (secp256k1), so the signature is 65 bytes. + let signature = EcdsaSignature::from_slice(&signature_bytes) + .map_err(|_| Error::::BadReceiverAccountSignature)?; + let public_compressed: EcdsaPublic = + signature.recover_prehashed(&hashed_message).ok_or(Error::::BadReceiverAccountSignature)?; + let public_uncompressed = k256::ecdsa::VerifyingKey::from_sec1_bytes(&public_compressed) + .map_err(|_| Error::::BadReceiverAccountSignature)?; + let public_uncompressed_point = public_uncompressed.to_encoded_point(false).to_bytes(); + let derived_ethereum_account: [u8; 20] = keccak_256(&public_uncompressed_point[1..])[12..32] + .try_into() + .map_err(|_| Error::::BadReceiverAccountSignature)?; + ensure!(*key == derived_ethereum_account, Error::::BadReceiverAccountSignature); + }, + _ => return Err(Error::::UnsupportedReceiverAccountJunction.into()), + }; + Ok(()) + } + pub fn get_decimals_aware_funding_asset_price(funding_asset: &AcceptedFundingAsset) -> Option> { let funding_asset_id = funding_asset.id(); let funding_asset_decimals = T::FundingCurrency::decimals(funding_asset_id); diff --git a/pallets/funding/src/functions/mod.rs b/pallets/funding/src/functions/mod.rs index c7b452d09..a7cd3d7fd 100644 --- a/pallets/funding/src/functions/mod.rs +++ b/pallets/funding/src/functions/mod.rs @@ -39,6 +39,6 @@ mod ct_migration; mod evaluation; #[path = "5_funding_end.rs"] mod funding_end; -mod misc; +pub mod misc; #[path = "6_settlement.rs"] mod settlement; diff --git a/pallets/funding/src/instantiator/chain_interactions.rs b/pallets/funding/src/instantiator/chain_interactions.rs index 421b4815f..32a7c3cee 100644 --- a/pallets/funding/src/instantiator/chain_interactions.rs +++ b/pallets/funding/src/instantiator/chain_interactions.rs @@ -441,8 +441,9 @@ impl< &account.clone(), project_id, usd_amount, - generate_did_from_account(account), + generate_did_from_account(account.clone()), project_policy.clone(), + Junction::AccountId32 { network: None, id: T::AccountId32Conversion::convert(account) }, ) })?; } @@ -456,7 +457,7 @@ impl< self.execute(|| { let did = generate_did_from_account(bid.bidder.clone()); let params = DoBidParams:: { - bidder: bid.bidder, + bidder: bid.bidder.clone(), project_id, ct_amount: bid.amount, mode: bid.mode, @@ -464,6 +465,10 @@ impl< did, investor_type: InvestorType::Institutional, whitelisted_policy: project_policy.clone(), + receiving_account: Junction::AccountId32 { + network: None, + id: T::AccountId32Conversion::convert(bid.bidder), + }, }; crate::Pallet::::do_bid(params) })?; @@ -485,7 +490,7 @@ impl< // We use institutional to be able to test most multipliers. let investor_type = InvestorType::Institutional; let params = DoContributeParams:: { - contributor: cont.contributor, + contributor: cont.contributor.clone(), project_id, ct_amount: cont.amount, mode: cont.mode, @@ -493,6 +498,10 @@ impl< did, investor_type, whitelisted_policy: project_policy.clone(), + receiving_account: Junction::AccountId32 { + network: None, + id: T::AccountId32Conversion::convert(cont.contributor), + }, }; self.execute(|| crate::Pallet::::do_contribute(params))?; }, diff --git a/pallets/funding/src/instantiator/tests.rs b/pallets/funding/src/instantiator/tests.rs index 415ddc197..2a00ca4f6 100644 --- a/pallets/funding/src/instantiator/tests.rs +++ b/pallets/funding/src/instantiator/tests.rs @@ -52,6 +52,7 @@ fn dry_run_wap() { participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), funding_destination_account: 0, policy_ipfs_cid: Some(metadata_hash), + participants_account_type: ParticipantsAccountType::Polkadot, }; // overfund with plmc @@ -132,6 +133,7 @@ fn find_bucket_for_wap() { participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), funding_destination_account: 0, policy_ipfs_cid: Some(metadata_hash), + participants_account_type: ParticipantsAccountType::Polkadot, }; // overfund with plmc diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index 630bc7a0d..4dd17f78b 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -1,4 +1,6 @@ -// Polimec Blockchain – https://www.polimec.org/ +#![feature(ascii_char)] +// Polimec Blockchain + // Copyright (C) Polimec 2022. All rights reserved. // The Polimec Blockchain is free software: you can redistribute it and/or modify @@ -70,6 +72,8 @@ #![cfg_attr(not(feature = "std"), no_std)] // Needed due to empty sections raising the warning #![allow(unreachable_patterns)] +// Needed for now beause receiving account extrinsics have too many arguments +#![allow(clippy::too_many_arguments)] // This recursion limit is needed because we have too many benchmarks and benchmarking will fail if // we add more without this limit. #![cfg_attr(feature = "runtime-benchmarks", recursion_limit = "512")] @@ -102,6 +106,7 @@ pub mod storage_migrations; pub mod traits; pub mod types; pub mod weights; +use alloc::string::String; #[cfg(test)] pub mod mock; @@ -176,7 +181,7 @@ pub mod pallet { #[pallet::config] pub trait Config: - frame_system::Config + frame_system::Config + pallet_balances::Config + pallet_xcm::Config + pallet_linear_release::Config> @@ -193,6 +198,9 @@ pub mod pallet { /// A way to convert from and to the account type used in CT migrations type AccountId32Conversion: ConvertBack; + /// A way to get the ss58 string representation of an account. Used for linking a polimec account to a receiving account. + type SS58Conversion: Convert; + /// Type used for testing and benchmarks #[cfg(any(test, feature = "runtime-benchmarks", feature = "std"))] type AllPalletsWithoutSystem: OnFinalize> @@ -568,6 +576,10 @@ pub mod pallet { ParticipationNotFound, /// The user investor type is not eligible for the action. WrongInvestorType, + /// Could not verify that the signature provided corresponds to the specified receiver account. + BadReceiverAccountSignature, + /// Used a Junction variant unsupported to represent a receving account. + UnsupportedReceiverAccountJunction, // * Project Error. Project information not found, or project has an incorrect state. * /// The project details were not found. Happens when the project with provided ID does @@ -749,7 +761,28 @@ pub mod pallet { let (account, did, _investor_type, whitelisted_policy) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; - Self::do_evaluate(&account, project_id, usd_amount, did, whitelisted_policy) + let receiving_account = + Junction::AccountId32 { network: None, id: T::AccountId32Conversion::convert(account.clone()) }; + + Self::do_evaluate(&account, project_id, usd_amount, did, whitelisted_policy, receiving_account) + } + + #[pallet::call_index(40)] + #[pallet::weight(WeightInfoOf::::evaluate(::MaxEvaluationsPerUser::get()))] + pub fn evaluate_with_receiving_account( + origin: OriginFor, + jwt: UntrustedToken, + project_id: ProjectId, + #[pallet::compact] usd_amount: Balance, + receiving_account: Junction, + signature_bytes: [u8; 65], + ) -> DispatchResultWithPostInfo { + let (account, did, _investor_type, whitelisted_policy) = + T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; + + Self::verify_receiving_account_signature(&account, project_id, &receiving_account, signature_bytes)?; + + Self::do_evaluate(&account, project_id, usd_amount, did, whitelisted_policy, receiving_account) } #[pallet::call_index(5)] @@ -779,6 +812,49 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let (bidder, did, investor_type, whitelisted_policy) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; + + let receiving_account = + Junction::AccountId32 { network: None, id: T::AccountId32Conversion::convert(bidder.clone()) }; + + let params = DoBidParams:: { + bidder, + project_id, + ct_amount, + mode, + funding_asset, + did, + investor_type, + whitelisted_policy, + receiving_account, + }; + + Self::do_bid(params) + } + + #[pallet::call_index(70)] + #[pallet::weight( + WeightInfoOf::::bid( + ::MaxBidsPerUser::get(), + // Assuming the current bucket is full, and has a price higher than the minimum. + // This user is buying 100% of the bid allocation. + // Since each bucket has 10% of the allocation, one bid can be split into a max of 10 + 10 + ))] + pub fn bid_with_receiving_account( + origin: OriginFor, + jwt: UntrustedToken, + project_id: ProjectId, + #[pallet::compact] ct_amount: Balance, + mode: ParticipationMode, + funding_asset: AcceptedFundingAsset, + receiving_account: Junction, + signature_bytes: [u8; 65], + ) -> DispatchResultWithPostInfo { + let (bidder, did, investor_type, whitelisted_policy) = + T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; + + Self::verify_receiving_account_signature(&bidder, project_id, &receiving_account, signature_bytes)?; + let params = DoBidParams:: { bidder, project_id, @@ -788,7 +864,9 @@ pub mod pallet { did, investor_type, whitelisted_policy, + receiving_account, }; + Self::do_bid(params) } @@ -825,6 +903,41 @@ pub mod pallet { ) -> DispatchResultWithPostInfo { let (contributor, did, investor_type, whitelisted_policy) = T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; + let receiving_account = + Junction::AccountId32 { network: None, id: T::AccountId32Conversion::convert(contributor.clone()) }; + let params = DoContributeParams:: { + contributor, + project_id, + ct_amount, + mode, + funding_asset, + did, + investor_type, + whitelisted_policy, + receiving_account, + }; + Self::do_contribute(params) + } + + #[pallet::call_index(90)] + #[pallet::weight( + WeightInfoOf::::contribute(T::MaxContributionsPerUser::get()) + )] + pub fn contribute_with_receiving_account( + origin: OriginFor, + jwt: UntrustedToken, + project_id: ProjectId, + #[pallet::compact] ct_amount: Balance, + mode: ParticipationMode, + funding_asset: AcceptedFundingAsset, + receiving_account: Junction, + signature_bytes: [u8; 65], + ) -> DispatchResultWithPostInfo { + let (contributor, did, investor_type, whitelisted_policy) = + T::InvestorOrigin::ensure_origin(origin, &jwt, T::VerifierPublicKey::get())?; + + Self::verify_receiving_account_signature(&contributor, project_id, &receiving_account, signature_bytes)?; + let params = DoContributeParams:: { contributor, project_id, @@ -834,6 +947,7 @@ pub mod pallet { did, investor_type, whitelisted_policy, + receiving_account, }; Self::do_contribute(params) } diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index fb54f3ee9..be66e64d1 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -23,6 +23,7 @@ use crate as pallet_funding; use crate::runtime_api::{ ExtrinsicHelpers, Leaderboards, ProjectInformation, ProjectParticipationIds, UserInformation, }; +use alloc::string::String; use core::ops::RangeInclusive; use frame_support::{ construct_runtime, derive_impl, @@ -37,9 +38,12 @@ use polimec_common::{credentials::EnsureInvestor, ProvideAssetPrice, USD_UNIT}; use polimec_common_test_utils::DummyXcmSender; use polkadot_parachain_primitives::primitives::Sibling; use sp_arithmetic::{Perbill, Percent}; -use sp_core::{ConstU8, H256}; +use sp_core::{ + crypto::{Ss58AddressFormat, Ss58Codec}, + ConstU8, H256, +}; use sp_runtime::{ - traits::{BlakeTwo256, ConvertBack, ConvertInto, Get, IdentityLookup, TryConvert}, + traits::{BlakeTwo256, Convert, ConvertBack, ConvertInto, Get, IdentityLookup, TryConvert}, BuildStorage, Perquintill, }; use sp_std::collections::btree_map::BTreeMap; @@ -245,17 +249,7 @@ impl system::Config for TestRuntime { type Hashing = BlakeTwo256; type Lookup = IdentityLookup; type MaxConsumers = frame_support::traits::ConstU32<16>; - type Nonce = u64; - type OnKilledAccount = (); - type OnNewAccount = (); - type OnSetCode = (); - type PalletInfo = PalletInfo; - type RuntimeCall = RuntimeCall; - type RuntimeEvent = RuntimeEvent; - type RuntimeOrigin = RuntimeOrigin; - type SS58Prefix = ConstU16<42>; - type SystemWeightInfo = (); - type Version = (); + type SS58Prefix = ConstU16<41>; } parameter_types! { @@ -380,6 +374,15 @@ impl ProvideAssetPrice for ConstPriceProvider { } } +pub struct SS58Converter; +impl Convert for SS58Converter { + fn convert(account: AccountId) -> String { + let account_bytes = DummyConverter::convert(account); + let account_id_32 = sp_runtime::AccountId32::new(account_bytes); + account_id_32.to_ss58check_with_version(Ss58AddressFormat::from(41u16)) + } +} + impl ConstPriceProvider { pub fn set_price(asset_id: AssetId, price: Price) { PRICE_MAP.with(|price_map| { @@ -425,6 +428,7 @@ impl Config for TestRuntime { type RuntimeEvent = RuntimeEvent; type RuntimeHoldReason = RuntimeHoldReason; type RuntimeOrigin = RuntimeOrigin; + type SS58Conversion = SS58Converter; #[cfg(feature = "runtime-benchmarks")] type SetPrices = (); type StringLimit = ConstU32<64>; @@ -591,6 +595,10 @@ sp_api::mock_impl_runtime_apis! { PolimecFunding::get_funding_asset_min_max_amounts(project_id, did, funding_asset, investor_type) } + fn get_message_to_sign_by_receiving_account(project_id: ProjectId, polimec_account: AccountId) -> Option { + PolimecFunding::get_message_to_sign_by_receiving_account(project_id, polimec_account) + } + } } diff --git a/pallets/funding/src/runtime_api.rs b/pallets/funding/src/runtime_api.rs index bfcc9752d..ffae00213 100644 --- a/pallets/funding/src/runtime_api.rs +++ b/pallets/funding/src/runtime_api.rs @@ -1,7 +1,7 @@ use crate::traits::BondingRequirementCalculation; #[allow(clippy::wildcard_imports)] use crate::*; -use alloc::collections::BTreeMap; +use alloc::{collections::BTreeMap, string::String}; use frame_support::traits::fungibles::{Inspect, InspectEnumerable}; use itertools::Itertools; use parity_scale_codec::{Decode, Encode}; @@ -9,7 +9,6 @@ use polimec_common::{credentials::InvestorType, ProvideAssetPrice, USD_DECIMALS} use scale_info::TypeInfo; use sp_core::Get; use sp_runtime::traits::Zero; - #[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] pub struct ProjectParticipationIds { account: AccountIdOf, @@ -55,7 +54,7 @@ sp_api::decl_runtime_apis! { fn projects_by_did(did: Did) -> Vec; } - #[api_version(3)] + #[api_version(4)] pub trait ExtrinsicHelpers { /// Get the current price of a contribution token (either current bucket in the auction, or WAP in contribution phase), /// and calculate the amount of tokens that can be bought with the given amount USDT/USDC/DOT. @@ -74,6 +73,10 @@ sp_api::decl_runtime_apis! { /// Gets the minimum and maximum amount of FundingAsset a user can input in the UI. fn get_funding_asset_min_max_amounts(project_id: ProjectId, did: Did, funding_asset: AcceptedFundingAsset, investor_type: InvestorType) -> Option<(Balance, Balance)>; + /// Gets the hex encoded bytes of the message needed to be signed by the receiving account to participate in the project. + /// The message will first be prefixed with a string depending on the blockchain, hashed, and then signed. + fn get_message_to_sign_by_receiving_account(project_id: ProjectId, polimec_account: AccountIdOf) -> Option; + } } @@ -350,6 +353,13 @@ impl Pallet { Some((funding_asset_min_ticket, funding_asset_max_ticket)) } + pub fn get_message_to_sign_by_receiving_account( + project_id: ProjectId, + polimec_account: AccountIdOf, + ) -> Option { + Pallet::::get_message_to_sign(polimec_account, project_id) + } + pub fn all_project_participations_by_did(project_id: ProjectId, did: Did) -> Vec> { let evaluations = Evaluations::::iter_prefix((project_id,)) .filter(|((_account_id, _evaluation_id), evaluation)| evaluation.did == did) diff --git a/pallets/funding/src/tests/1_application.rs b/pallets/funding/src/tests/1_application.rs index 73b36cbc7..ffdc19d7e 100644 --- a/pallets/funding/src/tests/1_application.rs +++ b/pallets/funding/src/tests/1_application.rs @@ -932,6 +932,7 @@ mod edit_project_extrinsic { funding_destination_account: ISSUER_2, policy_ipfs_cid: Some(new_policy_hash), + participants_account_type: ParticipantsAccountType::Polkadot, }; // No fields changed diff --git a/pallets/funding/src/tests/2_evaluation.rs b/pallets/funding/src/tests/2_evaluation.rs index 8895e4650..444962df4 100644 --- a/pallets/funding/src/tests/2_evaluation.rs +++ b/pallets/funding/src/tests/2_evaluation.rs @@ -586,6 +586,10 @@ mod evaluate_extrinsic { early_usd_amount: evaluation.usd_amount, late_usd_amount: 0, when: 1, + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(EVALUATOR_1), + }, }; assert_eq!(stored_evaluation, &expected_evaluation_item); }); @@ -871,6 +875,10 @@ mod evaluate_extrinsic { 500 * USD_UNIT, generate_did_from_account(ISSUER_1), project_metadata.clone().policy_ipfs_cid.unwrap(), + Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(ISSUER_1 + 1) + }, )), Error::::ParticipationToOwnProject ); diff --git a/pallets/funding/src/tests/3_auction.rs b/pallets/funding/src/tests/3_auction.rs index 62e4707b4..889e6cb7d 100644 --- a/pallets/funding/src/tests/3_auction.rs +++ b/pallets/funding/src/tests/3_auction.rs @@ -1,6 +1,7 @@ use super::*; use frame_support::traits::{fungible::InspectFreeze, fungibles::metadata::Inspect}; use sp_core::bounded_vec; +use sp_runtime::traits::Convert; use std::collections::HashSet; #[cfg(test)] @@ -324,6 +325,10 @@ mod round_flow { did, investor_type, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(BIDDER_1) + }, }), Error::::IncorrectRound ); @@ -1291,6 +1296,10 @@ mod bid_extrinsic { did, investor_type, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(BIDDER_2) + }, }), Error::::IncorrectRound ); @@ -1548,6 +1557,10 @@ mod bid_extrinsic { did: generate_did_from_account(BIDDER_1), investor_type: InvestorType::Professional, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(BIDDER_1) + }, }), Error::::TooLow ); @@ -1564,6 +1577,10 @@ mod bid_extrinsic { did: generate_did_from_account(BIDDER_1), investor_type: InvestorType::Institutional, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(BIDDER_2) + }, }), Error::::TooLow ); @@ -1631,6 +1648,10 @@ mod bid_extrinsic { did: generate_did_from_account(BIDDER_1), investor_type: InvestorType::Professional, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(BIDDER_2) + }, })); }); let smallest_ct_amount_at_20k_usd = bucket_increase_price @@ -1650,6 +1671,10 @@ mod bid_extrinsic { did: generate_did_from_account(BIDDER_1), investor_type: InvestorType::Institutional, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(BIDDER_3) + }, })); }); } @@ -1801,6 +1826,10 @@ mod bid_extrinsic { did: generate_did_from_account(ISSUER_1), investor_type: InvestorType::Professional, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(ISSUER_1) + }, })), Error::::ParticipationToOwnProject ); @@ -1832,6 +1861,10 @@ mod bid_extrinsic { did, investor_type, whitelisted_policy: project_metadata.clone().policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(bids[0].bidder), + }, }) }); frame_support::assert_err!(outcome, Error::::FundingAssetNotAccepted); @@ -1951,6 +1984,7 @@ mod end_auction_extrinsic { participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), funding_destination_account: ISSUER_1, policy_ipfs_cid: Some(metadata_hash), + participants_account_type: ParticipantsAccountType::Polkadot, }; // overfund with plmc diff --git a/pallets/funding/src/tests/4_contribution.rs b/pallets/funding/src/tests/4_contribution.rs index dafb7aec3..8c17ba416 100644 --- a/pallets/funding/src/tests/4_contribution.rs +++ b/pallets/funding/src/tests/4_contribution.rs @@ -1449,6 +1449,10 @@ mod contribute_extrinsic { did: generate_did_from_account(ISSUER_1), investor_type: InvestorType::Institutional, whitelisted_policy: project_metadata.policy_ipfs_cid.unwrap(), + receiving_account: Junction::AccountId32 { + network: None, + id: ::AccountId32Conversion::convert(ISSUER_1) + }, })), Error::::ParticipationToOwnProject ); diff --git a/pallets/funding/src/tests/misc.rs b/pallets/funding/src/tests/misc.rs index 3c3e5622f..3f29162b1 100644 --- a/pallets/funding/src/tests/misc.rs +++ b/pallets/funding/src/tests/misc.rs @@ -4,6 +4,7 @@ use super::*; mod helper_functions { use super::*; use polimec_common::USD_DECIMALS; + use sp_core::{ecdsa, hexdisplay::AsBytesRef, keccak_256, sr25519, Pair}; #[test] fn test_usd_price_decimal_aware() { @@ -189,6 +190,7 @@ mod helper_functions { participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), funding_destination_account: ISSUER_1, policy_ipfs_cid: Some(ipfs_hash()), + participants_account_type: ParticipantsAccountType::Polkadot, }; let project_id = inst.create_community_contributing_project( @@ -349,6 +351,67 @@ mod helper_functions { assert_close_enough!(expected, calculated, Perquintill::from_float(0.999)); } } + + #[test] + fn get_message_to_sign() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let account = 69u64; + let project_id = 4u32; + let message_to_sign = inst.execute(|| PolimecFunding::get_message_to_sign(account, project_id)).unwrap(); + + const EXPECTED_MESSAGE: &str = + "polimec account: 57qWuK1HShHMA5o1TX7Q6Xhino5iNwf9qgiSBdkQZMNYddKs - project id: 4 - nonce: 0"; + assert_eq!(&message_to_sign, EXPECTED_MESSAGE); + } + + #[test] + fn verify_receiving_account_signature() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let message_to_sign = inst.execute(|| PolimecFunding::get_message_to_sign(BUYER_1, 1)).unwrap(); + let message_to_sign = message_to_sign.into_bytes(); + + // Polkadot verification + let sr_pair = sr25519::Pair::from_seed_slice(&[69u8; 32]).unwrap(); + let signature = sr_pair.sign(&message_to_sign); + let mut signature_bytes = [0u8; 65]; + signature_bytes[..64].copy_from_slice(signature.as_bytes_ref()); + let junction = Junction::AccountId32 { network: None, id: sr_pair.public().to_raw() }; + assert_ok!(inst.execute(|| PolimecFunding::verify_receiving_account_signature( + &BUYER_1, + 1, + &junction, + signature_bytes + ))); + + // Ethereum verification + let ecdsa_pair = ecdsa::Pair::from_seed_slice(&[69u8; 32]).unwrap(); + let message_length = message_to_sign.len(); + let message_prefix = format!("\x19Ethereum Signed Message:\n{}", message_length).into_bytes(); + let expected_message = [&message_prefix[..], &message_to_sign[..]].concat(); + let signature = ecdsa_pair.sign_prehashed(&keccak_256(&expected_message)); + let mut signature_bytes = [0u8; 65]; + signature_bytes[..65].copy_from_slice(signature.as_bytes_ref()); + + match signature_bytes[64] { + 0x00 => signature_bytes[64] = 27, + 0x01 => signature_bytes[64] = 28, + _v => panic!("Invalid recovery byte"), + } + + let compressed_public_key = ecdsa_pair.public().to_raw(); + let public_uncompressed = k256::ecdsa::VerifyingKey::from_sec1_bytes(&compressed_public_key).unwrap(); + let public_uncompressed_point = public_uncompressed.to_encoded_point(false).to_bytes(); + let derived_ethereum_account: [u8; 20] = + keccak_256(&public_uncompressed_point[1..])[12..32].try_into().unwrap(); + let junction = + Junction::AccountKey20 { network: Some(Ethereum { chain_id: 1 }), key: derived_ethereum_account }; + assert_ok!(inst.execute(|| PolimecFunding::verify_receiving_account_signature( + &BUYER_1, + 1, + &junction, + signature_bytes + ))); + } } // logic of small functions that extrinsics use to process data or interact with storage diff --git a/pallets/funding/src/tests/mod.rs b/pallets/funding/src/tests/mod.rs index 8d23cfa2b..ee13c0ff3 100644 --- a/pallets/funding/src/tests/mod.rs +++ b/pallets/funding/src/tests/mod.rs @@ -15,7 +15,7 @@ use parachains_common::DAYS; use polimec_common::{ProvideAssetPrice, ReleaseSchedule, USD_DECIMALS, USD_UNIT}; use polimec_common_test_utils::{generate_did_from_account, get_mock_jwt_with_cid}; use sp_arithmetic::{traits::Zero, Percent, Perquintill}; -use sp_runtime::TokenError; +use sp_runtime::{traits::Convert, TokenError}; use sp_std::cell::RefCell; use std::iter::zip; use ParticipationMode::{Classic, OTM}; @@ -107,6 +107,7 @@ pub mod defaults { participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), funding_destination_account: issuer, policy_ipfs_cid: Some(metadata_hash), + participants_account_type: ParticipantsAccountType::Polkadot, } } @@ -142,6 +143,7 @@ pub mod defaults { participation_currencies: vec![AcceptedFundingAsset::USDT].try_into().unwrap(), funding_destination_account: ISSUER_1, policy_ipfs_cid: Some(metadata_hash), + participants_account_type: ParticipantsAccountType::Polkadot, } } diff --git a/pallets/funding/src/tests/runtime_api.rs b/pallets/funding/src/tests/runtime_api.rs index 135683f8b..a65d15972 100644 --- a/pallets/funding/src/tests/runtime_api.rs +++ b/pallets/funding/src/tests/runtime_api.rs @@ -2,7 +2,6 @@ use super::*; use crate::runtime_api::{ExtrinsicHelpers, Leaderboards, ProjectInformation, UserInformation}; use frame_support::traits::fungibles::{metadata::Inspect, Mutate}; use sp_runtime::bounded_vec; - #[test] fn top_evaluations() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); @@ -624,6 +623,25 @@ fn funding_asset_to_ct_amount_otm() { }); } +#[test] +fn get_message_to_sign_by_receiving_account() { + let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); + let _project_id_0 = inst.create_new_project(default_project_metadata(ISSUER_1), ISSUER_1, None); + let _project_id_1 = inst.create_new_project(default_project_metadata(ISSUER_2), ISSUER_2, None); + let project_id_2 = inst.create_new_project(default_project_metadata(ISSUER_3), ISSUER_3, None); + let block_hash = inst.execute(|| System::block_hash(System::block_number())); + let message = inst.execute(|| { + TestRuntime::get_message_to_sign_by_receiving_account(&TestRuntime, block_hash, project_id_2, BUYER_1) + .unwrap() + .unwrap() + }); + + const EXPECTED_MESSAGE: &str = + "polimec account: 57CoZYedYQwJMnCivQ7FnjCr4dfF912XuvgjUaKUzWSvvEF5 - project id: 2 - nonce: 0"; + + assert_eq!(&message, EXPECTED_MESSAGE); +} + #[test] fn get_next_vesting_schedule_merge_candidates() { let mut inst = MockInstantiator::new(Some(RefCell::new(new_test_ext()))); diff --git a/pallets/funding/src/types.rs b/pallets/funding/src/types.rs index ee3573bf7..55976cde2 100644 --- a/pallets/funding/src/types.rs +++ b/pallets/funding/src/types.rs @@ -155,6 +155,7 @@ pub mod storage { #[allow(clippy::wildcard_imports)] use super::*; use crate::Balance; + use xcm::v4::Junction; #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo, Serialize, Deserialize)] pub struct ProjectMetadata { @@ -180,6 +181,7 @@ pub mod storage { pub funding_destination_account: AccountId, /// Additional metadata pub policy_ipfs_cid: Option, + pub participants_account_type: ParticipantsAccountType, } impl ProjectMetadata { @@ -342,6 +344,7 @@ pub mod storage { pub early_usd_amount: Balance, pub late_usd_amount: Balance, pub when: BlockNumber, + pub receiving_account: Junction, } #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] @@ -359,6 +362,7 @@ pub mod storage { pub mode: ParticipationMode, pub plmc_bond: Balance, pub when: BlockNumber, + pub receiving_account: Junction, } impl BidInfo @@ -404,6 +408,7 @@ pub mod storage { pub funding_asset_amount: Balance, pub plmc_bond: Balance, pub when: BlockNumber, + pub receiving_account: Junction, } /// Represents a bucket that holds a specific amount of tokens at a given price. @@ -484,7 +489,7 @@ pub mod inner { use super::*; use crate::Balance; use variant_count::VariantCount; - use xcm::v4::QueryId; + use xcm::v4::{Junction, NetworkId, QueryId}; pub enum MetadataError { /// The minimum price per token is too low. @@ -819,6 +824,26 @@ pub mod inner { } } } + + #[derive( + Clone, Copy, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen, Serialize, Deserialize, + )] + pub enum ParticipantsAccountType { + Polkadot, + Ethereum, + } + impl ParticipantsAccountType { + pub fn junction_is_supported(&self, junction: &Junction) -> bool { + match self { + // This project expects users to submit a 32 byte account, and sign it with SR25519 crypto + ParticipantsAccountType::Polkadot => + matches!(junction, Junction::AccountId32 { network, .. } if network.is_none()), + // This project expects users to submit a 20 byte account, and sign it with ECDSA secp256k1 crypto + ParticipantsAccountType::Ethereum => + matches!(junction, Junction::AccountKey20 { network, .. } if network == &Some(NetworkId::Ethereum {chain_id: 1})), + } + } + } } pub mod extrinsic { @@ -828,6 +853,7 @@ pub mod extrinsic { }; use frame_system::pallet_prelude::BlockNumberFor; use polimec_common::credentials::{Cid, Did, InvestorType}; + use xcm::v4::Junction; pub struct DoBidParams { pub bidder: AccountIdOf, @@ -838,6 +864,7 @@ pub mod extrinsic { pub did: Did, pub investor_type: InvestorType, pub whitelisted_policy: Cid, + pub receiving_account: Junction, } pub struct DoPerformBidParams { @@ -853,6 +880,7 @@ pub mod extrinsic { pub metadata_ticket_size_bounds: TicketSize, pub total_bids_by_bidder: u32, pub total_bids_for_project: u32, + pub receiving_account: Junction, } pub struct DoContributeParams { @@ -864,6 +892,7 @@ pub mod extrinsic { pub did: Did, pub investor_type: InvestorType, pub whitelisted_policy: Cid, + pub receiving_account: Junction, } pub struct DoPerformContributionParams<'a, T: Config> { @@ -876,6 +905,7 @@ pub mod extrinsic { pub investor_type: InvestorType, pub did: Did, pub whitelisted_policy: Cid, + pub receiving_account: Junction, } pub struct BidRefund { diff --git a/runtimes/polimec/src/lib.rs b/runtimes/polimec/src/lib.rs index 5ec98261f..b169358d8 100644 --- a/runtimes/polimec/src/lib.rs +++ b/runtimes/polimec/src/lib.rs @@ -20,7 +20,7 @@ // Make the WASM binary available. #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); - +extern crate alloc; use assets_common::fungible_conversion::{convert, convert_balance}; use core::ops::RangeInclusive; use cumulus_pallet_parachain_system::RelayNumberMonotonicallyIncreases; @@ -101,10 +101,11 @@ pub use sp_runtime::{MultiAddress, Perbill, Permill}; #[cfg(feature = "std")] use sp_version::NativeVersion; +use alloc::string::String; +use sp_core::crypto::Ss58Codec; #[cfg(any(feature = "std", test))] pub use sp_runtime::BuildStorage; use xcm::VersionedAssetId; - #[cfg(feature = "runtime-benchmarks")] mod benchmark_helpers; mod custom_migrations; @@ -1044,6 +1045,12 @@ impl ConvertBack for ConvertSelf { bytes.into() } } +pub struct SS58Converter; +impl Convert for SS58Converter { + fn convert(account: AccountId) -> String { + account.to_ss58check_with_version(SS58Prefix::get().into()) + } +} impl pallet_funding::Config for Runtime { type AccountId32Conversion = ConvertSelf; @@ -1084,6 +1091,7 @@ impl pallet_funding::Config for Runtime { type RuntimeEvent = RuntimeEvent; type RuntimeHoldReason = RuntimeHoldReason; type RuntimeOrigin = RuntimeOrigin; + type SS58Conversion = SS58Converter; #[cfg(feature = "runtime-benchmarks")] type SetPrices = benchmark_helpers::SetOraclePrices; type StringLimit = ConstU32<64>; @@ -1549,6 +1557,9 @@ impl_runtime_apis! { fn get_funding_asset_min_max_amounts(project_id: ProjectId, did: Did, funding_asset: AcceptedFundingAsset, investor_type: InvestorType) -> Option<(Balance, Balance)> { Funding::get_funding_asset_min_max_amounts(project_id, did, funding_asset, investor_type) } + fn get_message_to_sign_by_receiving_account(project_id: ProjectId, polimec_account: AccountId) -> Option { + Funding::get_message_to_sign_by_receiving_account(project_id, polimec_account) + } } #[cfg(feature = "try-runtime")]