diff --git a/Cargo.lock b/Cargo.lock index 75ba85237..e0b581b6c 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", @@ -4916,6 +4918,7 @@ dependencies = [ "once_cell", "serdect", "sha2 0.10.8", + "signature 2.2.0", ] [[package]] @@ -7274,7 +7277,9 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "hex-literal", "itertools 0.11.0", + "k256", "log", "on-slash-vesting", "pallet-assets", diff --git a/Cargo.toml b/Cargo.toml index cb065e758..b0e62b29c 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 = "0.4.3" serde = { version = "1.0.204", default-features = false } serde_json = "1.0.120" smallvec = "1.13.2" 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..fd32aae6e --- /dev/null +++ b/integration-tests/src/tests/ethereum_support.rs @@ -0,0 +1,28 @@ +// use macros::generate_accounts; +// use crate::PolimecNet; +// use crate::tests::defaults::IntegrationInstantiator; +use crate::*; +use hex_literal::hex; +use sp_core::{crypto::Ss58Codec, ecdsa, ByteArray, Pair}; +use sp_io::hashing::keccak_256; + +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 ethereum_account: [u8; 20] = hex!("FCAd0B19bB29D4674531d6f115237E16AfCE377c"); + let signature: [u8; 65] = hex!("76889adb946708eb797edc559326c2053732b307874edb0a021fdab454db85e317169c709c905635223d135a150d54285c67f495ea6162bcbbf05d8d6c7d036b1b"); + + 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..a6df80686 100644 --- a/pallets/funding/Cargo.toml +++ b/pallets/funding/Cargo.toml @@ -45,6 +45,8 @@ 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 = "0.13.4" # Used in the instantiator. itertools.workspace = true @@ -90,6 +92,7 @@ std = [ "xcm-builder/std", "xcm-executor/std", "xcm/std", + "k256/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..b71d38039 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -18,7 +18,7 @@ use polimec_common::{ ReleaseSchedule, }; use sp_runtime::{ - traits::{Convert, Zero}, + traits::{Zero}, Perquintill, }; @@ -140,6 +140,7 @@ impl Pallet { ParticipationType::Evaluation, ct_rewarded, duration, + evaluation.receiving_account, )?; } Evaluations::::remove((project_id, evaluation.evaluator.clone(), evaluation.id)); @@ -204,6 +205,7 @@ impl Pallet { ParticipationType::Bid, final_ct_amount, ct_vesting_duration, + bid.receiving_account, )?; Self::release_funding_asset( @@ -310,6 +312,7 @@ impl Pallet { ParticipationType::Contribution, contribution.ct_amount, ct_vesting_duration, + contribution.receiving_account, )?; final_ct_amount = contribution.ct_amount; @@ -484,10 +487,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..f36bb1a4a 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::format; 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,71 @@ impl Pallet { Ok(()) } + pub fn get_message_to_sign(polimec_account: &AccountIdOf, project_id: ProjectId) -> Vec { + let mut message = Vec::new(); + let separator = b"-".to_vec(); + let nonce = frame_system::Pallet::::account_nonce(polimec_account); + let polimec_account_bytes: Vec = polimec_account.encode(); + let project_id_bytes: Vec = project_id.encode(); + let nonce_bytes: Vec = nonce.encode(); + message.extend(polimec_account_bytes); + message.extend(separator.clone()); + message.extend(project_id_bytes); + message.extend(separator); + message.extend(nonce_bytes); + 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, project_id); + 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_to_sign.as_slice(), &public), + Error::::BadReceiverAccountSignature + ); + }, + Junction::AccountKey20 { network, key } if *network == Some(NetworkId::Ethereum { chain_id: 1 }) => { + let message_length = message_to_sign.len(); + let message_prefix = format!("\x19Ethereum Signed Message:\n{}", message_length).into_bytes(); + let full_message = [&message_prefix[..], &message_to_sign[..]].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..0d0c9fa8f 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 @@ -568,6 +570,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 +755,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 +806,10 @@ 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, @@ -788,7 +819,48 @@ pub mod pallet { 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, + ct_amount, + mode, + funding_asset, + did, + investor_type, + whitelisted_policy, + receiving_account, + }; + Self::do_bid(params) } @@ -825,6 +897,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 +941,7 @@ pub mod pallet { did, investor_type, whitelisted_policy, + receiving_account, }; Self::do_contribute(params) } 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..c7e39478b 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,70 @@ 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)); + + // account is 8 bytes, the first being 69 base 10, or 45 base 16. + // project_id is 4 bytes, the first being 4 base 10, or 4 base 16. + // nonce is 8 bytes, the first being 0 base 10, or 0 base 16. + const EXPECTED_MESSAGE: &[u8] = + b"\x45\x00\x00\x00\x00\x00\x00\x00-\x04\x00\x00\x00-\x00\x00\x00\x00\x00\x00\x00\x00"; + assert_eq!(message_to_sign.as_slice(), 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)); + + // 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/types.rs b/pallets/funding/src/types.rs index ee3573bf7..d75a5798f 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 {