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}"); + } +}