solana: fix after cctp upgrade; uptick anchor 0.29

This commit is contained in:
A5 Pickle 2024-02-07 15:56:18 -06:00
parent 6885f97349
commit 3d6492f8b5
No known key found for this signature in database
GPG Key ID: DD6C727938DE8E65
55 changed files with 7597 additions and 8475 deletions

View File

@ -1,6 +1,6 @@
[toolchain]
anchor_version = "0.29.0" # CLI
solana_version = "1.16.16"
solana_version = "1.16.27"
[features]
seeds = false
@ -25,7 +25,7 @@ wallet = "ts/tests/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json"
test = "npx ts-mocha -p ./tsconfig.json -t 1000000 ts/tests/[0-9]*.ts"
[test]
startup_wait = 16000
startup_wait = 30000
[test.validator]
url = "https://api.devnet.solana.com"
@ -36,11 +36,11 @@ ticks_per_slot = 8
### Forked Wormhole Circle Integration Program
[[test.validator.clone]]
address = "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW"
address = "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d"
### Forked Wormhole Circle Integration PDA -- Custodian
[[test.validator.clone]]
address = "2LtnJESn3gEmte4pEBjnTjWX4Npb8esKKPeyWTN6cJP9"
address = "4tTfYz2SqRcZWqyBk1yHyEPzHjoHNbUErQbifBkLmzbT"
### Wormhole Core Bridge Program (Testnet)
[[test.validator.clone]]

661
solana/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
version = "0.0.1-alpha.9"
version = "0.1.0-alpha.5"
authors = ["Wormhole Contributors"]
license = "Apache-2.0"
homepage = "https://wormhole.com"
@ -22,27 +22,19 @@ version = "0.1.1"
features = ["ruint", "on-chain"]
default-features = false
[workspace.dependencies.wormhole-core-bridge-solana]
version = "0.0.1-alpha.5"
default-features = false
[workspace.dependencies.anchor-lang]
version = "0.28.0"
version = "0.29.0"
features = ["derive", "init-if-needed"]
[workspace.dependencies]
wormhole-io = "0.1.3"
anchor-spl = "0.28.0"
solana-program = "1.16.16"
anchor-spl = "0.29.0"
solana-program = "1.17.20"
hex = "0.4.3"
ruint = "1.9.0"
cfg-if = "1.0"
hex-literal = "0.4.1"
### https://github.com/coral-xyz/anchor/issues/2755
### This dependency must be added for each program.
ahash = "=0.8.6"
[profile.release]
overflow-checks = true
lto = "fat"
@ -51,4 +43,4 @@ codegen-units = 1
[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1
codegen-units = 1

View File

@ -12,14 +12,13 @@ repository.workspace = true
crate-type = ["cdylib", "lib"]
[features]
default = ["cpi", "mainnet"]
default = ["cpi"]
client = []
cpi = ["wormhole-core-bridge-solana/cpi", "dep:anchor-spl", "dep:solana-program"]
mainnet = ["wormhole-core-bridge-solana/mainnet"]
testnet = ["wormhole-core-bridge-solana/testnet"]
cpi = ["dep:anchor-spl", "dep:solana-program"]
mainnet = []
testnet = []
[dependencies]
wormhole-core-bridge-solana.workspace = true
wormhole-io.workspace = true
wormhole-raw-vaas.workspace = true

View File

@ -1,17 +1,15 @@
use anchor_lang::prelude::*;
/// Account context to invoke [receive_token_messenger_minter_message].
#[derive(Accounts)]
pub struct ReceiveTokenMessengerMinterMessage<'info> {
/// Mutable signer. Transaction payer.
#[account(mut, signer)]
pub payer: AccountInfo<'info>,
/// Signer. Specific caller, which must be encoded as the destination caller.
#[account(signer)]
pub caller: AccountInfo<'info>,
/// Seeds must be \["message_transmitter_authority"\] (CCTP Message Transmitter program).
/// Seeds must be \["message_transmitter_authority"\, token_messenger_minter_program] (CCTP
/// Message Transmitter program).
pub message_transmitter_authority: AccountInfo<'info>,
/// Seeds must be \["message_transmitter"\] (CCTP Message Transmitter program).
@ -19,7 +17,6 @@ pub struct ReceiveTokenMessengerMinterMessage<'info> {
/// Mutable. Seeds must be \["used_nonces", remote_domain.to_string(), first_nonce.to_string()\]
/// (CCTP Message Transmitter program).
#[account(mut)]
pub used_nonces: AccountInfo<'info>,
/// CCTP Token Messenger Minter program.
@ -27,6 +24,11 @@ pub struct ReceiveTokenMessengerMinterMessage<'info> {
pub system_program: AccountInfo<'info>,
/// Seeds must be \["__event_authority"\] (CCTP Message Transmitter program)).
pub message_transmitter_event_authority: AccountInfo<'info>,
pub message_transmitter_program: AccountInfo<'info>,
// The following accounts are expected to be passed in as remaining accounts. These accounts are
// meant for the Token Messenger Minter program because the Message Transmitter program performs
// CPI on this program so it can mint tokens.
@ -45,7 +47,6 @@ pub struct ReceiveTokenMessengerMinterMessage<'info> {
pub token_minter: AccountInfo<'info>,
/// Mutable. Seeds must be \["local_token", mint\] (CCTP Token Messenger Minter program).
#[account(mut)]
pub local_token: AccountInfo<'info>,
/// Seeds must be \["token_pair", remote_domain.to_string(), remote_token_address\] (CCTP Token
@ -54,14 +55,15 @@ pub struct ReceiveTokenMessengerMinterMessage<'info> {
/// Mutable. Mint recipient token account, which must be encoded as the mint recipient in the
/// CCTP mesage.
#[account(mut)]
pub mint_recipient: AccountInfo<'info>,
/// Mutable. Seeds must be \["custody", mint\] (CCTP Token Messenger Minter program).
#[account(mut)]
pub custody_token: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
/// Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
pub token_messenger_minter_event_authority: AccountInfo<'info>,
}
/// Method to call the receive message instruction on the CCTP Message Transmitter program, specific
@ -76,13 +78,63 @@ pub fn receive_token_messenger_minter_message<'info>(
const ANCHOR_IX_SELECTOR: [u8; 8] = [38, 144, 127, 225, 31, 225, 238, 25];
solana_program::program::invoke_signed(
&solana_program::instruction::Instruction::new_with_borsh(
crate::cctp::message_transmitter_program::ID,
&(ANCHOR_IX_SELECTOR, args),
ctx.to_account_metas(None),
),
&solana_program::instruction::Instruction {
program_id: crate::cctp::message_transmitter_program::ID,
accounts: ctx.to_account_metas(None),
data: (ANCHOR_IX_SELECTOR, args).try_to_vec()?,
},
&ctx.to_account_infos(),
ctx.signer_seeds,
)
.map_err(Into::into)
}
impl<'info> ToAccountMetas for ReceiveTokenMessengerMinterMessage<'info> {
fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
vec![
AccountMeta::new(self.payer.key(), true),
AccountMeta::new_readonly(self.caller.key(), true),
AccountMeta::new_readonly(self.message_transmitter_authority.key(), false),
AccountMeta::new_readonly(self.message_transmitter_config.key(), false),
AccountMeta::new(self.used_nonces.key(), false),
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
AccountMeta::new_readonly(self.system_program.key(), false),
AccountMeta::new_readonly(self.message_transmitter_event_authority.key(), false),
AccountMeta::new_readonly(self.message_transmitter_program.key(), false),
AccountMeta::new_readonly(self.token_messenger.key(), false),
AccountMeta::new_readonly(self.remote_token_messenger.key(), false),
AccountMeta::new_readonly(self.token_minter.key(), false),
AccountMeta::new(self.local_token.key(), false),
AccountMeta::new_readonly(self.token_pair.key(), false),
AccountMeta::new(self.mint_recipient.key(), false),
AccountMeta::new(self.custody_token.key(), false),
AccountMeta::new_readonly(self.token_program.key(), false),
AccountMeta::new_readonly(self.token_messenger_minter_event_authority.key(), false),
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
]
}
}
impl<'info> ToAccountInfos<'info> for ReceiveTokenMessengerMinterMessage<'info> {
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
vec![
self.payer.clone(),
self.caller.clone(),
self.message_transmitter_authority.clone(),
self.message_transmitter_config.clone(),
self.used_nonces.clone(),
self.token_messenger_minter_program.clone(),
self.system_program.clone(),
self.message_transmitter_event_authority.clone(),
self.token_messenger.clone(),
self.remote_token_messenger.clone(),
self.token_minter.clone(),
self.local_token.clone(),
self.token_pair.clone(),
self.mint_recipient.clone(),
self.custody_token.clone(),
self.token_program.clone(),
self.token_messenger_minter_event_authority.clone(),
]
}
}

View File

@ -13,7 +13,6 @@ pub struct MessageTransmitterConfig {
pub enabled_attesters: Vec<[u8; 32]>,
pub max_message_body_size: u64,
pub next_available_nonce: u64,
pub authority_bump: u8,
}
impl anchor_lang::Discriminator for MessageTransmitterConfig {

View File

@ -1,21 +1,23 @@
use anchor_lang::prelude::*;
/// Account context to invoke [deposit_for_burn_with_caller].
#[derive(Accounts)]
pub struct DepositForBurnWithCaller<'info> {
/// Signer. This account must be the owner of `src_token`.
#[account(signer)]
pub src_token_owner: AccountInfo<'info>,
/// Signer. This account must be the owner of `burn_token`.
//#[account(signer)]
pub burn_token_owner: AccountInfo<'info>,
//#[account(mut, signer)]
pub payer: AccountInfo<'info>,
/// Seeds must be \["sender_authority"\] (CCTP Token Messenger Minter program).
pub token_messenger_minter_sender_authority: AccountInfo<'info>,
/// Mutable. This token account must be owned by `src_token_owner`.
#[account(mut)]
pub src_token: AccountInfo<'info>,
/// Mutable. This token account must be owned by `burn_token_owner`.
//#[account(mut)]
pub burn_token: AccountInfo<'info>,
/// Mutable. Seeds must be \["message_transmitter"\] (CCTP Message Transmitter program).
#[account(mut)]
//#[account(mut)]
pub message_transmitter_config: AccountInfo<'info>,
/// Seeds must be \["token_messenger"\] (CCTP Token Messenger Minter program).
@ -29,13 +31,16 @@ pub struct DepositForBurnWithCaller<'info> {
pub token_minter: AccountInfo<'info>,
/// Mutable. Seeds must be \["local_token", mint\] (CCTP Token Messenger Minter program).
#[account(mut)]
//#[account(mut)]
pub local_token: AccountInfo<'info>,
/// Mutable. Mint to be burned via CCTP.
#[account(mut)]
//#[account(mut)]
pub mint: AccountInfo<'info>,
//#[account(mut, signer)]
pub cctp_message: AccountInfo<'info>,
/// CCTP Message Transmitter program.
pub message_transmitter_program: AccountInfo<'info>,
@ -43,6 +48,11 @@ pub struct DepositForBurnWithCaller<'info> {
pub token_messenger_minter_program: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
pub system_program: AccountInfo<'info>,
/// Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
pub event_authority: AccountInfo<'info>,
}
/// Parameters to invoke [deposit_for_burn_with_caller].
@ -81,13 +91,59 @@ pub fn deposit_for_burn_with_caller<'info>(
const ANCHOR_IX_SELECTOR: [u8; 8] = [167, 222, 19, 114, 85, 21, 14, 118];
solana_program::program::invoke_signed(
&solana_program::instruction::Instruction::new_with_borsh(
crate::cctp::token_messenger_minter_program::ID,
&(ANCHOR_IX_SELECTOR, args),
ctx.to_account_metas(None),
),
&solana_program::instruction::Instruction {
program_id: crate::cctp::token_messenger_minter_program::ID,
accounts: ctx.to_account_metas(None),
data: (ANCHOR_IX_SELECTOR, args).try_to_vec()?,
},
&ctx.to_account_infos(),
ctx.signer_seeds,
)
.map_err(Into::into)
}
impl<'info> ToAccountMetas for DepositForBurnWithCaller<'info> {
fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
vec![
AccountMeta::new_readonly(self.burn_token_owner.key(), true),
AccountMeta::new(self.payer.key(), true),
AccountMeta::new_readonly(self.token_messenger_minter_sender_authority.key(), false),
AccountMeta::new(self.burn_token.key(), false),
AccountMeta::new(self.message_transmitter_config.key(), false),
AccountMeta::new_readonly(self.token_messenger.key(), false),
AccountMeta::new_readonly(self.remote_token_messenger.key(), false),
AccountMeta::new_readonly(self.token_minter.key(), false),
AccountMeta::new(self.local_token.key(), false),
AccountMeta::new(self.mint.key(), false),
AccountMeta::new(self.cctp_message.key(), true),
AccountMeta::new_readonly(self.message_transmitter_program.key(), false),
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
AccountMeta::new_readonly(self.token_program.key(), false),
AccountMeta::new_readonly(self.system_program.key(), false),
AccountMeta::new_readonly(self.event_authority.key(), false),
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
]
}
}
impl<'info> ToAccountInfos<'info> for DepositForBurnWithCaller<'info> {
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
vec![
self.burn_token_owner.clone(),
self.payer.clone(),
self.token_messenger_minter_sender_authority.clone(),
self.burn_token.clone(),
self.message_transmitter_config.clone(),
self.token_messenger.clone(),
self.remote_token_messenger.clone(),
self.token_minter.clone(),
self.local_token.clone(),
self.mint.clone(),
self.cctp_message.clone(),
self.message_transmitter_program.clone(),
self.token_program.clone(),
self.system_program.clone(),
self.event_authority.clone(),
]
}
}

View File

@ -7,8 +7,8 @@ pub struct LocalToken {
pub burn_limit_per_message: u64,
pub messages_sent: u64,
pub messages_received: u64,
pub amount_sent: u64,
pub amount_received: u64,
pub amount_sent: u128,
pub amount_received: u128,
pub bump: u8,
pub custody_bump: u8,
}

View File

@ -4,7 +4,7 @@ use wormhole_io::TypePrefixedPayload;
/// Arguments used to burn Circle-supported tokens and publish a Wormhole Core Bridge message.
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
pub struct BurnAndPublishArgs<P: TypePrefixedPayload> {
pub struct BurnAndPublishArgs {
/// Token account where assets originated from. This pubkey is encoded in the [Deposit] message.
/// If this will be useful to an integrator, he should encode where the assets have been burned
/// from if it was not burned directly when calling [burn_and_publish].
@ -30,7 +30,7 @@ pub struct BurnAndPublishArgs<P: TypePrefixedPayload> {
/// Arbitrary payload, which can be used to encode instructions or data for another network's
/// smart contract.
pub payload: P,
pub payload: Vec<u8>,
}
/// Method to publish a Wormhole Core Bridge message alongside a CCTP message that burns a
@ -40,7 +40,7 @@ pub struct BurnAndPublishArgs<P: TypePrefixedPayload> {
/// assets originated from. A program calling this method will not necessarily be burning assets
/// from this token account directly. So this field is used to indicate the origin of the burned
/// assets.
pub fn burn_and_publish<'info, P>(
pub fn burn_and_publish<'info>(
cctp_ctx: CpiContext<
'_,
'_,
@ -49,11 +49,8 @@ pub fn burn_and_publish<'info, P>(
cctp::token_messenger_minter_program::cpi::DepositForBurnWithCaller<'info>,
>,
wormhole_ctx: CpiContext<'_, '_, '_, 'info, core_bridge_program::cpi::PostMessage<'info>>,
args: BurnAndPublishArgs<P>,
) -> Result<u64>
where
P: TypePrefixedPayload,
{
args: BurnAndPublishArgs,
) -> Result<u64> {
let BurnAndPublishArgs {
burn_source,
destination_caller,
@ -64,7 +61,7 @@ where
payload,
} = args;
let (source_cctp_domain, cctp_nonce) = {
let cctp_nonce = {
let mut data: &[_] = &cctp_ctx
.accounts
.message_transmitter_config
@ -73,18 +70,31 @@ where
cctp::message_transmitter_program::MessageTransmitterConfig,
>::try_deserialize_unchecked(&mut data)?;
(config.local_domain, config.next_available_nonce)
// Publish message via Core Bridge. This includes paying the message fee.
core_bridge_program::cpi::post_message(
wormhole_ctx,
core_bridge_program::cpi::PostMessageArgs {
nonce: wormhole_message_nonce,
payload: Deposit {
token_address: cctp_ctx.accounts.mint.key.to_bytes(),
amount: ruint::aliases::U256::from(amount),
source_cctp_domain: config.local_domain,
destination_cctp_domain,
cctp_nonce: config.next_available_nonce,
burn_source: burn_source
.unwrap_or(cctp_ctx.accounts.burn_token.key())
.to_bytes(),
mint_recipient,
payload,
}
.to_vec_payload(),
commitment: core_bridge_program::Commitment::Finalized,
},
)?;
config.next_available_nonce
};
let token_address = cctp_ctx.accounts.mint.key.to_bytes();
let burn_source = burn_source
.unwrap_or(cctp_ctx.accounts.src_token.key())
.to_bytes();
// We want to make this call as early as possible because the deposit for burn
// message is an Anchor event (i.e. written to the program log). We hope that integrators will
// not log too much prior to this call because this can push the event out of the log buffer,
// which is 10KB.
cctp::token_messenger_minter_program::cpi::deposit_for_burn_with_caller(
cctp_ctx,
cctp::token_messenger_minter_program::cpi::DepositForBurnWithCallerParams {
@ -95,24 +105,5 @@ where
},
)?;
// Publish message via Core Bridge. This includes paying the message fee.
core_bridge_program::cpi::post_message(
wormhole_ctx,
core_bridge_program::cpi::PostMessageArgs {
nonce: wormhole_message_nonce,
payload: wormhole_io::TypePrefixedPayload::to_vec_payload(&Deposit {
token_address,
amount: ruint::aliases::U256::from(amount),
source_cctp_domain,
destination_cctp_domain,
cctp_nonce,
burn_source,
mint_recipient,
payload,
}),
commitment: core_bridge_program::Commitment::Finalized,
},
)?;
Ok(cctp_nonce)
}

View File

@ -1,6 +1,8 @@
use crate::{cctp::message_transmitter_program, error::WormholeCctpError, utils::CctpMessage};
use crate::{
cctp::message_transmitter_program, error::WormholeCctpError, utils::CctpMessage,
wormhole::core_bridge_program::vaa::VaaAccount,
};
use anchor_lang::prelude::*;
use wormhole_core_bridge_solana::sdk::VaaAccount;
use wormhole_raw_vaas::cctp::WormholeCctpMessage;
/// Method to reconcile a CCTP message with a Wormhole VAA encoding the Wormhole CCTP deposit. After
@ -96,7 +98,7 @@ pub fn verify_vaa_and_mint<'ctx, 'info>(
// Wormhole Core Bridge program. Otherwise, an attacker can create a fake VAA account.
require_keys_eq!(
*vaa.owner,
wormhole_core_bridge_solana::sdk::id(),
crate::wormhole::core_bridge_program::id(),
ErrorCode::ConstraintOwner
);

View File

@ -8,6 +8,8 @@ pub mod cpi;
pub mod error;
pub use wormhole_io as io;
pub mod messages;
pub mod utils;

View File

@ -7,7 +7,7 @@ use ruint::aliases::U256;
use wormhole_io::{Readable, TypePrefixedPayload, Writeable};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Deposit<P: TypePrefixedPayload> {
pub struct Deposit {
pub token_address: [u8; 32],
pub amount: U256,
pub source_cctp_domain: u32,
@ -16,14 +16,14 @@ pub struct Deposit<P: TypePrefixedPayload> {
pub burn_source: [u8; 32],
pub mint_recipient: [u8; 32],
/// NOTE: This payload length is encoded as u16.
pub payload: P,
pub payload: Vec<u8>,
}
impl<P: TypePrefixedPayload> TypePrefixedPayload for Deposit<P> {
impl TypePrefixedPayload for Deposit {
const TYPE: Option<u8> = Some(1);
}
impl<P: TypePrefixedPayload> Readable for Deposit<P> {
impl Readable for Deposit {
const SIZE: Option<usize> = None;
fn read<R>(reader: &mut R) -> io::Result<Self>
@ -40,30 +40,25 @@ impl<P: TypePrefixedPayload> Readable for Deposit<P> {
let mint_recipient = Readable::read(reader)?;
let payload_len = u16::read(reader).map(usize::from)?;
let payload = P::read_payload(reader)?;
if payload.payload_written_size() != payload_len {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"payload length mismatch",
));
} else {
Ok(Self {
token_address,
amount,
source_cctp_domain,
destination_cctp_domain,
cctp_nonce,
burn_source,
mint_recipient,
payload,
})
}
let mut payload = vec![0u8; payload_len];
reader.read_exact(&mut payload)?;
Ok(Self {
token_address,
amount,
source_cctp_domain,
destination_cctp_domain,
cctp_nonce,
burn_source,
mint_recipient,
payload,
})
}
}
impl<P: TypePrefixedPayload> Writeable for Deposit<P> {
impl Writeable for Deposit {
fn written_size(&self) -> usize {
32 + 32 + 4 + 4 + 8 + 32 + 32 + 2 + self.payload.payload_written_size()
32 + 32 + 4 + 4 + 8 + 32 + 32 + 2 + self.payload.len()
}
fn write<W>(&self, writer: &mut W) -> std::io::Result<()>
@ -78,10 +73,10 @@ impl<P: TypePrefixedPayload> Writeable for Deposit<P> {
self.cctp_nonce.write(writer)?;
self.burn_source.write(writer)?;
self.mint_recipient.write(writer)?;
u16::try_from(self.payload.payload_written_size())
u16::try_from(self.payload.len())
.map_err(|_| std::io::ErrorKind::InvalidData.into())
.and_then(|len| len.write(writer))?;
self.payload.write_payload(writer)?;
writer.write_all(&self.payload)?;
Ok(())
}
}
@ -160,7 +155,7 @@ mod test {
mint_recipient: hex!(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
),
payload: payload.clone(),
payload: payload.to_vec_payload(),
};
let encoded = deposit.to_vec_payload();
@ -176,14 +171,14 @@ mod test {
cctp_nonce: parsed.cctp_nonce(),
burn_source: parsed.burn_source(),
mint_recipient: parsed.mint_recipient(),
payload,
payload: payload.to_vec_payload(),
};
assert_eq!(deposit, expected);
// Check for other encoded parameters.
assert_eq!(
usize::from(parsed.payload_len()),
deposit.payload.payload_written_size()
payload.payload_written_size()
);
// TODO: Recover by calling read_payload.

View File

@ -48,3 +48,52 @@ where
&self.0
}
}
/// Wrapper for external account schemas, where an Anchor [Discriminator] and [Owner] are defined.
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
pub struct BoxedExternalAccount<T>(Box<T>)
where
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner;
impl<T> AccountDeserialize for BoxedExternalAccount<T>
where
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner,
{
fn try_deserialize(buf: &mut &[u8]) -> Result<Self> {
require!(buf.len() >= 8, ErrorCode::AccountDidNotDeserialize);
require!(
buf[..8] == T::DISCRIMINATOR,
ErrorCode::AccountDiscriminatorMismatch,
);
Self::try_deserialize_unchecked(buf)
}
fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result<Self> {
Ok(Self(Box::new(T::deserialize(&mut &buf[8..])?)))
}
}
impl<T> AccountSerialize for BoxedExternalAccount<T> where
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner
{
}
impl<T> Owner for BoxedExternalAccount<T>
where
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner,
{
fn owner() -> Pubkey {
T::owner()
}
}
impl<T> std::ops::Deref for BoxedExternalAccount<T>
where
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -1,6 +1,8 @@
use crate::wormhole::core_bridge_program::Commitment;
use anchor_lang::{prelude::*, system_program};
use wormhole_core_bridge_solana::state::Config;
use crate::wormhole::core_bridge_program::{state::Config, Commitment};
use anchor_lang::{
prelude::{borsh::BorshSerialize, *},
system_program,
};
#[derive(Accounts)]
pub struct PostMessage<'info> {
@ -68,11 +70,11 @@ pub fn post_message<'info>(
const IX_SELECTOR: u8 = 1;
solana_program::program::invoke_signed(
&solana_program::instruction::Instruction::new_with_borsh(
crate::wormhole::core_bridge_program::id(),
&(IX_SELECTOR, args),
ctx.to_account_metas(None),
),
&solana_program::instruction::Instruction {
program_id: crate::wormhole::core_bridge_program::id(),
accounts: ctx.to_account_metas(None),
data: (IX_SELECTOR, args).try_to_vec()?,
},
&ctx.to_account_infos(),
ctx.signer_seeds,
)

View File

@ -1,6 +1,41 @@
pub use wormhole_core_bridge_solana::sdk;
#[cfg(feature = "cpi")]
pub mod cpi;
pub use sdk::{id, Commitment, CoreBridge, VaaAccount, SOLANA_CHAIN};
pub mod state;
pub mod vaa;
pub use vaa::VaaAccount;
use anchor_lang::prelude::*;
pub const SOLANA_CHAIN: u16 = 1;
cfg_if::cfg_if! {
if #[cfg(feature = "localnet")] {
declare_id!("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o");
} else if #[cfg(feature = "mainnet")] {
declare_id!("worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth");
} else if #[cfg(feature = "testnet")] {
declare_id!("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5");
}
}
pub struct CoreBridge;
impl Id for CoreBridge {
fn id() -> Pubkey {
ID
}
}
/// Representation of Solana's commitment levels. This enum is not exhaustive because Wormhole only
/// considers these two commitment levels in its Guardian observation.
///
/// See <https://docs.solana.com/cluster/commitments> for more info.
#[derive(Copy, Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
pub enum Commitment {
/// One confirmation.
Confirmed,
/// 32 confirmations.
Finalized,
}

View File

@ -0,0 +1,22 @@
use anchor_lang::prelude::*;
/// Account used to store the current configuration of the bridge, including tracking Wormhole fee
/// payments. For governance decrees, the guardian set index is used to determine whether a decree
/// was attested for using the latest guardian set.
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, InitSpace)]
pub struct Config {
/// The current guardian set index, used to decide which signature sets to accept.
pub guardian_set_index: u32,
/// Gap. In the old implementation, this was an amount that kept track of message fees that
/// were paid to the program's fee collector.
pub _gap_0: [u8; 8],
/// Period for how long a guardian set is valid after it has been replaced by a new one. This
/// guarantees that VAAs issued by that set can still be submitted for a certain period. In
/// this period we still trust the old guardian set.
pub guardian_set_ttl: u32,
/// Amount of lamports that needs to be paid to the protocol to post a message
pub fee_lamports: u64,
}

View File

@ -0,0 +1,2 @@
mod config;
pub use config::*;

View File

@ -0,0 +1,2 @@
mod zero_copy;
pub use zero_copy::*;

View File

@ -0,0 +1,81 @@
mod posted_vaa_v1;
pub use posted_vaa_v1::*;
use anchor_lang::prelude::*;
use wormhole_raw_vaas::Payload;
#[non_exhaustive]
pub enum VaaAccount<'a> {
PostedVaaV1(PostedVaaV1<'a>),
}
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Copy, Clone)]
pub struct EmitterInfo {
pub chain: u16,
pub address: [u8; 32],
pub sequence: u64,
}
impl<'a> VaaAccount<'a> {
pub fn version(&'a self) -> u8 {
match self {
Self::PostedVaaV1(_) => 1,
}
}
pub fn try_emitter_info(&self) -> Result<EmitterInfo> {
match self {
Self::PostedVaaV1(inner) => Ok(EmitterInfo {
chain: inner.emitter_chain(),
address: inner.emitter_address(),
sequence: inner.sequence(),
}),
}
}
pub fn try_emitter_chain(&self) -> Result<u16> {
match self {
Self::PostedVaaV1(inner) => Ok(inner.emitter_chain()),
}
}
pub fn try_emitter_address(&self) -> Result<[u8; 32]> {
match self {
Self::PostedVaaV1(inner) => Ok(inner.emitter_address()),
}
}
pub fn try_timestamp(&self) -> Result<u32> {
match self {
Self::PostedVaaV1(inner) => Ok(inner.timestamp()),
}
}
pub fn try_payload(&self) -> Result<Payload> {
match self {
Self::PostedVaaV1(inner) => Ok(Payload::parse(inner.payload())),
}
}
pub fn try_digest(&self) -> Result<solana_program::keccak::Hash> {
match self {
Self::PostedVaaV1(inner) => Ok(inner.digest()),
}
}
pub fn posted_vaa_v1(&'a self) -> Option<&'a PostedVaaV1<'a>> {
match self {
Self::PostedVaaV1(inner) => Some(inner),
}
}
pub fn load(acc_info: &'a AccountInfo) -> Result<Self> {
let data = acc_info.try_borrow_data()?;
require!(data.len() > 8, ErrorCode::AccountDidNotDeserialize);
match <[u8; 8]>::try_from(&data[..8]).unwrap() {
[118, 97, 97, 1, _, _, _, _] => Ok(Self::PostedVaaV1(PostedVaaV1::new(acc_info)?)),
_ => err!(ErrorCode::AccountDidNotDeserialize),
}
}
}

View File

@ -0,0 +1,115 @@
use std::cell::Ref;
use anchor_lang::{
prelude::{
error, require, require_eq, require_keys_eq, AccountInfo, ErrorCode, Pubkey, Result,
},
solana_program::keccak,
};
pub const POSTED_VAA_V1_SEED_PREFIX: &[u8] = b"PostedVAA";
const PAYLOAD_START: usize = 95;
/// Account used to store a verified VAA.
pub struct PostedVaaV1<'a>(Ref<'a, &'a mut [u8]>);
impl<'a> PostedVaaV1<'a> {
/// Level of consistency requested by the emitter.
pub fn consistency_level(&self) -> u8 {
self.0[4]
}
/// Time the message was submitted.
pub fn timestamp(&self) -> u32 {
u32::from_le_bytes(self.0[5..9].try_into().unwrap())
}
#[cfg(feature = "no-entrypoint")]
/// Pubkey of `SignatureSet` account that represent this VAA's signature verification.
pub fn signature_set(&self) -> Pubkey {
Pubkey::try_from(&self.0[9..41]).unwrap()
}
/// Guardian set index used to verify signatures for `SignatureSet`.
///
/// NOTE: In the previous implementation, this member was referred to as the `posted_timestamp`,
/// which is zero for VAA data (posted messages and VAAs resemble the same account schema). By
/// changing this to the guardian set index, we patch a bug with verifying governance VAAs for
/// the Core Bridge (other Core Bridge implementations require that the guardian set that
/// attested for the governance VAA is the current one).
pub fn guardian_set_index(&self) -> u32 {
u32::from_le_bytes(self.0[41..45].try_into().unwrap())
}
/// Unique ID for this message.
pub fn nonce(&self) -> u32 {
u32::from_le_bytes(self.0[45..49].try_into().unwrap())
}
/// Sequence number of this message.
pub fn sequence(&self) -> u64 {
u64::from_le_bytes(self.0[49..57].try_into().unwrap())
}
/// The Wormhole chain ID denoting the origin of this message.
pub fn emitter_chain(&self) -> u16 {
u16::from_le_bytes(self.0[57..59].try_into().unwrap())
}
/// Emitter of the message.
pub fn emitter_address(&self) -> [u8; 32] {
self.0[59..91].try_into().unwrap()
}
pub fn payload_size(&self) -> usize {
u32::from_le_bytes(self.0[91..PAYLOAD_START].try_into().unwrap())
.try_into()
.unwrap()
}
/// Message payload.
pub fn payload(&self) -> &[u8] {
&self.0[PAYLOAD_START..]
}
/// Recompute the message hash, which is used derive the [PostedVaaV1] PDA address.
pub fn message_hash(&self) -> keccak::Hash {
keccak::hashv(&[
self.timestamp().to_be_bytes().as_ref(),
self.nonce().to_be_bytes().as_ref(),
self.emitter_chain().to_be_bytes().as_ref(),
&self.emitter_address(),
&self.sequence().to_be_bytes(),
&[self.consistency_level()],
self.payload(),
])
}
/// Compute digest (hash of [message_hash](Self::message_hash)).
pub fn digest(&self) -> keccak::Hash {
keccak::hash(self.message_hash().as_ref())
}
pub(super) fn new(acc_info: &'a AccountInfo) -> Result<Self> {
let parsed = Self(acc_info.try_borrow_data()?);
require!(
parsed.0.len() >= PAYLOAD_START,
ErrorCode::AccountDidNotDeserialize
);
require_eq!(
parsed.0.len(),
PAYLOAD_START + parsed.payload_size(),
ErrorCode::AccountDidNotDeserialize
);
// Recompute message hash to re-derive PDA address.
let (expected_address, _) = Pubkey::find_program_address(
&[POSTED_VAA_V1_SEED_PREFIX, parsed.message_hash().as_ref()],
&crate::wormhole::core_bridge_program::id(),
);
require_keys_eq!(*acc_info.key, expected_address, ErrorCode::ConstraintSeeds);
Ok(parsed)
}
}

View File

@ -9,7 +9,7 @@
"version": "0.1.0",
"license": "Apache-2.0",
"devDependencies": {
"@certusone/wormhole-sdk": "^0.10.5",
"@certusone/wormhole-sdk": "^0.10.10",
"@coral-xyz/anchor": "^0.29.0",
"@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.87.3",
@ -82,12 +82,12 @@
}
},
"node_modules/@certusone/wormhole-sdk": {
"version": "0.10.5",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.10.5.tgz",
"integrity": "sha512-wKONuigkakoFx9HplBt2Jh5KPxc7xgtDJVrIb2/SqYWbFrdpiZrMC4H6kTZq2U4+lWtqaCa1aJ1q+3GOTNx2CQ==",
"version": "0.10.10",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.10.10.tgz",
"integrity": "sha512-2pYQ2/+cSfh/LVtOTXQDrTeZdXHgzq/hjkTevzW5+rEqITE54qUlnMhcVtSJQe+Yvgg3awrP2mIfDW3nvwPIPA==",
"dev": true,
"dependencies": {
"@certusone/wormhole-sdk-proto-web": "0.0.6",
"@certusone/wormhole-sdk-proto-web": "0.0.7",
"@certusone/wormhole-sdk-wasm": "^0.0.1",
"@coral-xyz/borsh": "0.2.6",
"@mysten/sui.js": "0.32.2",
@ -113,9 +113,9 @@
}
},
"node_modules/@certusone/wormhole-sdk-proto-web": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.6.tgz",
"integrity": "sha512-LTyjsrWryefx5WmkoBP6FQ2EjLxhMExAGxLkloHUhufVQZdrbGh0htBBUviP+HaDSJBCMPMtulNFwkBJV6muqQ==",
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk-proto-web/-/wormhole-sdk-proto-web-0.0.7.tgz",
"integrity": "sha512-GCe1/bcqMS0Mt+hsWp4SE4NLL59pWmK0lhQXO0oqAKl0G9AuuTdudySMDF/sLc7z5H2w34bSuSrIEKvPuuSC+w==",
"dev": true,
"dependencies": {
"@improbable-eng/grpc-web": "^0.15.0",

View File

@ -13,7 +13,7 @@
},
"homepage": "https://github.com/wormhole-foundation/wormhole-circle-integration#readme",
"devDependencies": {
"@certusone/wormhole-sdk": "^0.10.5",
"@certusone/wormhole-sdk": "^0.10.10",
"@coral-xyz/anchor": "^0.29.0",
"@solana/spl-token": "^0.3.8",
"@solana/web3.js": "^1.87.3",

View File

@ -34,7 +34,5 @@ ruint.workspace = true
cfg-if.workspace = true
ahash.workspace = true
[dev-dependencies]
hex-literal.workspace = true

View File

@ -8,7 +8,7 @@ cfg_if::cfg_if! {
// Placeholder for real address
declare_id!("Wormho1eCirc1e1ntegration111111111111111111");
} else if #[cfg(feature = "testnet")] {
declare_id!("wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW");
declare_id!("wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d");
}
}

View File

@ -1,10 +1,12 @@
use crate::{
error::CircleIntegrationError,
state::{Custodian, RegisteredEmitter},
state::{ConsumedVaa, Custodian, RegisteredEmitter},
};
use anchor_lang::prelude::*;
use wormhole_cctp_solana::{
cctp::token_messenger_minter_program, utils::ExternalAccount, wormhole::core_bridge_program,
cctp::token_messenger_minter_program,
utils::ExternalAccount,
wormhole::core_bridge_program::{self, VaaAccount},
};
use wormhole_raw_vaas::cctp::CircleIntegrationGovPayload;
@ -20,17 +22,9 @@ pub struct RegisterEmitterAndDomain<'info> {
custodian: Account<'info, Custodian>,
/// CHECK: We will be performing zero-copy deserialization in the instruction handler.
#[account(
mut,
owner = core_bridge_program::id()
)]
#[account(owner = core_bridge_program::id())]
vaa: AccountInfo<'info>,
/// CHECK: Account representing that a VAA has been consumed. Seeds are checked when
/// [claim_vaa](core_bridge::claim_vaa) is called.
#[account(mut)]
claim: AccountInfo<'info>,
#[account(
init,
payer = payer,
@ -43,6 +37,18 @@ pub struct RegisterEmitterAndDomain<'info> {
)]
registered_emitter: Account<'info, RegisteredEmitter>,
#[account(
init,
payer = payer,
space = 8 + ConsumedVaa::INIT_SPACE,
seeds = [
ConsumedVaa::SEED_PREFIX,
VaaAccount::load(&vaa)?.try_digest()?.as_ref(),
],
bump,
)]
consumed_vaa: Account<'info, ConsumedVaa>,
#[account(
seeds = [
token_messenger_minter_program::RemoteTokenMessenger::SEED_PREFIX,
@ -59,23 +65,11 @@ pub struct RegisterEmitterAndDomain<'info> {
#[access_control(handle_access_control(&ctx))]
pub fn register_emitter_and_domain(ctx: Context<RegisterEmitterAndDomain>) -> Result<()> {
let vaa = core_bridge_program::VaaAccount::load(&ctx.accounts.vaa).unwrap();
ctx.accounts.consumed_vaa.set_inner(ConsumedVaa {
bump: ctx.bumps.consumed_vaa,
});
// Create the claim account to provide replay protection. Because this instruction creates this
// account every time it is executed, this account cannot be created again with this emitter
// address, chain and sequence combination.
core_bridge_program::sdk::claim_vaa(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
core_bridge_program::sdk::ClaimVaa {
claim: ctx.accounts.claim.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
},
),
&crate::ID,
&vaa,
None,
)?;
let vaa = core_bridge_program::VaaAccount::load(&ctx.accounts.vaa).unwrap();
let registration = CircleIntegrationGovPayload::try_from(vaa.try_payload().unwrap())
.unwrap()
@ -85,7 +79,7 @@ pub fn register_emitter_and_domain(ctx: Context<RegisterEmitterAndDomain>) -> Re
ctx.accounts
.registered_emitter
.set_inner(RegisteredEmitter {
bump: ctx.bumps["registered_emitter"],
bump: ctx.bumps.registered_emitter,
cctp_domain: registration.cctp_domain(),
chain: registration.foreign_chain(),
address: registration.foreign_emitter(),

View File

@ -1,7 +1,11 @@
use crate::{constants::UPGRADE_SEED_PREFIX, error::CircleIntegrationError, state::Custodian};
use crate::{
constants::UPGRADE_SEED_PREFIX,
error::CircleIntegrationError,
state::{ConsumedVaa, Custodian},
};
use anchor_lang::prelude::*;
use solana_program::bpf_loader_upgradeable;
use wormhole_cctp_solana::wormhole::core_bridge_program;
use wormhole_cctp_solana::wormhole::core_bridge_program::{self, VaaAccount};
#[derive(Accounts)]
pub struct UpgradeContract<'info> {
@ -17,16 +21,20 @@ pub struct UpgradeContract<'info> {
/// CHECK: Posted VAA account, which will be read via zero-copy deserialization in the
/// instruction handler, which also checks this account discriminator (so there is no need to
/// check PDA seeds here).
#[account(
mut,
owner = core_bridge_program::id()
)]
#[account(owner = core_bridge_program::id())]
vaa: AccountInfo<'info>,
/// CHECK: Account representing that a VAA has been consumed. Seeds are checked when
/// [claim_vaa](core_bridge_sdk::claim_vaa) is called.
#[account(mut)]
claim: AccountInfo<'info>,
#[account(
init,
payer = payer,
space = 8 + ConsumedVaa::INIT_SPACE,
seeds = [
ConsumedVaa::SEED_PREFIX,
VaaAccount::load(&vaa)?.try_digest()?.as_ref(),
],
bump,
)]
consumed_vaa: Account<'info, ConsumedVaa>,
/// CHECK: We need this upgrade authority to invoke the BPF Loader Upgradeable program to
/// upgrade this program's executable. We verify this PDA address here out of convenience to get
@ -81,23 +89,9 @@ pub struct UpgradeContract<'info> {
/// Loader Upgradeable program to upgrade this program's executable to the provided buffer.
#[access_control(handle_access_control(&ctx))]
pub fn upgrade_contract(ctx: Context<UpgradeContract>) -> Result<()> {
let vaa = core_bridge_program::VaaAccount::load(&ctx.accounts.vaa).unwrap();
// Create the claim account to provide replay protection. Because this instruction creates this
// account every time it is executed, this account cannot be created again with this emitter
// address, chain and sequence combination.
core_bridge_program::sdk::claim_vaa(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
core_bridge_program::sdk::ClaimVaa {
claim: ctx.accounts.claim.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
},
),
&crate::ID,
&vaa,
None,
)?;
ctx.accounts.consumed_vaa.set_inner(ConsumedVaa {
bump: ctx.bumps.consumed_vaa,
});
// Finally upgrade.
solana_program::program::invoke_signed(
@ -117,7 +111,9 @@ pub fn upgrade_contract(ctx: Context<UpgradeContract>) -> Result<()> {
}
fn handle_access_control(ctx: &Context<UpgradeContract>) -> Result<()> {
msg!("okay... {:?}", ctx.accounts.vaa.key());
let vaa = core_bridge_program::VaaAccount::load(&ctx.accounts.vaa)?;
msg!("and...");
let gov_payload = crate::processor::require_valid_governance_vaa(&vaa)?;
let upgrade = gov_payload

View File

@ -46,8 +46,8 @@ pub struct Initialize<'info> {
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.custodian.set_inner(Custodian {
bump: ctx.bumps["custodian"],
upgrade_authority_bump: ctx.bumps["upgrade_authority"],
bump: ctx.bumps.custodian,
upgrade_authority_bump: ctx.bumps.upgrade_authority,
});
// Finally set the upgrade authority to this program's upgrade PDA.

View File

@ -1,6 +1,6 @@
use crate::{
error::CircleIntegrationError,
state::{Custodian, RegisteredEmitter},
state::{ConsumedVaa, Custodian, RegisteredEmitter},
};
use anchor_lang::prelude::*;
use anchor_spl::token;
@ -8,7 +8,7 @@ use wormhole_cctp_solana::{
cctp::{message_transmitter_program, token_messenger_minter_program},
cpi::ReceiveMessageArgs,
utils::ExternalAccount,
wormhole::core_bridge_program,
wormhole::core_bridge_program::VaaAccount,
};
/// Account context to invoke [redeem_tokens_with_payload].
@ -37,8 +37,22 @@ pub struct RedeemTokensWithPayload<'info> {
///
/// CHECK: Seeds must be [emitter_address, emitter_chain, sequence]. These seeds are checked
/// when [claim_vaa](core_bridge_program::sdk::claim_vaa) is called.
#[account(mut)]
claim: AccountInfo<'info>,
///
// NOTE: Because the message is already received at this point, this claim account may not be
// needed because there should be a "Nonce already used" error already thrown by this point. But
// this will remain here as an extra layer of protection (and will be consistent with the way
// the EVM implementation is written).
#[account(
init,
payer = payer,
space = 8 + ConsumedVaa::INIT_SPACE,
seeds = [
ConsumedVaa::SEED_PREFIX,
VaaAccount::load(&vaa)?.try_digest()?.as_ref(),
],
bump,
)]
consumed_vaa: Account<'info, ConsumedVaa>,
/// Redeemer, who owns the token account that will receive the minted tokens.
///
@ -73,13 +87,16 @@ pub struct RedeemTokensWithPayload<'info> {
message_transmitter_authority: UncheckedAccount<'info>,
/// CHECK: Seeds must be \["message_transmitter"\] (CCTP Message Transmitter program).
message_transmitter_config: AccountInfo<'info>,
message_transmitter_config: UncheckedAccount<'info>,
/// CHECK: Mutable. Seeds must be \["used_nonces", remote_domain.to_string(),
/// first_nonce.to_string()\] (CCTP Message Transmitter program).
#[account(mut)]
used_nonces: UncheckedAccount<'info>,
/// CHECK: Seeds must be \["__event_authority"\] (CCTP Message Transmitter program).
message_transmitter_event_authority: UncheckedAccount<'info>,
/// CHECK: Seeds must be \["token_messenger"\] (CCTP Token Messenger Minter program).
token_messenger: UncheckedAccount<'info>,
@ -107,11 +124,14 @@ pub struct RedeemTokensWithPayload<'info> {
/// CHECK: Seeds must be \["token_pair", remote_domain.to_string(), remote_token_address\] (CCTP
/// Token Messenger Minter program).
token_pair: AccountInfo<'info>,
token_pair: UncheckedAccount<'info>,
/// CHECK: Mutable. Seeds must be \["custody", mint\] (CCTP Token Messenger Minter program).
#[account(mut)]
token_messenger_minter_custody_token: AccountInfo<'info>,
token_messenger_minter_custody_token: UncheckedAccount<'info>,
/// CHECK: Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
token_messenger_minter_event_authority: UncheckedAccount<'info>,
token_messenger_minter_program:
Program<'info, token_messenger_minter_program::TokenMessengerMinter>,
@ -138,6 +158,10 @@ pub fn redeem_tokens_with_payload(
ctx: Context<RedeemTokensWithPayload>,
args: RedeemTokensWithPayloadArgs,
) -> Result<()> {
ctx.accounts.consumed_vaa.set_inner(ConsumedVaa {
bump: ctx.bumps.consumed_vaa,
});
let vaa = wormhole_cctp_solana::cpi::verify_vaa_and_mint(
&ctx.accounts.vaa,
CpiContext::new_with_signer(
@ -159,6 +183,14 @@ pub fn redeem_tokens_with_payload(
.token_messenger_minter_program
.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
message_transmitter_event_authority: ctx
.accounts
.message_transmitter_event_authority
.to_account_info(),
message_transmitter_program: ctx
.accounts
.message_transmitter_program
.to_account_info(),
token_messenger: ctx.accounts.token_messenger.to_account_info(),
remote_token_messenger: ctx.accounts.remote_token_messenger.to_account_info(),
token_minter: ctx.accounts.token_minter.to_account_info(),
@ -170,6 +202,10 @@ pub fn redeem_tokens_with_payload(
.token_messenger_minter_custody_token
.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
token_messenger_minter_event_authority: ctx
.accounts
.token_messenger_minter_event_authority
.to_account_info(),
},
&[&[Custodian::SEED_PREFIX, &[ctx.accounts.custodian.bump]]],
),
@ -179,27 +215,6 @@ pub fn redeem_tokens_with_payload(
},
)?;
// Create the claim account to provide replay protection. Because this instruction creates this
// account every time it is executed, this account cannot be created again with this emitter
// address, chain and sequence combination.
//
// NOTE: Because the message is already received at this point, this claim account may not be
// needed because there should be a "Nonce already used" error already thrown by this point. But
// this will remain here as an extra layer of protection (and will be consistent with the way
// the EVM implementation is written).
core_bridge_program::sdk::claim_vaa(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
core_bridge_program::sdk::ClaimVaa {
claim: ctx.accounts.claim.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
},
),
&crate::ID,
&vaa,
None,
)?;
// Validate that this message originated from a registered emitter.
let registered_emitter = &ctx.accounts.registered_emitter;
let emitter = vaa.try_emitter_info().unwrap();

View File

@ -4,10 +4,7 @@ use anchor_spl::token;
use wormhole_cctp_solana::{
cctp::{message_transmitter_program, token_messenger_minter_program},
utils::ExternalAccount,
wormhole::core_bridge_program::{
self,
sdk::io::{Readable, TypePrefixedPayload, Writeable},
},
wormhole::core_bridge_program,
};
/// Account context to invoke [transfer_tokens_with_payload].
@ -82,6 +79,10 @@ pub struct TransferTokensWithPayload<'info> {
#[account(mut)]
core_message: Signer<'info>,
/// CHECK: Mutable signer to create CCTP message.
#[account(mut)]
cctp_message: Signer<'info>,
/// CHECK: Seeds must be \["Sequence"\, custodian] (Wormhole Core Bridge program).
#[account(mut)]
core_emitter_sequence: UncheckedAccount<'info>,
@ -104,7 +105,7 @@ pub struct TransferTokensWithPayload<'info> {
/// Messenger Minter program).
remote_token_messenger: UncheckedAccount<'info>,
/// CHECK Seeds must be \["token_minter"\] (CCTP Token Messenger Minter program).
/// CHECK: Seeds must be \["token_minter"\] (CCTP Token Messenger Minter program).
token_minter: UncheckedAccount<'info>,
/// Local token account, which this program uses to validate the `mint` used to burn.
@ -113,6 +114,9 @@ pub struct TransferTokensWithPayload<'info> {
#[account(mut)]
local_token: Box<Account<'info, ExternalAccount<token_messenger_minter_program::LocalToken>>>,
/// CHECK: Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
token_messenger_minter_event_authority: UncheckedAccount<'info>,
core_bridge_program: Program<'info, core_bridge_program::CoreBridge>,
token_messenger_minter_program:
Program<'info, token_messenger_minter_program::TokenMessengerMinter>,
@ -147,39 +151,6 @@ pub struct TransferTokensWithPayloadArgs {
pub payload: Vec<u8>,
}
#[derive(Debug, Clone)]
pub struct WrappedVec(Vec<u8>);
impl Readable for WrappedVec {
const SIZE: Option<usize> = None;
fn read<R>(reader: &mut R) -> std::io::Result<Self>
where
R: std::io::Read,
{
let mut out = vec![];
reader.read_to_end(&mut out)?;
Ok(Self(out))
}
}
impl Writeable for WrappedVec {
fn written_size(&self) -> usize {
self.0.len()
}
fn write<W>(&self, writer: &mut W) -> std::io::Result<()>
where
W: std::io::Write,
{
writer.write_all(&self.0)
}
}
impl TypePrefixedPayload for WrappedVec {
const TYPE: Option<u8> = None;
}
/// This instruction invokes both Wormhole Core Bridge and CCTP Token Messenger Minter programs to
/// emit a Wormhole message associated with a CCTP message.
///
@ -218,12 +189,13 @@ pub fn transfer_tokens_with_payload(
.token_messenger_minter_program
.to_account_info(),
wormhole_cctp_solana::cpi::DepositForBurnWithCaller {
src_token_owner: ctx.accounts.custodian.to_account_info(),
burn_token_owner: ctx.accounts.custodian.to_account_info(),
payer: ctx.accounts.payer.to_account_info(),
token_messenger_minter_sender_authority: ctx
.accounts
.token_messenger_minter_sender_authority
.to_account_info(),
src_token: ctx.accounts.custody_token.to_account_info(),
burn_token: ctx.accounts.custody_token.to_account_info(),
message_transmitter_config: ctx
.accounts
.message_transmitter_config
@ -233,6 +205,7 @@ pub fn transfer_tokens_with_payload(
token_minter: ctx.accounts.token_minter.to_account_info(),
local_token: ctx.accounts.local_token.to_account_info(),
mint: ctx.accounts.mint.to_account_info(),
cctp_message: ctx.accounts.cctp_message.to_account_info(),
message_transmitter_program: ctx
.accounts
.message_transmitter_program
@ -242,6 +215,11 @@ pub fn transfer_tokens_with_payload(
.token_messenger_minter_program
.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
event_authority: ctx
.accounts
.token_messenger_minter_event_authority
.to_account_info(),
},
&[custodian_seeds],
),
@ -267,7 +245,7 @@ pub fn transfer_tokens_with_payload(
amount,
mint_recipient,
wormhole_message_nonce,
payload: WrappedVec(payload),
payload,
},
)?;

View File

@ -0,0 +1,11 @@
use anchor_lang::prelude::*;
#[account]
#[derive(Debug, InitSpace)]
pub struct ConsumedVaa {
pub bump: u8,
}
impl ConsumedVaa {
pub const SEED_PREFIX: &'static [u8] = b"consumed-vaa";
}

View File

@ -1,3 +1,6 @@
mod consumed_vaa;
pub use consumed_vaa::*;
mod custodian;
pub use custodian::*;

View File

@ -11,7 +11,7 @@ import { Connection, Keypair, Transaction, sendAndConfirmTransaction } from "@so
import "dotenv/config";
import { CircleIntegrationProgram } from "../src";
const PROGRAM_ID = "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW";
const PROGRAM_ID = "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d";
// Here we go.
main();
@ -19,7 +19,7 @@ main();
// impl
async function main() {
let govSequence = 6900n;
let govSequence = 6920n;
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const circleIntegration = new CircleIntegrationProgram(connection, PROGRAM_ID);
@ -30,12 +30,12 @@ async function main() {
const payer = Keypair.fromSecretKey(Buffer.from(process.env.SOLANA_PRIVATE_KEY, "hex"));
// Set up CCTP Program.
// await intialize(connection, payer);
//await intialize(circleIntegration, payer);
// Register emitter and domain.
{
const foreignChain = "ethereum";
const foreignEmitter = "0x0a69146716b3a21622287efa1607424c663069a4";
const foreignChain = "sepolia";
const foreignEmitter = "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c";
const cctpDomain = 0;
await registerEmitterAndDomain(
@ -62,7 +62,7 @@ async function main() {
);
}
{
const foreignChain = "optimism";
const foreignChain = "optimism_sepolia";
const foreignEmitter = "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c";
const cctpDomain = 2;
@ -76,10 +76,38 @@ async function main() {
);
}
{
const foreignChain = "arbitrum";
const foreignEmitter = "0x2E8F5E00a9C5D450A72700546B89E2b70DfB00f2";
const foreignChain = "arbitrum_sepolia";
const foreignEmitter = "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c";
const cctpDomain = 3;
await registerEmitterAndDomain(
circleIntegration,
payer,
govSequence++,
foreignChain,
foreignEmitter,
cctpDomain,
);
}
{
const foreignChain = "base_sepolia";
const foreignEmitter = "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c";
const cctpDomain = 6;
await registerEmitterAndDomain(
circleIntegration,
payer,
govSequence++,
foreignChain,
foreignEmitter,
cctpDomain,
);
}
{
const foreignChain = "polygon";
const foreignEmitter = "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c";
const cctpDomain = 7;
await registerEmitterAndDomain(
circleIntegration,
payer,

View File

@ -6,7 +6,7 @@ import { Connection, Keypair, Transaction, sendAndConfirmTransaction } from "@so
import "dotenv/config";
import { CircleIntegrationProgram } from "../src";
const PROGRAM_ID = "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW";
const PROGRAM_ID = "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d";
// Modify this to the new implementation address.
const NEW_IMPLEMENTATION = "HCUGGoihMthPN6d4VGpH8xUPYUofgTgqnpYtwyca7PEh";

View File

@ -1,6 +1,3 @@
export { MessageTransmitterProgram } from "./messageTransmitter";
export { CctpMessage, CctpTokenBurnMessage } from "./messages";
export {
DEPOSIT_FOR_BURN_WITH_CALLER_IX_SELECTOR,
TokenMessengerMinterProgram,
} from "./tokenMessengerMinter";
export { TokenMessengerMinterProgram } from "./tokenMessengerMinter";

View File

@ -0,0 +1,11 @@
import { PublicKey } from "@solana/web3.js";
export class MessageSent {
rentPayer: PublicKey;
message: Buffer;
constructor(rentPayer: PublicKey, message: Buffer) {
this.rentPayer = rentPayer;
this.message = message;
}
}

View File

@ -1,3 +1,4 @@
import { BN } from "@coral-xyz/anchor";
import { PublicKey } from "@solana/web3.js";
export class MessageTransmitterConfig {
@ -9,10 +10,9 @@ export class MessageTransmitterConfig {
localDomain: number;
version: number;
signatureThreshold: number;
enabledAttesters: Array<Array<number>>;
maxMessageBodySize: bigint;
nextAvailableNonce: bigint;
authorityBump: number;
enabledAttesters: Array<PublicKey>;
maxMessageBodySize: BN;
nextAvailableNonce: BN;
constructor(
owner: PublicKey,
@ -23,10 +23,9 @@ export class MessageTransmitterConfig {
localDomain: number,
version: number,
signatureThreshold: number,
enabledAttesters: Array<Array<number>>,
maxMessageBodySize: bigint,
nextAvailableNonce: bigint,
authorityBump: number,
enabledAttesters: Array<PublicKey>,
maxMessageBodySize: BN,
nextAvailableNonce: BN,
) {
this.owner = owner;
this.pendingOwner = pendingOwner;
@ -39,7 +38,6 @@ export class MessageTransmitterConfig {
this.enabledAttesters = enabledAttesters;
this.maxMessageBodySize = maxMessageBodySize;
this.nextAvailableNonce = nextAvailableNonce;
this.authorityBump = authorityBump;
}
static address(programId: PublicKey) {

View File

@ -1,9 +1,9 @@
import { Program } from "@coral-xyz/anchor";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Connection, PublicKey } from "@solana/web3.js";
import { CctpTokenBurnMessage } from "../messages";
import { TokenMessengerMinterProgram } from "../tokenMessengerMinter";
import { IDL, MessageTransmitter } from "../types/message_transmitter";
import { MessageSent } from "./MessageSent";
import { MessageTransmitterConfig } from "./MessageTransmitterConfig";
import { UsedNonses } from "./UsedNonces";
@ -11,18 +11,20 @@ export const PROGRAM_IDS = ["CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd"] as c
export type ProgramId = (typeof PROGRAM_IDS)[number];
export type ReceiveMessageAccounts = {
export type ReceiveTokenMessengerMinterMessageAccounts = {
authority: PublicKey;
messageTransmitterConfig: PublicKey;
usedNonces: PublicKey;
tokenMessengerMinterProgram: PublicKey;
messageTransmitterEventAuthority: PublicKey;
messageTransmitterProgram: PublicKey;
tokenMessenger: PublicKey;
remoteTokenMessenger: PublicKey;
tokenMinter: PublicKey;
localToken: PublicKey;
tokenPair: PublicKey;
custodyToken: PublicKey;
tokenProgram: PublicKey;
eventAuthority: PublicKey;
};
export class MessageTransmitterProgram {
@ -46,48 +48,28 @@ export class MessageTransmitterProgram {
}
async fetchMessageTransmitterConfig(addr: PublicKey): Promise<MessageTransmitterConfig> {
const {
owner,
pendingOwner,
attesterManager,
pauser,
paused,
localDomain,
version,
signatureThreshold,
enabledAttesters,
maxMessageBodySize,
nextAvailableNonce,
authorityBump,
} = await this.program.account.messageTransmitter.fetch(addr);
return new MessageTransmitterConfig(
owner,
pendingOwner,
attesterManager,
pauser,
paused,
localDomain,
version,
signatureThreshold,
enabledAttesters.map((addr) => Array.from(addr.toBuffer())),
BigInt(maxMessageBodySize.toString()),
BigInt(nextAvailableNonce.toString()),
authorityBump,
);
return this.program.account.messageTransmitter.fetch(addr);
}
usedNoncesAddress(remoteDomain: number, nonce: bigint): PublicKey {
return UsedNonses.address(this.ID, remoteDomain, nonce);
}
authorityAddress(): PublicKey {
authorityAddress(cpiProgramId: PublicKey): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("message_transmitter_authority")],
[Buffer.from("message_transmitter_authority"), cpiProgramId.toBuffer()],
this.ID,
)[0];
}
eventAuthorityAddress(): PublicKey {
return PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], this.ID)[0];
}
fetchMessageSent(addr: PublicKey): Promise<MessageSent> {
return this.program.account.messageSent.fetch(addr);
}
tokenMessengerMinterProgram(): TokenMessengerMinterProgram {
switch (this._programId) {
case testnet(): {
@ -108,10 +90,10 @@ export class MessageTransmitterProgram {
}
}
receiveMessageAccounts(
receiveTokenMessengerMinterMessageAccounts(
mint: PublicKey,
circleMessage: CctpTokenBurnMessage | Buffer,
): ReceiveMessageAccounts {
): ReceiveTokenMessengerMinterMessageAccounts {
const {
cctp: { sourceDomain, nonce },
burnTokenAddress,
@ -119,10 +101,12 @@ export class MessageTransmitterProgram {
const tokenMessengerMinterProgram = this.tokenMessengerMinterProgram();
return {
authority: this.authorityAddress(),
authority: this.authorityAddress(tokenMessengerMinterProgram.ID),
messageTransmitterConfig: this.messageTransmitterConfigAddress(),
usedNonces: this.usedNoncesAddress(sourceDomain, nonce),
tokenMessengerMinterProgram: tokenMessengerMinterProgram.ID,
messageTransmitterEventAuthority: this.eventAuthorityAddress(),
messageTransmitterProgram: this.ID,
tokenMessenger: tokenMessengerMinterProgram.tokenMessengerAddress(),
remoteTokenMessenger:
tokenMessengerMinterProgram.remoteTokenMessengerAddress(sourceDomain),
@ -130,7 +114,7 @@ export class MessageTransmitterProgram {
localToken: tokenMessengerMinterProgram.localTokenAddress(mint),
tokenPair: tokenMessengerMinterProgram.tokenPairAddress(sourceDomain, burnTokenAddress),
custodyToken: tokenMessengerMinterProgram.custodyTokenAddress(mint),
tokenProgram: TOKEN_PROGRAM_ID,
eventAuthority: tokenMessengerMinterProgram.eventAuthorityAddress(),
};
}
}

View File

@ -1,5 +1,4 @@
import { Program } from "@coral-xyz/anchor";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Connection, PublicKey } from "@solana/web3.js";
import { MessageTransmitterProgram } from "../messageTransmitter";
import { IDL, TokenMessengerMinter } from "../types/token_messenger_minter";
@ -7,10 +6,6 @@ import { RemoteTokenMessenger } from "./RemoteTokenMessenger";
export const PROGRAM_IDS = ["CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3"] as const;
export const DEPOSIT_FOR_BURN_WITH_CALLER_IX_SELECTOR = Uint8Array.from([
167, 222, 19, 114, 85, 21, 14, 118,
]);
export type ProgramId = (typeof PROGRAM_IDS)[number];
export type DepositForBurnWithCallerAccounts = {
@ -20,9 +15,9 @@ export type DepositForBurnWithCallerAccounts = {
remoteTokenMessenger: PublicKey;
tokenMinter: PublicKey;
localToken: PublicKey;
tokenMessengerMinterEventAuthority: PublicKey;
messageTransmitterProgram: PublicKey;
tokenMessengerMinterProgram: PublicKey;
tokenProgram: PublicKey;
};
export class TokenMessengerMinterProgram {
@ -84,10 +79,14 @@ export class TokenMessengerMinterProgram {
)[0];
}
senderAuthority(): PublicKey {
senderAuthorityAddress(): PublicKey {
return PublicKey.findProgramAddressSync([Buffer.from("sender_authority")], this.ID)[0];
}
eventAuthorityAddress(): PublicKey {
return PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], this.ID)[0];
}
messageTransmitterProgram(): MessageTransmitterProgram {
switch (this._programId) {
case testnet(): {
@ -114,15 +113,15 @@ export class TokenMessengerMinterProgram {
): DepositForBurnWithCallerAccounts {
const messageTransmitterProgram = this.messageTransmitterProgram();
return {
senderAuthority: this.senderAuthority(),
senderAuthority: this.senderAuthorityAddress(),
messageTransmitterConfig: messageTransmitterProgram.messageTransmitterConfigAddress(),
tokenMessenger: this.tokenMessengerAddress(),
remoteTokenMessenger: this.remoteTokenMessengerAddress(remoteDomain),
tokenMinter: this.tokenMinterAddress(),
localToken: this.localTokenAddress(mint),
tokenMessengerMinterEventAuthority: this.eventAuthorityAddress(),
messageTransmitterProgram: messageTransmitterProgram.ID,
tokenMessengerMinterProgram: this.ID,
tokenProgram: TOKEN_PROGRAM_ID,
};
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,35 @@
export * from "./circle";
export * from "./cctp";
export * from "./consts";
export * from "./messages";
export * from "./state";
export * from "./wormhole";
import { BN, EventParser, Program, utils as anchorUtils } from "@coral-xyz/anchor";
import { BN, Program } from "@coral-xyz/anchor";
import * as splToken from "@solana/spl-token";
import {
AddressLookupTableAccount,
Connection,
PublicKey,
SYSVAR_CLOCK_PUBKEY,
SYSVAR_RENT_PUBKEY,
SystemProgram,
TransactionInstruction,
VersionedTransactionResponse,
} from "@solana/web3.js";
import {
IDL,
WormholeCircleIntegrationSolana,
} from "../../target/types/wormhole_circle_integration_solana";
import {
CctpMessage,
CctpTokenBurnMessage,
MessageTransmitterProgram,
TokenMessengerMinterProgram,
} from "./circle";
} from "./cctp";
import { BPF_LOADER_UPGRADEABLE_ID } from "./consts";
import { Custodian, RegisteredEmitter } from "./state";
import { Claim, VaaAccount } from "./wormhole";
import { PostedMessageData } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { Deposit } from "./messages";
import { ConsumedVaa, Custodian, RegisteredEmitter } from "./state";
import { VaaAccount } from "./wormhole";
export const PROGRAM_IDS = [
"Wormho1eCirc1e1ntegration111111111111111111", // mainnet placeholder
"wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW", // testnet
"wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d", // testnet
] as const;
export type ProgramId = (typeof PROGRAM_IDS)[number];
@ -83,30 +78,31 @@ export type TransferTokensWithPayloadAccounts = PublishMessageAccounts & {
remoteTokenMessenger: PublicKey;
tokenMinter: PublicKey;
localToken: PublicKey;
tokenMessengerMinterEventAuthority: PublicKey;
coreBridgeProgram: PublicKey;
tokenMessengerMinterProgram: PublicKey;
messageTransmitterProgram: PublicKey;
tokenProgram: PublicKey;
};
export type RedeemTokensWithPayloadAccounts = {
custodian: PublicKey;
claim: PublicKey;
consumedVaa: PublicKey;
mintRecipientAuthority: PublicKey;
mintRecipient: PublicKey;
registeredEmitter: PublicKey;
messageTransmitterAuthority: PublicKey;
messageTransmitterConfig: PublicKey;
usedNonces: PublicKey;
messageTransmitterEventAuthority: PublicKey;
tokenMessenger: PublicKey;
remoteTokenMessenger: PublicKey;
tokenMinter: PublicKey;
localToken: PublicKey;
tokenPair: PublicKey;
tokenMessengerMinterCustodyToken: PublicKey;
tokenMessengerMinterEventAuthority: PublicKey;
tokenMessengerMinterProgram: PublicKey;
messageTransmitterProgram: PublicKey;
tokenProgram: PublicKey;
};
export type SolanaWormholeCctpTxData = {
@ -166,6 +162,10 @@ export class CircleIntegrationProgram {
return PublicKey.findProgramAddressSync([Buffer.from("custody")], this.ID)[0];
}
consumedVaaAddress(vaaHash: Array<number> | Uint8Array): PublicKey {
return ConsumedVaa.address(this.ID, vaaHash);
}
commonAccounts(mint?: PublicKey): WormholeCctpCommonAccounts {
const custodian = this.custodianAddress();
const { coreBridgeConfig, coreEmitterSequence, coreFeeCollector, coreBridgeProgram } =
@ -197,9 +197,12 @@ export class CircleIntegrationProgram {
coreBridgeProgram,
tokenMessenger: tokenMessengerMinterProgram.tokenMessengerAddress(),
tokenMinter: tokenMessengerMinterProgram.tokenMinterAddress(),
tokenMessengerMinterSenderAuthority: tokenMessengerMinterProgram.senderAuthority(),
tokenMessengerMinterSenderAuthority:
tokenMessengerMinterProgram.senderAuthorityAddress(),
tokenMessengerMinterProgram: tokenMessengerMinterProgram.ID,
messageTransmitterAuthority: messageTransmitterProgram.authorityAddress(),
messageTransmitterAuthority: messageTransmitterProgram.authorityAddress(
tokenMessengerMinterProgram.ID,
),
messageTransmitterConfig: messageTransmitterProgram.messageTransmitterConfigAddress(),
messageTransmitterProgram: messageTransmitterProgram.ID,
tokenProgram: splToken.TOKEN_PROGRAM_ID,
@ -230,11 +233,6 @@ export class CircleIntegrationProgram {
const { payer, vaa, remoteTokenMessenger: inputRemoteTokenMessenger } = accounts;
const vaaAcct = await VaaAccount.fetch(this.program.provider.connection, vaa);
// Determine claim PDA.
const { chain, address, sequence } = vaaAcct.emitterInfo();
const claim = Claim.address(this.ID, address, chain, sequence);
const payload = vaaAcct.payload();
const registeredEmitter = this.registeredEmitterAddress(payload.readUInt16BE(35));
const remoteTokenMessenger = (() => {
@ -254,7 +252,7 @@ export class CircleIntegrationProgram {
payer,
custodian: this.custodianAddress(),
vaa,
claim,
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
registeredEmitter,
remoteTokenMessenger,
})
@ -269,11 +267,6 @@ export class CircleIntegrationProgram {
const { payer, vaa, buffer: inputBuffer } = accounts;
const vaaAcct = await VaaAccount.fetch(this.program.provider.connection, vaa);
// Determine claim PDA.
const { chain, address, sequence } = vaaAcct.emitterInfo();
const claim = Claim.address(this.ID, address, chain, sequence);
const payload = vaaAcct.payload();
return this.program.methods
@ -282,7 +275,7 @@ export class CircleIntegrationProgram {
payer,
custodian: this.custodianAddress(),
vaa,
claim,
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
upgradeAuthority: this.upgradeAuthorityAddress(),
spill: payer,
buffer: inputBuffer ?? new PublicKey(payload.subarray(-32)),
@ -311,9 +304,9 @@ export class CircleIntegrationProgram {
remoteTokenMessenger,
tokenMinter,
localToken,
tokenMessengerMinterEventAuthority,
messageTransmitterProgram,
tokenMessengerMinterProgram,
tokenProgram,
} = this.tokenMessengerMinterProgram().depositForBurnWithCallerAccounts(mint, remoteDomain);
const custodian = this.custodianAddress();
@ -333,10 +326,10 @@ export class CircleIntegrationProgram {
remoteTokenMessenger,
tokenMinter,
localToken,
tokenMessengerMinterEventAuthority,
coreBridgeProgram,
tokenMessengerMinterProgram,
messageTransmitterProgram,
tokenProgram,
};
}
@ -346,10 +339,11 @@ export class CircleIntegrationProgram {
mint: PublicKey;
burnSource: PublicKey;
coreMessage: PublicKey;
cctpMessage: PublicKey;
},
args: TransferTokensWithPayloadArgs,
): Promise<TransactionInstruction> {
let { payer, burnSource, mint, coreMessage } = accounts;
let { payer, burnSource, mint, coreMessage, cctpMessage } = accounts;
const { amount, targetChain, mintRecipient, wormholeMessageNonce, payload } = args;
@ -367,9 +361,9 @@ export class CircleIntegrationProgram {
remoteTokenMessenger,
tokenMinter,
localToken,
tokenMessengerMinterEventAuthority,
tokenMessengerMinterProgram,
messageTransmitterProgram,
tokenProgram,
} = await this.transferTokensWithPayloadAccounts(mint, targetChain);
return this.program.methods
@ -388,6 +382,7 @@ export class CircleIntegrationProgram {
registeredEmitter,
coreBridgeConfig,
coreMessage,
cctpMessage,
coreEmitterSequence,
coreFeeCollector,
tokenMessengerMinterSenderAuthority,
@ -396,10 +391,10 @@ export class CircleIntegrationProgram {
remoteTokenMessenger,
tokenMinter,
localToken,
tokenMessengerMinterEventAuthority,
coreBridgeProgram,
tokenMessengerMinterProgram,
messageTransmitterProgram,
tokenProgram,
})
.instruction();
}
@ -416,42 +411,43 @@ export class CircleIntegrationProgram {
// Determine claim PDA.
const vaaAcct = await VaaAccount.fetch(this.program.provider.connection, vaa);
const { chain, address, sequence } = vaaAcct.emitterInfo();
const claim = Claim.address(this.ID, address, chain, sequence);
const { chain } = vaaAcct.emitterInfo();
const messageTransmitterProgram = this.messageTransmitterProgram();
const {
authority: messageTransmitterAuthority,
messageTransmitterConfig,
usedNonces,
tokenMessengerMinterProgram,
messageTransmitterEventAuthority,
messageTransmitterProgram,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
tokenPair,
custodyToken: tokenMessengerMinterCustodyToken,
tokenProgram,
} = messageTransmitterProgram.receiveMessageAccounts(mint, msg);
eventAuthority: tokenMessengerMinterEventAuthority,
} = this.messageTransmitterProgram().receiveTokenMessengerMinterMessageAccounts(mint, msg);
return {
custodian: this.custodianAddress(),
claim,
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
mintRecipientAuthority,
mintRecipient,
registeredEmitter: this.registeredEmitterAddress(chain),
messageTransmitterAuthority,
messageTransmitterConfig,
usedNonces,
messageTransmitterEventAuthority,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
tokenPair,
tokenMessengerMinterCustodyToken,
tokenMessengerMinterEventAuthority,
tokenMessengerMinterProgram,
messageTransmitterProgram: messageTransmitterProgram.ID,
tokenProgram,
messageTransmitterProgram,
};
}
@ -472,22 +468,23 @@ export class CircleIntegrationProgram {
const {
custodian,
claim,
consumedVaa,
mintRecipientAuthority,
mintRecipient,
registeredEmitter,
messageTransmitterAuthority,
messageTransmitterConfig,
usedNonces,
messageTransmitterEventAuthority,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
tokenPair,
tokenMessengerMinterCustodyToken,
tokenMessengerMinterEventAuthority,
tokenMessengerMinterProgram,
messageTransmitterProgram,
tokenProgram,
} = await this.redeemTokensWithPayloadAccounts(vaa, encodedCctpMessage);
return this.program.methods
@ -496,22 +493,23 @@ export class CircleIntegrationProgram {
payer,
custodian,
vaa,
claim,
consumedVaa,
mintRecipientAuthority: inputMintRecipientAuthority ?? mintRecipientAuthority,
mintRecipient,
registeredEmitter,
messageTransmitterAuthority,
messageTransmitterConfig,
usedNonces,
messageTransmitterEventAuthority,
tokenMessenger,
remoteTokenMessenger,
tokenMinter,
localToken,
tokenPair,
tokenMessengerMinterCustodyToken,
tokenMessengerMinterEventAuthority,
tokenMessengerMinterProgram,
messageTransmitterProgram,
tokenProgram,
})
.instruction();
}
@ -589,108 +587,6 @@ export class CircleIntegrationProgram {
}
}
}
async parseTransactionReceipt(
txReceipt: VersionedTransactionResponse,
addressLookupTableAccounts?: AddressLookupTableAccount[],
): Promise<SolanaWormholeCctpTxData[]> {
if (txReceipt.meta === null) {
throw new Error("meta not found in tx");
}
const txMeta = txReceipt.meta;
if (txMeta.logMessages === undefined || txMeta.logMessages === null) {
throw new Error("logMessages not found in tx");
}
const txLogMessages = txMeta.logMessages;
// Decode message field from MessageSent event.
const messageTransmitterProgram = this.messageTransmitterProgram();
const parser = new EventParser(
messageTransmitterProgram.ID,
messageTransmitterProgram.program.coder,
);
// Map these puppies based on nonce.
const encodedCctpMessages = new Map<bigint, Buffer>();
for (const parsed of parser.parseLogs(txLogMessages, false)) {
const msg = parsed.data.message as Buffer;
encodedCctpMessages.set(CctpMessage.decode(msg).cctp.nonce, msg);
}
const fetchedKeys = txReceipt.transaction.message.getAccountKeys({
addressLookupTableAccounts,
});
const accountKeys = fetchedKeys.staticAccountKeys;
if (fetchedKeys.accountKeysFromLookups !== undefined) {
accountKeys.push(
...fetchedKeys.accountKeysFromLookups.writable,
...fetchedKeys.accountKeysFromLookups.readonly,
);
}
const coreBridgeProgramIndex = accountKeys.findIndex((key) =>
key.equals(this.coreBridgeProgramId()),
);
const tokenMessengerMinterProgramIndex = accountKeys.findIndex((key) =>
key.equals(this.tokenMessengerMinterProgram().ID),
);
const messageTransmitterProgramIndex = accountKeys.findIndex((key) =>
key.equals(this.messageTransmitterProgram().ID),
);
if (
coreBridgeProgramIndex == -1 &&
tokenMessengerMinterProgramIndex == -1 &&
messageTransmitterProgramIndex == -1
) {
return [];
}
if (txMeta.innerInstructions === undefined || txMeta.innerInstructions === null) {
throw new Error("innerInstructions not found in tx");
}
const txInnerInstructions = txMeta.innerInstructions;
const custodian = this.custodianAddress();
const postedMessageKeys: PublicKey[] = [];
for (const innerIx of txInnerInstructions) {
// Traverse instructions to find messages posted by the Wormhole Circle Integration program.
for (const ixInfo of innerIx.instructions) {
if (
ixInfo.programIdIndex == coreBridgeProgramIndex &&
anchorUtils.bytes.bs58.decode(ixInfo.data)[0] == 1 &&
accountKeys[ixInfo.accounts[2]].equals(custodian)
) {
postedMessageKeys.push(accountKeys[ixInfo.accounts[1]]);
}
}
}
return this.program.provider.connection
.getMultipleAccountsInfo(postedMessageKeys)
.then((infos) =>
infos.map((info, i) => {
if (info === null) {
throw new Error("message info is null");
}
const payload = info.data.subarray(95);
const nonce = Deposit.decode(payload).deposit.cctpNonce;
const encodedCctpMessage = encodedCctpMessages.get(nonce);
if (encodedCctpMessage === undefined) {
throw new Error(
`cannot find CCTP message with nonce ${nonce} in tx receipt`,
);
}
return {
coreMessageAccount: postedMessageKeys[i],
coreMessageSequence: info.data.readBigUInt64LE(49),
encodedCctpMessage,
};
}),
);
}
}
export function mainnet(): ProgramId {
@ -698,5 +594,5 @@ export function mainnet(): ProgramId {
}
export function testnet(): ProgramId {
return "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW";
return "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d";
}

View File

@ -0,0 +1,10 @@
import { PublicKey } from "@solana/web3.js";
export class ConsumedVaa {
static address(programId: PublicKey, vaaHash: Array<number> | Uint8Array): PublicKey {
return PublicKey.findProgramAddressSync(
[Buffer.from("consumed-vaa"), Buffer.from(vaaHash)],
new PublicKey(programId),
)[0];
}
}

View File

@ -1,2 +1,3 @@
export * from "./ConsumedVaa";
export * from "./Custodian";
export * from "./RegisteredEmitter";

View File

@ -1,5 +1,6 @@
import { parseVaa } from "@certusone/wormhole-sdk";
import { Connection, PublicKey } from "@solana/web3.js";
import { ethers } from "ethers";
export type EncodedVaa = {
status: number;
@ -31,26 +32,43 @@ export class VaaAccount {
private _postedVaaV1?: PostedVaaV1;
static async fetch(connection: Connection, addr: PublicKey): Promise<VaaAccount> {
const data = await connection.getAccountInfo(addr).then((acct) => acct.data);
if (data.subarray(0, 8).equals(Uint8Array.from([226, 101, 163, 4, 133, 160, 84, 245]))) {
const status = data[8];
const writeAuthority = new PublicKey(data.subarray(9, 41));
const version = data[41];
const bufLen = data.readUInt32LE(42);
const buf = data.subarray(46, 46 + bufLen);
const accInfo = await connection.getAccountInfo(addr);
if (accInfo === null) {
throw new Error("no VAA account info found");
}
const { data } = accInfo;
let offset = 0;
const disc = data.subarray(offset, (offset += 8));
if (disc.equals(Uint8Array.from([226, 101, 163, 4, 133, 160, 84, 245]))) {
const status = data[offset];
offset += 1;
const writeAuthority = new PublicKey(data.subarray(offset, (offset += 32)));
const version = data[offset];
offset += 1;
const bufLen = data.readUInt32LE(offset);
offset += 4;
const buf = data.subarray(offset, (offset += bufLen));
return new VaaAccount({ encodedVaa: { status, writeAuthority, version, buf } });
} else if (data.subarray(0, 4).equals(Uint8Array.from([118, 97, 97, 1]))) {
const consistencyLevel = data[4];
const timestamp = data.readUInt32LE(5);
const signatureSet = new PublicKey(data.subarray(9, 41));
const guardianSetIndex = data.readUInt32LE(41);
const nonce = data.readUInt32LE(45);
const sequence = data.readBigUInt64LE(49);
const emitterChain = data.readUInt16LE(57);
const emitterAddress = Array.from(data.subarray(59, 91));
const payloadLen = data.readUInt32LE(91);
const payload = data.subarray(95, 95 + payloadLen);
} else if (disc.subarray(0, (offset -= 4)).equals(Uint8Array.from([118, 97, 97, 1]))) {
const consistencyLevel = data[offset];
offset += 1;
const timestamp = data.readUInt32LE(offset);
offset += 4;
const signatureSet = new PublicKey(data.subarray(offset, (offset += 32)));
const guardianSetIndex = data.readUInt32LE(offset);
offset += 4;
const nonce = data.readUInt32LE(offset);
offset += 4;
const sequence = data.readBigUInt64LE(offset);
offset += 8;
const emitterChain = data.readUInt16LE(offset);
offset += 2;
const emitterAddress = Array.from(data.subarray(offset, (offset += 32)));
const payloadLen = data.readUInt32LE(offset);
offset += 4;
const payload = data.subarray(offset, (offset += payloadLen));
return new VaaAccount({
postedVaaV1: {
@ -78,21 +96,58 @@ export class VaaAccount {
address: Array.from(parsed.emitterAddress),
sequence: parsed.sequence,
};
} else {
} else if (this._postedVaaV1 !== undefined) {
const { emitterChain: chain, emitterAddress: address, sequence } = this._postedVaaV1;
return {
chain,
address,
sequence,
};
} else {
throw new Error("impossible: emitterInfo() failed");
}
}
payload(): Buffer {
if (this._encodedVaa !== undefined) {
return parseVaa(this._encodedVaa.buf).payload;
} else {
} else if (this._postedVaaV1 !== undefined) {
return this._postedVaaV1.payload;
} else {
throw new Error("impossible: payload() failed");
}
}
digest(): Uint8Array {
if (this._encodedVaa !== undefined) {
return ethers.utils.arrayify(
ethers.utils.keccak256(parseVaa(this._encodedVaa.buf).hash),
);
} else if (this._postedVaaV1 !== undefined) {
const {
consistencyLevel,
timestamp,
nonce,
sequence,
emitterChain,
emitterAddress,
payload,
} = this._postedVaaV1;
let offset = 0;
const buf = Buffer.alloc(51 + payload.length);
offset = buf.writeUInt32BE(timestamp, offset);
offset = buf.writeUInt32BE(nonce, offset);
offset = buf.writeUInt16BE(emitterChain, offset);
buf.set(emitterAddress, offset);
offset += 32;
offset = buf.writeBigUInt64BE(sequence, offset);
offset = buf.writeUInt8(consistencyLevel, offset);
buf.set(payload, offset);
return ethers.utils.arrayify(ethers.utils.keccak256(ethers.utils.keccak256(buf)));
} else {
throw new Error("impossible: digest() failed");
}
}
@ -120,31 +175,3 @@ export class VaaAccount {
this._postedVaaV1 = postedVaaV1;
}
}
export class Claim {
static address(
programId: PublicKey,
address: Array<number>,
chain: number,
sequence: bigint,
prefix?: Buffer,
): PublicKey {
const chainBuf = Buffer.alloc(2);
chainBuf.writeUInt16BE(chain);
const sequenceBuf = Buffer.alloc(8);
sequenceBuf.writeBigUInt64BE(sequence);
if (prefix !== undefined) {
return PublicKey.findProgramAddressSync(
[prefix, Buffer.from(address), chainBuf, sequenceBuf],
new PublicKey(programId),
)[0];
} else {
return PublicKey.findProgramAddressSync(
[Buffer.from(address), chainBuf, sequenceBuf],
new PublicKey(programId),
)[0];
}
}
}

View File

@ -6,11 +6,10 @@ import * as splToken from "@solana/spl-token";
import { expect } from "chai";
import {
CctpTokenBurnMessage,
CircleIntegrationProgram,
Deposit,
DepositHeader,
CircleIntegrationProgram,
VaaAccount,
Claim,
} from "../src";
import {
CircleAttester,
@ -269,13 +268,15 @@ describe("Circle Integration -- Localnet", () => {
const wormholeMessageNonce = 420;
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const coreMessage = anchor.web3.Keypair.generate();
const cctpMessage = anchor.web3.Keypair.generate();
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
burnSource: payerToken,
coreMessage: message.publicKey,
coreMessage: coreMessage.publicKey,
cctpMessage: cctpMessage.publicKey,
},
{
amount: 0n,
@ -301,7 +302,7 @@ describe("Circle Integration -- Localnet", () => {
await expectIxErr(
connection,
[approveIx, ix],
[payer, message],
[payer, coreMessage, cctpMessage],
"Error Code: InvalidAmount",
{
addressLookupTableAccounts: [lookupTableAccount],
@ -320,13 +321,15 @@ describe("Circle Integration -- Localnet", () => {
const wormholeMessageNonce = 420;
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const coreMessage = anchor.web3.Keypair.generate();
const cctpMessage = anchor.web3.Keypair.generate();
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
burnSource: payerToken,
coreMessage: message.publicKey,
coreMessage: coreMessage.publicKey,
cctpMessage: cctpMessage.publicKey,
},
{
amount,
@ -352,7 +355,7 @@ describe("Circle Integration -- Localnet", () => {
await expectIxErr(
connection,
[approveIx, ix],
[payer, message],
[payer, coreMessage, cctpMessage],
"Error Code: InvalidMintRecipient",
{
addressLookupTableAccounts: [lookupTableAccount],
@ -372,13 +375,15 @@ describe("Circle Integration -- Localnet", () => {
const wormholeMessageNonce = 420;
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const coreMessage = anchor.web3.Keypair.generate();
const cctpMessage = anchor.web3.Keypair.generate();
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
burnSource: payerToken,
coreMessage: message.publicKey,
coreMessage: coreMessage.publicKey,
cctpMessage: cctpMessage.publicKey,
},
{
amount,
@ -394,9 +399,15 @@ describe("Circle Integration -- Localnet", () => {
.then((resp) => resp.value);
// NOTE: This is an SPL Token program error.
await expectIxErr(connection, [ix], [payer, message], "Error: owner does not match", {
addressLookupTableAccounts: [lookupTableAccount],
});
await expectIxErr(
connection,
[ix],
[payer, coreMessage, cctpMessage],
"Error: owner does not match",
{
addressLookupTableAccounts: [lookupTableAccount],
},
);
});
it("Invoke `transfer_tokens_with_payload`", async () => {
@ -411,13 +422,15 @@ describe("Circle Integration -- Localnet", () => {
const wormholeMessageNonce = 420;
const inputPayload = Buffer.from("All your base are belong to us.");
const message = anchor.web3.Keypair.generate();
const coreMessage = anchor.web3.Keypair.generate();
const cctpMessage = anchor.web3.Keypair.generate();
const ix = await circleIntegration.transferTokensWithPayloadIx(
{
payer: payer.publicKey,
mint: USDC_MINT_ADDRESS,
burnSource: payerToken,
coreMessage: message.publicKey,
coreMessage: coreMessage.publicKey,
cctpMessage: cctpMessage.publicKey,
},
{
amount,
@ -445,7 +458,7 @@ describe("Circle Integration -- Localnet", () => {
const txReceipt = await expectIxOkDetails(
connection,
[approveIx, ix],
[payer, message],
[payer, coreMessage, cctpMessage],
{
addressLookupTableAccounts: [lookupTableAccount],
},
@ -458,19 +471,15 @@ describe("Circle Integration -- Localnet", () => {
expect(balanceAfter + amount).to.equal(balanceBefore);
// Check messages.
const posted = await getPostedMessage(connection, message.publicKey);
const posted = await getPostedMessage(connection, coreMessage.publicKey);
const { deposit, payload } = Deposit.decode(posted.message.payload);
expect(payload).to.eql(inputPayload);
const parsedTxData = await circleIntegration.parseTransactionReceipt(txReceipt, [
lookupTableAccount,
]);
expect(parsedTxData).has.length(1);
const { message: encodedCctpMessage } = await circleIntegration
.messageTransmitterProgram()
.fetchMessageSent(cctpMessage.publicKey);
const txData = parsedTxData[0];
expect(txData.coreMessageAccount).is.eql(message.publicKey);
const burnMessage = CctpTokenBurnMessage.decode(txData.encodedCctpMessage);
const burnMessage = CctpTokenBurnMessage.decode(encodedCctpMessage);
expect(burnMessage.sender).to.eql(
Array.from(circleIntegration.custodianAddress().toBuffer()),
);
@ -562,7 +571,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -583,7 +592,7 @@ describe("Circle Integration -- Localnet", () => {
connection,
[computeIx, ix],
[payer, mintRecipientAuthority],
"ConstraintOwner",
"Error Code: AccountDidNotDeserialize",
{
addressLookupTableAccounts: [lookupTableAccount],
},
@ -643,7 +652,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -721,7 +730,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -797,7 +806,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -872,7 +881,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -946,7 +955,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -1020,7 +1029,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -1094,7 +1103,7 @@ describe("Circle Integration -- Localnet", () => {
);
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
units: 250_000,
units: 300_000,
});
const ix = await circleIntegration.redeemTokensWithPayloadIx(
{
@ -1150,12 +1159,20 @@ describe("Circle Integration -- Localnet", () => {
{ encodedCctpMessage, cctpAttestation },
);
// NOTE: This is a CCTP Message Transmitter program error.
const vaaHash = await VaaAccount.fetch(connection, vaa).then((vaa) => vaa.digest());
const consumedVaa = circleIntegration.consumedVaaAddress(vaaHash);
const lookupTableAccount = await connection
.getAddressLookupTable(lookupTableAddress)
.then((resp) => resp.value);
await expectIxErr(
connection,
[ix],
[payer, mintRecipientAuthority],
"Error Code: NonceAlreadyUsed",
`Allocate: account Address { address: ${consumedVaa.toString()}, base: None } already in use`,
{
addressLookupTableAccounts: [lookupTableAccount],
},
);
});
});

View File

@ -24,7 +24,7 @@ describe("Circle Integration -- Testnet Fork", () => {
const circleIntegration = new CircleIntegrationProgram(
connection,
"wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW",
"wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d",
);
describe("Upgrade Contract", () => {

View File

@ -1,14 +1,14 @@
{
"pubkey": "4xt9P42CcMHXAgvemTnzineHp6owfGUcrg1xD9V7mdk1",
"account": {
"lamports": 1684320,
"lamports": 1795680,
"data": [
"n4M6qsFUgLaJOQ9DWBY8Pr5GkgAm5md6yYsX7D3P0LYdBflhsqtwJDtELLORIVfxOpM9ATQoLQMrX/7NAaLb8bd5BgjfAC6nABCl1OgAAAAyAAAAAAAAAIYAAAAAAAAAjaVmAQAAAAAhU6gjdg8AAP//",
"n4M6qsFUgLaJOQ9DWBY8Pr5GkgAm5md6yYsX7D3P0LYdBflhsqtwJDtELLORIVfxOpM9ATQoLQMrX/7NAaLb8bd5BgjfAC6nABCl1OgAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAD//w==",
"base64"
],
"owner": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
"executable": false,
"rentEpoch": 18446744073709551615,
"space": 114
"space": 130
}
}
}