From 88aa357a6aa89bae2d96ea6af041e9c97ae61145 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Thu, 24 Oct 2024 23:39:24 +0530 Subject: [PATCH 01/10] add hooks to call bridge escrow program --- .../solana-ibc/programs/solana-ibc/src/lib.rs | 9 +- .../programs/solana-ibc/src/storage.rs | 5 + .../programs/solana-ibc/src/transfer/mod.rs | 119 ++++++++++++++++-- 3 files changed, 124 insertions(+), 9 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/lib.rs b/solana/solana-ibc/programs/solana-ibc/src/lib.rs index 49f8fb36..ba832a09 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/lib.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/lib.rs @@ -35,6 +35,11 @@ pub const WSOL_ADDRESS: &str = "So11111111111111111111111111111111111111112"; pub const MINIMUM_FEE_ACCOUNT_BALANCE: u64 = solana_program::native_token::LAMPORTS_PER_SOL; +pub const BRIDGE_ESCROW_PROGRAM_ID: &str = + "AhfoGVmS19tvkEG2hBuZJ1D6qYEjyFmXZ1qPoFD6H4Mj"; +pub const HOOK_TOKEN_ADDRESS: &str = + "0x36dd1bfe89d409f869fabbe72c3cf72ea8b460f6"; + declare_id!("2HLLVco5HvwWriNbUhmVwA2pCetRkpgrqwnjcsZdyTKT"); #[cfg(not(feature = "mocks"))] @@ -472,8 +477,8 @@ pub mod solana_ibc { /// doesnt exists. /// /// Would panic if it doesnt match the one that is in the packet - pub fn send_transfer( - ctx: Context, + pub fn send_transfer<'a, 'info>( + ctx: Context<'a, 'a, 'a, 'info, SendTransfer<'info>>, hashed_full_denom: CryptoHash, msg: ibc::MsgTransfer, ) -> Result<()> { diff --git a/solana/solana-ibc/programs/solana-ibc/src/storage.rs b/solana/solana-ibc/programs/solana-ibc/src/storage.rs index 1180f864..f9d1d60a 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/storage.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/storage.rs @@ -473,6 +473,9 @@ pub struct TransferAccounts<'a> { pub mint_authority: Option>, pub token_program: Option>, pub fee_collector: Option>, + /// Contains the list of accounts required for the hooks + /// if present + pub remaining_accounts: Vec>, } #[derive(Debug)] @@ -544,6 +547,7 @@ macro_rules! from_ctx { }; ($ctx:expr, with accounts) => {{ let accounts = &$ctx.accounts; + let remaining_accounts = &$ctx.remaining_accounts; let accounts = TransferAccounts { sender: Some(accounts.sender.as_ref().to_account_info()), receiver: accounts @@ -574,6 +578,7 @@ macro_rules! from_ctx { .fee_collector .as_deref() .map(ToAccountInfo::to_account_info), + remaining_accounts: remaining_accounts.to_vec() }; $crate::storage::from_ctx!($ctx, accounts = accounts) }}; diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index 9cfb3562..ad6ed6f2 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -1,13 +1,15 @@ use std::result::Result; -use std::str; +use std::str::{self, FromStr}; use anchor_lang::prelude::*; use serde::{Deserialize, Serialize}; +use spl_token::solana_program::instruction::Instruction; +use spl_token::solana_program::program::invoke; -use crate::ibc; use crate::ibc::apps::transfer::types::packet::PacketData; use crate::ibc::apps::transfer::types::proto::transfer::v2::FungibleTokenPacketData; use crate::storage::IbcStorage; +use crate::{ibc, BRIDGE_ESCROW_PROGRAM_ID, HOOK_TOKEN_ADDRESS}; pub(crate) mod impls; @@ -142,12 +144,115 @@ impl ibc::Module for IbcStorage<'_, '_> { .into_bytes(), ..packet.clone() }; - let (extras, ack) = ibc::apps::transfer::module::on_recv_packet_execute( - self, - &maybe_ft_packet, - ); - let ack_status = str::from_utf8(ack.as_bytes()) + let (extras, mut ack) = + ibc::apps::transfer::module::on_recv_packet_execute( + self, + &maybe_ft_packet, + ); + let cloned_ack = ack.clone(); + let ack_status = str::from_utf8(cloned_ack.as_bytes()) .expect("Invalid acknowledgement string"); + let status = serde_json::from_slice::( + ack.as_bytes(), + ); + let success = if let Ok(status) = status { + status.is_successful() + } else { + ack = ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::AckDeserialization.into(), + ) + .into(); + false + }; + fn call_bridge_escrow( + accounts: &[AccountInfo], + data: Vec, + ) -> Result<(), ibc::AcknowledgementStatus> { + // Perform hooks + let data = match serde_json::from_slice::(&data) { + Ok(data) => data, + Err(_) => { + return Err(ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::PacketDataDeserialization + .into(), + ) + .into()); + } + }; + // The hook would only be called if the transferred token is the one we are interested in + if data.token.denom.base_denom.as_str() == HOOK_TOKEN_ADDRESS { + // The memo is a string and the structure is as follow: + // ", ..... ,," + // + // The relayer would parse the memo and pass the relevant accounts + // The intent_id and memo needs to be stripped + let memo = data.memo.as_ref(); + let (accounts_size, rest) = memo.split_once(",").ok_or( + ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::Other( + "Invalid memo".to_string(), + ) + .into(), + ) + .into(), + )?; + // This is the 8 byte discriminant since the program is written in + // anchor. it is hash of ":" which is + // "global:on_receive_transfer" respectively. + let instruction_discriminant: Vec = + vec![149, 112, 68, 208, 4, 206, 248, 125]; + let values = rest.split(",").collect::>(); + let (_passed_accounts, ix_data) = + values.split_at(accounts_size.parse::().unwrap()); + let intent_id = ix_data.get(0).ok_or( + ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::Other( + "Invalid memo".to_string(), + ) + .into(), + ) + .into(), + )?; + let memo = ix_data[1..].join(","); + let mut instruction_data = instruction_discriminant; + instruction_data.extend_from_slice(&intent_id.as_bytes()); + instruction_data.extend_from_slice(memo.as_bytes()); + + let bridge_escrow_program_id = + Pubkey::from_str(BRIDGE_ESCROW_PROGRAM_ID).unwrap(); + + let account_metas = accounts + .iter() + .map(|account| AccountMeta { + pubkey: *account.key, + is_signer: account.is_signer, + is_writable: account.is_writable, + }) + .collect::>(); + let instruction = Instruction::new_with_bytes( + bridge_escrow_program_id, + &instruction_data, + account_metas, + ); + + invoke(&instruction, accounts).map_err(|err| { + return ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::Other(err.to_string()).into(), + ); + })?; + msg!("Hook: Bridge escrow call successful"); + } + Ok(()) + } + + if success { + let store = self.borrow(); + let accounts = &store.accounts.remaining_accounts; + let result = call_bridge_escrow(accounts, maybe_ft_packet.data); + if let Err(status) = result { + ack = status.into(); + } + } msg!("ibc::Packet acknowledgement: {}", ack_status); (extras, ack) } From 3d76e1e5f1440c57e206d73eb6bad3299f3e1812 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Thu, 24 Oct 2024 20:25:30 +0200 Subject: [PATCH 02/10] fix clippy --- .../programs/solana-ibc/src/transfer/mod.rs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index ad6ed6f2..10b353a5 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -175,8 +175,7 @@ impl ibc::Module for IbcStorage<'_, '_> { return Err(ibc::AcknowledgementStatus::error( ibc::TokenTransferError::PacketDataDeserialization .into(), - ) - .into()); + )); } }; // The hook would only be called if the transferred token is the one we are interested in @@ -187,35 +186,33 @@ impl ibc::Module for IbcStorage<'_, '_> { // The relayer would parse the memo and pass the relevant accounts // The intent_id and memo needs to be stripped let memo = data.memo.as_ref(); - let (accounts_size, rest) = memo.split_once(",").ok_or( + let (accounts_size, rest) = memo.split_once(',').ok_or( ibc::AcknowledgementStatus::error( ibc::TokenTransferError::Other( "Invalid memo".to_string(), ) .into(), - ) - .into(), + ), )?; // This is the 8 byte discriminant since the program is written in // anchor. it is hash of ":" which is // "global:on_receive_transfer" respectively. let instruction_discriminant: Vec = vec![149, 112, 68, 208, 4, 206, 248, 125]; - let values = rest.split(",").collect::>(); + let values = rest.split(',').collect::>(); let (_passed_accounts, ix_data) = values.split_at(accounts_size.parse::().unwrap()); - let intent_id = ix_data.get(0).ok_or( + let intent_id = ix_data.first().ok_or( ibc::AcknowledgementStatus::error( ibc::TokenTransferError::Other( "Invalid memo".to_string(), ) .into(), - ) - .into(), + ), )?; let memo = ix_data[1..].join(","); let mut instruction_data = instruction_discriminant; - instruction_data.extend_from_slice(&intent_id.as_bytes()); + instruction_data.extend_from_slice(intent_id.as_bytes()); instruction_data.extend_from_slice(memo.as_bytes()); let bridge_escrow_program_id = @@ -236,9 +233,9 @@ impl ibc::Module for IbcStorage<'_, '_> { ); invoke(&instruction, accounts).map_err(|err| { - return ibc::AcknowledgementStatus::error( + ibc::AcknowledgementStatus::error( ibc::TokenTransferError::Other(err.to_string()).into(), - ); + ) })?; msg!("Hook: Bridge escrow call successful"); } From 57ee3619836b1409b0184be958246827966c11e2 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 25 Oct 2024 02:12:52 +0200 Subject: [PATCH 03/10] use pubkey!; turned out to be a bit more complicated --- Cargo.lock | 1 + solana/solana-ibc/programs/solana-ibc/Cargo.toml | 5 +++++ solana/solana-ibc/programs/solana-ibc/src/lib.rs | 4 ++-- solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs | 7 ++----- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8084abd5..790e3a0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5724,6 +5724,7 @@ dependencies = [ "serde", "serde_json", "solana-allocator", + "solana-program 1.17.31", "solana-signature-verifier", "solana-trie", "solana-write-account", diff --git a/solana/solana-ibc/programs/solana-ibc/Cargo.toml b/solana/solana-ibc/programs/solana-ibc/Cargo.toml index 63771bae..e0251617 100644 --- a/solana/solana-ibc/programs/solana-ibc/Cargo.toml +++ b/solana/solana-ibc/programs/solana-ibc/Cargo.toml @@ -35,6 +35,11 @@ primitive-types.workspace = true prost.workspace = true serde.workspace = true serde_json.workspace = true +# We normally access solana_program via anchor_lang but to support +# pubkey! macro we need to have solana_program as direct dependency. +# TODO(mina86): Remove this once we upgrade Anchor to version with its +# own pubkey! macro. +solana-program.workspace = true spl-associated-token-account.workspace = true spl-token.workspace = true strum.workspace = true diff --git a/solana/solana-ibc/programs/solana-ibc/src/lib.rs b/solana/solana-ibc/programs/solana-ibc/src/lib.rs index ba832a09..a110f788 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/lib.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/lib.rs @@ -35,8 +35,8 @@ pub const WSOL_ADDRESS: &str = "So11111111111111111111111111111111111111112"; pub const MINIMUM_FEE_ACCOUNT_BALANCE: u64 = solana_program::native_token::LAMPORTS_PER_SOL; -pub const BRIDGE_ESCROW_PROGRAM_ID: &str = - "AhfoGVmS19tvkEG2hBuZJ1D6qYEjyFmXZ1qPoFD6H4Mj"; +pub const BRIDGE_ESCROW_PROGRAM_ID: Pubkey = + solana_program::pubkey!("AhfoGVmS19tvkEG2hBuZJ1D6qYEjyFmXZ1qPoFD6H4Mj"); pub const HOOK_TOKEN_ADDRESS: &str = "0x36dd1bfe89d409f869fabbe72c3cf72ea8b460f6"; diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index 10b353a5..de1d0535 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -1,5 +1,5 @@ use std::result::Result; -use std::str::{self, FromStr}; +use std::str; use anchor_lang::prelude::*; use serde::{Deserialize, Serialize}; @@ -215,9 +215,6 @@ impl ibc::Module for IbcStorage<'_, '_> { instruction_data.extend_from_slice(intent_id.as_bytes()); instruction_data.extend_from_slice(memo.as_bytes()); - let bridge_escrow_program_id = - Pubkey::from_str(BRIDGE_ESCROW_PROGRAM_ID).unwrap(); - let account_metas = accounts .iter() .map(|account| AccountMeta { @@ -227,7 +224,7 @@ impl ibc::Module for IbcStorage<'_, '_> { }) .collect::>(); let instruction = Instruction::new_with_bytes( - bridge_escrow_program_id, + BRIDGE_ESCROW_PROGRAM_ID, &instruction_data, account_metas, ); From 9e8c80977b89f9987d52d9773984b2ba84599bda Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 25 Oct 2024 02:29:28 +0200 Subject: [PATCH 04/10] avoid clone --- .../programs/solana-ibc/src/transfer/mod.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index de1d0535..de757f5e 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -149,12 +149,12 @@ impl ibc::Module for IbcStorage<'_, '_> { self, &maybe_ft_packet, ); - let cloned_ack = ack.clone(); - let ack_status = str::from_utf8(cloned_ack.as_bytes()) + let ack_status = str::from_utf8(ack.as_bytes()) .expect("Invalid acknowledgement string"); - let status = serde_json::from_slice::( - ack.as_bytes(), - ); + msg!("ibc::Packet acknowledgement: {}", ack_status); + + let status = + serde_json::from_str::(ack_status); let success = if let Ok(status) = status { status.is_successful() } else { @@ -247,7 +247,7 @@ impl ibc::Module for IbcStorage<'_, '_> { ack = status.into(); } } - msg!("ibc::Packet acknowledgement: {}", ack_status); + (extras, ack) } From 13d3650b6dfc68fdbaac2a2af9df7a4203d63630 Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 25 Oct 2024 02:31:59 +0200 Subject: [PATCH 05/10] minor refactoring --- .../programs/solana-ibc/src/transfer/mod.rs | 128 +++++++++--------- 1 file changed, 62 insertions(+), 66 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index de757f5e..d9947480 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -158,84 +158,80 @@ impl ibc::Module for IbcStorage<'_, '_> { let success = if let Ok(status) = status { status.is_successful() } else { - ack = ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::AckDeserialization.into(), - ) - .into(); + let status = ibc::TokenTransferError::AckDeserialization.into(); + ack = ibc::AcknowledgementStatus::error(status).into(); false }; + fn call_bridge_escrow( accounts: &[AccountInfo], data: Vec, ) -> Result<(), ibc::AcknowledgementStatus> { // Perform hooks - let data = match serde_json::from_slice::(&data) { - Ok(data) => data, - Err(_) => { - return Err(ibc::AcknowledgementStatus::error( + let data = + serde_json::from_slice::(&data).map_err(|_| { + ibc::AcknowledgementStatus::error( ibc::TokenTransferError::PacketDataDeserialization .into(), - )); - } - }; - // The hook would only be called if the transferred token is the one we are interested in - if data.token.denom.base_denom.as_str() == HOOK_TOKEN_ADDRESS { - // The memo is a string and the structure is as follow: - // ", ..... ,," - // - // The relayer would parse the memo and pass the relevant accounts - // The intent_id and memo needs to be stripped - let memo = data.memo.as_ref(); - let (accounts_size, rest) = memo.split_once(',').ok_or( - ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::Other( - "Invalid memo".to_string(), - ) - .into(), - ), - )?; - // This is the 8 byte discriminant since the program is written in - // anchor. it is hash of ":" which is - // "global:on_receive_transfer" respectively. - let instruction_discriminant: Vec = - vec![149, 112, 68, 208, 4, 206, 248, 125]; - let values = rest.split(',').collect::>(); - let (_passed_accounts, ix_data) = - values.split_at(accounts_size.parse::().unwrap()); - let intent_id = ix_data.first().ok_or( - ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::Other( - "Invalid memo".to_string(), - ) - .into(), - ), - )?; - let memo = ix_data[1..].join(","); - let mut instruction_data = instruction_discriminant; - instruction_data.extend_from_slice(intent_id.as_bytes()); - instruction_data.extend_from_slice(memo.as_bytes()); - - let account_metas = accounts - .iter() - .map(|account| AccountMeta { - pubkey: *account.key, - is_signer: account.is_signer, - is_writable: account.is_writable, - }) - .collect::>(); - let instruction = Instruction::new_with_bytes( - BRIDGE_ESCROW_PROGRAM_ID, - &instruction_data, - account_metas, - ); - - invoke(&instruction, accounts).map_err(|err| { - ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::Other(err.to_string()).into(), ) })?; - msg!("Hook: Bridge escrow call successful"); + + // The hook would only be called if the transferred token is the one + // we are interested in + if data.token.denom.base_denom.as_str() != HOOK_TOKEN_ADDRESS { + return Ok(()); } + + // The memo is a string and the structure is as follow: + // ", ..... ,," + // + // The relayer would parse the memo and pass the relevant accounts + // The intent_id and memo needs to be stripped + let memo = data.memo.as_ref(); + let (accounts_size, rest) = memo.split_once(',').ok_or( + ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::Other("Invalid memo".to_string()) + .into(), + ), + )?; + // This is the 8 byte discriminant since the program is written in + // anchor. it is hash of ":" which is + // "global:on_receive_transfer" respectively. + let instruction_discriminant: Vec = + vec![149, 112, 68, 208, 4, 206, 248, 125]; + let values = rest.split(',').collect::>(); + let (_passed_accounts, ix_data) = + values.split_at(accounts_size.parse::().unwrap()); + let intent_id = + ix_data.first().ok_or(ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::Other("Invalid memo".to_string()) + .into(), + ))?; + let memo = ix_data[1..].join(","); + let mut instruction_data = instruction_discriminant; + instruction_data.extend_from_slice(intent_id.as_bytes()); + instruction_data.extend_from_slice(memo.as_bytes()); + + let account_metas = accounts + .iter() + .map(|account| AccountMeta { + pubkey: *account.key, + is_signer: account.is_signer, + is_writable: account.is_writable, + }) + .collect::>(); + let instruction = Instruction::new_with_bytes( + BRIDGE_ESCROW_PROGRAM_ID, + &instruction_data, + account_metas, + ); + + invoke(&instruction, accounts).map_err(|err| { + ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::Other(err.to_string()).into(), + ) + })?; + msg!("Hook: Bridge escrow call successful"); Ok(()) } From 5e31872310a85c4f9c6fc8d1e70d63d378feb98e Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 25 Oct 2024 02:33:08 +0200 Subject: [PATCH 06/10] instruction data --- .../programs/solana-ibc/src/transfer/mod.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index d9947480..64644ffd 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -197,8 +197,8 @@ impl ibc::Module for IbcStorage<'_, '_> { // This is the 8 byte discriminant since the program is written in // anchor. it is hash of ":" which is // "global:on_receive_transfer" respectively. - let instruction_discriminant: Vec = - vec![149, 112, 68, 208, 4, 206, 248, 125]; + const INSTRUCTION_DISCRIMINANT: [u8; 8] = + [149, 112, 68, 208, 4, 206, 248, 125]; let values = rest.split(',').collect::>(); let (_passed_accounts, ix_data) = values.split_at(accounts_size.parse::().unwrap()); @@ -208,9 +208,12 @@ impl ibc::Module for IbcStorage<'_, '_> { .into(), ))?; let memo = ix_data[1..].join(","); - let mut instruction_data = instruction_discriminant; - instruction_data.extend_from_slice(intent_id.as_bytes()); - instruction_data.extend_from_slice(memo.as_bytes()); + let instruction_data = [ + &INSTRUCTION_DISCRIMINANT[..], + intent_id.as_bytes(), + memo.as_bytes(), + ] + .concat(); let account_metas = accounts .iter() From 593d5972c37c160dbb88d36655bc75d6296ce54b Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 25 Oct 2024 02:45:55 +0200 Subject: [PATCH 07/10] zero-alloc memo parsing --- .../programs/solana-ibc/src/transfer/mod.rs | 193 +++++++++++------- 1 file changed, 116 insertions(+), 77 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index 64644ffd..c688e1f7 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -1,5 +1,5 @@ use std::result::Result; -use std::str; +use std::str::{self, FromStr}; use anchor_lang::prelude::*; use serde::{Deserialize, Serialize}; @@ -163,85 +163,10 @@ impl ibc::Module for IbcStorage<'_, '_> { false }; - fn call_bridge_escrow( - accounts: &[AccountInfo], - data: Vec, - ) -> Result<(), ibc::AcknowledgementStatus> { - // Perform hooks - let data = - serde_json::from_slice::(&data).map_err(|_| { - ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::PacketDataDeserialization - .into(), - ) - })?; - - // The hook would only be called if the transferred token is the one - // we are interested in - if data.token.denom.base_denom.as_str() != HOOK_TOKEN_ADDRESS { - return Ok(()); - } - - // The memo is a string and the structure is as follow: - // ", ..... ,," - // - // The relayer would parse the memo and pass the relevant accounts - // The intent_id and memo needs to be stripped - let memo = data.memo.as_ref(); - let (accounts_size, rest) = memo.split_once(',').ok_or( - ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::Other("Invalid memo".to_string()) - .into(), - ), - )?; - // This is the 8 byte discriminant since the program is written in - // anchor. it is hash of ":" which is - // "global:on_receive_transfer" respectively. - const INSTRUCTION_DISCRIMINANT: [u8; 8] = - [149, 112, 68, 208, 4, 206, 248, 125]; - let values = rest.split(',').collect::>(); - let (_passed_accounts, ix_data) = - values.split_at(accounts_size.parse::().unwrap()); - let intent_id = - ix_data.first().ok_or(ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::Other("Invalid memo".to_string()) - .into(), - ))?; - let memo = ix_data[1..].join(","); - let instruction_data = [ - &INSTRUCTION_DISCRIMINANT[..], - intent_id.as_bytes(), - memo.as_bytes(), - ] - .concat(); - - let account_metas = accounts - .iter() - .map(|account| AccountMeta { - pubkey: *account.key, - is_signer: account.is_signer, - is_writable: account.is_writable, - }) - .collect::>(); - let instruction = Instruction::new_with_bytes( - BRIDGE_ESCROW_PROGRAM_ID, - &instruction_data, - account_metas, - ); - - invoke(&instruction, accounts).map_err(|err| { - ibc::AcknowledgementStatus::error( - ibc::TokenTransferError::Other(err.to_string()).into(), - ) - })?; - msg!("Hook: Bridge escrow call successful"); - Ok(()) - } - if success { let store = self.borrow(); let accounts = &store.accounts.remaining_accounts; - let result = call_bridge_escrow(accounts, maybe_ft_packet.data); + let result = call_bridge_escrow(accounts, &maybe_ft_packet.data); if let Err(status) = result { ack = status.into(); } @@ -463,3 +388,117 @@ impl From for FungibleTokenPacketData { } } } + + +/// Calls bridge escrow after receiving packet if necessary. +/// +/// If the packet is for a [`HOOK_TOKEN_ADDRESS`] token, parses the transfer +/// memo and invokes bridge escrow contract with instruction encoded in it. +/// (see [`parse_bridge_memo`] for format of the memo). +fn call_bridge_escrow( + accounts: &[AccountInfo], + data: &[u8], +) -> Result<(), ibc::AcknowledgementStatus> { + // Perform hooks + let data = serde_json::from_slice::(data).map_err(|_| { + ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::PacketDataDeserialization.into(), + ) + })?; + + // The hook would only be called if the transferred token is the one we are + // interested in + if data.token.denom.base_denom.as_str() != HOOK_TOKEN_ADDRESS { + return Ok(()); + } + + // The memo is a string and the structure is as follow: + // ", ..... ,," + // + // The relayer would parse the memo and pass the relevant accounts The + // intent_id and memo needs to be stripped + let (intent_id, memo) = + parse_bridge_memo(&data.memo.as_ref()).ok_or_else(|| { + let err = ibc::TokenTransferError::Other("Invalid memo".into()); + ibc::AcknowledgementStatus::error(err.into()) + })?; + + // This is the 8 byte discriminant since the program is written in + // anchor. it is hash of ":" which is + // "global:on_receive_transfer" respectively. + const INSTRUCTION_DISCRIMINANT: [u8; 8] = + [149, 112, 68, 208, 4, 206, 248, 125]; + + let instruction_data = + [&INSTRUCTION_DISCRIMINANT[..], intent_id.as_bytes(), memo.as_bytes()] + .concat(); + + let account_metas = accounts + .iter() + .map(|account| AccountMeta { + pubkey: *account.key, + is_signer: account.is_signer, + is_writable: account.is_writable, + }) + .collect(); + let instruction = Instruction::new_with_bytes( + BRIDGE_ESCROW_PROGRAM_ID, + &instruction_data, + account_metas, + ); + + invoke(&instruction, accounts).map_err(|err| { + ibc::AcknowledgementStatus::error( + ibc::TokenTransferError::Other(err.to_string()).into(), + ) + })?; + msg!("Hook: Bridge escrow call successful"); + Ok(()) +} + + +/// Parses memo of a transaction directed at the bridge escrow. +/// +/// Memo is comma separated list of the form +/// `N,account-0,account-1,...,account-N-1,intent-id,embedded-memo`. Embedded +/// memo can contain commas. Returns `intent-id` and `embedded-memo` or `None` +/// if the memo does not conform to this format. Note that no validation on +/// accounts is performed. +fn parse_bridge_memo(memo: &str) -> Option<(&str, &str)> { + let (count, mut memo) = memo.split_once(',')?; + // Skip accounts + for _ in 0..usize::from_str(count).ok()? { + let (_, rest) = memo.split_once(',')?; + memo = rest + } + memo.split_once(',') +} + +#[test] +fn test_parse_bridge_memo() { + for (intent, memo, data) in [ + ("intent", "memo", "0,intent,memo"), + ("intent", "memo,with,comma", "0,intent,memo,with,comma"), + ("intent", "memo", "1,account0,intent,memo"), + ("intent", "memo", "3,account0,account1,account2,intent,memo"), + ("intent", "memo,comma", "1,account0,intent,memo,comma"), + ("intent", "", "1,account0,intent,"), + ("", "memo", "1,account0,,memo"), + ("", "", "1,account0,,"), + ] { + assert_eq!( + Some((intent, memo)), + parse_bridge_memo(data), + "memo: {data}" + ); + } + + for data in [ + "-1,intent,memo", + "foo,intent,memo", + ",intent,memo", + "1,account0,intent", + ] { + assert!(parse_bridge_memo(data).is_none(), "memo: {data}"); + } +} From aee98fce27f9960ece2e276b9a20f35d5e1fc26c Mon Sep 17 00:00:00 2001 From: Michal Nazarewicz Date: Fri, 25 Oct 2024 03:13:37 +0200 Subject: [PATCH 08/10] fix clippy --- solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index c688e1f7..cbbddbdc 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -418,7 +418,7 @@ fn call_bridge_escrow( // The relayer would parse the memo and pass the relevant accounts The // intent_id and memo needs to be stripped let (intent_id, memo) = - parse_bridge_memo(&data.memo.as_ref()).ok_or_else(|| { + parse_bridge_memo(data.memo.as_ref()).ok_or_else(|| { let err = ibc::TokenTransferError::Other("Invalid memo".into()); ibc::AcknowledgementStatus::error(err.into()) })?; From 61bd6a4540d0e5c668a386822cb27ecd9d4a5ef1 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Sun, 27 Oct 2024 11:29:56 +0530 Subject: [PATCH 09/10] update comments --- solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index cbbddbdc..b3e41936 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -415,8 +415,10 @@ fn call_bridge_escrow( // The memo is a string and the structure is as follow: // ", ..... ,," // - // The relayer would parse the memo and pass the relevant accounts The - // intent_id and memo needs to be stripped + // The relayer would parse the memo and pass the relevant accounts. + // + // The intent_id and memo needs to be stripped so that it can be sent to the + // bridge escrow contract. let (intent_id, memo) = parse_bridge_memo(data.memo.as_ref()).ok_or_else(|| { let err = ibc::TokenTransferError::Other("Invalid memo".into()); @@ -425,7 +427,7 @@ fn call_bridge_escrow( // This is the 8 byte discriminant since the program is written in // anchor. it is hash of ":" which is - // "global:on_receive_transfer" respectively. + // "global:on_receive_transfer" in our case. const INSTRUCTION_DISCRIMINANT: [u8; 8] = [149, 112, 68, 208, 4, 206, 248, 125]; From 7ce01e70073c0f306feb9a3c8bb8a47bb352babc Mon Sep 17 00:00:00 2001 From: dhruvja Date: Mon, 28 Oct 2024 20:17:22 +0530 Subject: [PATCH 10/10] serialize the parameters to the method as borsh --- .../programs/solana-ibc/src/transfer/mod.rs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs index b3e41936..b22afdc6 100644 --- a/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs +++ b/solana/solana-ibc/programs/solana-ibc/src/transfer/mod.rs @@ -389,7 +389,6 @@ impl From for FungibleTokenPacketData { } } - /// Calls bridge escrow after receiving packet if necessary. /// /// If the packet is for a [`HOOK_TOKEN_ADDRESS`] token, parses the transfer @@ -431,9 +430,13 @@ fn call_bridge_escrow( const INSTRUCTION_DISCRIMINANT: [u8; 8] = [149, 112, 68, 208, 4, 206, 248, 125]; + // Serialize the intent id and memo with borsh since the destination contract + // is written with anchor and expects the data to be in borsh encoded. let instruction_data = - [&INSTRUCTION_DISCRIMINANT[..], intent_id.as_bytes(), memo.as_bytes()] - .concat(); + [&INSTRUCTION_DISCRIMINANT[..], + &intent_id.try_to_vec().unwrap(), + &memo.try_to_vec().unwrap()] + .concat(); let account_metas = accounts .iter() @@ -458,7 +461,6 @@ fn call_bridge_escrow( Ok(()) } - /// Parses memo of a transaction directed at the bridge escrow. /// /// Memo is comma separated list of the form @@ -504,3 +506,13 @@ fn test_parse_bridge_memo() { assert!(parse_bridge_memo(data).is_none(), "memo: {data}"); } } + +#[test] +fn test_memo() { + let memo = "8,WdFwv2TiGksf6x5CCwC6Svrz6JYzgCw4P1MC4Kcn3UE,7BgBvyjrZX1YKz4oh9mjb8ZScatkkwb8DzFx7LoiVkM3,XSUoLRkKahnVkrVteuJuLcPuhn2uPecFHM3zCcgsAQs,8q4qp8hMSfUZZcetiJrW7jD9n4pWmSA8ua19CcdT6p3H,Sysvar1nstructions1111111111111111111111111,TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA,H77KMAJhXEq82LmCNckaUHmXXU1RTUh5FePLVD9UAHUh,FFFhqkq4DKhdeGeLqsi72u7g8GqdgQyrqu4mdRo9kKDt,100000,false,0x0362110922F923B57b7EfF68eE7A51827b2dF4b4,0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48,0xd41fb9e1dA5255dD994b029bC3C7e06ea8105BF3,1000000"; + let (intent_id, memo) = parse_bridge_memo(memo).unwrap(); + println!("intent_id: {intent_id}"); + println!("memo: {memo}"); + let parts: Vec<&str> = memo.split(',').collect(); + println!("parts: {:?}", parts.len()); +}