瀏覽代碼

solana: token bridge transfer with payload

Hendrik Hofstadt 3 年之前
父節點
當前提交
ee4583099f

+ 3 - 0
sdk/rust/sdk/Cargo.toml

@@ -8,6 +8,9 @@ edition = "2018"
 # Helper methods will target the Wormhole mainnet contract addresses.
 mainnet   = []
 
+# Helper methosd will target the Wormhole testnet contract addresses.
+testnet    = []
+
 # Helper methosd will target the Wormhole devnet contract addresses.
 devnet    = []
 

+ 2 - 0
solana/modules/token_bridge/program/src/api.rs

@@ -1,5 +1,6 @@
 pub mod attest;
 pub mod complete_transfer;
+pub mod complete_transfer_payload;
 pub mod create_wrapped;
 pub mod governance;
 pub mod initialize;
@@ -7,6 +8,7 @@ pub mod transfer;
 
 pub use attest::*;
 pub use complete_transfer::*;
+pub use complete_transfer_payload::*;
 pub use create_wrapped::*;
 pub use governance::*;
 pub use initialize::*;

+ 312 - 0
solana/modules/token_bridge/program/src/api/complete_transfer_payload.rs

@@ -0,0 +1,312 @@
+use crate::{
+    accounts::{
+        ConfigAccount,
+        CustodyAccount,
+        CustodyAccountDerivationData,
+        CustodySigner,
+        Endpoint,
+        EndpointDerivationData,
+        MintSigner,
+        WrappedDerivationData,
+        WrappedMetaDerivationData,
+        WrappedMint,
+        WrappedTokenMeta,
+    },
+    messages::{
+        PayloadTransfer,
+        PayloadTransferWithPayload,
+    },
+    types::*,
+    TokenBridgeError::*,
+};
+use bridge::{
+    vaa::ClaimableVAA,
+    CHAIN_ID_SOLANA,
+};
+use solana_program::{
+    account_info::AccountInfo,
+    program::invoke_signed,
+    program_error::ProgramError,
+    pubkey::Pubkey,
+};
+use solitaire::{
+    processors::seeded::{
+        invoke_seeded,
+        Seeded,
+    },
+    CreationLamports::Exempt,
+    *,
+};
+use spl_token::state::{
+    Account,
+    Mint,
+};
+use std::ops::{
+    Deref,
+    DerefMut,
+};
+
+#[derive(FromAccounts)]
+pub struct CompleteNativeWithPayload<'b> {
+    pub payer: Mut<Signer<AccountInfo<'b>>>,
+    pub config: ConfigAccount<'b, { AccountState::Initialized }>,
+
+    pub vaa: ClaimableVAA<'b, PayloadTransferWithPayload>,
+    pub chain_registration: Endpoint<'b, { AccountState::Initialized }>,
+
+    pub to: Mut<Data<'b, SplAccount, { AccountState::Initialized }>>,
+    /// Transfer with payload can only be redeemed by the recipient. The idea is
+    /// to target contracts which can then decide how to process the payload.
+    ///
+    /// The actual recipient (the `to` field above) is an associated token
+    /// account of the target contract and not the contract itself, so we also need
+    /// to take the target contract's address directly. This will be the owner
+    /// of the associated token account. This ownership check cannot be
+    /// expressed in Solitaire, so we have to check it explicitly in
+    /// [`complete_native_with_payload`]
+    /// We require that the contract is a signer of this transaction.
+    pub to_owner: MaybeMut<Signer<Info<'b>>>,
+    pub to_fees: Mut<Data<'b, SplAccount, { AccountState::Initialized }>>,
+    pub custody: Mut<CustodyAccount<'b, { AccountState::Initialized }>>,
+    pub mint: Data<'b, SplMint, { AccountState::Initialized }>,
+
+    pub custody_signer: CustodySigner<'b>,
+}
+
+impl<'a> From<&CompleteNativeWithPayload<'a>> for EndpointDerivationData {
+    fn from(accs: &CompleteNativeWithPayload<'a>) -> Self {
+        EndpointDerivationData {
+            emitter_chain: accs.vaa.meta().emitter_chain,
+            emitter_address: accs.vaa.meta().emitter_address,
+        }
+    }
+}
+
+impl<'a> From<&CompleteNativeWithPayload<'a>> for CustodyAccountDerivationData {
+    fn from(accs: &CompleteNativeWithPayload<'a>) -> Self {
+        CustodyAccountDerivationData {
+            mint: *accs.mint.info().key,
+        }
+    }
+}
+
+impl<'b> InstructionContext<'b> for CompleteNativeWithPayload<'b> {
+}
+
+#[derive(BorshDeserialize, BorshSerialize, Default)]
+pub struct CompleteNativeWithPayloadData {}
+
+pub fn complete_native_with_payload(
+    ctx: &ExecutionContext,
+    accs: &mut CompleteNativeWithPayload,
+    data: CompleteNativeWithPayloadData,
+) -> Result<()> {
+    // Verify the chain registration
+    let derivation_data: EndpointDerivationData = (&*accs).into();
+    accs.chain_registration
+        .verify_derivation(ctx.program_id, &derivation_data)?;
+
+    // Verify that the custody account is derived correctly
+    let derivation_data: CustodyAccountDerivationData = (&*accs).into();
+    accs.custody
+        .verify_derivation(ctx.program_id, &derivation_data)?;
+
+    // Verify mints
+    if *accs.mint.info().key != accs.to.mint {
+        return Err(InvalidMint.into());
+    }
+    if *accs.mint.info().key != accs.to_fees.mint {
+        return Err(InvalidMint.into());
+    }
+    if *accs.mint.info().key != accs.custody.mint {
+        return Err(InvalidMint.into());
+    }
+    if *accs.custody_signer.key != accs.custody.owner {
+        return Err(WrongAccountOwner.into());
+    }
+
+    // Verify VAA
+    if accs.vaa.token_address != accs.mint.info().key.to_bytes() {
+        return Err(InvalidMint.into());
+    }
+    if accs.vaa.token_chain != 1 {
+        return Err(InvalidChain.into());
+    }
+    if accs.vaa.to_chain != CHAIN_ID_SOLANA {
+        return Err(InvalidChain.into());
+    }
+    if accs.vaa.to != accs.to_owner.info().key.to_bytes() {
+        return Err(InvalidRecipient.into());
+    }
+
+    // VAA-specified recipient must be token account owner
+    if *accs.to_owner.info().key != accs.to.owner {
+        return Err(InvalidRecipient.into());
+    }
+
+    // Prevent vaa double signing
+    accs.vaa.verify(ctx.program_id)?;
+    accs.vaa.claim(ctx, accs.payer.key)?;
+
+    let mut amount = accs.vaa.amount.as_u64();
+    let mut fee = accs.vaa.fee.as_u64();
+
+    // Wormhole always caps transfers at 8 decimals; un-truncate if the local token has more
+    if accs.mint.decimals > 8 {
+        amount *= 10u64.pow((accs.mint.decimals - 8) as u32);
+        fee *= 10u64.pow((accs.mint.decimals - 8) as u32);
+    }
+
+    // Transfer tokens
+    let transfer_ix = spl_token::instruction::transfer(
+        &spl_token::id(),
+        accs.custody.info().key,
+        accs.to.info().key,
+        accs.custody_signer.key,
+        &[],
+        amount.checked_sub(fee).unwrap(),
+    )?;
+    invoke_seeded(&transfer_ix, ctx, &accs.custody_signer, None)?;
+
+    // Transfer fees
+    let transfer_ix = spl_token::instruction::transfer(
+        &spl_token::id(),
+        accs.custody.info().key,
+        accs.to_fees.info().key,
+        accs.custody_signer.key,
+        &[],
+        fee,
+    )?;
+    invoke_seeded(&transfer_ix, ctx, &accs.custody_signer, None)?;
+
+    Ok(())
+}
+
+#[derive(FromAccounts)]
+pub struct CompleteWrappedWithPayload<'b> {
+    pub payer: Mut<Signer<AccountInfo<'b>>>,
+    pub config: ConfigAccount<'b, { AccountState::Initialized }>,
+
+    // Signed message for the transfer
+    pub vaa: ClaimableVAA<'b, PayloadTransferWithPayload>,
+
+    pub chain_registration: Endpoint<'b, { AccountState::Initialized }>,
+
+    pub to: Mut<Data<'b, SplAccount, { AccountState::Initialized }>>,
+    /// Transfer with payload can only be redeemed by the recipient. The idea is
+    /// to target contracts which can then decide how to process the payload.
+    ///
+    /// The actual recipient (the `to` field above) is an associated token
+    /// account of the target contract and not the contract itself, so we also need
+    /// to take the target contract's address directly. This will be the owner
+    /// of the associated token account. This ownership check cannot be
+    /// expressed in Solitaire, so we have to check it explicitly in
+    /// [`complete_native_with_payload`]
+    /// We require that the contract is a signer of this transaction.
+    pub to_owner: MaybeMut<Signer<Info<'b>>>,
+    pub to_fees: Mut<Data<'b, SplAccount, { AccountState::Initialized }>>,
+    pub mint: Mut<WrappedMint<'b, { AccountState::Initialized }>>,
+    pub wrapped_meta: WrappedTokenMeta<'b, { AccountState::Initialized }>,
+
+    pub mint_authority: MintSigner<'b>,
+}
+
+impl<'a> From<&CompleteWrappedWithPayload<'a>> for EndpointDerivationData {
+    fn from(accs: &CompleteWrappedWithPayload<'a>) -> Self {
+        EndpointDerivationData {
+            emitter_chain: accs.vaa.meta().emitter_chain,
+            emitter_address: accs.vaa.meta().emitter_address,
+        }
+    }
+}
+
+impl<'a> From<&CompleteWrappedWithPayload<'a>> for WrappedDerivationData {
+    fn from(accs: &CompleteWrappedWithPayload<'a>) -> Self {
+        WrappedDerivationData {
+            token_chain: accs.vaa.token_chain,
+            token_address: accs.vaa.token_address,
+        }
+    }
+}
+
+impl<'b> InstructionContext<'b> for CompleteWrappedWithPayload<'b> {
+}
+
+#[derive(BorshDeserialize, BorshSerialize, Default)]
+pub struct CompleteWrappedWithPayloadData {}
+
+pub fn complete_wrapped_with_payload(
+    ctx: &ExecutionContext,
+    accs: &mut CompleteWrappedWithPayload,
+    data: CompleteWrappedWithPayloadData,
+) -> Result<()> {
+    // Verify the chain registration
+    let derivation_data: EndpointDerivationData = (&*accs).into();
+    accs.chain_registration
+        .verify_derivation(ctx.program_id, &derivation_data)?;
+
+    // Verify mint
+    accs.wrapped_meta.verify_derivation(
+        ctx.program_id,
+        &WrappedMetaDerivationData {
+            mint_key: *accs.mint.info().key,
+        },
+    )?;
+    if accs.wrapped_meta.token_address != accs.vaa.token_address
+        || accs.wrapped_meta.chain != accs.vaa.token_chain
+    {
+        return Err(InvalidMint.into());
+    }
+
+    // Verify mints
+    if *accs.mint.info().key != accs.to.mint {
+        return Err(InvalidMint.into());
+    }
+    if *accs.mint.info().key != accs.to_fees.mint {
+        return Err(InvalidMint.into());
+    }
+
+    // Verify VAA
+    if accs.vaa.to_chain != CHAIN_ID_SOLANA {
+        return Err(InvalidChain.into());
+    }
+    if accs.vaa.to != accs.to_owner.info().key.to_bytes() {
+        return Err(InvalidRecipient.into());
+    }
+
+    // VAA-specified recipient must be token account owner
+    if *accs.to_owner.info().key != accs.to.owner {
+        return Err(InvalidRecipient.into());
+    }
+
+    accs.vaa.verify(ctx.program_id)?;
+    accs.vaa.claim(ctx, accs.payer.key)?;
+
+    // Mint tokens
+    let mint_ix = spl_token::instruction::mint_to(
+        &spl_token::id(),
+        accs.mint.info().key,
+        accs.to.info().key,
+        accs.mint_authority.key,
+        &[],
+        accs.vaa
+            .amount
+            .as_u64()
+            .checked_sub(accs.vaa.fee.as_u64())
+            .unwrap(),
+    )?;
+    invoke_seeded(&mint_ix, ctx, &accs.mint_authority, None)?;
+
+    // Mint fees
+    let mint_ix = spl_token::instruction::mint_to(
+        &spl_token::id(),
+        accs.mint.info().key,
+        accs.to_fees.info().key,
+        accs.mint_authority.key,
+        &[],
+        accs.vaa.fee.as_u64(),
+    )?;
+    invoke_seeded(&mint_ix, ctx, &accs.mint_authority, None)?;
+
+    Ok(())
+}

+ 225 - 74
solana/modules/token_bridge/program/src/api/transfer.rs

@@ -13,7 +13,10 @@ use crate::{
         WrappedMint,
         WrappedTokenMeta,
     },
-    messages::PayloadTransfer,
+    messages::{
+        PayloadTransfer,
+        PayloadTransferWithPayload,
+    },
     types::*,
     TokenBridgeError,
     TokenBridgeError::{
@@ -68,6 +71,8 @@ use std::ops::{
     DerefMut,
 };
 
+pub type TransferNativeWithPayload<'b> = TransferNative<'b>;
+
 #[derive(FromAccounts)]
 pub struct TransferNative<'b> {
     pub payer: Mut<Signer<AccountInfo<'b>>>,
@@ -134,6 +139,113 @@ pub fn transfer_native(
         return Err(InvalidChain.into());
     }
 
+    let (amount, fee) = verify_and_execute_native_transfers(ctx, accs, data.amount, data.fee)?;
+
+    // Post message
+    let payload = PayloadTransfer {
+        amount: U256::from(amount),
+        token_address: accs.mint.info().key.to_bytes(),
+        token_chain: CHAIN_ID_SOLANA,
+        to: data.target_address,
+        to_chain: data.target_chain,
+        fee: U256::from(fee),
+    };
+    let params = (
+        bridge::instruction::Instruction::PostMessage,
+        PostMessageData {
+            nonce: data.nonce,
+            payload: payload.try_to_vec()?,
+            consistency_level: ConsistencyLevel::Finalized,
+        },
+    );
+
+    let ix = Instruction::new_with_bytes(
+        accs.config.wormhole_bridge,
+        params.try_to_vec()?.as_slice(),
+        vec![
+            AccountMeta::new(*accs.bridge.info().key, false),
+            AccountMeta::new(*accs.message.key, true),
+            AccountMeta::new_readonly(*accs.emitter.key, true),
+            AccountMeta::new(*accs.sequence.key, false),
+            AccountMeta::new(*accs.payer.key, true),
+            AccountMeta::new(*accs.fee_collector.key, false),
+            AccountMeta::new_readonly(*accs.clock.info().key, false),
+            AccountMeta::new_readonly(solana_program::system_program::id(), false),
+            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
+        ],
+    );
+    invoke_seeded(&ix, ctx, &accs.emitter, None)?;
+
+    Ok(())
+}
+
+#[derive(BorshDeserialize, BorshSerialize, Default)]
+pub struct TransferNativeWithPayloadData {
+    pub nonce: u32,
+    pub amount: u64,
+    pub fee: u64,
+    pub target_address: Address,
+    pub target_chain: ChainID,
+    pub payload: Vec<u8>,
+}
+
+pub fn transfer_native_with_payload(
+    ctx: &ExecutionContext,
+    accs: &mut TransferNative,
+    data: TransferNativeWithPayloadData,
+) -> Result<()> {
+    // Prevent transferring to the same chain.
+    if data.target_chain == CHAIN_ID_SOLANA {
+        return Err(InvalidChain.into());
+    }
+
+    let (amount, fee) = verify_and_execute_native_transfers(ctx, accs, data.amount, data.fee)?;
+
+    // Post message
+    let payload = PayloadTransferWithPayload {
+        amount: U256::from(amount),
+        token_address: accs.mint.info().key.to_bytes(),
+        token_chain: CHAIN_ID_SOLANA,
+        to: data.target_address,
+        to_chain: data.target_chain,
+        fee: U256::from(fee),
+        payload: data.payload,
+    };
+    let params = (
+        bridge::instruction::Instruction::PostMessage,
+        PostMessageData {
+            nonce: data.nonce,
+            payload: payload.try_to_vec()?,
+            consistency_level: ConsistencyLevel::Finalized,
+        },
+    );
+
+    let ix = Instruction::new_with_bytes(
+        accs.config.wormhole_bridge,
+        params.try_to_vec()?.as_slice(),
+        vec![
+            AccountMeta::new(*accs.bridge.info().key, false),
+            AccountMeta::new(*accs.message.key, true),
+            AccountMeta::new_readonly(*accs.emitter.key, true),
+            AccountMeta::new(*accs.sequence.key, false),
+            AccountMeta::new(*accs.payer.key, true),
+            AccountMeta::new(*accs.fee_collector.key, false),
+            AccountMeta::new_readonly(*accs.clock.info().key, false),
+            AccountMeta::new_readonly(solana_program::system_program::id(), false),
+            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
+        ],
+    );
+    invoke_seeded(&ix, ctx, &accs.emitter, None)?;
+
+    Ok(())
+}
+
+pub fn verify_and_execute_native_transfers(
+    ctx: &ExecutionContext,
+    accs: &mut TransferNative,
+    raw_amount: u64,
+    raw_fee: u64,
+) -> Result<(u64, u64)> {
     // Verify that the custody account is derived correctly
     let derivation_data: CustodyAccountDerivationData = (&*accs).into();
     accs.custody
@@ -145,7 +257,7 @@ pub fn transfer_native(
     }
 
     // Fee must be less than amount
-    if data.fee > data.amount {
+    if raw_fee > raw_amount {
         return Err(InvalidFee.into());
     }
 
@@ -171,8 +283,8 @@ pub fn transfer_native(
 
     let trunc_divisor = 10u64.pow(8.max(accs.mint.decimals as u32) - 8);
     // Truncate to 8 decimals
-    let amount: u64 = data.amount / trunc_divisor;
-    let fee: u64 = data.fee / trunc_divisor;
+    let amount: u64 = raw_amount / trunc_divisor;
+    let fee: u64 = raw_fee / trunc_divisor;
     // Untruncate the amount to drop the remainder so we don't  "burn" user's funds.
     let amount_trunc: u64 = amount * trunc_divisor;
 
@@ -195,42 +307,7 @@ pub fn transfer_native(
     );
     invoke(&transfer_ix, ctx.accounts)?;
 
-    // Post message
-    let payload = PayloadTransfer {
-        amount: U256::from(amount),
-        token_address: accs.mint.info().key.to_bytes(),
-        token_chain: CHAIN_ID_SOLANA,
-        to: data.target_address,
-        to_chain: data.target_chain,
-        fee: U256::from(fee),
-    };
-    let params = (
-        bridge::instruction::Instruction::PostMessage,
-        PostMessageData {
-            nonce: data.nonce,
-            payload: payload.try_to_vec()?,
-            consistency_level: ConsistencyLevel::Finalized,
-        },
-    );
-
-    let ix = Instruction::new_with_bytes(
-        accs.config.wormhole_bridge,
-        params.try_to_vec()?.as_slice(),
-        vec![
-            AccountMeta::new(*accs.bridge.info().key, false),
-            AccountMeta::new(*accs.message.key, true),
-            AccountMeta::new_readonly(*accs.emitter.key, true),
-            AccountMeta::new(*accs.sequence.key, false),
-            AccountMeta::new(*accs.payer.key, true),
-            AccountMeta::new(*accs.fee_collector.key, false),
-            AccountMeta::new_readonly(*accs.clock.info().key, false),
-            AccountMeta::new_readonly(solana_program::system_program::id(), false),
-            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
-        ],
-    );
-    invoke_seeded(&ix, ctx, &accs.emitter, None)?;
-
-    Ok(())
+    Ok((amount, fee))
 }
 
 #[derive(FromAccounts)]
@@ -263,6 +340,8 @@ pub struct TransferWrapped<'b> {
     pub clock: Sysvar<'b, Clock>,
 }
 
+pub type TransferWrappedWithPayload<'b> = TransferWrapped<'b>;
+
 impl<'a> From<&TransferWrapped<'a>> for WrappedDerivationData {
     fn from(accs: &TransferWrapped<'a>) -> Self {
         WrappedDerivationData {
@@ -302,54 +381,77 @@ pub fn transfer_wrapped(
         return Err(InvalidChain.into());
     }
 
-    // Verify that the from account is owned by the from_owner
-    if &accs.from.owner != accs.from_owner.key {
-        return Err(WrongAccountOwner.into());
-    }
+    verify_and_execute_wrapped_transfers(ctx, accs, data.amount, data.fee)?;
 
-    // Verify mints
-    if accs.mint.info().key != &accs.from.mint {
-        return Err(TokenBridgeError::InvalidMint.into());
-    }
+    // Post message
+    let payload = PayloadTransfer {
+        amount: U256::from(data.amount),
+        token_address: accs.wrapped_meta.token_address,
+        token_chain: accs.wrapped_meta.chain,
+        to: data.target_address,
+        to_chain: data.target_chain,
+        fee: U256::from(data.fee),
+    };
+    let params = (
+        bridge::instruction::Instruction::PostMessage,
+        PostMessageData {
+            nonce: data.nonce,
+            payload: payload.try_to_vec()?,
+            consistency_level: ConsistencyLevel::Finalized,
+        },
+    );
 
-    // Fee must be less than amount
-    if data.fee > data.amount {
-        return Err(InvalidFee.into());
-    }
+    let ix = Instruction::new_with_bytes(
+        accs.config.wormhole_bridge,
+        params.try_to_vec()?.as_slice(),
+        vec![
+            AccountMeta::new(*accs.bridge.info().key, false),
+            AccountMeta::new(*accs.message.key, true),
+            AccountMeta::new_readonly(*accs.emitter.key, true),
+            AccountMeta::new(*accs.sequence.key, false),
+            AccountMeta::new(*accs.payer.key, true),
+            AccountMeta::new(*accs.fee_collector.key, false),
+            AccountMeta::new_readonly(*accs.clock.info().key, false),
+            AccountMeta::new_readonly(solana_program::system_program::id(), false),
+            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
+        ],
+    );
+    invoke_seeded(&ix, ctx, &accs.emitter, None)?;
 
-    // Verify that meta is correct
-    let derivation_data: WrappedMetaDerivationData = (&*accs).into();
-    accs.wrapped_meta
-        .verify_derivation(ctx.program_id, &derivation_data)?;
+    Ok(())
+}
 
-    // Burn tokens
-    let burn_ix = spl_token::instruction::burn(
-        &spl_token::id(),
-        accs.from.info().key,
-        accs.mint.info().key,
-        accs.authority_signer.key,
-        &[],
-        data.amount,
-    )?;
-    invoke_seeded(&burn_ix, ctx, &accs.authority_signer, None)?;
+#[derive(BorshDeserialize, BorshSerialize, Default)]
+pub struct TransferWrappedWithPayloadData {
+    pub nonce: u32,
+    pub amount: u64,
+    pub fee: u64,
+    pub target_address: Address,
+    pub target_chain: ChainID,
+    pub payload: Vec<u8>,
+}
 
-    // Pay fee
-    let transfer_ix = solana_program::system_instruction::transfer(
-        accs.payer.key,
-        accs.fee_collector.key,
-        accs.bridge.config.fee,
-    );
+pub fn transfer_wrapped_with_payload(
+    ctx: &ExecutionContext,
+    accs: &mut TransferWrapped,
+    data: TransferWrappedWithPayloadData,
+) -> Result<()> {
+    // Prevent transferring to the same chain.
+    if data.target_chain == CHAIN_ID_SOLANA {
+        return Err(InvalidChain.into());
+    }
 
-    invoke(&transfer_ix, ctx.accounts)?;
+    verify_and_execute_wrapped_transfers(ctx, accs, data.amount, data.fee)?;
 
     // Post message
-    let payload = PayloadTransfer {
+    let payload = PayloadTransferWithPayload {
         amount: U256::from(data.amount),
         token_address: accs.wrapped_meta.token_address,
         token_chain: accs.wrapped_meta.chain,
         to: data.target_address,
         to_chain: data.target_chain,
         fee: U256::from(data.fee),
+        payload: data.payload,
     };
     let params = (
         bridge::instruction::Instruction::PostMessage,
@@ -379,3 +481,52 @@ pub fn transfer_wrapped(
 
     Ok(())
 }
+
+pub fn verify_and_execute_wrapped_transfers(
+    ctx: &ExecutionContext,
+    accs: &mut TransferWrapped,
+    amount: u64,
+    fee: u64,
+) -> Result<()> {
+    // Verify that the from account is owned by the from_owner
+    if &accs.from.owner != accs.from_owner.key {
+        return Err(WrongAccountOwner.into());
+    }
+
+    // Verify mints
+    if accs.mint.info().key != &accs.from.mint {
+        return Err(TokenBridgeError::InvalidMint.into());
+    }
+
+    // Fee must be less than amount
+    if fee > amount {
+        return Err(InvalidFee.into());
+    }
+
+    // Verify that meta is correct
+    let derivation_data: WrappedMetaDerivationData = (&*accs).into();
+    accs.wrapped_meta
+        .verify_derivation(ctx.program_id, &derivation_data)?;
+
+    // Burn tokens
+    let burn_ix = spl_token::instruction::burn(
+        &spl_token::id(),
+        accs.from.info().key,
+        accs.mint.info().key,
+        accs.authority_signer.key,
+        &[],
+        amount,
+    )?;
+    invoke_seeded(&burn_ix, ctx, &accs.authority_signer, None)?;
+
+    // Pay fee
+    let transfer_ix = solana_program::system_instruction::transfer(
+        accs.payer.key,
+        accs.fee_collector.key,
+        accs.bridge.config.fee,
+    );
+
+    invoke(&transfer_ix, ctx.accounts)?;
+
+    Ok(())
+}

+ 234 - 19
solana/modules/token_bridge/program/src/instructions.rs

@@ -32,7 +32,12 @@ use crate::{
         PayloadAssetMeta,
         PayloadGovernanceRegisterChain,
         PayloadTransfer,
+        PayloadTransferWithPayload,
     },
+    CompleteNativeWithPayloadData,
+    CompleteWrappedWithPayloadData,
+    TransferNativeWithPayloadData,
+    TransferWrappedWithPayloadData,
 };
 use borsh::BorshSerialize;
 use bridge::{
@@ -72,7 +77,10 @@ use solitaire::{
     AccountState,
 };
 use spl_token::state::Mint;
-use std::str::FromStr;
+use std::{
+    cmp::min,
+    str::FromStr,
+};
 
 pub fn initialize(
     program_id: Pubkey,
@@ -147,6 +155,66 @@ pub fn complete_native(
     })
 }
 
+pub fn complete_native_with_payload(
+    program_id: Pubkey,
+    bridge_id: Pubkey,
+    payer: Pubkey,
+    message_key: Pubkey,
+    vaa: PostVAAData,
+    to: Pubkey,
+    to_owner: Pubkey,
+    fee_recipient: Option<Pubkey>,
+    mint: Pubkey,
+    data: CompleteNativeWithPayloadData,
+) -> solitaire::Result<Instruction> {
+    let config_key = ConfigAccount::<'_, { AccountState::Uninitialized }>::key(None, &program_id);
+    let (message_acc, claim_acc) = claimable_vaa(program_id, message_key, vaa.clone());
+    let endpoint = Endpoint::<'_, { AccountState::Initialized }>::key(
+        &EndpointDerivationData {
+            emitter_chain: vaa.emitter_chain,
+            emitter_address: vaa.emitter_address,
+        },
+        &program_id,
+    );
+    let custody_key = CustodyAccount::<'_, { AccountState::Initialized }>::key(
+        &CustodyAccountDerivationData { mint },
+        &program_id,
+    );
+    let custody_signer_key = CustodySigner::key(None, &program_id);
+
+    Ok(Instruction {
+        program_id,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new_readonly(config_key, false),
+            message_acc,
+            claim_acc,
+            AccountMeta::new_readonly(endpoint, false),
+            AccountMeta::new(to, false),
+            AccountMeta::new_readonly(to_owner, true),
+            if let Some(fee_r) = fee_recipient {
+                AccountMeta::new(fee_r, false)
+            } else {
+                AccountMeta::new(to, false)
+            },
+            AccountMeta::new(custody_key, false),
+            AccountMeta::new_readonly(mint, false),
+            AccountMeta::new_readonly(custody_signer_key, false),
+            // Dependencies
+            AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false),
+            AccountMeta::new_readonly(solana_program::system_program::id(), false),
+            // Program
+            AccountMeta::new_readonly(bridge_id, false),
+            AccountMeta::new_readonly(spl_token::id(), false),
+        ],
+        data: (
+            crate::instruction::Instruction::CompleteNativeWithPayload,
+            data,
+        )
+            .try_to_vec()?,
+    })
+}
+
 pub fn complete_wrapped(
     program_id: Pubkey,
     bridge_id: Pubkey,
@@ -208,6 +276,73 @@ pub fn complete_wrapped(
     })
 }
 
+pub fn complete_wrapped_with_payload(
+    program_id: Pubkey,
+    bridge_id: Pubkey,
+    payer: Pubkey,
+    message_key: Pubkey,
+    vaa: PostVAAData,
+    payload: PayloadTransferWithPayload,
+    to: Pubkey,
+    to_owner: Pubkey,
+    fee_recipient: Option<Pubkey>,
+    data: CompleteWrappedWithPayloadData,
+) -> solitaire::Result<Instruction> {
+    let config_key = ConfigAccount::<'_, { AccountState::Uninitialized }>::key(None, &program_id);
+    let (message_acc, claim_acc) = claimable_vaa(program_id, message_key, vaa.clone());
+    let endpoint = Endpoint::<'_, { AccountState::Initialized }>::key(
+        &EndpointDerivationData {
+            emitter_chain: vaa.emitter_chain,
+            emitter_address: vaa.emitter_address,
+        },
+        &program_id,
+    );
+    let mint_key = WrappedMint::<'_, { AccountState::Uninitialized }>::key(
+        &WrappedDerivationData {
+            token_chain: payload.token_chain,
+            token_address: payload.token_address,
+        },
+        &program_id,
+    );
+    let meta_key = WrappedTokenMeta::<'_, { AccountState::Uninitialized }>::key(
+        &WrappedMetaDerivationData { mint_key },
+        &program_id,
+    );
+    let mint_authority_key = MintSigner::key(None, &program_id);
+
+    Ok(Instruction {
+        program_id,
+        accounts: vec![
+            AccountMeta::new(payer, true),
+            AccountMeta::new_readonly(config_key, false),
+            message_acc,
+            claim_acc,
+            AccountMeta::new_readonly(endpoint, false),
+            AccountMeta::new(to, false),
+            AccountMeta::new_readonly(to_owner, true),
+            if let Some(fee_r) = fee_recipient {
+                AccountMeta::new(fee_r, false)
+            } else {
+                AccountMeta::new(to, false)
+            },
+            AccountMeta::new(mint_key, false),
+            AccountMeta::new_readonly(meta_key, false),
+            AccountMeta::new_readonly(mint_authority_key, false),
+            // Dependencies
+            AccountMeta::new_readonly(solana_program::sysvar::rent::id(), false),
+            AccountMeta::new_readonly(solana_program::system_program::id(), false),
+            // Program
+            AccountMeta::new_readonly(bridge_id, false),
+            AccountMeta::new_readonly(spl_token::id(), false),
+        ],
+        data: (
+            crate::instruction::Instruction::CompleteWrappedWithPayload,
+            data,
+        )
+            .try_to_vec()?,
+    })
+}
+
 pub fn create_wrapped(
     program_id: Pubkey,
     bridge_id: Pubkey,
@@ -333,6 +468,50 @@ pub fn transfer_native(
     from: Pubkey,
     mint: Pubkey,
     data: TransferNativeData,
+) -> solitaire::Result<Instruction> {
+    transfer_native_raw(
+        program_id,
+        bridge_id,
+        payer,
+        message_key,
+        from,
+        mint,
+        (crate::instruction::Instruction::TransferNative, data).try_to_vec()?,
+    )
+}
+
+pub fn transfer_native_with_payload(
+    program_id: Pubkey,
+    bridge_id: Pubkey,
+    payer: Pubkey,
+    message_key: Pubkey,
+    from: Pubkey,
+    mint: Pubkey,
+    data: TransferNativeWithPayloadData,
+) -> solitaire::Result<Instruction> {
+    transfer_native_raw(
+        program_id,
+        bridge_id,
+        payer,
+        message_key,
+        from,
+        mint,
+        (
+            crate::instruction::Instruction::TransferNativeWithPayload,
+            data,
+        )
+            .try_to_vec()?,
+    )
+}
+
+fn transfer_native_raw(
+    program_id: Pubkey,
+    bridge_id: Pubkey,
+    payer: Pubkey,
+    message_key: Pubkey,
+    from: Pubkey,
+    mint: Pubkey,
+    data: Vec<u8>,
 ) -> solitaire::Result<Instruction> {
     let config_key = ConfigAccount::<'_, { AccountState::Uninitialized }>::key(None, &program_id);
     let custody_key = CustodyAccount::<'_, { AccountState::Initialized }>::key(
@@ -346,14 +525,6 @@ pub fn transfer_native(
 
     // Bridge keys
     let bridge_config = Bridge::<'_, { AccountState::Uninitialized }>::key(None, &bridge_id);
-    let payload = PayloadTransfer {
-        amount: U256::from(data.amount),
-        token_address: mint.to_bytes(),
-        token_chain: 1,
-        to: data.target_address,
-        to_chain: data.target_chain,
-        fee: U256::from(data.fee),
-    };
     let sequence_key = Sequence::key(
         &SequenceDerivationData {
             emitter_key: &emitter_key,
@@ -385,7 +556,7 @@ pub fn transfer_native(
             AccountMeta::new_readonly(bridge_id, false),
             AccountMeta::new_readonly(spl_token::id(), false),
         ],
-        data: (crate::instruction::Instruction::TransferNative, data).try_to_vec()?,
+        data,
     })
 }
 
@@ -399,6 +570,58 @@ pub fn transfer_wrapped(
     token_chain: u16,
     token_address: ForeignAddress,
     data: TransferWrappedData,
+) -> solitaire::Result<Instruction> {
+    transfer_wrapped_raw(
+        program_id,
+        bridge_id,
+        payer,
+        message_key,
+        from,
+        from_owner,
+        token_chain,
+        token_address,
+        (crate::instruction::Instruction::TransferWrapped, data).try_to_vec()?,
+    )
+}
+
+pub fn transfer_wrapped_with_payload(
+    program_id: Pubkey,
+    bridge_id: Pubkey,
+    payer: Pubkey,
+    message_key: Pubkey,
+    from: Pubkey,
+    from_owner: Pubkey,
+    token_chain: u16,
+    token_address: ForeignAddress,
+    data: TransferWrappedWithPayloadData,
+) -> solitaire::Result<Instruction> {
+    transfer_wrapped_raw(
+        program_id,
+        bridge_id,
+        payer,
+        message_key,
+        from,
+        from_owner,
+        token_chain,
+        token_address,
+        (
+            crate::instruction::Instruction::TransferWrappedWithPayload,
+            data,
+        )
+            .try_to_vec()?,
+    )
+}
+
+fn transfer_wrapped_raw(
+    program_id: Pubkey,
+    bridge_id: Pubkey,
+    payer: Pubkey,
+    message_key: Pubkey,
+    from: Pubkey,
+    from_owner: Pubkey,
+    token_chain: u16,
+    token_address: ForeignAddress,
+    data: Vec<u8>,
 ) -> solitaire::Result<Instruction> {
     let config_key = ConfigAccount::<'_, { AccountState::Uninitialized }>::key(None, &program_id);
 
@@ -421,14 +644,6 @@ pub fn transfer_wrapped(
 
     // Bridge keys
     let bridge_config = Bridge::<'_, { AccountState::Uninitialized }>::key(None, &bridge_id);
-    let payload = PayloadTransfer {
-        amount: U256::from(data.amount),
-        token_address,
-        token_chain,
-        to: data.target_address,
-        to_chain: data.target_chain,
-        fee: U256::from(data.fee),
-    };
     let sequence_key = Sequence::key(
         &SequenceDerivationData {
             emitter_key: &emitter_key,
@@ -460,7 +675,7 @@ pub fn transfer_wrapped(
             AccountMeta::new_readonly(bridge_id, false),
             AccountMeta::new_readonly(spl_token::id(), false),
         ],
-        data: (crate::instruction::Instruction::TransferWrapped, data).try_to_vec()?,
+        data,
     })
 }
 

+ 22 - 7
solana/modules/token_bridge/program/src/lib.rs

@@ -1,4 +1,3 @@
-
 #![feature(adt_const_params)]
 #![deny(unused_must_use)]
 
@@ -23,19 +22,27 @@ pub mod types;
 pub use api::{
     attest_token,
     complete_native,
+    complete_native_with_payload,
     complete_wrapped,
+    complete_wrapped_with_payload,
     create_wrapped,
     initialize,
     register_chain,
     transfer_native,
+    transfer_native_with_payload,
     transfer_wrapped,
+    transfer_wrapped_with_payload,
     upgrade_contract,
     AttestToken,
     AttestTokenData,
     CompleteNative,
     CompleteNativeData,
+    CompleteNativeWithPayload,
+    CompleteNativeWithPayloadData,
     CompleteWrapped,
     CompleteWrappedData,
+    CompleteWrappedWithPayload,
+    CompleteWrappedWithPayloadData,
     CreateWrapped,
     CreateWrappedData,
     Initialize,
@@ -44,8 +51,12 @@ pub use api::{
     RegisterChainData,
     TransferNative,
     TransferNativeData,
+    TransferNativeWithPayload,
+    TransferNativeWithPayloadData,
     TransferWrapped,
     TransferWrappedData,
+    TransferWrappedWithPayload,
+    TransferWrappedWithPayloadData,
     UpgradeContract,
     UpgradeContractData,
 };
@@ -87,13 +98,17 @@ impl From<TokenBridgeError> for SolitaireError {
 }
 
 solitaire! {
-    Initialize      => initialize,
-    AttestToken     => attest_token,
-    CompleteNative  => complete_native,
+    Initialize => initialize,
+    AttestToken => attest_token,
+    CompleteNative => complete_native,
     CompleteWrapped => complete_wrapped,
     TransferWrapped => transfer_wrapped,
-    TransferNative  => transfer_native,
-    RegisterChain   => register_chain,
-    CreateWrapped   => create_wrapped,
+    TransferNative => transfer_native,
+    RegisterChain => register_chain,
+    CreateWrapped => create_wrapped,
     UpgradeContract => upgrade_contract,
+    CompleteNativeWithPayload => complete_native_with_payload,
+    CompleteWrappedWithPayload => complete_wrapped_with_payload,
+    TransferWrappedWithPayload => transfer_wrapped_with_payload,
+    TransferNativeWithPayload => transfer_native_with_payload,
 }

+ 83 - 0
solana/modules/token_bridge/program/src/messages.rs

@@ -122,6 +122,89 @@ impl SerializePayload for PayloadTransfer {
     }
 }
 
+impl DeserializePayload for PayloadTransferWithPayload {
+    fn deserialize(buf: &mut &[u8]) -> Result<Self, SolitaireError> {
+        let mut v = Cursor::new(buf);
+
+        if v.read_u8()? != 3 {
+            return Err(SolitaireError::Custom(0));
+        };
+
+        let mut am_data: [u8; 32] = [0; 32];
+        v.read_exact(&mut am_data)?;
+        let amount = U256::from_big_endian(&am_data);
+
+        let mut token_address = Address::default();
+        v.read_exact(&mut token_address)?;
+
+        let token_chain = v.read_u16::<BigEndian>()?;
+
+        let mut to = Address::default();
+        v.read_exact(&mut to)?;
+
+        let to_chain = v.read_u16::<BigEndian>()?;
+
+        let mut fee_data: [u8; 32] = [0; 32];
+        v.read_exact(&mut fee_data)?;
+        let fee = U256::from_big_endian(&fee_data);
+
+        let mut payload = vec![];
+        v.read_to_end(&mut payload)?;
+
+        Ok(PayloadTransferWithPayload {
+            amount,
+            token_address,
+            token_chain,
+            to,
+            to_chain,
+            fee,
+            payload,
+        })
+    }
+}
+
+impl SerializePayload for PayloadTransferWithPayload {
+    fn serialize<W: Write>(&self, writer: &mut W) -> Result<(), SolitaireError> {
+        // Payload ID
+        writer.write_u8(3)?;
+
+        let mut am_data: [u8; 32] = [0; 32];
+        self.amount.to_big_endian(&mut am_data);
+        writer.write(&am_data)?;
+
+        writer.write(&self.token_address)?;
+        writer.write_u16::<BigEndian>(self.token_chain)?;
+        writer.write(&self.to)?;
+        writer.write_u16::<BigEndian>(self.to_chain)?;
+
+        let mut fee_data: [u8; 32] = [0; 32];
+        self.fee.to_big_endian(&mut fee_data);
+        writer.write(&fee_data)?;
+
+        writer.write(self.payload.as_slice())?;
+
+        Ok(())
+    }
+}
+
+#[derive(PartialEq, Debug, Clone)]
+pub struct PayloadTransferWithPayload {
+    // Amount being transferred (big-endian uint256)
+    pub amount: U256,
+    // Address of the token. Left-zero-padded if shorter than 32 bytes
+    pub token_address: Address,
+    // Chain ID of the token
+    pub token_chain: ChainID,
+    // Address of the recipient. Left-zero-padded if shorter than 32 bytes
+    pub to: Address,
+    // Chain ID of the recipient
+    pub to_chain: ChainID,
+    // Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount.
+    pub fee: U256,
+    // Arbitrary payload
+    pub payload: Vec<u8>,
+}
+
 #[derive(PartialEq, Debug)]
 pub struct PayloadAssetMeta {
     // Address of the token. Left-zero-padded if shorter than 32 bytes

+ 30 - 0
solana/modules/token_bridge/program/src/types.rs

@@ -28,24 +28,44 @@ pub struct Config {
     pub wormhole_bridge: Pubkey,
 }
 
+#[cfg(not(feature = "cpi"))]
 impl Owned for Config {
     fn owner(&self) -> AccountOwner {
         AccountOwner::This
     }
 }
 
+#[cfg(feature = "cpi")]
+impl Owned for Config {
+    fn owner(&self) -> AccountOwner {
+        use solana_program::pubkey::Pubkey;
+        use std::str::FromStr;
+        AccountOwner::Other(Pubkey::from_str(env!("TOKEN_BRIDGE_ADDRESS")).unwrap())
+    }
+}
+
 #[derive(Default, Clone, Copy, BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
 pub struct EndpointRegistration {
     pub chain: ChainID,
     pub contract: Address,
 }
 
+#[cfg(not(feature = "cpi"))]
 impl Owned for EndpointRegistration {
     fn owner(&self) -> AccountOwner {
         AccountOwner::This
     }
 }
 
+#[cfg(feature = "cpi")]
+impl Owned for EndpointRegistration {
+    fn owner(&self) -> AccountOwner {
+        use solana_program::pubkey::Pubkey;
+        use std::str::FromStr;
+        AccountOwner::Other(Pubkey::from_str(env!("TOKEN_BRIDGE_ADDRESS")).unwrap())
+    }
+}
+
 #[derive(Default, Clone, Copy, BorshDeserialize, BorshSerialize, Serialize, Deserialize)]
 pub struct WrappedMeta {
     pub chain: ChainID,
@@ -53,11 +73,21 @@ pub struct WrappedMeta {
     pub original_decimals: u8,
 }
 
+#[cfg(not(feature = "cpi"))]
 impl Owned for WrappedMeta {
     fn owner(&self) -> AccountOwner {
         AccountOwner::This
     }
 }
 
+#[cfg(feature = "cpi")]
+impl Owned for WrappedMeta {
+    fn owner(&self) -> AccountOwner {
+        use solana_program::pubkey::Pubkey;
+        use std::str::FromStr;
+        AccountOwner::Other(Pubkey::from_str(env!("TOKEN_BRIDGE_ADDRESS")).unwrap())
+    }
+}
+
 pack_type!(SplMint, Mint, AccountOwner::Other(spl_token::id()));
 pack_type!(SplAccount, Account, AccountOwner::Other(spl_token::id()));

+ 102 - 0
solana/modules/token_bridge/program/src/wasm.rs

@@ -15,7 +15,9 @@ use crate::{
         create_wrapped,
         register_chain,
         transfer_native,
+        transfer_native_with_payload,
         transfer_wrapped,
+        transfer_wrapped_with_payload,
         upgrade_contract,
     },
     messages::{
@@ -33,7 +35,9 @@ use crate::{
     CreateWrappedData,
     RegisterChainData,
     TransferNativeData,
+    TransferNativeWithPayloadData,
     TransferWrappedData,
+    TransferWrappedWithPayloadData,
 };
 use borsh::BorshDeserialize;
 use bridge::{
@@ -115,6 +119,52 @@ pub fn transfer_native_ix(
     JsValue::from_serde(&ix).unwrap()
 }
 
+#[wasm_bindgen]
+pub fn transfer_native_with_payload_ix(
+    program_id: String,
+    bridge_id: String,
+    payer: String,
+    message: String,
+    from: String,
+    mint: String,
+    nonce: u32,
+    amount: u64,
+    fee: u64,
+    target_address: Vec<u8>,
+    target_chain: u16,
+    payload: Vec<u8>,
+) -> JsValue {
+    let program_id = Pubkey::from_str(program_id.as_str()).unwrap();
+    let bridge_id = Pubkey::from_str(bridge_id.as_str()).unwrap();
+    let payer = Pubkey::from_str(payer.as_str()).unwrap();
+    let message = Pubkey::from_str(message.as_str()).unwrap();
+    let from = Pubkey::from_str(from.as_str()).unwrap();
+    let mint = Pubkey::from_str(mint.as_str()).unwrap();
+
+    let mut target_addr = [0u8; 32];
+    target_addr.copy_from_slice(target_address.as_slice());
+
+    let ix = transfer_native_with_payload(
+        program_id,
+        bridge_id,
+        payer,
+        message,
+        from,
+        mint,
+        TransferNativeWithPayloadData {
+            nonce,
+            amount,
+            fee,
+            target_address: target_addr,
+            target_chain,
+            payload,
+        },
+    )
+    .unwrap();
+
+    JsValue::from_serde(&ix).unwrap()
+}
+
 #[wasm_bindgen]
 pub fn transfer_wrapped_ix(
     program_id: String,
@@ -165,6 +215,58 @@ pub fn transfer_wrapped_ix(
     JsValue::from_serde(&ix).unwrap()
 }
 
+#[wasm_bindgen]
+pub fn transfer_wrapped_with_payload_ix(
+    program_id: String,
+    bridge_id: String,
+    payer: String,
+    message: String,
+    from: String,
+    from_owner: String,
+    token_chain: u16,
+    token_address: Vec<u8>,
+    nonce: u32,
+    amount: u64,
+    fee: u64,
+    target_address: Vec<u8>,
+    target_chain: u16,
+    payload: Vec<u8>,
+) -> JsValue {
+    let program_id = Pubkey::from_str(program_id.as_str()).unwrap();
+    let bridge_id = Pubkey::from_str(bridge_id.as_str()).unwrap();
+    let payer = Pubkey::from_str(payer.as_str()).unwrap();
+    let message = Pubkey::from_str(message.as_str()).unwrap();
+    let from = Pubkey::from_str(from.as_str()).unwrap();
+    let from_owner = Pubkey::from_str(from_owner.as_str()).unwrap();
+
+    let mut target_addr = [0u8; 32];
+    target_addr.copy_from_slice(target_address.as_slice());
+    let mut token_addr = [0u8; 32];
+    token_addr.copy_from_slice(token_address.as_slice());
+
+    let ix = transfer_wrapped_with_payload(
+        program_id,
+        bridge_id,
+        payer,
+        message,
+        from,
+        from_owner,
+        token_chain,
+        token_addr,
+        TransferWrappedWithPayloadData {
+            nonce,
+            amount,
+            fee,
+            target_address: target_addr,
+            target_chain,
+            payload,
+        },
+    )
+    .unwrap();
+
+    JsValue::from_serde(&ix).unwrap()
+}
+
 #[wasm_bindgen]
 pub fn complete_transfer_native_ix(
     program_id: String,