Compare commits

..

2 Commits

Author SHA1 Message Date
A5 Pickle 3d6492f8b5
solana: fix after cctp upgrade; uptick anchor 0.29 2024-02-08 15:21:40 -06:00
A5 Pickle 6885f97349
solana: add program and tests v1 2024-02-08 15:21:17 -06:00
38 changed files with 1060 additions and 594 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.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

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

@ -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,
)

View File

@ -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,
)

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.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)
}

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.
///
@ -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();

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].
@ -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,
},
)?;

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

@ -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";
}

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

@ -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],
},

View File

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