Compare commits
2 Commits
687cf29a29
...
3d6492f8b5
Author | SHA1 | Date |
---|---|---|
A5 Pickle | 3d6492f8b5 | |
A5 Pickle | 6885f97349 |
|
@ -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]]
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,7 @@ resolver = "2"
|
|||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "0.1.0-alpha.1"
|
||||
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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -78,11 +78,11 @@ 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,
|
||||
)
|
||||
|
|
|
@ -91,11 +91,11 @@ 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,
|
||||
)
|
||||
|
|
|
@ -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.burn_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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ pub mod cpi;
|
|||
|
||||
pub mod error;
|
||||
|
||||
pub use wormhole_io as io;
|
||||
|
||||
pub mod messages;
|
||||
|
||||
pub mod utils;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
mod config;
|
||||
pub use config::*;
|
|
@ -0,0 +1,2 @@
|
|||
mod zero_copy;
|
||||
pub use zero_copy::*;
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -34,7 +34,5 @@ ruint.workspace = true
|
|||
|
||||
cfg-if.workspace = true
|
||||
|
||||
ahash.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal.workspace = true
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
@ -144,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(
|
||||
|
@ -197,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();
|
||||
|
|
|
@ -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].
|
||||
|
@ -154,39 +151,6 @@ pub struct TransferTokensWithPayloadArgs {
|
|||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
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.
|
||||
///
|
||||
|
@ -281,7 +245,7 @@ pub fn transfer_tokens_with_payload(
|
|||
amount,
|
||||
mint_recipient,
|
||||
wormhole_message_nonce,
|
||||
payload: WrappedVec(payload),
|
||||
payload,
|
||||
},
|
||||
)?;
|
||||
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -1,3 +1,6 @@
|
|||
mod consumed_vaa;
|
||||
pub use consumed_vaa::*;
|
||||
|
||||
mod custodian;
|
||||
pub use custodian::*;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -24,12 +24,12 @@ import {
|
|||
TokenMessengerMinterProgram,
|
||||
} from "./cctp";
|
||||
import { BPF_LOADER_UPGRADEABLE_ID } from "./consts";
|
||||
import { Custodian, RegisteredEmitter } from "./state";
|
||||
import { Claim, VaaAccount } from "./wormhole";
|
||||
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];
|
||||
|
@ -86,7 +86,7 @@ export type TransferTokensWithPayloadAccounts = PublishMessageAccounts & {
|
|||
|
||||
export type RedeemTokensWithPayloadAccounts = {
|
||||
custodian: PublicKey;
|
||||
claim: PublicKey;
|
||||
consumedVaa: PublicKey;
|
||||
mintRecipientAuthority: PublicKey;
|
||||
mintRecipient: PublicKey;
|
||||
registeredEmitter: PublicKey;
|
||||
|
@ -162,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 } =
|
||||
|
@ -229,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 = (() => {
|
||||
|
@ -253,7 +252,7 @@ export class CircleIntegrationProgram {
|
|||
payer,
|
||||
custodian: this.custodianAddress(),
|
||||
vaa,
|
||||
claim,
|
||||
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
|
||||
registeredEmitter,
|
||||
remoteTokenMessenger,
|
||||
})
|
||||
|
@ -268,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
|
||||
|
@ -281,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)),
|
||||
|
@ -417,8 +411,7 @@ 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 {
|
||||
authority: messageTransmitterAuthority,
|
||||
|
@ -438,7 +431,7 @@ export class CircleIntegrationProgram {
|
|||
|
||||
return {
|
||||
custodian: this.custodianAddress(),
|
||||
claim,
|
||||
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
|
||||
mintRecipientAuthority,
|
||||
mintRecipient,
|
||||
registeredEmitter: this.registeredEmitterAddress(chain),
|
||||
|
@ -475,7 +468,7 @@ export class CircleIntegrationProgram {
|
|||
|
||||
const {
|
||||
custodian,
|
||||
claim,
|
||||
consumedVaa,
|
||||
mintRecipientAuthority,
|
||||
mintRecipient,
|
||||
registeredEmitter,
|
||||
|
@ -500,7 +493,7 @@ export class CircleIntegrationProgram {
|
|||
payer,
|
||||
custodian,
|
||||
vaa,
|
||||
claim,
|
||||
consumedVaa,
|
||||
mintRecipientAuthority: inputMintRecipientAuthority ?? mintRecipientAuthority,
|
||||
mintRecipient,
|
||||
registeredEmitter,
|
||||
|
@ -601,5 +594,5 @@ export function mainnet(): ProgramId {
|
|||
}
|
||||
|
||||
export function testnet(): ProgramId {
|
||||
return "wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW";
|
||||
return "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d";
|
||||
}
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -1,2 +1,3 @@
|
|||
export * from "./ConsumedVaa";
|
||||
export * from "./Custodian";
|
||||
export * from "./RegisteredEmitter";
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,13 @@ import { getPostedMessage } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhol
|
|||
import * as anchor from "@coral-xyz/anchor";
|
||||
import * as splToken from "@solana/spl-token";
|
||||
import { expect } from "chai";
|
||||
import { CctpTokenBurnMessage, CircleIntegrationProgram, Deposit, DepositHeader } from "../src";
|
||||
import {
|
||||
CctpTokenBurnMessage,
|
||||
CircleIntegrationProgram,
|
||||
Deposit,
|
||||
DepositHeader,
|
||||
VaaAccount,
|
||||
} from "../src";
|
||||
import {
|
||||
CircleAttester,
|
||||
ETHEREUM_USDC_ADDRESS,
|
||||
|
@ -565,7 +571,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -586,7 +592,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
connection,
|
||||
[computeIx, ix],
|
||||
[payer, mintRecipientAuthority],
|
||||
"ConstraintOwner",
|
||||
"Error Code: AccountDidNotDeserialize",
|
||||
{
|
||||
addressLookupTableAccounts: [lookupTableAccount],
|
||||
},
|
||||
|
@ -646,7 +652,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -724,7 +730,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -800,7 +806,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -875,7 +881,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -949,7 +955,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -1023,7 +1029,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -1097,7 +1103,7 @@ describe("Circle Integration -- Localnet", () => {
|
|||
);
|
||||
|
||||
const computeIx = anchor.web3.ComputeBudgetProgram.setComputeUnitLimit({
|
||||
units: 250_000,
|
||||
units: 300_000,
|
||||
});
|
||||
const ix = await circleIntegration.redeemTokensWithPayloadIx(
|
||||
{
|
||||
|
@ -1153,15 +1159,17 @@ describe("Circle Integration -- Localnet", () => {
|
|||
{ encodedCctpMessage, cctpAttestation },
|
||||
);
|
||||
|
||||
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);
|
||||
// NOTE: This is a CCTP Message Transmitter program error.
|
||||
await expectIxErr(
|
||||
connection,
|
||||
[ix],
|
||||
[payer, mintRecipientAuthority],
|
||||
"Error Code: NonceAlreadyUsed",
|
||||
`Allocate: account Address { address: ${consumedVaa.toString()}, base: None } already in use`,
|
||||
{
|
||||
addressLookupTableAccounts: [lookupTableAccount],
|
||||
},
|
||||
|
|
|
@ -24,7 +24,7 @@ describe("Circle Integration -- Testnet Fork", () => {
|
|||
|
||||
const circleIntegration = new CircleIntegrationProgram(
|
||||
connection,
|
||||
"wCCTPvsyeL9qYqbHTv3DUAyzEfYcyHoYw5c4mgcbBeW",
|
||||
"wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d",
|
||||
);
|
||||
|
||||
describe("Upgrade Contract", () => {
|
||||
|
|
Loading…
Reference in New Issue