diff --git a/core/apps/agent/src/slack/dispatch.rs b/core/apps/agent/src/slack/dispatch.rs index a42859fac..d66a7dd22 100644 --- a/core/apps/agent/src/slack/dispatch.rs +++ b/core/apps/agent/src/slack/dispatch.rs @@ -262,10 +262,7 @@ async fn collect_image_attachments(state: &AppState, files: &[SlackFile]) -> Vec fn strip_mention(text: &str) -> String { let mut s = text.trim_start(); - loop { - let Some(rest) = s.strip_prefix("<@") else { - break; - }; + while let Some(rest) = s.strip_prefix("<@") { let Some(end) = rest.find('>') else { break; }; diff --git a/core/crates/gem_solana/src/signer/chain_signer.rs b/core/crates/gem_solana/src/signer/chain_signer.rs index 6163b7542..bafd200f7 100644 --- a/core/crates/gem_solana/src/signer/chain_signer.rs +++ b/core/crates/gem_solana/src/signer/chain_signer.rs @@ -1,5 +1,5 @@ use super::{instructions, swap, transaction}; -use crate::decode_transaction; +use crate::{decode_transaction, transaction::is_transaction_bytes}; use gem_encoding::encode_base64; use primitives::{ChainSigner, SignerError, SignerInput, TransferDataOutputType}; use solana_primitives::{Pubkey, sign_message as sign_solana_message}; @@ -7,6 +7,8 @@ use solana_primitives::{Pubkey, sign_message as sign_solana_message}; #[derive(Default)] pub struct SolanaChainSigner; +const SIGN_MESSAGE_PAYLOAD_REJECTION: &str = "Serialized Solana transaction or transaction message received in signMessage request; use signTransaction instead"; + impl ChainSigner for SolanaChainSigner { fn sign_transfer(&self, input: &SignerInput, private_key: &[u8]) -> Result { let sender = Pubkey::from_base58(&input.sender_address).map_err(SignerError::from_display)?; @@ -38,6 +40,9 @@ impl ChainSigner for SolanaChainSigner { } fn sign_message(&self, message: &[u8], private_key: &[u8]) -> Result { + if is_transaction_bytes(message) { + return Err(SignerError::invalid_input(SIGN_MESSAGE_PAYLOAD_REJECTION)); + } let signature = sign_solana_message(private_key, message).map_err(|e| SignerError::signing_error(format!("sign: {e}")))?; Ok(bs58::encode(signature.as_bytes()).into_string()) } @@ -69,7 +74,7 @@ impl ChainSigner for SolanaChainSigner { #[cfg(test)] mod tests { use super::*; - use crate::signer::testkit::{DOUBLE_SIG_TX, EXPECTED_MESSAGE_HEX, SINGLE_SIG_TX}; + use crate::signer::testkit::{DOUBLE_SIG_TX, EXPECTED_MESSAGE_HEX, SINGLE_SIG_TX, mock_legacy_transaction}; use gem_encoding::decode_base64; use primitives::testkit::signer_mock::TEST_PRIVATE_KEY; use primitives::{Chain, ChainSigner, SignerInput, TransactionLoadInput, TransferDataOutputType}; @@ -125,10 +130,27 @@ mod tests { #[test] fn test_sign_message() { - let signer = SolanaChainSigner; - - let result = signer.sign_message(b"hello", &TEST_PRIVATE_KEY).unwrap(); + let result = SolanaChainSigner.sign_message(b"hello", &TEST_PRIVATE_KEY).unwrap(); assert_eq!(bs58::decode(result).into_vec().unwrap().len(), 64); } + + #[test] + fn test_sign_message_rejects_transaction_payloads() { + let bytes = decode_base64(SINGLE_SIG_TX).unwrap(); + let result = SolanaChainSigner.sign_message(&bytes, &TEST_PRIVATE_KEY); + + assert_eq!(result.unwrap_err().to_string(), format!("Invalid input: {SIGN_MESSAGE_PAYLOAD_REJECTION}")); + + let transaction = VersionedTransaction::deserialize_with_version(&bytes).unwrap(); + let message = transaction.serialize_message().unwrap(); + let result = SolanaChainSigner.sign_message(&message, &TEST_PRIVATE_KEY); + + assert_eq!(result.unwrap_err().to_string(), format!("Invalid input: {SIGN_MESSAGE_PAYLOAD_REJECTION}")); + + let message = mock_legacy_transaction().serialize_message().unwrap(); + let result = SolanaChainSigner.sign_message(&message, &TEST_PRIVATE_KEY); + + assert_eq!(result.unwrap_err().to_string(), format!("Invalid input: {SIGN_MESSAGE_PAYLOAD_REJECTION}")); + } } diff --git a/core/crates/gem_solana/src/signer/testkit.rs b/core/crates/gem_solana/src/signer/testkit.rs index 5de8dfff7..87742f178 100644 --- a/core/crates/gem_solana/src/signer/testkit.rs +++ b/core/crates/gem_solana/src/signer/testkit.rs @@ -1,7 +1,7 @@ use gem_encoding::decode_base64; use primitives::testkit::signer_mock::TEST_PRIVATE_KEY; use primitives::{SolanaTokenProgramId, TransactionLoadMetadata}; -use solana_primitives::{Pubkey, VersionedTransaction, get_address}; +use solana_primitives::{CompiledInstruction, LegacyMessage, MessageHeader, Pubkey, VersionedTransaction, get_address}; pub const TEST_RECIPIENT: &str = "EN2sCsJ1WDV8UFqsiTXHcUPUxQ4juE71eCknHYYMifkd"; pub const TEST_SENDER_TOKEN_ADDRESS: &str = "HEeranxp3y7kVQKVSLdZW1rUmnbs7bAtUTMu8o88Jash"; @@ -26,6 +26,26 @@ pub fn base58_transaction(encoded_base64: &str) -> String { bs58::encode(decode_base64(encoded_base64).unwrap()).into_string() } +pub fn mock_legacy_transaction() -> VersionedTransaction { + VersionedTransaction::Legacy { + signatures: vec![], + message: LegacyMessage { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + account_keys: vec![Pubkey::new([1; 32]), Pubkey::new([2; 32])], + recent_blockhash: [3; 32], + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + }, + } +} + pub fn program_id(transaction: &VersionedTransaction, index: usize) -> String { let instruction = &transaction.instructions()[index]; transaction.account_keys()[instruction.program_id_index as usize].to_base58() diff --git a/core/crates/gem_solana/src/transaction.rs b/core/crates/gem_solana/src/transaction.rs index d21b5e6a1..c6f624ffc 100644 --- a/core/crates/gem_solana/src/transaction.rs +++ b/core/crates/gem_solana/src/transaction.rs @@ -4,7 +4,25 @@ use solana_primitives::{AccountMeta, AddressLookupTableAccount, Instruction, Pub pub fn try_decode_transaction(transaction_base64: &str) -> Option { let data = decode_base64(transaction_base64).ok()?; - VersionedTransaction::deserialize_with_version(&data).ok() + try_decode_transaction_bytes(&data) +} + +pub(crate) fn try_decode_transaction_bytes(transaction: &[u8]) -> Option { + let decoded = VersionedTransaction::deserialize_with_version(transaction).ok()?; + (decoded.serialize().ok()? == transaction).then_some(decoded) +} + +pub(crate) fn is_transaction_bytes(transaction: &[u8]) -> bool { + try_decode_transaction_bytes(transaction).is_some() || try_decode_transaction_message(transaction).is_some() +} + +fn try_decode_transaction_message(message: &[u8]) -> Option { + let mut transaction = Vec::with_capacity(message.len() + 1); + transaction.push(0); + transaction.extend_from_slice(message); + + let decoded = VersionedTransaction::deserialize_with_version(&transaction).ok()?; + (decoded.serialize_message().ok()? == message).then_some(decoded) } pub fn decode_transaction(transaction_base64: &str) -> Result { @@ -49,6 +67,8 @@ pub fn instructions_from_primitives(instructions: Vec) -> Res #[cfg(test)] mod tests { use super::*; + #[cfg(feature = "signer")] + use crate::signer::testkit::{SINGLE_SIG_TX, mock_legacy_transaction}; #[test] fn test_try_decode_blockhash() { @@ -56,4 +76,23 @@ mod tests { assert!(try_decode_blockhash("invalid blockhash").is_none()); assert!(try_decode_blockhash("1111111111111111111111111111111").is_none()); } + + #[cfg(feature = "signer")] + #[test] + fn test_is_transaction_bytes() { + let full_transaction = gem_encoding::decode_base64(SINGLE_SIG_TX).unwrap(); + let transaction = VersionedTransaction::deserialize_with_version(&full_transaction).unwrap(); + let mut v0_message = transaction.serialize_message().unwrap(); + let mut transaction_with_trailing_byte = full_transaction.clone(); + + assert!(is_transaction_bytes(&full_transaction)); + assert!(is_transaction_bytes(&v0_message)); + assert!(is_transaction_bytes(&mock_legacy_transaction().serialize_message().unwrap())); + + transaction_with_trailing_byte.push(0); + v0_message.push(0); + assert!(!is_transaction_bytes(&transaction_with_trailing_byte)); + assert!(!is_transaction_bytes(&v0_message)); + assert!(!is_transaction_bytes(b"hello")); + } } diff --git a/core/gemstone/src/message/signer.rs b/core/gemstone/src/message/signer.rs index 53528b7f1..f03a9a03b 100644 --- a/core/gemstone/src/message/signer.rs +++ b/core/gemstone/src/message/signer.rs @@ -4,11 +4,12 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as BASE64; use bs58; use gem_evm::message::eip191_hash_message; +use gem_solana::signer::SolanaChainSigner; use gem_sui::signer as sui_signer; use gem_ton::address::base64_to_hex_address; use gem_ton::signer::{TonSignDataResponse, TonSignMessageData, TonSignResult, TonSigner}; use primitives::hex::encode_with_0x; -use signer::{SIGNATURE_LENGTH, SignatureScheme, Signer, ensure_ethereum_signature_recovery_id_offset, hash_eip712}; +use signer::{SIGNATURE_LENGTH, Signer, ensure_ethereum_signature_recovery_id_offset, hash_eip712}; use std::time::{SystemTime, UNIX_EPOCH}; use sui_types::PersonalMessage; @@ -19,7 +20,7 @@ use super::{ }; use crate::{GemstoneError, siwe::SiweMessage}; use gem_tron::signer::tron_hash_message; -use primitives::SimulationPayloadField; +use primitives::{Chain, ChainSigner, SimulationPayloadField}; use zeroize::Zeroizing; fn siwe_or_text_preview(chain: primitives::Chain, data: &[u8]) -> MessagePreview { @@ -133,10 +134,7 @@ impl MessageSigner { let digest = hash_eip712(&json)?; Ok(digest.to_vec()) } - SignDigestType::Base58 => { - let decoded = bs58::decode(&self.message.data).into_vec().map_err(|e| GemstoneError::from(e.to_string()))?; - Ok(decoded) - } + SignDigestType::Base58 => bs58::decode(&self.message.data).into_vec().map_err(|e| GemstoneError::from(e.to_string())), } } @@ -172,9 +170,11 @@ impl MessageSigner { Ok(encode_with_0x(&signature)) } SignDigestType::Base58 => { - let hash = self.hash()?; - let signed = Signer::sign_digest(SignatureScheme::Ed25519, &hash, private_key.as_slice())?; - Ok(self.get_result(&signed)) + if self.message.chain != Chain::Solana { + return Err(GemstoneError::from(format!("Base58 sign message is not supported for {:?}", self.message.chain))); + } + let message = self.hash()?; + SolanaChainSigner.sign_message(&message, private_key.as_slice()).map_err(GemstoneError::from) } } } @@ -377,6 +377,20 @@ Issued At: 2026-03-09T15:48:34.458Z"#; assert_eq!(result, "3LRFsmWKLfsR7G5PqjytR"); } + #[test] + fn test_base58_sign_rejects_non_solana_chain() { + let decoder = MessageSigner::new(SignMessage { + chain: Chain::Ethereum, + sign_type: SignDigestType::Base58, + data: b"hello".to_vec(), + }); + + assert_eq!( + decoder.sign(TEST_PRIVATE_KEY.to_vec()).unwrap_err().to_string(), + "Base58 sign message is not supported for ethereum" + ); + } + #[test] fn test_eip712_hash() { let json_str = include_str!("./test/eip712_seaport.json");