From 098e6934e18e98f3ec5d91508eba3142c7f38845 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Mon, 25 Nov 2024 10:27:46 +0100 Subject: [PATCH] Update runtime_api.rs --- pallets/funding/src/functions/misc.rs | 144 +++++++++++++++++++++----- pallets/funding/src/runtime_api.rs | 2 +- 2 files changed, 121 insertions(+), 25 deletions(-) diff --git a/pallets/funding/src/functions/misc.rs b/pallets/funding/src/functions/misc.rs index af0a76078..62eed008d 100644 --- a/pallets/funding/src/functions/misc.rs +++ b/pallets/funding/src/functions/misc.rs @@ -438,6 +438,47 @@ impl Pallet { Some(message) } + pub fn verify_ethereum_account( + mut signature_bytes: [u8; 65], + expected_ethereum_account: [u8; 20], + polimec_account: AccountIdOf, + project_id: ProjectId, + ) -> bool { + match signature_bytes[64] { + 27 => signature_bytes[64] = 0x00, + 28 => signature_bytes[64] = 0x01, + _v => return false, + } + + let hashed_message = typed_data_v4::get_eip_712_message( + &T::SS58Conversion::convert(polimec_account.clone()), + project_id, + frame_system::Pallet::::account_nonce(polimec_account), + ); + + let ecdsa_signature = EcdsaSignature::from_slice(&signature_bytes).unwrap(); + let public_compressed: EcdsaPublic = ecdsa_signature.recover_prehashed(&hashed_message).unwrap(); + let public_uncompressed = k256::ecdsa::VerifyingKey::from_sec1_bytes(&public_compressed).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(); + + derived_ethereum_account == expected_ethereum_account + } + + pub fn verify_substrate_account( + signature_bytes: [u8; 65], + expected_substrate_account: [u8; 32], + polimec_account: AccountIdOf, + project_id: ProjectId, + ) -> bool { + let message_to_sign = Self::get_substrate_message_to_sign(polimec_account.clone(), project_id).unwrap(); + let message_bytes = message_to_sign.into_bytes(); + let signature = SrSignature::from_slice(&signature_bytes[..64]).unwrap(); + let public = SrPublic::from_slice(&expected_substrate_account).unwrap(); + signature.verify(message_bytes.as_slice(), &public) + } + pub fn verify_receiving_account_signature( polimec_account: &AccountIdOf, project_id: ProjectId, @@ -456,30 +497,16 @@ impl Pallet { ); }, - 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); + Junction::AccountKey20 { key: expected_ethereum_account, .. } => { + ensure!( + Self::verify_ethereum_account( + signature_bytes, + *expected_ethereum_account, + polimec_account.clone(), + project_id, + ), + Error::::BadReceiverAccountSignature + ); }, _ => return Err(Error::::UnsupportedReceiverAccountJunction.into()), }; @@ -492,3 +519,72 @@ impl Pallet { >::get_decimals_aware_price(funding_asset_id, USD_DECIMALS, funding_asset_decimals) } } + +pub mod typed_data_v4 { + use super::*; + + /// Returns the first part needed for a typed data v4 message. It specifies the entity that requires the signature. + pub fn get_domain_separator(name: &str, version: &str, chain_id: u32, verifying_contract: &str) -> [u8; 32] { + let encoded_domain_type = + keccak_256(b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + let encoded_name = keccak_256(name.as_bytes()); + let encoded_version = keccak_256(version.as_bytes()); + + // TODO: Handle panics from copy_from_slice + // u32 should be converted to u256 in big endian notation + let chain_id_bytes: [u8; 4] = chain_id.to_be_bytes(); + let mut encoded_chain_id: [u8; 32] = [0u8; 32]; + encoded_chain_id[28..].copy_from_slice(&chain_id_bytes); + // Should be 32 bytes to comply with ABI encoding, so we pad with zeroes to the left + let mut encoded_contract: [u8; 32] = [0u8; 32]; + encoded_contract[12..].copy_from_slice(hex::decode(verifying_contract).unwrap().as_slice()); + + let mut data = Vec::new(); + data.extend_from_slice(&encoded_domain_type); + data.extend_from_slice(&encoded_name); + data.extend_from_slice(&encoded_version); + data.extend_from_slice(&encoded_chain_id); + data.extend_from_slice(&encoded_contract); + + keccak_256(&data) + } + + /// Returns the second part needed for a typed data v4 message. It specifies the message details with type information. + pub fn get_message(polimec_account: &str, project_id: u32, nonce: u32) -> [u8; 32] { + let encoded_message_type = + keccak_256(b"ParticipationAuthorization(string polimecAccount,uint32 projectId,uint32 nonce)"); + + let encoded_polimec_account_string = keccak_256(polimec_account.as_bytes()); + + let project_id_bytes: [u8; 4] = project_id.to_be_bytes(); + let mut encoded_project_id: [u8; 32] = [0u8; 32]; + encoded_project_id[28..].copy_from_slice(&project_id_bytes); + + let nonce_bytes: [u8; 4] = nonce.to_be_bytes(); + let mut encoded_nonce: [u8; 32] = [0u8; 32]; + encoded_nonce[28..].copy_from_slice(&nonce_bytes); + + let mut data = Vec::new(); + data.extend_from_slice(&encoded_message_type); + data.extend_from_slice(&encoded_polimec_account_string); + data.extend_from_slice(&encoded_project_id); + data.extend_from_slice(&encoded_nonce); + + let output = keccak_256(&data); + + output + } + + /// Returns the final message hash that will be signed by the user. + pub fn get_eip_712_message(polimec_account: &str, project_id: u32, nonce: u32) -> [u8; 32] { + let domain_separator = get_domain_separator("Polimec", "1", 1, "0000000000000000000000000000000000003344"); + let message = get_message(polimec_account, project_id, nonce); + + let mut data = Vec::new(); + data.extend_from_slice(b"\x19\x01"); + data.extend_from_slice(&domain_separator); + data.extend_from_slice(&message); + + keccak_256(&data) + } +} diff --git a/pallets/funding/src/runtime_api.rs b/pallets/funding/src/runtime_api.rs index aac7c2756..e99789261 100644 --- a/pallets/funding/src/runtime_api.rs +++ b/pallets/funding/src/runtime_api.rs @@ -74,7 +74,7 @@ sp_api::decl_runtime_apis! { 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. + /// The message will first be prefixed with a blockchain-dependent string, then hashed, and then signed. fn get_message_to_sign_by_receiving_account(project_id: ProjectId, polimec_account: AccountIdOf) -> Option; }