Compare commits
2 Commits
687cf29a29
...
3d6492f8b5
Author | SHA1 | Date |
---|---|---|
A5 Pickle | 3d6492f8b5 | |
A5 Pickle | 6885f97349 |
|
@ -0,0 +1,8 @@
|
|||
.anchor
|
||||
.env
|
||||
.private
|
||||
.vscode
|
||||
artifacts-*
|
||||
node_modules
|
||||
target
|
||||
ts/tests/artifacts
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.ts",
|
||||
"options": {
|
||||
"printWidth": 100,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
[toolchain]
|
||||
anchor_version = "0.29.0" # CLI
|
||||
solana_version = "1.16.27"
|
||||
|
||||
[features]
|
||||
seeds = false
|
||||
skip-lint = false
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"programs/circle-integration"
|
||||
]
|
||||
|
||||
[programs.localnet]
|
||||
wormhole_circle_integration_solana = "Wormho1eCirc1e1ntegration111111111111111111"
|
||||
|
||||
[registry]
|
||||
url = "https://api.apr.dev"
|
||||
|
||||
[provider]
|
||||
cluster = "Localnet"
|
||||
wallet = "ts/tests/keys/pFCBP4bhqdSsrWUVTgqhPsLrfEdChBK17vgFM7TxjxQ.json"
|
||||
|
||||
[scripts]
|
||||
test = "npx ts-mocha -p ./tsconfig.json -t 1000000 ts/tests/[0-9]*.ts"
|
||||
|
||||
[test]
|
||||
startup_wait = 30000
|
||||
|
||||
[test.validator]
|
||||
url = "https://api.devnet.solana.com"
|
||||
|
||||
### At 160 ticks/s, 64 ticks per slot implies that leader rotation and voting will happen
|
||||
### every 400 ms. A fast voting cadence ensures faster finality and convergence
|
||||
ticks_per_slot = 8
|
||||
|
||||
### Forked Wormhole Circle Integration Program
|
||||
[[test.validator.clone]]
|
||||
address = "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d"
|
||||
|
||||
### Forked Wormhole Circle Integration PDA -- Custodian
|
||||
[[test.validator.clone]]
|
||||
address = "4tTfYz2SqRcZWqyBk1yHyEPzHjoHNbUErQbifBkLmzbT"
|
||||
|
||||
### Wormhole Core Bridge Program (Testnet)
|
||||
[[test.validator.clone]]
|
||||
address = "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"
|
||||
|
||||
### Circle Message Transmitter Program
|
||||
[[test.validator.clone]]
|
||||
address = "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd"
|
||||
|
||||
### Circle Token Messenger Minter Program
|
||||
[[test.validator.clone]]
|
||||
address = "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3"
|
||||
|
||||
### Mint -- USDC
|
||||
[[test.validator.account]]
|
||||
address = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU"
|
||||
filename = "ts/tests/accounts/usdc_mint.json"
|
||||
|
||||
### Payer Token Account -- USDC
|
||||
[[test.validator.account]]
|
||||
address = "6s9vuDVXZsJY1Qp29cFxKgbSmpTH2QWnrjZzPHWmFXCz"
|
||||
filename = "ts/tests/accounts/usdc_payer_token.json"
|
||||
|
||||
### Circle Token Messenger Minter PDA -- Token Messenger
|
||||
[[test.validator.account]]
|
||||
address = "Afgq3BHEfCE7d78D2XE9Bfyu2ieDqvE24xX8KDwreBms"
|
||||
filename = "ts/tests/accounts/token_messenger_minter/token_messenger.json"
|
||||
|
||||
### Circle Token Messenger Minter PDA -- Token Minter
|
||||
[[test.validator.account]]
|
||||
address = "DBD8hAwLDRQkTsu6EqviaYNGKPnsAMmQonxf7AH8ZcFY"
|
||||
filename = "ts/tests/accounts/token_messenger_minter/token_minter.json"
|
||||
|
||||
### Circle Token Messenger Minter PDA -- USDC Custody Token Account
|
||||
[[test.validator.account]]
|
||||
address = "AEfKU8wHGtYgsXpymQ6e1cGHJJeKqCj95pw82iyRUKEs"
|
||||
filename = "ts/tests/accounts/token_messenger_minter/usdc_custody_token.json"
|
||||
|
||||
### Circle Token Messenger Minter PDA -- USDC Local Token
|
||||
[[test.validator.account]]
|
||||
address = "4xt9P42CcMHXAgvemTnzineHp6owfGUcrg1xD9V7mdk1"
|
||||
filename = "ts/tests/accounts/token_messenger_minter/usdc_local_token.json"
|
||||
|
||||
### Circle Token Messenger Minter PDA -- USDC Token Pair
|
||||
[[test.validator.account]]
|
||||
address = "ADcG1d7znq6wR73BJgEh7dR4vTJcETLLyfXMNZjJVwk4"
|
||||
filename = "ts/tests/accounts/token_messenger_minter/usdc_token_pair.json"
|
||||
|
||||
### Circle Token Messenger Minter PDA -- Ethereum Remote Token Messenger
|
||||
[[test.validator.account]]
|
||||
address = "Hazwi3jFQtLKc2ughi7HFXPkpDeso7DQaMR9Ks4afh3j"
|
||||
filename = "ts/tests/accounts/token_messenger_minter/ethereum_remote_token_messenger.json"
|
||||
|
||||
### Circle Token Messenger Minter PDA -- Base Remote Token Messenger
|
||||
[[test.validator.account]]
|
||||
address = "BWyFzH6LsnmDAaDWbGsriQ9SiiKq1CF6pbH4Ye3kzSBV"
|
||||
filename = "ts/tests/accounts/token_messenger_minter/misconfigured_remote_token_messenger.json"
|
||||
|
||||
### Circle Message Transmitter PDA -- Message Transmitter Config
|
||||
[[test.validator.account]]
|
||||
address = "BWrwSWjbikT3H7qHAkUEbLmwDQoB4ZDJ4wcSEhSPTZCu"
|
||||
filename = "ts/tests/accounts/message_transmitter/message_transmitter_config.json"
|
||||
|
||||
### Wormhole Core Bridge (Testnet) -- Config
|
||||
[[test.validator.account]]
|
||||
address = "6bi4JGDoRwUs9TYBuvoA7dUVyikTJDrJsJU1ew6KVLiu"
|
||||
filename = "ts/tests/accounts/core_bridge_testnet/config.json"
|
||||
|
||||
### Wormhole Core Bridge (Testnet) -- Fee Collector
|
||||
[[test.validator.account]]
|
||||
address = "7s3a1ycs16d6SNDumaRtjcoyMaTDZPavzgsmS3uUZYWX"
|
||||
filename = "ts/tests/accounts/core_bridge_testnet/fee_collector.json"
|
||||
|
||||
### Wormhole Core Bridge (Testnet) -- Guardian Set 0
|
||||
[[test.validator.account]]
|
||||
address = "dxZtypiKT5D9LYzdPxjvSZER9MgYfeRVU5qpMTMTRs4"
|
||||
filename = "ts/tests/accounts/core_bridge_testnet/guardian_set_0.json"
|
||||
|
||||
### Wormhole Core Bridge Program (Mainnet)
|
||||
[[test.genesis]]
|
||||
address = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"
|
||||
program = "ts/tests/artifacts/mainnet_core_bridge.so"
|
||||
|
||||
### Wormhole Core Bridge (Mainnet) -- Config
|
||||
[[test.validator.account]]
|
||||
address = "2yVjuQwpsvdsrywzsJJVs9Ueh4zayyo5DYJbBNc3DDpn"
|
||||
filename = "ts/tests/accounts/core_bridge_mainnet/config.json"
|
||||
|
||||
### Wormhole Core Bridge (Mainnet) -- Fee Collector
|
||||
[[test.validator.account]]
|
||||
address = "9bFNrXNb2WTx8fMHXCheaZqkLZ3YCCaiqTftHxeintHy"
|
||||
filename = "ts/tests/accounts/core_bridge_mainnet/fee_collector.json"
|
||||
|
||||
### Wormhole Core Bridge (Mainnet) -- Guardian Set 0
|
||||
[[test.validator.account]]
|
||||
address = "DS7qfSAgYsonPpKoAjcGhX9VFjXdGkiHjEDkTidf8H2P"
|
||||
filename = "ts/tests/accounts/core_bridge_mainnet/guardian_set_0.json"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,46 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"modules/wormhole-cctp",
|
||||
"programs/circle-integration",
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
version = "0.1.0-alpha.5"
|
||||
authors = ["Wormhole Contributors"]
|
||||
license = "Apache-2.0"
|
||||
homepage = "https://wormhole.com"
|
||||
repository = "https://github.com/wormhole-foundation/wormhole-circle-integration"
|
||||
|
||||
[workspace.dependencies.wormhole-cctp-solana]
|
||||
path = "modules/wormhole-cctp"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.wormhole-raw-vaas]
|
||||
version = "0.1.1"
|
||||
features = ["ruint", "on-chain"]
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.anchor-lang]
|
||||
version = "0.29.0"
|
||||
features = ["derive", "init-if-needed"]
|
||||
|
||||
[workspace.dependencies]
|
||||
wormhole-io = "0.1.3"
|
||||
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"
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
|
||||
[profile.release.build-override]
|
||||
opt-level = 3
|
||||
incremental = false
|
||||
codegen-units = 1
|
|
@ -0,0 +1,49 @@
|
|||
SOLANA_CLI="v1.16.16"
|
||||
ANCHOR_CLI="v0.28.0"
|
||||
|
||||
out_mainnet=artifacts-mainnet
|
||||
out_testnet=artifacts-testnet
|
||||
|
||||
.PHONY: all clean check build test lint ci
|
||||
|
||||
all: check
|
||||
|
||||
check:
|
||||
cargo check --all-features
|
||||
|
||||
clean:
|
||||
anchor clean
|
||||
rm -rf node_modules artifacts-mainnet artifacts-testnet ts/tests/artifacts
|
||||
|
||||
node_modules:
|
||||
npm ci
|
||||
|
||||
prune_idl: node_modules ts/scripts/pruneIdlTypes.ts
|
||||
cd ts/scripts && npx ts-node pruneIdlTypes.ts
|
||||
|
||||
build: $(out_$(NETWORK))
|
||||
$(out_$(NETWORK)):
|
||||
ifdef out_$(NETWORK)
|
||||
anchor build -p wormhole_circle_integration_solana --arch sbf -- --features "$(NETWORK),no-idl" -- --no-default-features
|
||||
mkdir -p $(out_$(NETWORK))
|
||||
cp target/deploy/*.so $(out_$(NETWORK))/
|
||||
endif
|
||||
|
||||
test: node_modules
|
||||
cargo test --all-features
|
||||
anchor build -p wormhole_circle_integration_solana --arch sbf -- --features testnet -- --no-default-features
|
||||
mkdir -p ts/tests/artifacts && cp target/deploy/wormhole_circle_integration_solana.so ts/tests/artifacts/testnet_wormhole_circle_integration_solana.so
|
||||
solana program dump -u m worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth ts/tests/artifacts/mainnet_core_bridge.so
|
||||
anchor build --arch sbf -- --features integration-test -- --no-default-features
|
||||
$(MAKE) prune_idl
|
||||
anchor test --skip-build
|
||||
|
||||
lint:
|
||||
cargo fmt --check
|
||||
cargo clippy --no-deps --all-targets --all-features -- -D warnings
|
||||
|
||||
ci:
|
||||
DOCKER_BUILDKIT=1 docker build -f Dockerfile.ci \
|
||||
--build-arg SOLANA_CLI=$(SOLANA_CLI) \
|
||||
--build-arg ANCHOR_CLI=$(ANCHOR_CLI) \
|
||||
.
|
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "wormhole-cctp-solana"
|
||||
description = "Wormhole CCTP Utilities for Solana"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[features]
|
||||
default = ["cpi"]
|
||||
client = []
|
||||
cpi = ["dep:anchor-spl", "dep:solana-program"]
|
||||
mainnet = []
|
||||
testnet = []
|
||||
|
||||
[dependencies]
|
||||
wormhole-io.workspace = true
|
||||
wormhole-raw-vaas.workspace = true
|
||||
|
||||
anchor-lang.workspace = true
|
||||
anchor-spl = { optional = true, workspace = true }
|
||||
solana-program = { optional = true, workspace = true }
|
||||
|
||||
hex.workspace = true
|
||||
ruint.workspace = true
|
||||
|
||||
cfg-if.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal.workspace = true
|
|
@ -0,0 +1,2 @@
|
|||
mod receive_message;
|
||||
pub use receive_message::*;
|
|
@ -0,0 +1,15 @@
|
|||
mod token_messenger_minter;
|
||||
pub use token_messenger_minter::*;
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Common arguments to redeem messages via the CCTP Message Transmitter program using its receive
|
||||
/// message instruction.
|
||||
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct ReceiveMessageArgs {
|
||||
/// CCTP message.
|
||||
pub encoded_message: Vec<u8>,
|
||||
|
||||
/// Attestation of [encoded_message](Self::encoded_message).
|
||||
pub attestation: Vec<u8>,
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Account context to invoke [receive_token_messenger_minter_message].
|
||||
pub struct ReceiveTokenMessengerMinterMessage<'info> {
|
||||
/// Mutable signer. Transaction payer.
|
||||
pub payer: AccountInfo<'info>,
|
||||
|
||||
/// Signer. Specific caller, which must be encoded as the destination caller.
|
||||
pub caller: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["message_transmitter_authority"\, token_messenger_minter_program] (CCTP
|
||||
/// Message Transmitter program).
|
||||
pub message_transmitter_authority: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["message_transmitter"\] (CCTP Message Transmitter program).
|
||||
pub message_transmitter_config: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. Seeds must be \["used_nonces", remote_domain.to_string(), first_nonce.to_string()\]
|
||||
/// (CCTP Message Transmitter program).
|
||||
pub used_nonces: AccountInfo<'info>,
|
||||
|
||||
/// CCTP Token Messenger Minter program.
|
||||
pub token_messenger_minter_program: AccountInfo<'info>,
|
||||
|
||||
pub system_program: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["__event_authority"\] (CCTP Message Transmitter program)).
|
||||
pub message_transmitter_event_authority: AccountInfo<'info>,
|
||||
|
||||
pub message_transmitter_program: AccountInfo<'info>,
|
||||
|
||||
// The following accounts are expected to be passed in as remaining accounts. These accounts are
|
||||
// meant for the Token Messenger Minter program because the Message Transmitter program performs
|
||||
// CPI on this program so it can mint tokens.
|
||||
//
|
||||
// For this integration, we are defining these accounts explicitly in this account context.
|
||||
|
||||
//
|
||||
/// Seeds must be \["token_messenger"\] (CCTP Token Messenger Minter program).
|
||||
pub token_messenger: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["remote_token_messenger"\, remote_domain.to_string()] (CCTP Token Messenger
|
||||
/// Minter program).
|
||||
pub remote_token_messenger: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["token_minter"\] (CCTP Token Messenger Minter program).
|
||||
pub token_minter: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. Seeds must be \["local_token", mint\] (CCTP Token Messenger Minter program).
|
||||
pub local_token: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["token_pair", remote_domain.to_string(), remote_token_address\] (CCTP Token
|
||||
/// Messenger Minter program).
|
||||
pub token_pair: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. Mint recipient token account, which must be encoded as the mint recipient in the
|
||||
/// CCTP mesage.
|
||||
pub mint_recipient: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. Seeds must be \["custody", mint\] (CCTP Token Messenger Minter program).
|
||||
pub custody_token: AccountInfo<'info>,
|
||||
|
||||
pub token_program: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
|
||||
pub token_messenger_minter_event_authority: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
/// Method to call the receive message instruction on the CCTP Message Transmitter program, specific
|
||||
/// to receiving a Token Messenger Minter message to mint Circle-supported tokens.
|
||||
///
|
||||
/// NOTE: The [caller](ReceiveTokenMessengerMinterMessage::caller) account must be encoded in the
|
||||
/// CCTP message as the destination caller.
|
||||
pub fn receive_token_messenger_minter_message<'info>(
|
||||
ctx: CpiContext<'_, '_, '_, 'info, ReceiveTokenMessengerMinterMessage<'info>>,
|
||||
args: super::ReceiveMessageArgs,
|
||||
) -> Result<()> {
|
||||
const ANCHOR_IX_SELECTOR: [u8; 8] = [38, 144, 127, 225, 31, 225, 238, 25];
|
||||
|
||||
solana_program::program::invoke_signed(
|
||||
&solana_program::instruction::Instruction {
|
||||
program_id: crate::cctp::message_transmitter_program::ID,
|
||||
accounts: ctx.to_account_metas(None),
|
||||
data: (ANCHOR_IX_SELECTOR, args).try_to_vec()?,
|
||||
},
|
||||
&ctx.to_account_infos(),
|
||||
ctx.signer_seeds,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
impl<'info> ToAccountMetas for ReceiveTokenMessengerMinterMessage<'info> {
|
||||
fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
|
||||
vec![
|
||||
AccountMeta::new(self.payer.key(), true),
|
||||
AccountMeta::new_readonly(self.caller.key(), true),
|
||||
AccountMeta::new_readonly(self.message_transmitter_authority.key(), false),
|
||||
AccountMeta::new_readonly(self.message_transmitter_config.key(), false),
|
||||
AccountMeta::new(self.used_nonces.key(), false),
|
||||
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
|
||||
AccountMeta::new_readonly(self.system_program.key(), false),
|
||||
AccountMeta::new_readonly(self.message_transmitter_event_authority.key(), false),
|
||||
AccountMeta::new_readonly(self.message_transmitter_program.key(), false),
|
||||
AccountMeta::new_readonly(self.token_messenger.key(), false),
|
||||
AccountMeta::new_readonly(self.remote_token_messenger.key(), false),
|
||||
AccountMeta::new_readonly(self.token_minter.key(), false),
|
||||
AccountMeta::new(self.local_token.key(), false),
|
||||
AccountMeta::new_readonly(self.token_pair.key(), false),
|
||||
AccountMeta::new(self.mint_recipient.key(), false),
|
||||
AccountMeta::new(self.custody_token.key(), false),
|
||||
AccountMeta::new_readonly(self.token_program.key(), false),
|
||||
AccountMeta::new_readonly(self.token_messenger_minter_event_authority.key(), false),
|
||||
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info> ToAccountInfos<'info> for ReceiveTokenMessengerMinterMessage<'info> {
|
||||
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
|
||||
vec![
|
||||
self.payer.clone(),
|
||||
self.caller.clone(),
|
||||
self.message_transmitter_authority.clone(),
|
||||
self.message_transmitter_config.clone(),
|
||||
self.used_nonces.clone(),
|
||||
self.token_messenger_minter_program.clone(),
|
||||
self.system_program.clone(),
|
||||
self.message_transmitter_event_authority.clone(),
|
||||
self.token_messenger.clone(),
|
||||
self.remote_token_messenger.clone(),
|
||||
self.token_minter.clone(),
|
||||
self.local_token.clone(),
|
||||
self.token_pair.clone(),
|
||||
self.mint_recipient.clone(),
|
||||
self.custody_token.clone(),
|
||||
self.token_program.clone(),
|
||||
self.token_messenger_minter_event_authority.clone(),
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
#[cfg(feature = "cpi")]
|
||||
pub mod cpi;
|
||||
|
||||
mod state;
|
||||
pub use state::*;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "mainnet")] {
|
||||
// Placeholder for real address
|
||||
anchor_lang::declare_id!("CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd");
|
||||
} else if #[cfg(feature = "testnet")] {
|
||||
anchor_lang::declare_id!("CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MessageTransmitter {}
|
||||
|
||||
impl anchor_lang::Id for MessageTransmitter {
|
||||
fn id() -> solana_program::pubkey::Pubkey {
|
||||
ID
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct MessageTransmitterConfig {
|
||||
pub owner: Pubkey,
|
||||
pub pending_owner: Pubkey,
|
||||
pub attester_manager: Pubkey,
|
||||
pub pauser: Pubkey,
|
||||
pub paused: bool,
|
||||
pub local_domain: u32,
|
||||
pub version: u32,
|
||||
pub signature_threshold: u32,
|
||||
pub enabled_attesters: Vec<[u8; 32]>,
|
||||
pub max_message_body_size: u64,
|
||||
pub next_available_nonce: u64,
|
||||
}
|
||||
|
||||
impl anchor_lang::Discriminator for MessageTransmitterConfig {
|
||||
const DISCRIMINATOR: [u8; 8] = [71, 40, 180, 142, 19, 203, 35, 252];
|
||||
}
|
||||
|
||||
impl Owner for MessageTransmitterConfig {
|
||||
fn owner() -> Pubkey {
|
||||
crate::cctp::message_transmitter_program::ID
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
mod message_transmitter_config;
|
||||
pub use message_transmitter_config::*;
|
|
@ -0,0 +1,3 @@
|
|||
pub mod message_transmitter_program;
|
||||
|
||||
pub mod token_messenger_minter_program;
|
|
@ -0,0 +1,149 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Account context to invoke [deposit_for_burn_with_caller].
|
||||
pub struct DepositForBurnWithCaller<'info> {
|
||||
/// Signer. This account must be the owner of `burn_token`.
|
||||
//#[account(signer)]
|
||||
pub burn_token_owner: AccountInfo<'info>,
|
||||
|
||||
//#[account(mut, signer)]
|
||||
pub payer: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["sender_authority"\] (CCTP Token Messenger Minter program).
|
||||
pub token_messenger_minter_sender_authority: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. This token account must be owned by `burn_token_owner`.
|
||||
//#[account(mut)]
|
||||
pub burn_token: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. Seeds must be \["message_transmitter"\] (CCTP Message Transmitter program).
|
||||
//#[account(mut)]
|
||||
pub message_transmitter_config: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["token_messenger"\] (CCTP Token Messenger Minter program).
|
||||
pub token_messenger: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["remote_token_messenger"\, remote_domain.to_string()] (CCTP Token Messenger
|
||||
/// Minter program).
|
||||
pub remote_token_messenger: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["token_minter"\] (CCTP Token Messenger Minter program).
|
||||
pub token_minter: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. Seeds must be \["local_token", mint\] (CCTP Token Messenger Minter program).
|
||||
//#[account(mut)]
|
||||
pub local_token: AccountInfo<'info>,
|
||||
|
||||
/// Mutable. Mint to be burned via CCTP.
|
||||
//#[account(mut)]
|
||||
pub mint: AccountInfo<'info>,
|
||||
|
||||
//#[account(mut, signer)]
|
||||
pub cctp_message: AccountInfo<'info>,
|
||||
|
||||
/// CCTP Message Transmitter program.
|
||||
pub message_transmitter_program: AccountInfo<'info>,
|
||||
|
||||
/// CCTP Token Messenger Minter program.
|
||||
pub token_messenger_minter_program: AccountInfo<'info>,
|
||||
|
||||
pub token_program: AccountInfo<'info>,
|
||||
|
||||
pub system_program: AccountInfo<'info>,
|
||||
|
||||
/// Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
|
||||
pub event_authority: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
/// Parameters to invoke [deposit_for_burn_with_caller].
|
||||
#[derive(Clone, AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct DepositForBurnWithCallerParams {
|
||||
/// Transfer (burn) amount.
|
||||
pub amount: u64,
|
||||
|
||||
/// CCTP domain value of the token to be transferred.
|
||||
pub destination_domain: u32,
|
||||
|
||||
/// Recipient of assets on target network.
|
||||
///
|
||||
/// NOTE: In the Token Messenger Minter program IDL, this is encoded as a Pubkey, which is
|
||||
/// weird because this address is one for another network. We are making it a 32-byte fixed
|
||||
/// array instead.
|
||||
pub mint_recipient: [u8; 32],
|
||||
|
||||
/// Expected caller on target network.
|
||||
///
|
||||
/// NOTE: In the Token Messenger Minter program IDL, this is encoded as a Pubkey, which is
|
||||
/// weird because this address is one for another network. We are making it a 32-byte fixed
|
||||
/// array instead.
|
||||
pub destination_caller: [u8; 32],
|
||||
}
|
||||
|
||||
/// CPI call to invoke the CCTP Token Messenger Minter program to burn Circle-supported assets.
|
||||
///
|
||||
/// NOTE: This instruction requires specifying a specific caller on the destination network. Only
|
||||
/// this caller can mint tokens on behalf of the
|
||||
/// [mint_recipient](DepositForBurnWithCallerParams::mint_recipient).
|
||||
pub fn deposit_for_burn_with_caller<'info>(
|
||||
ctx: CpiContext<'_, '_, '_, 'info, DepositForBurnWithCaller<'info>>,
|
||||
args: DepositForBurnWithCallerParams,
|
||||
) -> Result<()> {
|
||||
const ANCHOR_IX_SELECTOR: [u8; 8] = [167, 222, 19, 114, 85, 21, 14, 118];
|
||||
|
||||
solana_program::program::invoke_signed(
|
||||
&solana_program::instruction::Instruction {
|
||||
program_id: crate::cctp::token_messenger_minter_program::ID,
|
||||
accounts: ctx.to_account_metas(None),
|
||||
data: (ANCHOR_IX_SELECTOR, args).try_to_vec()?,
|
||||
},
|
||||
&ctx.to_account_infos(),
|
||||
ctx.signer_seeds,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
impl<'info> ToAccountMetas for DepositForBurnWithCaller<'info> {
|
||||
fn to_account_metas(&self, _is_signer: Option<bool>) -> Vec<AccountMeta> {
|
||||
vec![
|
||||
AccountMeta::new_readonly(self.burn_token_owner.key(), true),
|
||||
AccountMeta::new(self.payer.key(), true),
|
||||
AccountMeta::new_readonly(self.token_messenger_minter_sender_authority.key(), false),
|
||||
AccountMeta::new(self.burn_token.key(), false),
|
||||
AccountMeta::new(self.message_transmitter_config.key(), false),
|
||||
AccountMeta::new_readonly(self.token_messenger.key(), false),
|
||||
AccountMeta::new_readonly(self.remote_token_messenger.key(), false),
|
||||
AccountMeta::new_readonly(self.token_minter.key(), false),
|
||||
AccountMeta::new(self.local_token.key(), false),
|
||||
AccountMeta::new(self.mint.key(), false),
|
||||
AccountMeta::new(self.cctp_message.key(), true),
|
||||
AccountMeta::new_readonly(self.message_transmitter_program.key(), false),
|
||||
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
|
||||
AccountMeta::new_readonly(self.token_program.key(), false),
|
||||
AccountMeta::new_readonly(self.system_program.key(), false),
|
||||
AccountMeta::new_readonly(self.event_authority.key(), false),
|
||||
AccountMeta::new_readonly(self.token_messenger_minter_program.key(), false),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'info> ToAccountInfos<'info> for DepositForBurnWithCaller<'info> {
|
||||
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
|
||||
vec![
|
||||
self.burn_token_owner.clone(),
|
||||
self.payer.clone(),
|
||||
self.token_messenger_minter_sender_authority.clone(),
|
||||
self.burn_token.clone(),
|
||||
self.message_transmitter_config.clone(),
|
||||
self.token_messenger.clone(),
|
||||
self.remote_token_messenger.clone(),
|
||||
self.token_minter.clone(),
|
||||
self.local_token.clone(),
|
||||
self.mint.clone(),
|
||||
self.cctp_message.clone(),
|
||||
self.message_transmitter_program.clone(),
|
||||
self.token_program.clone(),
|
||||
self.system_program.clone(),
|
||||
self.event_authority.clone(),
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
mod deposit_for_burn_with_caller;
|
||||
pub use deposit_for_burn_with_caller::*;
|
|
@ -0,0 +1,22 @@
|
|||
#[cfg(feature = "cpi")]
|
||||
pub mod cpi;
|
||||
|
||||
mod state;
|
||||
pub use state::*;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "mainnet")] {
|
||||
// Placeholder for real address
|
||||
anchor_lang::declare_id!("CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3");
|
||||
} else if #[cfg(feature = "testnet")] {
|
||||
anchor_lang::declare_id!("CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TokenMessengerMinter {}
|
||||
|
||||
impl anchor_lang::Id for TokenMessengerMinter {
|
||||
fn id() -> solana_program::pubkey::Pubkey {
|
||||
ID
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct LocalToken {
|
||||
pub custody_token: Pubkey,
|
||||
pub mint: Pubkey,
|
||||
pub burn_limit_per_message: u64,
|
||||
pub messages_sent: u64,
|
||||
pub messages_received: u64,
|
||||
pub amount_sent: u128,
|
||||
pub amount_received: u128,
|
||||
pub bump: u8,
|
||||
pub custody_bump: u8,
|
||||
}
|
||||
|
||||
impl LocalToken {
|
||||
pub const SEED_PREFIX: &'static [u8] = b"local_token";
|
||||
}
|
||||
|
||||
impl anchor_lang::Discriminator for LocalToken {
|
||||
const DISCRIMINATOR: [u8; 8] = [159, 131, 58, 170, 193, 84, 128, 182];
|
||||
}
|
||||
|
||||
impl Owner for LocalToken {
|
||||
fn owner() -> Pubkey {
|
||||
crate::cctp::token_messenger_minter_program::ID
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
mod local_token;
|
||||
pub use local_token::*;
|
||||
|
||||
mod remote_token_messenger;
|
||||
pub use remote_token_messenger::*;
|
||||
|
||||
mod token_pair;
|
||||
pub use token_pair::*;
|
|
@ -0,0 +1,21 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct RemoteTokenMessenger {
|
||||
pub domain: u32,
|
||||
pub token_messenger: [u8; 32],
|
||||
}
|
||||
|
||||
impl RemoteTokenMessenger {
|
||||
pub const SEED_PREFIX: &'static [u8] = b"remote_token_messenger";
|
||||
}
|
||||
|
||||
impl anchor_lang::Discriminator for RemoteTokenMessenger {
|
||||
const DISCRIMINATOR: [u8; 8] = [105, 115, 174, 34, 95, 233, 138, 252];
|
||||
}
|
||||
|
||||
impl Owner for RemoteTokenMessenger {
|
||||
fn owner() -> Pubkey {
|
||||
crate::cctp::token_messenger_minter_program::ID
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct TokenPair {
|
||||
pub remote_domain: u32,
|
||||
pub remote_token_address: [u8; 32],
|
||||
pub local_token: Pubkey,
|
||||
pub bump: u8,
|
||||
}
|
||||
|
||||
impl TokenPair {
|
||||
pub const SEED_PREFIX: &'static [u8] = b"token_pair";
|
||||
}
|
||||
|
||||
impl anchor_lang::Discriminator for TokenPair {
|
||||
const DISCRIMINATOR: [u8; 8] = [17, 214, 45, 176, 229, 149, 197, 71];
|
||||
}
|
||||
|
||||
impl Owner for TokenPair {
|
||||
fn owner() -> Pubkey {
|
||||
crate::cctp::token_messenger_minter_program::ID
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
use crate::{cctp, messages::Deposit, wormhole::core_bridge_program};
|
||||
use anchor_lang::prelude::*;
|
||||
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 {
|
||||
/// 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].
|
||||
pub burn_source: Option<Pubkey>,
|
||||
|
||||
/// Destination caller address, which is encoded in the CCTP message. Only this address can
|
||||
/// receive a CCTP message via the CCTP Message Transmitter.
|
||||
pub destination_caller: [u8; 32],
|
||||
|
||||
/// Destination CCTP domain, which is encoded both the Wormhole CCTP [Deposit] and CCTP
|
||||
/// messages. This domain indicates the intended foreign network.
|
||||
pub destination_cctp_domain: u32,
|
||||
|
||||
/// Amount of tokens to burn.
|
||||
pub amount: u64,
|
||||
|
||||
/// Intended mint recipient on destination network.
|
||||
pub mint_recipient: [u8; 32],
|
||||
|
||||
/// Arbitrary value which may be meaningful to an integrator. This nonce is encoded in the
|
||||
/// Wormhole message.
|
||||
pub wormhole_message_nonce: u32,
|
||||
|
||||
/// Arbitrary payload, which can be used to encode instructions or data for another network's
|
||||
/// smart contract.
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Method to publish a Wormhole Core Bridge message alongside a CCTP message that burns a
|
||||
/// Circle-supported token.
|
||||
///
|
||||
/// NOTE: The [burn_source](BurnAndPublishArgs::burn_source) should be the token account where the
|
||||
/// 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>(
|
||||
cctp_ctx: CpiContext<
|
||||
'_,
|
||||
'_,
|
||||
'_,
|
||||
'info,
|
||||
cctp::token_messenger_minter_program::cpi::DepositForBurnWithCaller<'info>,
|
||||
>,
|
||||
wormhole_ctx: CpiContext<'_, '_, '_, 'info, core_bridge_program::cpi::PostMessage<'info>>,
|
||||
args: BurnAndPublishArgs,
|
||||
) -> Result<u64> {
|
||||
let BurnAndPublishArgs {
|
||||
burn_source,
|
||||
destination_caller,
|
||||
destination_cctp_domain,
|
||||
amount,
|
||||
mint_recipient,
|
||||
wormhole_message_nonce,
|
||||
payload,
|
||||
} = args;
|
||||
|
||||
let cctp_nonce = {
|
||||
let mut data: &[_] = &cctp_ctx
|
||||
.accounts
|
||||
.message_transmitter_config
|
||||
.try_borrow_data()?;
|
||||
let config = crate::utils::ExternalAccount::<
|
||||
cctp::message_transmitter_program::MessageTransmitterConfig,
|
||||
>::try_deserialize_unchecked(&mut data)?;
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
cctp::token_messenger_minter_program::cpi::deposit_for_burn_with_caller(
|
||||
cctp_ctx,
|
||||
cctp::token_messenger_minter_program::cpi::DepositForBurnWithCallerParams {
|
||||
amount,
|
||||
destination_domain: destination_cctp_domain,
|
||||
mint_recipient,
|
||||
destination_caller,
|
||||
},
|
||||
)?;
|
||||
|
||||
Ok(cctp_nonce)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
mod burn_and_publish;
|
||||
pub use burn_and_publish::*;
|
||||
|
||||
mod verify_vaa_and_mint;
|
||||
pub use verify_vaa_and_mint::*;
|
||||
|
||||
pub use crate::{
|
||||
cctp::{
|
||||
message_transmitter_program::cpi::{
|
||||
ReceiveMessageArgs, ReceiveTokenMessengerMinterMessage,
|
||||
},
|
||||
token_messenger_minter_program::cpi::DepositForBurnWithCaller,
|
||||
},
|
||||
wormhole::core_bridge_program::cpi::PostMessage,
|
||||
};
|
|
@ -0,0 +1,111 @@
|
|||
use crate::{
|
||||
cctp::message_transmitter_program, error::WormholeCctpError, utils::CctpMessage,
|
||||
wormhole::core_bridge_program::vaa::VaaAccount,
|
||||
};
|
||||
use anchor_lang::prelude::*;
|
||||
use wormhole_raw_vaas::cctp::WormholeCctpMessage;
|
||||
|
||||
/// Method to reconcile a CCTP message with a Wormhole VAA encoding the Wormhole CCTP deposit. After
|
||||
/// reconciliation, the method invokes the CCTP Message Transmitter to mint the local tokens to the
|
||||
/// provided token account in the account context.
|
||||
///
|
||||
/// This method reconciles both messages by making sure the source domain, destination domain and
|
||||
/// nonce match.
|
||||
///
|
||||
/// NOTE: It is the integrator's responsibility to ensure that the owner of this account is Wormhole
|
||||
/// Core Bridge program if this method is used. Otherwise, please use [verify_vaa_and_mint], which
|
||||
/// performs the account owner check.
|
||||
pub fn verify_vaa_and_mint_unchecked<'info>(
|
||||
vaa: &VaaAccount<'_>,
|
||||
cctp_ctx: CpiContext<
|
||||
'_,
|
||||
'_,
|
||||
'_,
|
||||
'info,
|
||||
message_transmitter_program::cpi::ReceiveTokenMessengerMinterMessage<'info>,
|
||||
>,
|
||||
args: message_transmitter_program::cpi::ReceiveMessageArgs,
|
||||
) -> Result<()> {
|
||||
let msg = WormholeCctpMessage::try_from(vaa.try_payload()?)
|
||||
.map_err(|_| error!(WormholeCctpError::CannotParseMessage))?;
|
||||
|
||||
// This should always succeed. But we keep this check just in case we add more message types
|
||||
// in the future.
|
||||
let deposit = msg
|
||||
.deposit()
|
||||
.ok_or(error!(WormholeCctpError::InvalidDepositMessage))?;
|
||||
|
||||
// We need to check the source domain, target domain and nonce to tie the Wormhole Circle Integration
|
||||
// message to the Circle message.
|
||||
let cctp_message = CctpMessage::parse(&args.encoded_message)
|
||||
.map_err(|_| WormholeCctpError::InvalidCctpMessage)?;
|
||||
|
||||
require_eq!(
|
||||
deposit.source_cctp_domain(),
|
||||
cctp_message.source_domain(),
|
||||
WormholeCctpError::SourceCctpDomainMismatch
|
||||
);
|
||||
require_eq!(
|
||||
deposit.destination_cctp_domain(),
|
||||
cctp_message.destination_domain(),
|
||||
WormholeCctpError::DestinationCctpDomainMismatch
|
||||
);
|
||||
require_eq!(
|
||||
deposit.cctp_nonce(),
|
||||
cctp_message.nonce(),
|
||||
WormholeCctpError::CctpNonceMismatch
|
||||
);
|
||||
|
||||
// This check is defense-in-depth (but can possibly be taken out in the future). We verify that
|
||||
// the mint recipient encoded in the deposit (the same one encoded in the CCTP message) is the
|
||||
// mint recipient token account found in the account context.
|
||||
require_keys_eq!(
|
||||
cctp_ctx.accounts.mint_recipient.key(),
|
||||
Pubkey::from(deposit.mint_recipient()),
|
||||
WormholeCctpError::InvalidMintRecipient
|
||||
);
|
||||
|
||||
// Invoke CCTP Messasge Transmitter, which in this case performs a CPI call to the CCTP
|
||||
// Token Messenger Minter program to mint tokens.
|
||||
message_transmitter_program::cpi::receive_token_messenger_minter_message(cctp_ctx, args)?;
|
||||
|
||||
// Done.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Method to reconcile a CCTP message with a Wormhole VAA encoding the Wormhole CCTP deposit. After
|
||||
/// reconciliation, the method invokes the CCTP Message Transmitter to mint the local tokens to the
|
||||
/// provided token account in the account context. This method returns a zero-copy [VaaAccount]
|
||||
/// reader so an integrator can verify emitter information.
|
||||
///
|
||||
/// This method reconciles both messages by making sure the source domain, destination domain and
|
||||
/// nonce match.
|
||||
///
|
||||
/// NOTE: In order to return a zero-copy [VaaAccount] reader, this method takes a reference to the
|
||||
/// [AccountInfo] of the VAA account.
|
||||
pub fn verify_vaa_and_mint<'ctx, 'info>(
|
||||
vaa: &'ctx AccountInfo<'info>,
|
||||
cctp_ctx: CpiContext<
|
||||
'_,
|
||||
'_,
|
||||
'_,
|
||||
'info,
|
||||
message_transmitter_program::cpi::ReceiveTokenMessengerMinterMessage<'info>,
|
||||
>,
|
||||
args: message_transmitter_program::cpi::ReceiveMessageArgs,
|
||||
) -> Result<VaaAccount<'ctx>> {
|
||||
// This is a very important check. We need to make sure that the VAA account is owned by the
|
||||
// Wormhole Core Bridge program. Otherwise, an attacker can create a fake VAA account.
|
||||
require_keys_eq!(
|
||||
*vaa.owner,
|
||||
crate::wormhole::core_bridge_program::id(),
|
||||
ErrorCode::ConstraintOwner
|
||||
);
|
||||
|
||||
let vaa = VaaAccount::load(vaa)?;
|
||||
|
||||
verify_vaa_and_mint_unchecked(&vaa, cctp_ctx, args)?;
|
||||
|
||||
// Finally return the VAA account reader.
|
||||
Ok(vaa)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
//! Errors for the Wormhole CCTP module.
|
||||
//!
|
||||
//! NOTE: These error values span from 0xffff0000 to 0xffff00ff so as to not collide with an
|
||||
//! integrator's errors in his program.
|
||||
|
||||
#[anchor_lang::error_code(offset = 0)]
|
||||
pub enum WormholeCctpError {
|
||||
#[msg("Cannot parse VAA payload as Wormhole CCTP message")]
|
||||
CannotParseMessage = 0xffff0001,
|
||||
|
||||
#[msg("Cannot parse encoded CCTP message")]
|
||||
InvalidCctpMessage = 0xffff0002,
|
||||
|
||||
#[msg("Not a Wormhole CCTP deposit message")]
|
||||
InvalidDepositMessage = 0xffff0003,
|
||||
|
||||
#[msg("Source CCTP domain mismatch")]
|
||||
SourceCctpDomainMismatch = 0xffff0010,
|
||||
|
||||
#[msg("Destination CCTP domain mismatch")]
|
||||
DestinationCctpDomainMismatch = 0xffff0011,
|
||||
|
||||
#[msg("CCTP nonce mismatch")]
|
||||
CctpNonceMismatch = 0xffff0012,
|
||||
|
||||
#[msg("Encoded mint recipient does not match mint recipient token account")]
|
||||
InvalidMintRecipient = 0xffff0014,
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![allow(clippy::result_large_err)]
|
||||
|
||||
pub mod cctp;
|
||||
|
||||
#[cfg(feature = "cpi")]
|
||||
pub mod cpi;
|
||||
|
||||
pub mod error;
|
||||
|
||||
pub use wormhole_io as io;
|
||||
|
||||
pub mod messages;
|
||||
|
||||
pub mod utils;
|
||||
|
||||
pub mod wormhole;
|
|
@ -0,0 +1,186 @@
|
|||
//! Messages relevant to the Token Bridge across all networks. These messages are serialized and
|
||||
//! then published via the Core Bridge program.
|
||||
|
||||
use std::io;
|
||||
|
||||
use ruint::aliases::U256;
|
||||
use wormhole_io::{Readable, TypePrefixedPayload, Writeable};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Deposit {
|
||||
pub token_address: [u8; 32],
|
||||
pub amount: U256,
|
||||
pub source_cctp_domain: u32,
|
||||
pub destination_cctp_domain: u32,
|
||||
pub cctp_nonce: u64,
|
||||
pub burn_source: [u8; 32],
|
||||
pub mint_recipient: [u8; 32],
|
||||
/// NOTE: This payload length is encoded as u16.
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
impl TypePrefixedPayload for Deposit {
|
||||
const TYPE: Option<u8> = Some(1);
|
||||
}
|
||||
|
||||
impl Readable for Deposit {
|
||||
const SIZE: Option<usize> = None;
|
||||
|
||||
fn read<R>(reader: &mut R) -> io::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
R: io::Read,
|
||||
{
|
||||
let token_address = Readable::read(reader)?;
|
||||
let amount = <[u8; 32]>::read(reader).map(U256::from_be_bytes)?;
|
||||
let source_cctp_domain = Readable::read(reader)?;
|
||||
let destination_cctp_domain = Readable::read(reader)?;
|
||||
let cctp_nonce = Readable::read(reader)?;
|
||||
let burn_source = Readable::read(reader)?;
|
||||
let mint_recipient = Readable::read(reader)?;
|
||||
|
||||
let payload_len = u16::read(reader).map(usize::from)?;
|
||||
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 Writeable for Deposit {
|
||||
fn written_size(&self) -> usize {
|
||||
32 + 32 + 4 + 4 + 8 + 32 + 32 + 2 + self.payload.len()
|
||||
}
|
||||
|
||||
fn write<W>(&self, writer: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
W: std::io::Write,
|
||||
{
|
||||
self.token_address.write(writer)?;
|
||||
self.amount.to_be_bytes::<32>().write(writer)?;
|
||||
self.source_cctp_domain.write(writer)?;
|
||||
self.destination_cctp_domain.write(writer)?;
|
||||
self.cctp_nonce.write(writer)?;
|
||||
self.burn_source.write(writer)?;
|
||||
self.mint_recipient.write(writer)?;
|
||||
u16::try_from(self.payload.len())
|
||||
.map_err(|_| std::io::ErrorKind::InvalidData.into())
|
||||
.and_then(|len| len.write(writer))?;
|
||||
writer.write_all(&self.payload)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use hex_literal::hex;
|
||||
use wormhole_io::WriteableBytes;
|
||||
use wormhole_raw_vaas::cctp;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct AllYourBase {
|
||||
pub are: u16,
|
||||
pub belong: u32,
|
||||
pub to: u64,
|
||||
pub us: WriteableBytes,
|
||||
}
|
||||
|
||||
impl TypePrefixedPayload for AllYourBase {
|
||||
const TYPE: Option<u8> = Some(69);
|
||||
}
|
||||
|
||||
impl Readable for AllYourBase {
|
||||
const SIZE: Option<usize> = None;
|
||||
|
||||
fn read<R>(reader: &mut R) -> io::Result<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
R: io::Read,
|
||||
{
|
||||
Ok(Self {
|
||||
are: Readable::read(reader)?,
|
||||
belong: Readable::read(reader)?,
|
||||
to: Readable::read(reader)?,
|
||||
us: Readable::read(reader)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Writeable for AllYourBase {
|
||||
fn written_size(&self) -> usize {
|
||||
2 + 4 + 8 + self.us.written_size()
|
||||
}
|
||||
|
||||
fn write<W>(&self, writer: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
Self: Sized,
|
||||
W: std::io::Write,
|
||||
{
|
||||
self.are.write(writer)?;
|
||||
self.belong.write(writer)?;
|
||||
self.to.write(writer)?;
|
||||
self.us.write(writer)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde() {
|
||||
let payload = AllYourBase {
|
||||
are: 42,
|
||||
belong: 1337,
|
||||
to: 9001,
|
||||
us: b"Beep boop".to_vec().into(),
|
||||
};
|
||||
|
||||
let deposit = Deposit {
|
||||
token_address: hex!("deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"),
|
||||
amount: U256::from(69420u64),
|
||||
source_cctp_domain: 5,
|
||||
destination_cctp_domain: 1,
|
||||
cctp_nonce: 69,
|
||||
burn_source: hex!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||
mint_recipient: hex!(
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
|
||||
),
|
||||
payload: payload.to_vec_payload(),
|
||||
};
|
||||
|
||||
let encoded = deposit.to_vec_payload();
|
||||
|
||||
let msg = cctp::WormholeCctpMessage::parse(&encoded).unwrap();
|
||||
let parsed = msg.deposit().unwrap();
|
||||
|
||||
let expected = Deposit {
|
||||
token_address: parsed.token_address(),
|
||||
amount: U256::from_be_bytes(parsed.amount()),
|
||||
source_cctp_domain: parsed.source_cctp_domain(),
|
||||
destination_cctp_domain: parsed.destination_cctp_domain(),
|
||||
cctp_nonce: parsed.cctp_nonce(),
|
||||
burn_source: parsed.burn_source(),
|
||||
mint_recipient: parsed.mint_recipient(),
|
||||
payload: payload.to_vec_payload(),
|
||||
};
|
||||
assert_eq!(deposit, expected);
|
||||
|
||||
// Check for other encoded parameters.
|
||||
assert_eq!(
|
||||
usize::from(parsed.payload_len()),
|
||||
payload.payload_written_size()
|
||||
);
|
||||
|
||||
// TODO: Recover by calling read_payload.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
mod deposit;
|
||||
pub use deposit::*;
|
|
@ -0,0 +1,99 @@
|
|||
use anchor_lang::{prelude::*, Discriminator};
|
||||
|
||||
/// Wrapper for external account schemas, where an Anchor [Discriminator] and [Owner] are defined.
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct ExternalAccount<T>(T)
|
||||
where
|
||||
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner;
|
||||
|
||||
impl<T> AccountDeserialize for ExternalAccount<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(T::deserialize(&mut &buf[8..])?))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AccountSerialize for ExternalAccount<T> where
|
||||
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner
|
||||
{
|
||||
}
|
||||
|
||||
impl<T> Owner for ExternalAccount<T>
|
||||
where
|
||||
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner,
|
||||
{
|
||||
fn owner() -> Pubkey {
|
||||
T::owner()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::ops::Deref for ExternalAccount<T>
|
||||
where
|
||||
T: AnchorSerialize + AnchorDeserialize + Clone + Discriminator + Owner,
|
||||
{
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
mod accounts;
|
||||
pub use accounts::*;
|
||||
|
||||
mod zero_copy;
|
||||
pub use zero_copy::*;
|
|
@ -0,0 +1,55 @@
|
|||
/// Circle Message generated by the Message Transmitter program.
|
||||
///
|
||||
/// See https://developers.circle.com/stablecoins/docs/message-format for more info.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct CctpMessage<'a> {
|
||||
span: &'a [u8],
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for CctpMessage<'_> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.span
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> CctpMessage<'a> {
|
||||
pub fn version(&self) -> u32 {
|
||||
u32::from_be_bytes(self.span[..4].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn source_domain(&self) -> u32 {
|
||||
u32::from_be_bytes(self.span[4..8].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn destination_domain(&self) -> u32 {
|
||||
u32::from_be_bytes(self.span[8..12].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn nonce(&self) -> u64 {
|
||||
u64::from_be_bytes(self.span[12..20].try_into().unwrap())
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> [u8; 32] {
|
||||
self.span[20..52].try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn recipient(&self) -> [u8; 32] {
|
||||
self.span[52..84].try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn destination_caller(&self) -> [u8; 32] {
|
||||
self.span[84..116].try_into().unwrap()
|
||||
}
|
||||
|
||||
pub fn message(&self) -> &[u8] {
|
||||
&self.span[116..]
|
||||
}
|
||||
|
||||
pub fn parse(span: &'a [u8]) -> Result<CctpMessage<'a>, &'static str> {
|
||||
if span.len() < 116 {
|
||||
return Err("CctpMessage span too short. Need at least 116 bytes");
|
||||
}
|
||||
|
||||
Ok(CctpMessage { span })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub use wormhole_raw_vaas::cctp::{Deposit, WormholeCctpMessage, WormholeCctpPayload};
|
||||
|
||||
mod cctp;
|
||||
pub use cctp::*;
|
|
@ -0,0 +1,2 @@
|
|||
mod post_message;
|
||||
pub use post_message::*;
|
|
@ -0,0 +1,82 @@
|
|||
use crate::wormhole::core_bridge_program::{state::Config, Commitment};
|
||||
use anchor_lang::{
|
||||
prelude::{borsh::BorshSerialize, *},
|
||||
system_program,
|
||||
};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct PostMessage<'info> {
|
||||
#[account(mut)]
|
||||
pub config: AccountInfo<'info>,
|
||||
|
||||
#[account(mut, signer)]
|
||||
pub message: AccountInfo<'info>,
|
||||
|
||||
#[account(signer)]
|
||||
pub emitter: AccountInfo<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
pub emitter_sequence: AccountInfo<'info>,
|
||||
|
||||
#[account(mut, signer)]
|
||||
pub payer: AccountInfo<'info>,
|
||||
|
||||
#[account(mut)]
|
||||
pub fee_collector: AccountInfo<'info>,
|
||||
|
||||
pub clock: AccountInfo<'info>,
|
||||
|
||||
pub system_program: AccountInfo<'info>,
|
||||
|
||||
pub rent: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct PostMessageArgs {
|
||||
/// Unique id for this message.
|
||||
pub nonce: u32,
|
||||
/// Encoded message.
|
||||
pub payload: Vec<u8>,
|
||||
/// Solana commitment level for Guardian observation.
|
||||
pub commitment: Commitment,
|
||||
}
|
||||
|
||||
/// Processor to post (publish) a Wormhole message by setting up the message account for
|
||||
/// Guardian observation.
|
||||
///
|
||||
/// A message is either created beforehand using the new Anchor instruction to process a message
|
||||
/// or is created at this point.
|
||||
pub fn post_message<'info>(
|
||||
ctx: CpiContext<'_, '_, '_, 'info, PostMessage<'info>>,
|
||||
args: PostMessageArgs,
|
||||
) -> Result<()> {
|
||||
// Pay Wormhole message fee.
|
||||
{
|
||||
let mut data: &[_] = &ctx.accounts.config.try_borrow_data()?;
|
||||
let Config { fee_lamports, .. } = Config::deserialize(&mut data)?;
|
||||
|
||||
system_program::transfer(
|
||||
CpiContext::new(
|
||||
ctx.accounts.system_program.to_account_info(),
|
||||
system_program::Transfer {
|
||||
from: ctx.accounts.payer.to_account_info(),
|
||||
to: ctx.accounts.fee_collector.to_account_info(),
|
||||
},
|
||||
),
|
||||
fee_lamports,
|
||||
)?;
|
||||
}
|
||||
|
||||
const IX_SELECTOR: u8 = 1;
|
||||
|
||||
solana_program::program::invoke_signed(
|
||||
&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,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
#[cfg(feature = "cpi")]
|
||||
pub mod cpi;
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod core_bridge_program;
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "wormhole-cctp-solana",
|
||||
"version": "0.1.0",
|
||||
"description": "Wormhole Circle Integration",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wormhole-foundation/wormhole-circle-integration.git"
|
||||
},
|
||||
"author": "Wormhole Contributors",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wormhole-foundation/wormhole-circle-integration/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wormhole-foundation/wormhole-circle-integration#readme",
|
||||
"devDependencies": {
|
||||
"@certusone/wormhole-sdk": "^0.10.10",
|
||||
"@coral-xyz/anchor": "^0.29.0",
|
||||
"@solana/spl-token": "^0.3.8",
|
||||
"@solana/web3.js": "^1.87.3",
|
||||
"@types/chai": "^4.3.9",
|
||||
"@types/mocha": "^10.0.3",
|
||||
"chai": "^4.3.10",
|
||||
"dotenv": "^16.3.1",
|
||||
"ethers": "^5.7.2",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^3.0.3",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-results": "^3.3.0",
|
||||
"typescript": "^5.2.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
[package]
|
||||
name = "wormhole-circle-integration-solana"
|
||||
description = "Wormhole Circle Integration Program for Solana"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
license.workspace = true
|
||||
homepage.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
||||
[features]
|
||||
default = ["mainnet", "no-idl", "cpi"]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
no-log-ix-name = []
|
||||
cpi = ["no-entrypoint"]
|
||||
testnet = ["wormhole-cctp-solana/testnet"]
|
||||
mainnet = ["wormhole-cctp-solana/mainnet"]
|
||||
integration-test = ["mainnet"]
|
||||
|
||||
[dependencies]
|
||||
wormhole-cctp-solana = { workspace = true, features = ["cpi"] }
|
||||
wormhole-raw-vaas.workspace = true
|
||||
|
||||
anchor-lang.workspace = true
|
||||
anchor-spl.workspace = true
|
||||
solana-program.workspace = true
|
||||
|
||||
hex.workspace = true
|
||||
ruint.workspace = true
|
||||
|
||||
cfg-if.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal.workspace = true
|
|
@ -0,0 +1,17 @@
|
|||
//! Constants used by the Wormhole Circle Integration Program.
|
||||
|
||||
use anchor_lang::prelude::constant;
|
||||
|
||||
/// Seed for upgrade authority.
|
||||
#[constant]
|
||||
pub const UPGRADE_SEED_PREFIX: &[u8] = b"upgrade";
|
||||
|
||||
/// Seed for custody token account.
|
||||
#[constant]
|
||||
pub const CUSTODY_TOKEN_SEED_PREFIX: &[u8] = b"custody";
|
||||
|
||||
pub(crate) const GOVERNANCE_CHAIN: u16 = 1;
|
||||
|
||||
pub(crate) const GOVERNANCE_EMITTER: [u8; 32] = [
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
|
||||
];
|
|
@ -0,0 +1,32 @@
|
|||
//! Errors that may arise when interacting with the Wormhole Circle Integration Program.
|
||||
//!
|
||||
|
||||
#[anchor_lang::prelude::error_code]
|
||||
pub enum CircleIntegrationError {
|
||||
#[msg("InvalidGovernanceEmitter")]
|
||||
InvalidGovernanceEmitter = 0x2,
|
||||
|
||||
#[msg("InvalidGovernanceVaa")]
|
||||
InvalidGovernanceVaa = 0x4,
|
||||
|
||||
#[msg("InvalidGovernanceAction")]
|
||||
InvalidGovernanceAction = 0x6,
|
||||
|
||||
#[msg("GovernanceForAnotherChain")]
|
||||
GovernanceForAnotherChain = 0x8,
|
||||
|
||||
#[msg("ImplementationMismatch")]
|
||||
ImplementationMismatch = 0x20,
|
||||
|
||||
#[msg("InvalidForeignChain")]
|
||||
InvalidForeignChain = 0x40,
|
||||
|
||||
#[msg("InvalidForeignEmitter")]
|
||||
InvalidForeignEmitter = 0x42,
|
||||
|
||||
#[msg("InvalidCctpDomain")]
|
||||
InvalidCctpDomain = 0x44,
|
||||
|
||||
#[msg("UnknownEmitter")]
|
||||
UnknownEmitter = 0x102,
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
#![allow(clippy::result_large_err)]
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "mainnet")] {
|
||||
// Placeholder for real address
|
||||
declare_id!("Wormho1eCirc1e1ntegration111111111111111111");
|
||||
} else if #[cfg(feature = "testnet")] {
|
||||
declare_id!("wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d");
|
||||
}
|
||||
}
|
||||
|
||||
pub mod constants;
|
||||
|
||||
pub mod error;
|
||||
|
||||
mod processor;
|
||||
pub(crate) use processor::*;
|
||||
pub use processor::{RedeemTokensWithPayloadArgs, TransferTokensWithPayloadArgs};
|
||||
|
||||
pub mod state;
|
||||
|
||||
#[program]
|
||||
pub mod wormhole_circle_integration_solana {
|
||||
use super::*;
|
||||
|
||||
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
|
||||
processor::initialize(ctx)
|
||||
}
|
||||
|
||||
pub fn transfer_tokens_with_payload(
|
||||
ctx: Context<TransferTokensWithPayload>,
|
||||
args: TransferTokensWithPayloadArgs,
|
||||
) -> Result<()> {
|
||||
processor::transfer_tokens_with_payload(ctx, args)
|
||||
}
|
||||
|
||||
pub fn redeem_tokens_with_payload(
|
||||
ctx: Context<RedeemTokensWithPayload>,
|
||||
args: RedeemTokensWithPayloadArgs,
|
||||
) -> Result<()> {
|
||||
processor::redeem_tokens_with_payload(ctx, args)
|
||||
}
|
||||
|
||||
// Governance
|
||||
|
||||
pub fn register_emitter_and_domain(ctx: Context<RegisterEmitterAndDomain>) -> Result<()> {
|
||||
processor::register_emitter_and_domain(ctx)
|
||||
}
|
||||
|
||||
pub fn upgrade_contract(ctx: Context<UpgradeContract>) -> Result<()> {
|
||||
processor::upgrade_contract(ctx)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
mod register_emitter_and_domain;
|
||||
pub use register_emitter_and_domain::*;
|
||||
|
||||
mod upgrade_contract;
|
||||
pub use upgrade_contract::*;
|
||||
|
||||
use crate::error::CircleIntegrationError;
|
||||
use anchor_lang::prelude::*;
|
||||
use wormhole_cctp_solana::wormhole::core_bridge_program;
|
||||
use wormhole_raw_vaas::cctp::{CircleIntegrationDecree, CircleIntegrationGovPayload};
|
||||
|
||||
pub fn require_valid_governance_vaa<'ctx>(
|
||||
vaa: &'ctx core_bridge_program::VaaAccount<'ctx>,
|
||||
) -> Result<CircleIntegrationDecree<'ctx>> {
|
||||
let emitter = vaa.try_emitter_info()?;
|
||||
require!(
|
||||
emitter.chain == crate::constants::GOVERNANCE_CHAIN
|
||||
&& emitter.address == crate::constants::GOVERNANCE_EMITTER,
|
||||
CircleIntegrationError::InvalidGovernanceEmitter
|
||||
);
|
||||
|
||||
// Because emitter_chain and emitter_address getters have succeeded, we can safely unwrap this
|
||||
// payload call.
|
||||
CircleIntegrationGovPayload::try_from(vaa.try_payload().unwrap())
|
||||
.map(|msg| msg.decree())
|
||||
.map_err(|_| error!(CircleIntegrationError::InvalidGovernanceVaa))
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
use crate::{
|
||||
error::CircleIntegrationError,
|
||||
state::{ConsumedVaa, Custodian, RegisteredEmitter},
|
||||
};
|
||||
use anchor_lang::prelude::*;
|
||||
use wormhole_cctp_solana::{
|
||||
cctp::token_messenger_minter_program,
|
||||
utils::ExternalAccount,
|
||||
wormhole::core_bridge_program::{self, VaaAccount},
|
||||
};
|
||||
use wormhole_raw_vaas::cctp::CircleIntegrationGovPayload;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct RegisterEmitterAndDomain<'info> {
|
||||
#[account(mut)]
|
||||
payer: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
seeds = [Custodian::SEED_PREFIX],
|
||||
bump = custodian.bump,
|
||||
)]
|
||||
custodian: Account<'info, Custodian>,
|
||||
|
||||
/// CHECK: We will be performing zero-copy deserialization in the instruction handler.
|
||||
#[account(owner = core_bridge_program::id())]
|
||||
vaa: AccountInfo<'info>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
payer = payer,
|
||||
space = 8 + RegisteredEmitter::INIT_SPACE,
|
||||
seeds = [
|
||||
RegisteredEmitter::SEED_PREFIX,
|
||||
try_decree(&vaa, |decree| decree.foreign_chain())?.to_be_bytes().as_ref(),
|
||||
],
|
||||
bump,
|
||||
)]
|
||||
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,
|
||||
try_decree(&vaa, |decree| decree.cctp_domain())?.to_string().as_ref(),
|
||||
],
|
||||
bump,
|
||||
seeds::program = token_messenger_minter_program::id(),
|
||||
)]
|
||||
remote_token_messenger:
|
||||
Account<'info, ExternalAccount<token_messenger_minter_program::RemoteTokenMessenger>>,
|
||||
|
||||
system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
#[access_control(handle_access_control(&ctx))]
|
||||
pub fn register_emitter_and_domain(ctx: Context<RegisterEmitterAndDomain>) -> Result<()> {
|
||||
ctx.accounts.consumed_vaa.set_inner(ConsumedVaa {
|
||||
bump: ctx.bumps.consumed_vaa,
|
||||
});
|
||||
|
||||
let vaa = core_bridge_program::VaaAccount::load(&ctx.accounts.vaa).unwrap();
|
||||
|
||||
let registration = CircleIntegrationGovPayload::try_from(vaa.try_payload().unwrap())
|
||||
.unwrap()
|
||||
.decree()
|
||||
.to_register_emitter_and_domain_unchecked();
|
||||
|
||||
ctx.accounts
|
||||
.registered_emitter
|
||||
.set_inner(RegisteredEmitter {
|
||||
bump: ctx.bumps.registered_emitter,
|
||||
cctp_domain: registration.cctp_domain(),
|
||||
chain: registration.foreign_chain(),
|
||||
address: registration.foreign_emitter(),
|
||||
});
|
||||
|
||||
// Done.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_decree<F, T>(vaa_acc_info: &AccountInfo, func: F) -> Result<T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
F: FnOnce(&wormhole_raw_vaas::cctp::RegisterEmitterAndDomain) -> T,
|
||||
{
|
||||
let vaa = core_bridge_program::VaaAccount::load(vaa_acc_info)?;
|
||||
let payload = vaa.try_payload()?;
|
||||
|
||||
let gov_payload = CircleIntegrationGovPayload::parse(payload.as_ref())
|
||||
.map_err(|_| error!(CircleIntegrationError::InvalidGovernanceVaa))?;
|
||||
gov_payload
|
||||
.decree()
|
||||
.register_emitter_and_domain()
|
||||
.map(func)
|
||||
.ok_or(error!(CircleIntegrationError::InvalidGovernanceAction))
|
||||
}
|
||||
|
||||
fn handle_access_control(ctx: &Context<RegisterEmitterAndDomain>) -> Result<()> {
|
||||
let vaa = core_bridge_program::VaaAccount::load(&ctx.accounts.vaa)?;
|
||||
let gov_payload = crate::processor::require_valid_governance_vaa(&vaa)?;
|
||||
|
||||
let registration = gov_payload
|
||||
.register_emitter_and_domain()
|
||||
.ok_or(error!(CircleIntegrationError::InvalidGovernanceAction))?;
|
||||
|
||||
// Registration is either for this chain (Solana) or for all chains (encoded as zero).
|
||||
let decree_chain = registration.chain();
|
||||
require!(
|
||||
decree_chain == 0 || decree_chain == core_bridge_program::SOLANA_CHAIN,
|
||||
CircleIntegrationError::GovernanceForAnotherChain
|
||||
);
|
||||
|
||||
// Foreign chain and emitter address cannot be zero or Solana's.
|
||||
let foreign_chain = registration.foreign_chain();
|
||||
require!(
|
||||
foreign_chain != 0 && foreign_chain != core_bridge_program::SOLANA_CHAIN,
|
||||
CircleIntegrationError::InvalidForeignChain
|
||||
);
|
||||
require!(
|
||||
registration.foreign_emitter() != [0; 32],
|
||||
CircleIntegrationError::InvalidForeignEmitter
|
||||
);
|
||||
|
||||
// CCTP domain must equal the one in the Remote Token Messenger account.
|
||||
//
|
||||
// NOTE: This statement should always pass. But we keep this check just in case the owner
|
||||
// of the Token Messenger Minter program misconfigured the Remote Token Messenger account.
|
||||
require!(
|
||||
registration.cctp_domain() == ctx.accounts.remote_token_messenger.domain,
|
||||
CircleIntegrationError::InvalidCctpDomain
|
||||
);
|
||||
|
||||
// Done.
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
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::{self, VaaAccount};
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct UpgradeContract<'info> {
|
||||
#[account(mut)]
|
||||
payer: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
seeds = [Custodian::SEED_PREFIX],
|
||||
bump = custodian.bump,
|
||||
)]
|
||||
custodian: Account<'info, Custodian>,
|
||||
|
||||
/// 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(owner = core_bridge_program::id())]
|
||||
vaa: 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
|
||||
/// the PDA bump seed to invoke the upgrade.
|
||||
#[account(
|
||||
seeds = [UPGRADE_SEED_PREFIX],
|
||||
bump = custodian.upgrade_authority_bump,
|
||||
)]
|
||||
upgrade_authority: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: This account receives any lamports after the result of the upgrade.
|
||||
#[account(mut)]
|
||||
spill: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: Deployed implementation. The pubkey of this account is checked in access control
|
||||
/// against the one encoded in the governance VAA.
|
||||
#[account(mut)]
|
||||
buffer: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: Token Bridge program data needed for BPF Loader Upgradable program.
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [crate::ID.as_ref()],
|
||||
bump,
|
||||
seeds::program = bpf_loader_upgradeable::id(),
|
||||
)]
|
||||
program_data: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: This must equal the Token Bridge program ID for the BPF Loader Upgradeable program.
|
||||
#[account(
|
||||
mut,
|
||||
address = crate::ID
|
||||
)]
|
||||
this_program: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: BPF Loader Upgradeable program needs this sysvar.
|
||||
#[account(address = solana_program::sysvar::rent::id())]
|
||||
rent: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: BPF Loader Upgradeable program needs this sysvar.
|
||||
#[account(address = solana_program::sysvar::clock::id())]
|
||||
clock: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: BPF Loader Upgradeable program.
|
||||
#[account(address = bpf_loader_upgradeable::id())]
|
||||
bpf_loader_upgradeable_program: AccountInfo<'info>,
|
||||
|
||||
system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
/// Processor for contract upgrade governance decrees. This instruction handler invokes the BPF
|
||||
/// 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<()> {
|
||||
ctx.accounts.consumed_vaa.set_inner(ConsumedVaa {
|
||||
bump: ctx.bumps.consumed_vaa,
|
||||
});
|
||||
|
||||
// Finally upgrade.
|
||||
solana_program::program::invoke_signed(
|
||||
&bpf_loader_upgradeable::upgrade(
|
||||
&crate::ID,
|
||||
&ctx.accounts.buffer.key(),
|
||||
&ctx.accounts.upgrade_authority.key(),
|
||||
&ctx.accounts.spill.key(),
|
||||
),
|
||||
&ctx.accounts.to_account_infos(),
|
||||
&[&[
|
||||
UPGRADE_SEED_PREFIX,
|
||||
&[ctx.accounts.custodian.upgrade_authority_bump],
|
||||
]],
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
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
|
||||
.contract_upgrade()
|
||||
.ok_or(error!(CircleIntegrationError::InvalidGovernanceAction))?;
|
||||
|
||||
// Make sure that the contract upgrade is intended for this network.
|
||||
require_eq!(
|
||||
upgrade.chain(),
|
||||
core_bridge_program::SOLANA_CHAIN,
|
||||
CircleIntegrationError::GovernanceForAnotherChain
|
||||
);
|
||||
|
||||
// Read the implementation pubkey and check against the buffer in our account context.
|
||||
require_keys_eq!(
|
||||
Pubkey::from(upgrade.implementation()),
|
||||
ctx.accounts.buffer.key(),
|
||||
CircleIntegrationError::ImplementationMismatch
|
||||
);
|
||||
|
||||
// Done.
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
use crate::{constants::UPGRADE_SEED_PREFIX, state::Custodian};
|
||||
use anchor_lang::prelude::*;
|
||||
use solana_program::bpf_loader_upgradeable;
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Initialize<'info> {
|
||||
#[account(mut)]
|
||||
deployer: Signer<'info>,
|
||||
|
||||
#[account(
|
||||
init,
|
||||
payer = deployer,
|
||||
space = 8 + Custodian::INIT_SPACE,
|
||||
seeds = [Custodian::SEED_PREFIX],
|
||||
bump,
|
||||
)]
|
||||
custodian: Account<'info, Custodian>,
|
||||
|
||||
/// 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
|
||||
/// the PDA bump seed to invoke the upgrade.
|
||||
#[account(
|
||||
seeds = [UPGRADE_SEED_PREFIX],
|
||||
bump,
|
||||
)]
|
||||
upgrade_authority: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: Wormhole Circle Integration program data needed for BPF Loader Upgradable program.
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [crate::ID.as_ref()],
|
||||
bump,
|
||||
seeds::program = bpf_loader_upgradeable_program,
|
||||
)]
|
||||
program_data: AccountInfo<'info>,
|
||||
|
||||
/// BPF Loader Upgradeable program.
|
||||
///
|
||||
/// CHECK: In order to upgrade the program, we need to invoke the BPF Loader Upgradeable
|
||||
/// program.
|
||||
#[account(address = bpf_loader_upgradeable::id())]
|
||||
bpf_loader_upgradeable_program: AccountInfo<'info>,
|
||||
|
||||
system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
|
||||
ctx.accounts.custodian.set_inner(Custodian {
|
||||
bump: ctx.bumps.custodian,
|
||||
upgrade_authority_bump: ctx.bumps.upgrade_authority,
|
||||
});
|
||||
|
||||
// Finally set the upgrade authority to this program's upgrade PDA.
|
||||
#[cfg(not(feature = "integration-test"))]
|
||||
{
|
||||
solana_program::program::invoke_signed(
|
||||
&bpf_loader_upgradeable::set_upgrade_authority_checked(
|
||||
&crate::ID,
|
||||
&ctx.accounts.deployer.key(),
|
||||
&ctx.accounts.upgrade_authority.key(),
|
||||
),
|
||||
&ctx.accounts.to_account_infos(),
|
||||
&[&[
|
||||
UPGRADE_SEED_PREFIX,
|
||||
&[ctx.accounts.custodian.upgrade_authority_bump],
|
||||
]],
|
||||
)?;
|
||||
}
|
||||
|
||||
// Done.
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
mod governance;
|
||||
pub use governance::*;
|
||||
|
||||
mod initialize;
|
||||
pub use initialize::*;
|
||||
|
||||
mod redeem_tokens_with_payload;
|
||||
pub use redeem_tokens_with_payload::*;
|
||||
|
||||
mod transfer_tokens_with_payload;
|
||||
pub use transfer_tokens_with_payload::*;
|
|
@ -0,0 +1,228 @@
|
|||
use crate::{
|
||||
error::CircleIntegrationError,
|
||||
state::{ConsumedVaa, Custodian, RegisteredEmitter},
|
||||
};
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
use wormhole_cctp_solana::{
|
||||
cctp::{message_transmitter_program, token_messenger_minter_program},
|
||||
cpi::ReceiveMessageArgs,
|
||||
utils::ExternalAccount,
|
||||
wormhole::core_bridge_program::VaaAccount,
|
||||
};
|
||||
|
||||
/// Account context to invoke [redeem_tokens_with_payload].
|
||||
#[derive(Accounts)]
|
||||
pub struct RedeemTokensWithPayload<'info> {
|
||||
#[account(mut)]
|
||||
payer: Signer<'info>,
|
||||
|
||||
/// This program's Wormhole (Core Bridge) emitter authority.
|
||||
///
|
||||
/// CHECK: Seeds must be \["emitter"\].
|
||||
#[account(
|
||||
seeds = [Custodian::SEED_PREFIX],
|
||||
bump = custodian.bump,
|
||||
)]
|
||||
custodian: Account<'info, Custodian>,
|
||||
|
||||
/// CHECK: Must be owned by the Wormhole Core Bridge program. This account will be read via
|
||||
/// zero-copy using the [VaaAccount](core_bridge_program::sdk::VaaAccount) reader.
|
||||
///
|
||||
/// NOTE: The owner of this account is checked in
|
||||
/// [verify_vaa_and_mint](wormhole_cctp_solana::cpi::verify_vaa_and_mint).
|
||||
vaa: AccountInfo<'info>,
|
||||
|
||||
/// Account representing that a VAA has been consumed.
|
||||
///
|
||||
/// CHECK: Seeds must be [emitter_address, emitter_chain, sequence]. These seeds are checked
|
||||
/// when [claim_vaa](core_bridge_program::sdk::claim_vaa) is called.
|
||||
///
|
||||
// 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.
|
||||
///
|
||||
/// CHECK: Signer who must be the owner of the `mint_recipient` token account.
|
||||
mint_recipient_authority: Signer<'info>,
|
||||
|
||||
/// Mint recipient token account, which is encoded as the mint recipient in the CCTP message.
|
||||
/// The CCTP Token Messenger Minter program will transfer the amount encoded in the CCTP message
|
||||
/// from its custody account to this account.
|
||||
///
|
||||
/// NOTE: This account must be owned by the `mint_recipient_authority`.
|
||||
#[account(
|
||||
mut,
|
||||
token::mint = local_token.mint,
|
||||
token::authority = mint_recipient_authority,
|
||||
)]
|
||||
mint_recipient: Account<'info, token::TokenAccount>,
|
||||
|
||||
/// Registered emitter account representing a Circle Integration on another network.
|
||||
///
|
||||
/// Seeds must be \["registered_emitter", target_chain.to_be_bytes()\].
|
||||
#[account(
|
||||
seeds = [
|
||||
RegisteredEmitter::SEED_PREFIX,
|
||||
registered_emitter.chain.to_be_bytes().as_ref(),
|
||||
],
|
||||
bump = registered_emitter.bump,
|
||||
)]
|
||||
registered_emitter: Account<'info, RegisteredEmitter>,
|
||||
|
||||
/// CHECK: Seeds must be \["message_transmitter_authority"\] (CCTP Message Transmitter program).
|
||||
message_transmitter_authority: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["message_transmitter"\] (CCTP Message Transmitter program).
|
||||
message_transmitter_config: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Mutable. Seeds must be \["used_nonces", remote_domain.to_string(),
|
||||
/// first_nonce.to_string()\] (CCTP Message Transmitter program).
|
||||
#[account(mut)]
|
||||
used_nonces: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["__event_authority"\] (CCTP Message Transmitter program).
|
||||
message_transmitter_event_authority: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["token_messenger"\] (CCTP Token Messenger Minter program).
|
||||
token_messenger: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["remote_token_messenger"\, remote_domain.to_string()] (CCTP Token
|
||||
/// Messenger Minter program).
|
||||
remote_token_messenger: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["token_minter"\] (CCTP Token Messenger Minter program).
|
||||
token_minter: UncheckedAccount<'info>,
|
||||
|
||||
/// Token Messenger Minter's Local Token account. This program uses the mint of this account to
|
||||
/// validate the `mint_recipient` token account's mint.
|
||||
///
|
||||
/// Mutable. Seeds must be \["local_token", mint\] (CCTP Token Messenger Minter program).
|
||||
#[account(
|
||||
mut,
|
||||
seeds = [
|
||||
token_messenger_minter_program::LocalToken::SEED_PREFIX,
|
||||
local_token.mint.as_ref(),
|
||||
],
|
||||
bump = local_token.bump,
|
||||
seeds::program = token_messenger_minter_program,
|
||||
)]
|
||||
local_token: Account<'info, ExternalAccount<token_messenger_minter_program::LocalToken>>,
|
||||
|
||||
/// CHECK: Seeds must be \["token_pair", remote_domain.to_string(), remote_token_address\] (CCTP
|
||||
/// Token Messenger Minter program).
|
||||
token_pair: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Mutable. Seeds must be \["custody", mint\] (CCTP Token Messenger Minter program).
|
||||
#[account(mut)]
|
||||
token_messenger_minter_custody_token: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
|
||||
token_messenger_minter_event_authority: UncheckedAccount<'info>,
|
||||
|
||||
token_messenger_minter_program:
|
||||
Program<'info, token_messenger_minter_program::TokenMessengerMinter>,
|
||||
message_transmitter_program: Program<'info, message_transmitter_program::MessageTransmitter>,
|
||||
token_program: Program<'info, token::Token>,
|
||||
system_program: Program<'info, System>,
|
||||
}
|
||||
|
||||
/// Arguments used to invoke [redeem_tokens_with_payload].
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct RedeemTokensWithPayloadArgs {
|
||||
/// CCTP message.
|
||||
pub encoded_cctp_message: Vec<u8>,
|
||||
|
||||
/// Attestation of [encoded_cctp_message](Self::encoded_cctp_message).
|
||||
pub cctp_attestation: Vec<u8>,
|
||||
}
|
||||
|
||||
/// This instruction reconciles a Wormhole CCTP deposit message with a CCTP message to mint tokens
|
||||
/// for the [mint_recipient](RedeemTokensWithPayload::mint_recipient) token account.
|
||||
///
|
||||
/// See [verify_vaa_and_mint](wormhole_cctp_solana::cpi::verify_vaa_and_mint) for more details.
|
||||
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(
|
||||
ctx.accounts.message_transmitter_program.to_account_info(),
|
||||
message_transmitter_program::cpi::ReceiveTokenMessengerMinterMessage {
|
||||
payer: ctx.accounts.payer.to_account_info(),
|
||||
caller: ctx.accounts.custodian.to_account_info(),
|
||||
message_transmitter_authority: ctx
|
||||
.accounts
|
||||
.message_transmitter_authority
|
||||
.to_account_info(),
|
||||
message_transmitter_config: ctx
|
||||
.accounts
|
||||
.message_transmitter_config
|
||||
.to_account_info(),
|
||||
used_nonces: ctx.accounts.used_nonces.to_account_info(),
|
||||
token_messenger_minter_program: ctx
|
||||
.accounts
|
||||
.token_messenger_minter_program
|
||||
.to_account_info(),
|
||||
system_program: ctx.accounts.system_program.to_account_info(),
|
||||
message_transmitter_event_authority: ctx
|
||||
.accounts
|
||||
.message_transmitter_event_authority
|
||||
.to_account_info(),
|
||||
message_transmitter_program: ctx
|
||||
.accounts
|
||||
.message_transmitter_program
|
||||
.to_account_info(),
|
||||
token_messenger: ctx.accounts.token_messenger.to_account_info(),
|
||||
remote_token_messenger: ctx.accounts.remote_token_messenger.to_account_info(),
|
||||
token_minter: ctx.accounts.token_minter.to_account_info(),
|
||||
local_token: ctx.accounts.local_token.to_account_info(),
|
||||
token_pair: ctx.accounts.token_pair.to_account_info(),
|
||||
mint_recipient: ctx.accounts.mint_recipient.to_account_info(),
|
||||
custody_token: ctx
|
||||
.accounts
|
||||
.token_messenger_minter_custody_token
|
||||
.to_account_info(),
|
||||
token_program: ctx.accounts.token_program.to_account_info(),
|
||||
token_messenger_minter_event_authority: ctx
|
||||
.accounts
|
||||
.token_messenger_minter_event_authority
|
||||
.to_account_info(),
|
||||
},
|
||||
&[&[Custodian::SEED_PREFIX, &[ctx.accounts.custodian.bump]]],
|
||||
),
|
||||
ReceiveMessageArgs {
|
||||
encoded_message: args.encoded_cctp_message,
|
||||
attestation: args.cctp_attestation,
|
||||
},
|
||||
)?;
|
||||
|
||||
// Validate that this message originated from a registered emitter.
|
||||
let registered_emitter = &ctx.accounts.registered_emitter;
|
||||
let emitter = vaa.try_emitter_info().unwrap();
|
||||
require!(
|
||||
emitter.chain == registered_emitter.chain && emitter.address == registered_emitter.address,
|
||||
CircleIntegrationError::UnknownEmitter
|
||||
);
|
||||
|
||||
// Done.
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
use crate::state::{Custodian, RegisteredEmitter};
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_spl::token;
|
||||
use wormhole_cctp_solana::{
|
||||
cctp::{message_transmitter_program, token_messenger_minter_program},
|
||||
utils::ExternalAccount,
|
||||
wormhole::core_bridge_program,
|
||||
};
|
||||
|
||||
/// Account context to invoke [transfer_tokens_with_payload].
|
||||
#[derive(Accounts)]
|
||||
pub struct TransferTokensWithPayload<'info> {
|
||||
#[account(mut)]
|
||||
payer: Signer<'info>,
|
||||
|
||||
/// This program's Wormhole (Core Bridge) emitter authority.
|
||||
///
|
||||
/// Seeds must be \["emitter"\].
|
||||
#[account(
|
||||
seeds = [Custodian::SEED_PREFIX],
|
||||
bump = custodian.bump,
|
||||
)]
|
||||
custodian: Account<'info, Custodian>,
|
||||
|
||||
/// Circle-supported mint.
|
||||
///
|
||||
/// CHECK: Mutable. This token account's mint must be the same as the one found in the CCTP
|
||||
/// Token Messenger Minter program's local token account.
|
||||
#[account(
|
||||
mut,
|
||||
address = local_token.mint,
|
||||
)]
|
||||
mint: AccountInfo<'info>,
|
||||
|
||||
/// Token account where assets are burned from. The CCTP Token Messenger Minter program will
|
||||
/// burn the configured [amount](TransferTokensWithPayloadArgs::amount) from this account.
|
||||
///
|
||||
/// NOTE: Transfer authority must be delegated to the custodian because this instruction
|
||||
/// transfers assets from this account to the custody token account.
|
||||
#[account(
|
||||
mut,
|
||||
token::mint = mint
|
||||
)]
|
||||
burn_source: Account<'info, token::TokenAccount>,
|
||||
|
||||
/// Temporary custody token account. This account will be closed at the end of this instruction.
|
||||
/// It just acts as a conduit to allow this program to be the transfer initiator in the CCTP
|
||||
/// message.
|
||||
///
|
||||
/// Seeds must be \["custody"\].
|
||||
#[account(
|
||||
init,
|
||||
payer = payer,
|
||||
token::mint = mint,
|
||||
token::authority = custodian,
|
||||
seeds = [crate::constants::CUSTODY_TOKEN_SEED_PREFIX],
|
||||
bump,
|
||||
)]
|
||||
custody_token: Account<'info, token::TokenAccount>,
|
||||
|
||||
/// Registered emitter account representing a foreign Circle Integration emitter. This account
|
||||
/// exists only when another CCTP network is registered.
|
||||
///
|
||||
/// Seeds must be \["registered_emitter", target_chain.to_be_bytes()\].
|
||||
#[account(
|
||||
seeds = [
|
||||
RegisteredEmitter::SEED_PREFIX,
|
||||
registered_emitter.chain.to_be_bytes().as_ref(),
|
||||
],
|
||||
bump = registered_emitter.bump,
|
||||
)]
|
||||
registered_emitter: Account<'info, RegisteredEmitter>,
|
||||
|
||||
/// CHECK: Seeds must be \["Bridge"\] (Wormhole Core Bridge program).
|
||||
#[account(mut)]
|
||||
core_bridge_config: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Mutable signer to create Wormhole message account.
|
||||
#[account(mut)]
|
||||
core_message: Signer<'info>,
|
||||
|
||||
/// CHECK: Mutable signer to create CCTP message.
|
||||
#[account(mut)]
|
||||
cctp_message: Signer<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["Sequence"\, custodian] (Wormhole Core Bridge program).
|
||||
#[account(mut)]
|
||||
core_emitter_sequence: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["fee_collector"\] (Wormhole Core Bridge program).
|
||||
#[account(mut)]
|
||||
core_fee_collector: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["sender_authority"\] (CCTP Token Messenger Minter program).
|
||||
token_messenger_minter_sender_authority: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Mutable. Seeds must be \["message_transmitter"\] (CCTP Message Transmitter program).
|
||||
#[account(mut)]
|
||||
message_transmitter_config: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["token_messenger"\] (CCTP Token Messenger Minter program).
|
||||
token_messenger: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["remote_token_messenger"\, remote_domain.to_string()] (CCTP Token
|
||||
/// Messenger Minter program).
|
||||
remote_token_messenger: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Seeds must be \["token_minter"\] (CCTP Token Messenger Minter program).
|
||||
token_minter: UncheckedAccount<'info>,
|
||||
|
||||
/// Local token account, which this program uses to validate the `mint` used to burn.
|
||||
///
|
||||
/// Mutable. Seeds must be \["local_token", mint\] (CCTP Token Messenger Minter program).
|
||||
#[account(mut)]
|
||||
local_token: Box<Account<'info, ExternalAccount<token_messenger_minter_program::LocalToken>>>,
|
||||
|
||||
/// CHECK: Seeds must be \["__event_authority"\] (CCTP Token Messenger Minter program).
|
||||
token_messenger_minter_event_authority: UncheckedAccount<'info>,
|
||||
|
||||
core_bridge_program: Program<'info, core_bridge_program::CoreBridge>,
|
||||
token_messenger_minter_program:
|
||||
Program<'info, token_messenger_minter_program::TokenMessengerMinter>,
|
||||
message_transmitter_program: Program<'info, message_transmitter_program::MessageTransmitter>,
|
||||
token_program: Program<'info, token::Token>,
|
||||
system_program: Program<'info, System>,
|
||||
|
||||
/// CHECK: Wormhole Core Bridge needs the clock sysvar based on its legacy implementation.
|
||||
#[account(address = solana_program::sysvar::clock::id())]
|
||||
clock: AccountInfo<'info>,
|
||||
|
||||
/// CHECK: Wormhole Core Bridge needs the rent sysvar based on its legacy implementation.
|
||||
#[account(address = solana_program::sysvar::rent::id())]
|
||||
rent: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
/// Arguments used to invoke [transfer_tokens_with_payload].
|
||||
#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)]
|
||||
pub struct TransferTokensWithPayloadArgs {
|
||||
/// Transfer (burn) amount.
|
||||
pub amount: u64,
|
||||
|
||||
/// Recipient of assets on target network.
|
||||
pub mint_recipient: [u8; 32],
|
||||
|
||||
/// Arbitrary value which may be meaningful to an integrator. This nonce is encoded in the
|
||||
/// Wormhole message.
|
||||
pub wormhole_message_nonce: u32,
|
||||
|
||||
/// Arbitrary payload, which can be used to encode instructions or data for another network's
|
||||
/// smart contract.
|
||||
pub payload: Vec<u8>,
|
||||
}
|
||||
|
||||
/// This instruction invokes both Wormhole Core Bridge and CCTP Token Messenger Minter programs to
|
||||
/// emit a Wormhole message associated with a CCTP message.
|
||||
///
|
||||
/// See [burn_and_publish](wormhole_cctp_solana::cpi::burn_and_publish) for more details.
|
||||
pub fn transfer_tokens_with_payload(
|
||||
ctx: Context<TransferTokensWithPayload>,
|
||||
args: TransferTokensWithPayloadArgs,
|
||||
) -> Result<()> {
|
||||
let TransferTokensWithPayloadArgs {
|
||||
amount,
|
||||
mint_recipient,
|
||||
wormhole_message_nonce,
|
||||
payload,
|
||||
} = args;
|
||||
|
||||
let custodian_seeds = &[Custodian::SEED_PREFIX, &[ctx.accounts.custodian.bump]];
|
||||
|
||||
// Because the transfer initiator in the Circle message is whoever signs to burn assets, we need
|
||||
// to transfer assets from the source token account to one that belongs to this program.
|
||||
token::transfer(
|
||||
CpiContext::new_with_signer(
|
||||
ctx.accounts.token_program.to_account_info(),
|
||||
token::Transfer {
|
||||
from: ctx.accounts.burn_source.to_account_info(),
|
||||
to: ctx.accounts.custody_token.to_account_info(),
|
||||
authority: ctx.accounts.custodian.to_account_info(),
|
||||
},
|
||||
&[custodian_seeds],
|
||||
),
|
||||
amount,
|
||||
)?;
|
||||
|
||||
wormhole_cctp_solana::cpi::burn_and_publish(
|
||||
CpiContext::new_with_signer(
|
||||
ctx.accounts
|
||||
.token_messenger_minter_program
|
||||
.to_account_info(),
|
||||
wormhole_cctp_solana::cpi::DepositForBurnWithCaller {
|
||||
burn_token_owner: ctx.accounts.custodian.to_account_info(),
|
||||
payer: ctx.accounts.payer.to_account_info(),
|
||||
token_messenger_minter_sender_authority: ctx
|
||||
.accounts
|
||||
.token_messenger_minter_sender_authority
|
||||
.to_account_info(),
|
||||
burn_token: ctx.accounts.custody_token.to_account_info(),
|
||||
message_transmitter_config: ctx
|
||||
.accounts
|
||||
.message_transmitter_config
|
||||
.to_account_info(),
|
||||
token_messenger: ctx.accounts.token_messenger.to_account_info(),
|
||||
remote_token_messenger: ctx.accounts.remote_token_messenger.to_account_info(),
|
||||
token_minter: ctx.accounts.token_minter.to_account_info(),
|
||||
local_token: ctx.accounts.local_token.to_account_info(),
|
||||
mint: ctx.accounts.mint.to_account_info(),
|
||||
cctp_message: ctx.accounts.cctp_message.to_account_info(),
|
||||
message_transmitter_program: ctx
|
||||
.accounts
|
||||
.message_transmitter_program
|
||||
.to_account_info(),
|
||||
token_messenger_minter_program: ctx
|
||||
.accounts
|
||||
.token_messenger_minter_program
|
||||
.to_account_info(),
|
||||
token_program: ctx.accounts.token_program.to_account_info(),
|
||||
system_program: ctx.accounts.system_program.to_account_info(),
|
||||
event_authority: ctx
|
||||
.accounts
|
||||
.token_messenger_minter_event_authority
|
||||
.to_account_info(),
|
||||
},
|
||||
&[custodian_seeds],
|
||||
),
|
||||
CpiContext::new_with_signer(
|
||||
ctx.accounts.core_bridge_program.to_account_info(),
|
||||
wormhole_cctp_solana::cpi::PostMessage {
|
||||
payer: ctx.accounts.payer.to_account_info(),
|
||||
message: ctx.accounts.core_message.to_account_info(),
|
||||
emitter: ctx.accounts.custodian.to_account_info(),
|
||||
config: ctx.accounts.core_bridge_config.to_account_info(),
|
||||
emitter_sequence: ctx.accounts.core_emitter_sequence.to_account_info(),
|
||||
fee_collector: ctx.accounts.core_fee_collector.to_account_info(),
|
||||
system_program: ctx.accounts.system_program.to_account_info(),
|
||||
clock: ctx.accounts.clock.to_account_info(),
|
||||
rent: ctx.accounts.rent.to_account_info(),
|
||||
},
|
||||
&[custodian_seeds],
|
||||
),
|
||||
wormhole_cctp_solana::cpi::BurnAndPublishArgs {
|
||||
burn_source: Some(ctx.accounts.burn_source.key()),
|
||||
destination_caller: ctx.accounts.registered_emitter.address,
|
||||
destination_cctp_domain: ctx.accounts.registered_emitter.cctp_domain,
|
||||
amount,
|
||||
mint_recipient,
|
||||
wormhole_message_nonce,
|
||||
payload,
|
||||
},
|
||||
)?;
|
||||
|
||||
// Finally close the custody token account.
|
||||
token::close_account(CpiContext::new_with_signer(
|
||||
ctx.accounts.token_program.to_account_info(),
|
||||
token::CloseAccount {
|
||||
account: ctx.accounts.custody_token.to_account_info(),
|
||||
destination: ctx.accounts.payer.to_account_info(),
|
||||
authority: ctx.accounts.custodian.to_account_info(),
|
||||
},
|
||||
&[custodian_seeds],
|
||||
))
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
/// Emitter config account. This account is used to perform the following:
|
||||
/// 1. It is the emitter authority for the Core Bridge program.
|
||||
/// 2. It acts as the custody token account owner for token transfers.
|
||||
#[account]
|
||||
#[derive(Debug, InitSpace)]
|
||||
pub struct Custodian {
|
||||
pub bump: u8,
|
||||
pub upgrade_authority_bump: u8,
|
||||
}
|
||||
|
||||
impl Custodian {
|
||||
pub const SEED_PREFIX: &'static [u8] = b"emitter";
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
mod consumed_vaa;
|
||||
pub use consumed_vaa::*;
|
||||
|
||||
mod custodian;
|
||||
pub use custodian::*;
|
||||
|
||||
mod registered_emitter;
|
||||
pub use registered_emitter::*;
|
|
@ -0,0 +1,14 @@
|
|||
use anchor_lang::prelude::*;
|
||||
|
||||
#[account]
|
||||
#[derive(Debug, InitSpace)]
|
||||
pub struct RegisteredEmitter {
|
||||
pub bump: u8,
|
||||
pub cctp_domain: u32,
|
||||
pub chain: u16,
|
||||
pub address: [u8; 32],
|
||||
}
|
||||
|
||||
impl RegisteredEmitter {
|
||||
pub const SEED_PREFIX: &'static [u8] = b"registered_emitter";
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
import * as fs from "fs";
|
||||
|
||||
const BASENAME = "wormhole_circle_integration_solana";
|
||||
|
||||
const ROOT = `${__dirname}/../..`;
|
||||
const TYPES = `${ROOT}/target/types/${BASENAME}.ts`;
|
||||
|
||||
const IGNORE_TYPES = [
|
||||
'"name": "LocalToken"',
|
||||
'"name": "TokenPair"',
|
||||
'"name": "MessageTransmitterConfig"',
|
||||
'"name": "WormholeCctp"',
|
||||
];
|
||||
|
||||
main();
|
||||
|
||||
function main() {
|
||||
if (!fs.existsSync(TYPES)) {
|
||||
throw new Error("Types non-existent");
|
||||
}
|
||||
|
||||
const types = fs.readFileSync(TYPES, "utf8").split("\n");
|
||||
for (const matchStr of IGNORE_TYPES) {
|
||||
while (spliceType(types, matchStr));
|
||||
}
|
||||
fs.writeFileSync(TYPES, types.join("\n"), "utf8");
|
||||
}
|
||||
|
||||
function spliceType(lines: string[], matchStr: string) {
|
||||
let lineNumber = 0;
|
||||
let start = -1;
|
||||
let spaces = -1;
|
||||
for (const line of lines) {
|
||||
if (line.includes(matchStr)) {
|
||||
start = lineNumber - 1;
|
||||
spaces = line.indexOf('"') - 2;
|
||||
} else if (start > -1) {
|
||||
if (line == "}".padStart(spaces + 1, " ")) {
|
||||
lines[start - 1] = lines[start - 1].replace("},", "}");
|
||||
lines.splice(start, lineNumber - start + 1);
|
||||
return true;
|
||||
} else if (line == "},".padStart(spaces + 2, " ")) {
|
||||
lines.splice(start, lineNumber - start + 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
++lineNumber;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
import {
|
||||
ChainName,
|
||||
coalesceChainId,
|
||||
parseVaa,
|
||||
tryNativeToUint8Array,
|
||||
} from "@certusone/wormhole-sdk";
|
||||
import { MockEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||
import { NodeWallet, postVaaSolana } from "@certusone/wormhole-sdk/lib/cjs/solana";
|
||||
import { derivePostedVaaKey } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
|
||||
import { Connection, Keypair, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
|
||||
import "dotenv/config";
|
||||
import { CircleIntegrationProgram } from "../src";
|
||||
|
||||
const PROGRAM_ID = "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d";
|
||||
|
||||
// Here we go.
|
||||
main();
|
||||
|
||||
// impl
|
||||
|
||||
async function main() {
|
||||
let govSequence = 6920n;
|
||||
|
||||
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
|
||||
const circleIntegration = new CircleIntegrationProgram(connection, PROGRAM_ID);
|
||||
|
||||
if (process.env.SOLANA_PRIVATE_KEY === undefined) {
|
||||
throw new Error("SOLANA_PRIVATE_KEY is undefined");
|
||||
}
|
||||
const payer = Keypair.fromSecretKey(Buffer.from(process.env.SOLANA_PRIVATE_KEY, "hex"));
|
||||
|
||||
// Set up CCTP Program.
|
||||
//await intialize(circleIntegration, payer);
|
||||
|
||||
// Register emitter and domain.
|
||||
{
|
||||
const foreignChain = "sepolia";
|
||||
const foreignEmitter = "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c";
|
||||
const cctpDomain = 0;
|
||||
|
||||
await registerEmitterAndDomain(
|
||||
circleIntegration,
|
||||
payer,
|
||||
govSequence++,
|
||||
foreignChain,
|
||||
foreignEmitter,
|
||||
cctpDomain,
|
||||
);
|
||||
}
|
||||
{
|
||||
const foreignChain = "avalanche";
|
||||
const foreignEmitter = "0x58f4C17449c90665891C42E14D34aae7a26A472e";
|
||||
const cctpDomain = 1;
|
||||
|
||||
await registerEmitterAndDomain(
|
||||
circleIntegration,
|
||||
payer,
|
||||
govSequence++,
|
||||
foreignChain,
|
||||
foreignEmitter,
|
||||
cctpDomain,
|
||||
);
|
||||
}
|
||||
{
|
||||
const foreignChain = "optimism_sepolia";
|
||||
const foreignEmitter = "0x2703483B1a5a7c577e8680de9Df8Be03c6f30e3c";
|
||||
const cctpDomain = 2;
|
||||
|
||||
await registerEmitterAndDomain(
|
||||
circleIntegration,
|
||||
payer,
|
||||
govSequence++,
|
||||
foreignChain,
|
||||
foreignEmitter,
|
||||
cctpDomain,
|
||||
);
|
||||
}
|
||||
{
|
||||
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,
|
||||
govSequence++,
|
||||
foreignChain,
|
||||
foreignEmitter,
|
||||
cctpDomain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function intialize(circleIntegration: CircleIntegrationProgram, payer: Keypair) {
|
||||
console.log("custodian", circleIntegration.custodianAddress().toString());
|
||||
|
||||
const ix = await circleIntegration.initializeIx(payer.publicKey);
|
||||
|
||||
const connection = circleIntegration.program.provider.connection;
|
||||
const txSig = await sendAndConfirmTransaction(connection, new Transaction().add(ix), [payer]);
|
||||
console.log("intialize", txSig);
|
||||
}
|
||||
|
||||
async function registerEmitterAndDomain(
|
||||
circleIntegration: CircleIntegrationProgram,
|
||||
payer: Keypair,
|
||||
govSequence: bigint,
|
||||
foreignChain: ChainName,
|
||||
foreignEmitter: string,
|
||||
cctpDomain: number,
|
||||
) {
|
||||
const connection = circleIntegration.program.provider.connection;
|
||||
|
||||
const registeredEmitter = circleIntegration.registeredEmitterAddress(
|
||||
coalesceChainId(foreignChain),
|
||||
);
|
||||
const emitterAddress = Array.from(tryNativeToUint8Array(foreignEmitter, foreignChain));
|
||||
|
||||
const exists = await connection.getAccountInfo(registeredEmitter).then((acct) => acct != null);
|
||||
if (exists) {
|
||||
const registered = await circleIntegration.fetchRegisteredEmitter(registeredEmitter);
|
||||
if (Buffer.from(registered.address).equals(Buffer.from(emitterAddress))) {
|
||||
console.log("already registered", foreignChain, foreignEmitter, cctpDomain);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const govEmitter = new MockEmitter(
|
||||
"0000000000000000000000000000000000000000000000000000000000000004",
|
||||
1,
|
||||
Number(govSequence),
|
||||
);
|
||||
|
||||
const payload = Buffer.alloc(32 + 1 + 2 + 2 + 32 + 4);
|
||||
// Action.
|
||||
payload.writeUInt8(2, 32);
|
||||
// Data.
|
||||
payload.writeUInt16BE(1, 33); // targetChain
|
||||
payload.writeUInt16BE(coalesceChainId(foreignChain), 35);
|
||||
payload.set(tryNativeToUint8Array(foreignEmitter, foreignChain), 37);
|
||||
payload.writeUInt32BE(cctpDomain, 69);
|
||||
|
||||
const moduleName = "CircleIntegration";
|
||||
payload.set(Buffer.from(moduleName), 32 - moduleName.length);
|
||||
|
||||
const published = govEmitter.publishMessage(
|
||||
0, // nonce,
|
||||
payload,
|
||||
0, // consistencyLevel
|
||||
12345678, // timestamp
|
||||
);
|
||||
|
||||
if (process.env.GUARDIAN_PRIVATE_KEY === undefined) {
|
||||
throw new Error("GUARDIAN_PRIVATE_KEY is undefined");
|
||||
}
|
||||
const guardians = new MockGuardians(0, [process.env.GUARDIAN_PRIVATE_KEY]);
|
||||
const vaaBuf = guardians.addSignatures(published, [0]);
|
||||
|
||||
await postVaaSolana(
|
||||
connection,
|
||||
new NodeWallet(payer).signTransaction,
|
||||
circleIntegration.coreBridgeProgramId(),
|
||||
payer.publicKey,
|
||||
vaaBuf,
|
||||
);
|
||||
|
||||
const vaa = derivePostedVaaKey(circleIntegration.coreBridgeProgramId(), parseVaa(vaaBuf).hash);
|
||||
|
||||
const ix = await circleIntegration.registerEmitterAndDomainIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
});
|
||||
const txSig = await sendAndConfirmTransaction(connection, new Transaction().add(ix), [payer]);
|
||||
console.log(
|
||||
"register emitter and domain",
|
||||
txSig,
|
||||
"chain",
|
||||
foreignChain,
|
||||
"addr",
|
||||
foreignEmitter,
|
||||
"domain",
|
||||
cctpDomain,
|
||||
);
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": true,
|
||||
"inlineSourceMap": false,
|
||||
"inlineSources": true,
|
||||
"declaration": false,
|
||||
"noEmit": false,
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { parseVaa, tryNativeToUint8Array } from "@certusone/wormhole-sdk";
|
||||
import { MockEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||
import { NodeWallet, postVaaSolana } from "@certusone/wormhole-sdk/lib/cjs/solana";
|
||||
import { derivePostedVaaKey } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
|
||||
import { Connection, Keypair, Transaction, sendAndConfirmTransaction } from "@solana/web3.js";
|
||||
import "dotenv/config";
|
||||
import { CircleIntegrationProgram } from "../src";
|
||||
|
||||
const PROGRAM_ID = "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d";
|
||||
|
||||
// Modify this to the new implementation address.
|
||||
const NEW_IMPLEMENTATION = "HCUGGoihMthPN6d4VGpH8xUPYUofgTgqnpYtwyca7PEh";
|
||||
|
||||
// Here we go.
|
||||
main();
|
||||
|
||||
// impl
|
||||
|
||||
async function main() {
|
||||
let govSequence = 6910n;
|
||||
|
||||
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
|
||||
const circleIntegration = new CircleIntegrationProgram(connection, PROGRAM_ID);
|
||||
|
||||
if (process.env.SOLANA_PRIVATE_KEY === undefined) {
|
||||
throw new Error("SOLANA_PRIVATE_KEY is undefined");
|
||||
}
|
||||
const payer = Keypair.fromSecretKey(Buffer.from(process.env.SOLANA_PRIVATE_KEY, "hex"));
|
||||
|
||||
await upgradeContract(circleIntegration, payer, govSequence);
|
||||
}
|
||||
|
||||
async function upgradeContract(
|
||||
circleIntegration: CircleIntegrationProgram,
|
||||
payer: Keypair,
|
||||
govSequence: bigint,
|
||||
) {
|
||||
const connection = circleIntegration.program.provider.connection;
|
||||
|
||||
const govEmitter = new MockEmitter(
|
||||
"0000000000000000000000000000000000000000000000000000000000000004",
|
||||
1,
|
||||
Number(govSequence),
|
||||
);
|
||||
|
||||
const payload = Buffer.alloc(32 + 1 + 2 + 32);
|
||||
// Action.
|
||||
payload.writeUInt8(3, 32);
|
||||
// Data.
|
||||
payload.writeUInt16BE(1, 33); // targetChain
|
||||
payload.set(tryNativeToUint8Array(NEW_IMPLEMENTATION, "solana"), 35);
|
||||
|
||||
const moduleName = "CircleIntegration";
|
||||
payload.set(Buffer.from(moduleName), 32 - moduleName.length);
|
||||
|
||||
const published = govEmitter.publishMessage(
|
||||
0, // nonce,
|
||||
payload,
|
||||
0, // consistencyLevel
|
||||
12345678, // timestamp
|
||||
);
|
||||
|
||||
if (process.env.GUARDIAN_PRIVATE_KEY === undefined) {
|
||||
throw new Error("GUARDIAN_PRIVATE_KEY is undefined");
|
||||
}
|
||||
const guardians = new MockGuardians(0, [process.env.GUARDIAN_PRIVATE_KEY]);
|
||||
const vaaBuf = guardians.addSignatures(published, [0]);
|
||||
|
||||
await postVaaSolana(
|
||||
connection,
|
||||
new NodeWallet(payer).signTransaction,
|
||||
circleIntegration.coreBridgeProgramId(),
|
||||
payer.publicKey,
|
||||
vaaBuf,
|
||||
);
|
||||
|
||||
const vaa = derivePostedVaaKey(circleIntegration.coreBridgeProgramId(), parseVaa(vaaBuf).hash);
|
||||
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
});
|
||||
const txSig = await sendAndConfirmTransaction(connection, new Transaction().add(ix), [payer]);
|
||||
console.log("upgrade contract", txSig);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { MessageTransmitterProgram } from "./messageTransmitter";
|
||||
export { CctpMessage, CctpTokenBurnMessage } from "./messages";
|
||||
export { TokenMessengerMinterProgram } from "./tokenMessengerMinter";
|
|
@ -0,0 +1,11 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export class MessageSent {
|
||||
rentPayer: PublicKey;
|
||||
message: Buffer;
|
||||
|
||||
constructor(rentPayer: PublicKey, message: Buffer) {
|
||||
this.rentPayer = rentPayer;
|
||||
this.message = message;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { BN } from "@coral-xyz/anchor";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export class MessageTransmitterConfig {
|
||||
owner: PublicKey;
|
||||
pendingOwner: PublicKey;
|
||||
attesterManager: PublicKey;
|
||||
pauser: PublicKey;
|
||||
paused: boolean;
|
||||
localDomain: number;
|
||||
version: number;
|
||||
signatureThreshold: number;
|
||||
enabledAttesters: Array<PublicKey>;
|
||||
maxMessageBodySize: BN;
|
||||
nextAvailableNonce: BN;
|
||||
|
||||
constructor(
|
||||
owner: PublicKey,
|
||||
pendingOwner: PublicKey,
|
||||
attesterManager: PublicKey,
|
||||
pauser: PublicKey,
|
||||
paused: boolean,
|
||||
localDomain: number,
|
||||
version: number,
|
||||
signatureThreshold: number,
|
||||
enabledAttesters: Array<PublicKey>,
|
||||
maxMessageBodySize: BN,
|
||||
nextAvailableNonce: BN,
|
||||
) {
|
||||
this.owner = owner;
|
||||
this.pendingOwner = pendingOwner;
|
||||
this.attesterManager = attesterManager;
|
||||
this.pauser = pauser;
|
||||
this.paused = paused;
|
||||
this.localDomain = localDomain;
|
||||
this.version = version;
|
||||
this.signatureThreshold = signatureThreshold;
|
||||
this.enabledAttesters = enabledAttesters;
|
||||
this.maxMessageBodySize = maxMessageBodySize;
|
||||
this.nextAvailableNonce = nextAvailableNonce;
|
||||
}
|
||||
|
||||
static address(programId: PublicKey) {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("message_transmitter")], programId)[0];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export const MAX_NONCES = 6400n;
|
||||
|
||||
export class UsedNonses {
|
||||
static address(programId: PublicKey, remoteDomain: number, nonce: bigint) {
|
||||
const firstNonce = ((nonce - 1n) / MAX_NONCES) * MAX_NONCES + 1n;
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[
|
||||
Buffer.from("used_nonces"),
|
||||
Buffer.from(remoteDomain.toString()),
|
||||
Buffer.from(firstNonce.toString()),
|
||||
],
|
||||
programId,
|
||||
)[0];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
import { Program } from "@coral-xyz/anchor";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { CctpTokenBurnMessage } from "../messages";
|
||||
import { TokenMessengerMinterProgram } from "../tokenMessengerMinter";
|
||||
import { IDL, MessageTransmitter } from "../types/message_transmitter";
|
||||
import { MessageSent } from "./MessageSent";
|
||||
import { MessageTransmitterConfig } from "./MessageTransmitterConfig";
|
||||
import { UsedNonses } from "./UsedNonces";
|
||||
|
||||
export const PROGRAM_IDS = ["CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd"] as const;
|
||||
|
||||
export type ProgramId = (typeof PROGRAM_IDS)[number];
|
||||
|
||||
export type ReceiveTokenMessengerMinterMessageAccounts = {
|
||||
authority: PublicKey;
|
||||
messageTransmitterConfig: PublicKey;
|
||||
usedNonces: PublicKey;
|
||||
tokenMessengerMinterProgram: PublicKey;
|
||||
messageTransmitterEventAuthority: PublicKey;
|
||||
messageTransmitterProgram: PublicKey;
|
||||
tokenMessenger: PublicKey;
|
||||
remoteTokenMessenger: PublicKey;
|
||||
tokenMinter: PublicKey;
|
||||
localToken: PublicKey;
|
||||
tokenPair: PublicKey;
|
||||
custodyToken: PublicKey;
|
||||
eventAuthority: PublicKey;
|
||||
};
|
||||
|
||||
export class MessageTransmitterProgram {
|
||||
private _programId: ProgramId;
|
||||
|
||||
program: Program<MessageTransmitter>;
|
||||
|
||||
constructor(connection: Connection, programId?: ProgramId) {
|
||||
this._programId = programId ?? testnet();
|
||||
this.program = new Program(IDL, new PublicKey(this._programId), {
|
||||
connection,
|
||||
});
|
||||
}
|
||||
|
||||
get ID(): PublicKey {
|
||||
return this.program.programId;
|
||||
}
|
||||
|
||||
messageTransmitterConfigAddress(): PublicKey {
|
||||
return MessageTransmitterConfig.address(this.ID);
|
||||
}
|
||||
|
||||
async fetchMessageTransmitterConfig(addr: PublicKey): Promise<MessageTransmitterConfig> {
|
||||
return this.program.account.messageTransmitter.fetch(addr);
|
||||
}
|
||||
|
||||
usedNoncesAddress(remoteDomain: number, nonce: bigint): PublicKey {
|
||||
return UsedNonses.address(this.ID, remoteDomain, nonce);
|
||||
}
|
||||
|
||||
authorityAddress(cpiProgramId: PublicKey): PublicKey {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("message_transmitter_authority"), cpiProgramId.toBuffer()],
|
||||
this.ID,
|
||||
)[0];
|
||||
}
|
||||
|
||||
eventAuthorityAddress(): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], this.ID)[0];
|
||||
}
|
||||
|
||||
fetchMessageSent(addr: PublicKey): Promise<MessageSent> {
|
||||
return this.program.account.messageSent.fetch(addr);
|
||||
}
|
||||
|
||||
tokenMessengerMinterProgram(): TokenMessengerMinterProgram {
|
||||
switch (this._programId) {
|
||||
case testnet(): {
|
||||
return new TokenMessengerMinterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
);
|
||||
}
|
||||
case mainnet(): {
|
||||
return new TokenMessengerMinterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error("unsupported network");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
receiveTokenMessengerMinterMessageAccounts(
|
||||
mint: PublicKey,
|
||||
circleMessage: CctpTokenBurnMessage | Buffer,
|
||||
): ReceiveTokenMessengerMinterMessageAccounts {
|
||||
const {
|
||||
cctp: { sourceDomain, nonce },
|
||||
burnTokenAddress,
|
||||
} = CctpTokenBurnMessage.from(circleMessage);
|
||||
|
||||
const tokenMessengerMinterProgram = this.tokenMessengerMinterProgram();
|
||||
return {
|
||||
authority: this.authorityAddress(tokenMessengerMinterProgram.ID),
|
||||
messageTransmitterConfig: this.messageTransmitterConfigAddress(),
|
||||
usedNonces: this.usedNoncesAddress(sourceDomain, nonce),
|
||||
tokenMessengerMinterProgram: tokenMessengerMinterProgram.ID,
|
||||
messageTransmitterEventAuthority: this.eventAuthorityAddress(),
|
||||
messageTransmitterProgram: this.ID,
|
||||
tokenMessenger: tokenMessengerMinterProgram.tokenMessengerAddress(),
|
||||
remoteTokenMessenger:
|
||||
tokenMessengerMinterProgram.remoteTokenMessengerAddress(sourceDomain),
|
||||
tokenMinter: tokenMessengerMinterProgram.tokenMinterAddress(),
|
||||
localToken: tokenMessengerMinterProgram.localTokenAddress(mint),
|
||||
tokenPair: tokenMessengerMinterProgram.tokenPairAddress(sourceDomain, burnTokenAddress),
|
||||
custodyToken: tokenMessengerMinterProgram.custodyTokenAddress(mint),
|
||||
eventAuthority: tokenMessengerMinterProgram.eventAuthorityAddress(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function mainnet(): ProgramId {
|
||||
return "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd";
|
||||
}
|
||||
|
||||
export function testnet(): ProgramId {
|
||||
return "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd";
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
import { ethers } from "ethers";
|
||||
|
||||
export type Cctp = {
|
||||
version: number;
|
||||
sourceDomain: number;
|
||||
destinationDomain: number;
|
||||
nonce: bigint;
|
||||
sender: Array<number>;
|
||||
recipient: Array<number>;
|
||||
targetCaller: Array<number>;
|
||||
};
|
||||
|
||||
// Taken from https://developers.circle.com/stablecoins/docs/message-format.
|
||||
export class CctpMessage {
|
||||
cctp: Cctp;
|
||||
message: Buffer;
|
||||
|
||||
constructor(cctp: Cctp, message: Buffer) {
|
||||
this.cctp = cctp;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static from(message: CctpMessage | Buffer): CctpMessage {
|
||||
if (message instanceof CctpMessage) {
|
||||
return message;
|
||||
} else {
|
||||
return CctpMessage.decode(message);
|
||||
}
|
||||
}
|
||||
|
||||
static decode(buf: Readonly<Buffer>): CctpMessage {
|
||||
const version = buf.readUInt32BE(0);
|
||||
const sourceDomain = buf.readUInt32BE(4);
|
||||
const destinationDomain = buf.readUInt32BE(8);
|
||||
const nonce = buf.readBigUInt64BE(12);
|
||||
const sender = Array.from(buf.slice(20, 52));
|
||||
const recipient = Array.from(buf.slice(52, 84));
|
||||
const targetCaller = Array.from(buf.slice(84, 116));
|
||||
const message = buf.subarray(116);
|
||||
|
||||
return new CctpMessage(
|
||||
{
|
||||
version,
|
||||
sourceDomain,
|
||||
destinationDomain,
|
||||
nonce,
|
||||
sender,
|
||||
recipient,
|
||||
targetCaller,
|
||||
},
|
||||
message,
|
||||
);
|
||||
}
|
||||
|
||||
encode(): Buffer {
|
||||
const { cctp, message } = this;
|
||||
return Buffer.concat([encodeCctp(cctp), message]);
|
||||
}
|
||||
}
|
||||
|
||||
export class CctpTokenBurnMessage {
|
||||
cctp: Cctp;
|
||||
version: number;
|
||||
burnTokenAddress: Array<number>;
|
||||
mintRecipient: Array<number>;
|
||||
amount: bigint;
|
||||
sender: Array<number>;
|
||||
|
||||
constructor(
|
||||
cctp: Cctp,
|
||||
version: number,
|
||||
burnTokenAddress: Array<number>,
|
||||
mintRecipient: Array<number>,
|
||||
amount: bigint,
|
||||
sender: Array<number>,
|
||||
) {
|
||||
this.cctp = cctp;
|
||||
this.version = version;
|
||||
this.burnTokenAddress = burnTokenAddress;
|
||||
this.mintRecipient = mintRecipient;
|
||||
this.amount = amount;
|
||||
this.sender = sender;
|
||||
}
|
||||
|
||||
static from(message: CctpTokenBurnMessage | Buffer): CctpTokenBurnMessage {
|
||||
if (message instanceof CctpTokenBurnMessage) {
|
||||
return message;
|
||||
} else {
|
||||
return CctpTokenBurnMessage.decode(message);
|
||||
}
|
||||
}
|
||||
|
||||
static decode(buf: Readonly<Buffer>): CctpTokenBurnMessage {
|
||||
const { cctp, message } = CctpMessage.decode(buf);
|
||||
const version = message.readUInt32BE(0);
|
||||
const burnTokenAddress = Array.from(message.subarray(4, 36));
|
||||
const mintRecipient = Array.from(message.subarray(36, 68));
|
||||
const amount = BigInt(ethers.BigNumber.from(message.subarray(68, 100)).toString());
|
||||
const sender = Array.from(message.subarray(100, 132));
|
||||
|
||||
return new CctpTokenBurnMessage(
|
||||
cctp,
|
||||
version,
|
||||
burnTokenAddress,
|
||||
mintRecipient,
|
||||
amount,
|
||||
sender,
|
||||
);
|
||||
}
|
||||
|
||||
encode(): Buffer {
|
||||
const buf = Buffer.alloc(132);
|
||||
|
||||
const { cctp, version, burnTokenAddress, mintRecipient, amount, sender } = this;
|
||||
|
||||
let offset = 0;
|
||||
offset = buf.writeUInt32BE(version, offset);
|
||||
buf.set(burnTokenAddress, offset);
|
||||
offset += 32;
|
||||
buf.set(mintRecipient, offset);
|
||||
offset += 32;
|
||||
|
||||
// Special handling w/ uint256. This value will most likely encoded in < 32 bytes, so we
|
||||
// jump ahead by 32 and subtract the length of the encoded value.
|
||||
const encodedAmount = ethers.utils.arrayify(ethers.BigNumber.from(amount.toString()));
|
||||
buf.set(encodedAmount, (offset += 32) - encodedAmount.length);
|
||||
|
||||
buf.set(sender, offset);
|
||||
offset += 32;
|
||||
|
||||
return Buffer.concat([encodeCctp(cctp), buf]);
|
||||
}
|
||||
}
|
||||
|
||||
function encodeCctp(cctp: Cctp): Buffer {
|
||||
const buf = Buffer.alloc(116);
|
||||
|
||||
const { version, sourceDomain, destinationDomain, nonce, sender, recipient, targetCaller } =
|
||||
cctp;
|
||||
|
||||
let offset = 0;
|
||||
offset = buf.writeUInt32BE(version, offset);
|
||||
offset = buf.writeUInt32BE(sourceDomain, offset);
|
||||
offset = buf.writeUInt32BE(destinationDomain, offset);
|
||||
offset = buf.writeBigUInt64BE(nonce, offset);
|
||||
buf.set(sender, offset);
|
||||
offset += 32;
|
||||
buf.set(recipient, offset);
|
||||
offset += 32;
|
||||
buf.set(targetCaller, offset);
|
||||
|
||||
return buf;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export class RemoteTokenMessenger {
|
||||
domain: number;
|
||||
tokenMessenger: Array<number>;
|
||||
|
||||
constructor(domain: number, tokenMessenger: Array<number>) {
|
||||
this.domain = domain;
|
||||
this.tokenMessenger = tokenMessenger;
|
||||
}
|
||||
|
||||
static address(programId: PublicKey, remoteDomain: number) {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("remote_token_messenger"), Buffer.from(remoteDomain.toString())],
|
||||
programId,
|
||||
)[0];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
import { Program } from "@coral-xyz/anchor";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { MessageTransmitterProgram } from "../messageTransmitter";
|
||||
import { IDL, TokenMessengerMinter } from "../types/token_messenger_minter";
|
||||
import { RemoteTokenMessenger } from "./RemoteTokenMessenger";
|
||||
|
||||
export const PROGRAM_IDS = ["CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3"] as const;
|
||||
|
||||
export type ProgramId = (typeof PROGRAM_IDS)[number];
|
||||
|
||||
export type DepositForBurnWithCallerAccounts = {
|
||||
senderAuthority: PublicKey;
|
||||
messageTransmitterConfig: PublicKey;
|
||||
tokenMessenger: PublicKey;
|
||||
remoteTokenMessenger: PublicKey;
|
||||
tokenMinter: PublicKey;
|
||||
localToken: PublicKey;
|
||||
tokenMessengerMinterEventAuthority: PublicKey;
|
||||
messageTransmitterProgram: PublicKey;
|
||||
tokenMessengerMinterProgram: PublicKey;
|
||||
};
|
||||
|
||||
export class TokenMessengerMinterProgram {
|
||||
private _programId: ProgramId;
|
||||
|
||||
program: Program<TokenMessengerMinter>;
|
||||
|
||||
constructor(connection: Connection, programId?: ProgramId) {
|
||||
this._programId = programId ?? testnet();
|
||||
this.program = new Program(IDL, new PublicKey(this._programId), {
|
||||
connection,
|
||||
});
|
||||
}
|
||||
|
||||
get ID(): PublicKey {
|
||||
return this.program.programId;
|
||||
}
|
||||
|
||||
tokenMessengerAddress(): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("token_messenger")], this.ID)[0];
|
||||
}
|
||||
|
||||
tokenMinterAddress(): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("token_minter")], this.ID)[0];
|
||||
}
|
||||
|
||||
custodyTokenAddress(mint: PublicKey): PublicKey {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("custody"), mint.toBuffer()],
|
||||
this.ID,
|
||||
)[0];
|
||||
}
|
||||
|
||||
tokenPairAddress(remoteDomain: number, remoteTokenAddress: Array<number>): PublicKey {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[
|
||||
Buffer.from("token_pair"),
|
||||
Buffer.from(remoteDomain.toString()),
|
||||
Buffer.from(remoteTokenAddress),
|
||||
],
|
||||
this.ID,
|
||||
)[0];
|
||||
}
|
||||
|
||||
remoteTokenMessengerAddress(remoteDomain: number): PublicKey {
|
||||
return RemoteTokenMessenger.address(this.ID, remoteDomain);
|
||||
}
|
||||
|
||||
async fetchRemoteTokenMessenger(addr: PublicKey): Promise<RemoteTokenMessenger> {
|
||||
const { domain, tokenMessenger } =
|
||||
await this.program.account.remoteTokenMessenger.fetch(addr);
|
||||
return new RemoteTokenMessenger(domain, Array.from(tokenMessenger.toBuffer()));
|
||||
}
|
||||
|
||||
localTokenAddress(mint: PublicKey): PublicKey {
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("local_token"), mint.toBuffer()],
|
||||
this.ID,
|
||||
)[0];
|
||||
}
|
||||
|
||||
senderAuthorityAddress(): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("sender_authority")], this.ID)[0];
|
||||
}
|
||||
|
||||
eventAuthorityAddress(): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("__event_authority")], this.ID)[0];
|
||||
}
|
||||
|
||||
messageTransmitterProgram(): MessageTransmitterProgram {
|
||||
switch (this._programId) {
|
||||
case testnet(): {
|
||||
return new MessageTransmitterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd",
|
||||
);
|
||||
}
|
||||
case mainnet(): {
|
||||
return new MessageTransmitterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd",
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error("unsupported network");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
depositForBurnWithCallerAccounts(
|
||||
mint: PublicKey,
|
||||
remoteDomain: number,
|
||||
): DepositForBurnWithCallerAccounts {
|
||||
const messageTransmitterProgram = this.messageTransmitterProgram();
|
||||
return {
|
||||
senderAuthority: this.senderAuthorityAddress(),
|
||||
messageTransmitterConfig: messageTransmitterProgram.messageTransmitterConfigAddress(),
|
||||
tokenMessenger: this.tokenMessengerAddress(),
|
||||
remoteTokenMessenger: this.remoteTokenMessengerAddress(remoteDomain),
|
||||
tokenMinter: this.tokenMinterAddress(),
|
||||
localToken: this.localTokenAddress(mint),
|
||||
tokenMessengerMinterEventAuthority: this.eventAuthorityAddress(),
|
||||
messageTransmitterProgram: messageTransmitterProgram.ID,
|
||||
tokenMessengerMinterProgram: this.ID,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function mainnet(): ProgramId {
|
||||
return "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3";
|
||||
}
|
||||
|
||||
export function testnet(): ProgramId {
|
||||
return "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3";
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export const BPF_LOADER_UPGRADEABLE_ID = new PublicKey(
|
||||
"BPFLoaderUpgradeab1e11111111111111111111111",
|
||||
);
|
|
@ -0,0 +1,598 @@
|
|||
export * from "./cctp";
|
||||
export * from "./consts";
|
||||
export * from "./messages";
|
||||
export * from "./state";
|
||||
export * from "./wormhole";
|
||||
|
||||
import { BN, Program } from "@coral-xyz/anchor";
|
||||
import * as splToken from "@solana/spl-token";
|
||||
import {
|
||||
Connection,
|
||||
PublicKey,
|
||||
SYSVAR_CLOCK_PUBKEY,
|
||||
SYSVAR_RENT_PUBKEY,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import {
|
||||
IDL,
|
||||
WormholeCircleIntegrationSolana,
|
||||
} from "../../target/types/wormhole_circle_integration_solana";
|
||||
import {
|
||||
CctpTokenBurnMessage,
|
||||
MessageTransmitterProgram,
|
||||
TokenMessengerMinterProgram,
|
||||
} from "./cctp";
|
||||
import { BPF_LOADER_UPGRADEABLE_ID } from "./consts";
|
||||
import { ConsumedVaa, Custodian, RegisteredEmitter } from "./state";
|
||||
import { VaaAccount } from "./wormhole";
|
||||
|
||||
export const PROGRAM_IDS = [
|
||||
"Wormho1eCirc1e1ntegration111111111111111111", // mainnet placeholder
|
||||
"wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d", // testnet
|
||||
] as const;
|
||||
|
||||
export type ProgramId = (typeof PROGRAM_IDS)[number];
|
||||
|
||||
export type TransferTokensWithPayloadArgs = {
|
||||
amount: bigint;
|
||||
targetChain: number;
|
||||
mintRecipient: Array<number>;
|
||||
wormholeMessageNonce: number;
|
||||
payload: Buffer;
|
||||
};
|
||||
|
||||
export type PublishMessageAccounts = {
|
||||
coreBridgeConfig: PublicKey;
|
||||
coreEmitterSequence: PublicKey;
|
||||
coreFeeCollector: PublicKey;
|
||||
coreBridgeProgram: PublicKey;
|
||||
};
|
||||
|
||||
export type WormholeCctpCommonAccounts = PublishMessageAccounts & {
|
||||
wormholeCctpProgram: PublicKey;
|
||||
systemProgram: PublicKey;
|
||||
rent: PublicKey;
|
||||
custodian: PublicKey;
|
||||
custodyToken: PublicKey;
|
||||
tokenMessenger: PublicKey;
|
||||
tokenMinter: PublicKey;
|
||||
tokenMessengerMinterSenderAuthority: PublicKey;
|
||||
tokenMessengerMinterProgram: PublicKey;
|
||||
messageTransmitterAuthority: PublicKey;
|
||||
messageTransmitterConfig: PublicKey;
|
||||
messageTransmitterProgram: PublicKey;
|
||||
tokenProgram: PublicKey;
|
||||
mint?: PublicKey;
|
||||
localToken?: PublicKey;
|
||||
tokenMessengerMinterCustodyToken?: PublicKey;
|
||||
};
|
||||
|
||||
export type TransferTokensWithPayloadAccounts = PublishMessageAccounts & {
|
||||
custodian: PublicKey;
|
||||
custodyToken: PublicKey;
|
||||
registeredEmitter: PublicKey;
|
||||
tokenMessengerMinterSenderAuthority: PublicKey;
|
||||
messageTransmitterConfig: PublicKey;
|
||||
tokenMessenger: PublicKey;
|
||||
remoteTokenMessenger: PublicKey;
|
||||
tokenMinter: PublicKey;
|
||||
localToken: PublicKey;
|
||||
tokenMessengerMinterEventAuthority: PublicKey;
|
||||
coreBridgeProgram: PublicKey;
|
||||
tokenMessengerMinterProgram: PublicKey;
|
||||
messageTransmitterProgram: PublicKey;
|
||||
};
|
||||
|
||||
export type RedeemTokensWithPayloadAccounts = {
|
||||
custodian: PublicKey;
|
||||
consumedVaa: PublicKey;
|
||||
mintRecipientAuthority: PublicKey;
|
||||
mintRecipient: PublicKey;
|
||||
registeredEmitter: PublicKey;
|
||||
messageTransmitterAuthority: PublicKey;
|
||||
messageTransmitterConfig: PublicKey;
|
||||
usedNonces: PublicKey;
|
||||
messageTransmitterEventAuthority: PublicKey;
|
||||
tokenMessenger: PublicKey;
|
||||
remoteTokenMessenger: PublicKey;
|
||||
tokenMinter: PublicKey;
|
||||
localToken: PublicKey;
|
||||
tokenPair: PublicKey;
|
||||
tokenMessengerMinterCustodyToken: PublicKey;
|
||||
tokenMessengerMinterEventAuthority: PublicKey;
|
||||
tokenMessengerMinterProgram: PublicKey;
|
||||
messageTransmitterProgram: PublicKey;
|
||||
};
|
||||
|
||||
export type SolanaWormholeCctpTxData = {
|
||||
coreMessageAccount: PublicKey;
|
||||
coreMessageSequence: bigint;
|
||||
encodedCctpMessage: Buffer;
|
||||
};
|
||||
|
||||
export class CircleIntegrationProgram {
|
||||
private _programId: ProgramId;
|
||||
|
||||
program: Program<WormholeCircleIntegrationSolana>;
|
||||
|
||||
constructor(connection: Connection, programId?: ProgramId) {
|
||||
this._programId = programId ?? testnet();
|
||||
this.program = new Program(IDL, new PublicKey(this._programId), {
|
||||
connection,
|
||||
});
|
||||
}
|
||||
|
||||
get ID(): PublicKey {
|
||||
return this.program.programId;
|
||||
}
|
||||
|
||||
upgradeAuthorityAddress(): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("upgrade")], this.ID)[0];
|
||||
}
|
||||
|
||||
programDataAddress(): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([this.ID.toBuffer()], BPF_LOADER_UPGRADEABLE_ID)[0];
|
||||
}
|
||||
|
||||
custodianAddress(): PublicKey {
|
||||
return Custodian.address(this.ID);
|
||||
}
|
||||
|
||||
async fetchCustodian(addr: PublicKey): Promise<Custodian> {
|
||||
const { bump, upgradeAuthorityBump } = await this.program.account.custodian.fetch(addr);
|
||||
return new Custodian(bump, upgradeAuthorityBump);
|
||||
}
|
||||
|
||||
registeredEmitterAddress(chain: number): PublicKey {
|
||||
return RegisteredEmitter.address(this.ID, chain);
|
||||
}
|
||||
|
||||
async fetchRegisteredEmitter(addr: PublicKey): Promise<RegisteredEmitter> {
|
||||
const {
|
||||
bump,
|
||||
chain: registeredChain,
|
||||
cctpDomain,
|
||||
address,
|
||||
} = await this.program.account.registeredEmitter.fetch(addr);
|
||||
return new RegisteredEmitter(bump, cctpDomain, registeredChain, address);
|
||||
}
|
||||
|
||||
custodyTokenAccountAddress(): PublicKey {
|
||||
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 } =
|
||||
this.publishMessageAccounts(custodian);
|
||||
|
||||
const tokenMessengerMinterProgram = this.tokenMessengerMinterProgram();
|
||||
const messageTransmitterProgram = this.messageTransmitterProgram();
|
||||
|
||||
const [localToken, tokenMessengerMinterCustodyToken] = (() => {
|
||||
if (mint === undefined) {
|
||||
return [undefined, undefined];
|
||||
} else {
|
||||
return [
|
||||
tokenMessengerMinterProgram.localTokenAddress(mint),
|
||||
tokenMessengerMinterProgram.custodyTokenAddress(mint),
|
||||
];
|
||||
}
|
||||
})();
|
||||
|
||||
return {
|
||||
wormholeCctpProgram: this.ID,
|
||||
systemProgram: SystemProgram.programId,
|
||||
rent: SYSVAR_RENT_PUBKEY,
|
||||
custodian,
|
||||
custodyToken: this.custodyTokenAccountAddress(),
|
||||
coreBridgeConfig,
|
||||
coreEmitterSequence,
|
||||
coreFeeCollector,
|
||||
coreBridgeProgram,
|
||||
tokenMessenger: tokenMessengerMinterProgram.tokenMessengerAddress(),
|
||||
tokenMinter: tokenMessengerMinterProgram.tokenMinterAddress(),
|
||||
tokenMessengerMinterSenderAuthority:
|
||||
tokenMessengerMinterProgram.senderAuthorityAddress(),
|
||||
tokenMessengerMinterProgram: tokenMessengerMinterProgram.ID,
|
||||
messageTransmitterAuthority: messageTransmitterProgram.authorityAddress(
|
||||
tokenMessengerMinterProgram.ID,
|
||||
),
|
||||
messageTransmitterConfig: messageTransmitterProgram.messageTransmitterConfigAddress(),
|
||||
messageTransmitterProgram: messageTransmitterProgram.ID,
|
||||
tokenProgram: splToken.TOKEN_PROGRAM_ID,
|
||||
mint,
|
||||
localToken,
|
||||
tokenMessengerMinterCustodyToken,
|
||||
};
|
||||
}
|
||||
|
||||
async initializeIx(deployer: PublicKey): Promise<TransactionInstruction> {
|
||||
return this.program.methods
|
||||
.initialize()
|
||||
.accounts({
|
||||
deployer,
|
||||
custodian: this.custodianAddress(),
|
||||
upgradeAuthority: this.upgradeAuthorityAddress(),
|
||||
programData: this.programDataAddress(),
|
||||
bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_ID,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async registerEmitterAndDomainIx(accounts: {
|
||||
payer: PublicKey;
|
||||
vaa: PublicKey;
|
||||
remoteTokenMessenger?: PublicKey;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { payer, vaa, remoteTokenMessenger: inputRemoteTokenMessenger } = accounts;
|
||||
|
||||
const vaaAcct = await VaaAccount.fetch(this.program.provider.connection, vaa);
|
||||
const payload = vaaAcct.payload();
|
||||
const registeredEmitter = this.registeredEmitterAddress(payload.readUInt16BE(35));
|
||||
const remoteTokenMessenger = (() => {
|
||||
if (payload.length >= 73) {
|
||||
const cctpDomain = payload.readUInt32BE(69);
|
||||
return this.tokenMessengerMinterProgram().remoteTokenMessengerAddress(cctpDomain);
|
||||
} else if (inputRemoteTokenMessenger !== undefined) {
|
||||
return inputRemoteTokenMessenger;
|
||||
} else {
|
||||
throw new Error("remoteTokenMessenger must be provided");
|
||||
}
|
||||
})();
|
||||
|
||||
return this.program.methods
|
||||
.registerEmitterAndDomain()
|
||||
.accounts({
|
||||
payer,
|
||||
custodian: this.custodianAddress(),
|
||||
vaa,
|
||||
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
|
||||
registeredEmitter,
|
||||
remoteTokenMessenger,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async upgradeContractIx(accounts: {
|
||||
payer: PublicKey;
|
||||
vaa: PublicKey;
|
||||
buffer?: PublicKey;
|
||||
}): Promise<TransactionInstruction> {
|
||||
const { payer, vaa, buffer: inputBuffer } = accounts;
|
||||
|
||||
const vaaAcct = await VaaAccount.fetch(this.program.provider.connection, vaa);
|
||||
const payload = vaaAcct.payload();
|
||||
|
||||
return this.program.methods
|
||||
.upgradeContract()
|
||||
.accounts({
|
||||
payer,
|
||||
custodian: this.custodianAddress(),
|
||||
vaa,
|
||||
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
|
||||
upgradeAuthority: this.upgradeAuthorityAddress(),
|
||||
spill: payer,
|
||||
buffer: inputBuffer ?? new PublicKey(payload.subarray(-32)),
|
||||
programData: this.programDataAddress(),
|
||||
thisProgram: this.ID,
|
||||
rent: SYSVAR_RENT_PUBKEY,
|
||||
clock: SYSVAR_CLOCK_PUBKEY,
|
||||
bpfLoaderUpgradeableProgram: BPF_LOADER_UPGRADEABLE_ID,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async transferTokensWithPayloadAccounts(
|
||||
mint: PublicKey,
|
||||
targetChain: number,
|
||||
): Promise<TransferTokensWithPayloadAccounts> {
|
||||
const registeredEmitter = this.registeredEmitterAddress(targetChain);
|
||||
const remoteDomain = await this.fetchRegisteredEmitter(registeredEmitter).then(
|
||||
(acct) => acct.cctpDomain,
|
||||
);
|
||||
|
||||
const {
|
||||
senderAuthority: tokenMessengerMinterSenderAuthority,
|
||||
messageTransmitterConfig,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenMessengerMinterEventAuthority,
|
||||
messageTransmitterProgram,
|
||||
tokenMessengerMinterProgram,
|
||||
} = this.tokenMessengerMinterProgram().depositForBurnWithCallerAccounts(mint, remoteDomain);
|
||||
|
||||
const custodian = this.custodianAddress();
|
||||
const { coreBridgeConfig, coreEmitterSequence, coreFeeCollector, coreBridgeProgram } =
|
||||
this.publishMessageAccounts(custodian);
|
||||
|
||||
return {
|
||||
custodian,
|
||||
custodyToken: this.custodyTokenAccountAddress(),
|
||||
registeredEmitter,
|
||||
coreBridgeConfig,
|
||||
coreEmitterSequence,
|
||||
coreFeeCollector,
|
||||
tokenMessengerMinterSenderAuthority,
|
||||
messageTransmitterConfig,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenMessengerMinterEventAuthority,
|
||||
coreBridgeProgram,
|
||||
tokenMessengerMinterProgram,
|
||||
messageTransmitterProgram,
|
||||
};
|
||||
}
|
||||
|
||||
async transferTokensWithPayloadIx(
|
||||
accounts: {
|
||||
payer: PublicKey;
|
||||
mint: PublicKey;
|
||||
burnSource: PublicKey;
|
||||
coreMessage: PublicKey;
|
||||
cctpMessage: PublicKey;
|
||||
},
|
||||
args: TransferTokensWithPayloadArgs,
|
||||
): Promise<TransactionInstruction> {
|
||||
let { payer, burnSource, mint, coreMessage, cctpMessage } = accounts;
|
||||
|
||||
const { amount, targetChain, mintRecipient, wormholeMessageNonce, payload } = args;
|
||||
|
||||
const {
|
||||
custodian,
|
||||
custodyToken,
|
||||
registeredEmitter,
|
||||
coreBridgeConfig,
|
||||
coreEmitterSequence,
|
||||
coreFeeCollector,
|
||||
coreBridgeProgram,
|
||||
tokenMessengerMinterSenderAuthority,
|
||||
messageTransmitterConfig,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenMessengerMinterEventAuthority,
|
||||
tokenMessengerMinterProgram,
|
||||
messageTransmitterProgram,
|
||||
} = await this.transferTokensWithPayloadAccounts(mint, targetChain);
|
||||
|
||||
return this.program.methods
|
||||
.transferTokensWithPayload({
|
||||
amount: new BN(amount.toString()),
|
||||
mintRecipient,
|
||||
wormholeMessageNonce,
|
||||
payload,
|
||||
})
|
||||
.accounts({
|
||||
payer,
|
||||
custodian,
|
||||
mint,
|
||||
burnSource,
|
||||
custodyToken,
|
||||
registeredEmitter,
|
||||
coreBridgeConfig,
|
||||
coreMessage,
|
||||
cctpMessage,
|
||||
coreEmitterSequence,
|
||||
coreFeeCollector,
|
||||
tokenMessengerMinterSenderAuthority,
|
||||
messageTransmitterConfig,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenMessengerMinterEventAuthority,
|
||||
coreBridgeProgram,
|
||||
tokenMessengerMinterProgram,
|
||||
messageTransmitterProgram,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
async redeemTokensWithPayloadAccounts(
|
||||
vaa: PublicKey,
|
||||
circleMessage: CctpTokenBurnMessage | Buffer,
|
||||
): Promise<RedeemTokensWithPayloadAccounts> {
|
||||
const msg = CctpTokenBurnMessage.from(circleMessage);
|
||||
const mintRecipient = new PublicKey(msg.mintRecipient);
|
||||
const [mint, mintRecipientAuthority] = await splToken
|
||||
.getAccount(this.program.provider.connection, mintRecipient)
|
||||
.then((token) => [token.mint, token.owner]);
|
||||
|
||||
// Determine claim PDA.
|
||||
const vaaAcct = await VaaAccount.fetch(this.program.provider.connection, vaa);
|
||||
const { chain } = vaaAcct.emitterInfo();
|
||||
|
||||
const {
|
||||
authority: messageTransmitterAuthority,
|
||||
messageTransmitterConfig,
|
||||
usedNonces,
|
||||
tokenMessengerMinterProgram,
|
||||
messageTransmitterEventAuthority,
|
||||
messageTransmitterProgram,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenPair,
|
||||
custodyToken: tokenMessengerMinterCustodyToken,
|
||||
eventAuthority: tokenMessengerMinterEventAuthority,
|
||||
} = this.messageTransmitterProgram().receiveTokenMessengerMinterMessageAccounts(mint, msg);
|
||||
|
||||
return {
|
||||
custodian: this.custodianAddress(),
|
||||
consumedVaa: this.consumedVaaAddress(vaaAcct.digest()),
|
||||
mintRecipientAuthority,
|
||||
mintRecipient,
|
||||
registeredEmitter: this.registeredEmitterAddress(chain),
|
||||
messageTransmitterAuthority,
|
||||
messageTransmitterConfig,
|
||||
usedNonces,
|
||||
messageTransmitterEventAuthority,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenPair,
|
||||
tokenMessengerMinterCustodyToken,
|
||||
tokenMessengerMinterEventAuthority,
|
||||
tokenMessengerMinterProgram,
|
||||
messageTransmitterProgram,
|
||||
};
|
||||
}
|
||||
|
||||
async redeemTokensWithPayloadIx(
|
||||
accounts: {
|
||||
payer: PublicKey;
|
||||
vaa: PublicKey;
|
||||
mintRecipientAuthority?: PublicKey;
|
||||
},
|
||||
args: {
|
||||
encodedCctpMessage: Buffer;
|
||||
cctpAttestation: Buffer;
|
||||
},
|
||||
): Promise<TransactionInstruction> {
|
||||
const { payer, vaa, mintRecipientAuthority: inputMintRecipientAuthority } = accounts;
|
||||
|
||||
const { encodedCctpMessage } = args;
|
||||
|
||||
const {
|
||||
custodian,
|
||||
consumedVaa,
|
||||
mintRecipientAuthority,
|
||||
mintRecipient,
|
||||
registeredEmitter,
|
||||
messageTransmitterAuthority,
|
||||
messageTransmitterConfig,
|
||||
usedNonces,
|
||||
messageTransmitterEventAuthority,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenPair,
|
||||
tokenMessengerMinterCustodyToken,
|
||||
tokenMessengerMinterEventAuthority,
|
||||
tokenMessengerMinterProgram,
|
||||
messageTransmitterProgram,
|
||||
} = await this.redeemTokensWithPayloadAccounts(vaa, encodedCctpMessage);
|
||||
|
||||
return this.program.methods
|
||||
.redeemTokensWithPayload(args)
|
||||
.accounts({
|
||||
payer,
|
||||
custodian,
|
||||
vaa,
|
||||
consumedVaa,
|
||||
mintRecipientAuthority: inputMintRecipientAuthority ?? mintRecipientAuthority,
|
||||
mintRecipient,
|
||||
registeredEmitter,
|
||||
messageTransmitterAuthority,
|
||||
messageTransmitterConfig,
|
||||
usedNonces,
|
||||
messageTransmitterEventAuthority,
|
||||
tokenMessenger,
|
||||
remoteTokenMessenger,
|
||||
tokenMinter,
|
||||
localToken,
|
||||
tokenPair,
|
||||
tokenMessengerMinterCustodyToken,
|
||||
tokenMessengerMinterEventAuthority,
|
||||
tokenMessengerMinterProgram,
|
||||
messageTransmitterProgram,
|
||||
})
|
||||
.instruction();
|
||||
}
|
||||
|
||||
tokenMessengerMinterProgram(): TokenMessengerMinterProgram {
|
||||
switch (this._programId) {
|
||||
case testnet(): {
|
||||
return new TokenMessengerMinterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
);
|
||||
}
|
||||
case mainnet(): {
|
||||
return new TokenMessengerMinterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error("unsupported network");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messageTransmitterProgram(): MessageTransmitterProgram {
|
||||
switch (this._programId) {
|
||||
case testnet(): {
|
||||
return new MessageTransmitterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd",
|
||||
);
|
||||
}
|
||||
case mainnet(): {
|
||||
return new MessageTransmitterProgram(
|
||||
this.program.provider.connection,
|
||||
"CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd",
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error("unsupported network");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishMessageAccounts(emitter: PublicKey): PublishMessageAccounts {
|
||||
const coreBridgeProgram = this.coreBridgeProgramId();
|
||||
|
||||
return {
|
||||
coreBridgeConfig: PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("Bridge")],
|
||||
coreBridgeProgram,
|
||||
)[0],
|
||||
coreEmitterSequence: PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("Sequence"), emitter.toBuffer()],
|
||||
coreBridgeProgram,
|
||||
)[0],
|
||||
coreFeeCollector: PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("fee_collector")],
|
||||
coreBridgeProgram,
|
||||
)[0],
|
||||
coreBridgeProgram,
|
||||
};
|
||||
}
|
||||
|
||||
coreBridgeProgramId(): PublicKey {
|
||||
switch (this._programId) {
|
||||
case testnet(): {
|
||||
return new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5");
|
||||
}
|
||||
case mainnet(): {
|
||||
return new PublicKey("worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth");
|
||||
}
|
||||
default: {
|
||||
throw new Error("unsupported network");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mainnet(): ProgramId {
|
||||
return "Wormho1eCirc1e1ntegration111111111111111111";
|
||||
}
|
||||
|
||||
export function testnet(): ProgramId {
|
||||
return "wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d";
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
import { ethers } from "ethers";
|
||||
|
||||
export type DepositHeader = {
|
||||
tokenAddress: Array<number>;
|
||||
amount: bigint;
|
||||
sourceCctpDomain: number;
|
||||
destinationCctpDomain: number;
|
||||
cctpNonce: bigint;
|
||||
burnSource: Array<number>;
|
||||
mintRecipient: Array<number>;
|
||||
payloadLen: number;
|
||||
};
|
||||
|
||||
export class Deposit {
|
||||
deposit: DepositHeader;
|
||||
payload: Buffer;
|
||||
|
||||
constructor(deposit: DepositHeader, payload: Buffer) {
|
||||
this.deposit = deposit;
|
||||
this.payload = payload;
|
||||
}
|
||||
|
||||
static decode(buf: Buffer): Deposit {
|
||||
if (buf.readUInt8(0) != 1) {
|
||||
throw new Error("Invalid Wormhole CCTP deposit message");
|
||||
}
|
||||
buf = buf.subarray(1);
|
||||
|
||||
const tokenAddress = Array.from(buf.subarray(0, 32));
|
||||
const amount = BigInt(ethers.BigNumber.from(buf.subarray(32, 64)).toString());
|
||||
const sourceCctpDomain = buf.readUInt32BE(64);
|
||||
const destinationCctpDomain = buf.readUInt32BE(68);
|
||||
const cctpNonce = buf.readBigUint64BE(72);
|
||||
const burnSource = Array.from(buf.subarray(80, 112));
|
||||
const mintRecipient = Array.from(buf.subarray(112, 144));
|
||||
const payloadLen = buf.readUInt16BE(144);
|
||||
const payload = buf.subarray(146, 146 + payloadLen);
|
||||
|
||||
return new Deposit(
|
||||
{
|
||||
tokenAddress,
|
||||
amount,
|
||||
sourceCctpDomain,
|
||||
destinationCctpDomain,
|
||||
cctpNonce,
|
||||
burnSource,
|
||||
mintRecipient,
|
||||
payloadLen,
|
||||
},
|
||||
payload,
|
||||
);
|
||||
}
|
||||
|
||||
encode(): Buffer {
|
||||
const buf = Buffer.alloc(146);
|
||||
|
||||
const { deposit, payload } = this;
|
||||
const {
|
||||
tokenAddress,
|
||||
amount,
|
||||
sourceCctpDomain,
|
||||
destinationCctpDomain,
|
||||
cctpNonce,
|
||||
burnSource,
|
||||
mintRecipient,
|
||||
payloadLen,
|
||||
} = deposit;
|
||||
|
||||
let offset = 0;
|
||||
buf.set(tokenAddress, offset);
|
||||
offset += 32;
|
||||
|
||||
// Special handling w/ uint256. This value will most likely encoded in < 32 bytes, so we
|
||||
// jump ahead by 32 and subtract the length of the encoded value.
|
||||
const encodedAmount = ethers.utils.arrayify(ethers.BigNumber.from(amount.toString()));
|
||||
buf.set(encodedAmount, (offset += 32) - encodedAmount.length);
|
||||
|
||||
offset = buf.writeUInt32BE(sourceCctpDomain, offset);
|
||||
offset = buf.writeUInt32BE(destinationCctpDomain, offset);
|
||||
offset = buf.writeBigUInt64BE(cctpNonce, offset);
|
||||
buf.set(burnSource, offset);
|
||||
offset += 32;
|
||||
buf.set(mintRecipient, offset);
|
||||
offset += 32;
|
||||
offset = buf.writeUInt16BE(payloadLen, offset);
|
||||
|
||||
return Buffer.concat([Buffer.alloc(1, 1), buf, payload]);
|
||||
}
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export class Custodian {
|
||||
bump: number;
|
||||
upgradeAuthorityBump: number;
|
||||
|
||||
constructor(bump: number, upgradeAuthorityBump: number) {
|
||||
this.bump = bump;
|
||||
this.upgradeAuthorityBump = upgradeAuthorityBump;
|
||||
}
|
||||
|
||||
static address(programId: PublicKey): PublicKey {
|
||||
return PublicKey.findProgramAddressSync([Buffer.from("emitter")], programId)[0];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export class RegisteredEmitter {
|
||||
bump: number;
|
||||
cctpDomain: number;
|
||||
chain: number;
|
||||
address: Array<number>;
|
||||
|
||||
constructor(bump: number, cctpDomain: number, chain: number, address: Array<number>) {
|
||||
this.bump = bump;
|
||||
this.cctpDomain = cctpDomain;
|
||||
this.chain = chain;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
static address(programId: PublicKey, chain: number): PublicKey {
|
||||
const encodedChain = Buffer.alloc(2);
|
||||
encodedChain.writeUInt16BE(chain, 0);
|
||||
return PublicKey.findProgramAddressSync(
|
||||
[Buffer.from("registered_emitter"), encodedChain],
|
||||
programId,
|
||||
)[0];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./ConsumedVaa";
|
||||
export * from "./Custodian";
|
||||
export * from "./RegisteredEmitter";
|
|
@ -0,0 +1,177 @@
|
|||
import { parseVaa } from "@certusone/wormhole-sdk";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import { ethers } from "ethers";
|
||||
|
||||
export type EncodedVaa = {
|
||||
status: number;
|
||||
writeAuthority: PublicKey;
|
||||
version: number;
|
||||
buf: Buffer;
|
||||
};
|
||||
|
||||
export type PostedVaaV1 = {
|
||||
consistencyLevel: number;
|
||||
timestamp: number;
|
||||
signatureSet: PublicKey;
|
||||
guardianSetIndex: number;
|
||||
nonce: number;
|
||||
sequence: bigint;
|
||||
emitterChain: number;
|
||||
emitterAddress: Array<number>;
|
||||
payload: Buffer;
|
||||
};
|
||||
|
||||
export type EmitterInfo = {
|
||||
chain: number;
|
||||
address: Array<number>;
|
||||
sequence: bigint;
|
||||
};
|
||||
|
||||
export class VaaAccount {
|
||||
private _encodedVaa?: EncodedVaa;
|
||||
private _postedVaaV1?: PostedVaaV1;
|
||||
|
||||
static async fetch(connection: Connection, addr: PublicKey): Promise<VaaAccount> {
|
||||
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 (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: {
|
||||
consistencyLevel,
|
||||
timestamp,
|
||||
signatureSet,
|
||||
guardianSetIndex,
|
||||
nonce,
|
||||
sequence,
|
||||
emitterChain,
|
||||
emitterAddress,
|
||||
payload,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("invalid VAA account data");
|
||||
}
|
||||
}
|
||||
|
||||
emitterInfo(): EmitterInfo {
|
||||
if (this._encodedVaa !== undefined) {
|
||||
const parsed = parseVaa(this._encodedVaa.buf);
|
||||
return {
|
||||
chain: parsed.emitterChain,
|
||||
address: Array.from(parsed.emitterAddress),
|
||||
sequence: parsed.sequence,
|
||||
};
|
||||
} 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 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");
|
||||
}
|
||||
}
|
||||
|
||||
get encodedVaa(): EncodedVaa {
|
||||
if (this._encodedVaa === undefined) {
|
||||
throw new Error("VaaAccount does not have encodedVaa");
|
||||
}
|
||||
return this._encodedVaa;
|
||||
}
|
||||
|
||||
get postedVaaV1(): PostedVaaV1 {
|
||||
if (this._postedVaaV1 === undefined) {
|
||||
throw new Error("VaaAccount does not have postedVaaV1");
|
||||
}
|
||||
return this._postedVaaV1;
|
||||
}
|
||||
|
||||
private constructor(data: { encodedVaa?: EncodedVaa; postedVaaV1?: PostedVaaV1 }) {
|
||||
const { encodedVaa, postedVaaV1 } = data;
|
||||
if (encodedVaa !== undefined && postedVaaV1 !== undefined) {
|
||||
throw new Error("VaaAccount cannot have both encodedVaa and postedVaaV1");
|
||||
}
|
||||
|
||||
this._encodedVaa = encodedVaa;
|
||||
this._postedVaaV1 = postedVaaV1;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,262 @@
|
|||
import { MockEmitter, MockGuardians } from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||
import * as anchor from "@coral-xyz/anchor";
|
||||
import { expect } from "chai";
|
||||
import { CircleIntegrationProgram } from "../src";
|
||||
import {
|
||||
GUARDIAN_KEY,
|
||||
PAYER_PRIVATE_KEY,
|
||||
expectIxErr,
|
||||
expectIxOk,
|
||||
loadProgramBpf,
|
||||
postGovVaa,
|
||||
} from "./helpers";
|
||||
|
||||
const WORMHOLE_CORE_BRIDGE_ADDRESS = new anchor.web3.PublicKey(
|
||||
"3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5",
|
||||
);
|
||||
const ARTIFACTS_PATH = `${__dirname}/artifacts/testnet_wormhole_circle_integration_solana.so`;
|
||||
|
||||
const guardians = new MockGuardians(0, [GUARDIAN_KEY]);
|
||||
|
||||
describe("Circle Integration -- Testnet Fork", () => {
|
||||
const connection = new anchor.web3.Connection("http://localhost:8899", "processed");
|
||||
const payer = anchor.web3.Keypair.fromSecretKey(PAYER_PRIVATE_KEY);
|
||||
|
||||
const circleIntegration = new CircleIntegrationProgram(
|
||||
connection,
|
||||
"wcihrWf1s91vfukW7LW8ZvR1rzpeZ9BrtZ8oyPkWK5d",
|
||||
);
|
||||
|
||||
describe("Upgrade Contract", () => {
|
||||
const localVariables = new Map<string, any>();
|
||||
|
||||
it("Deploy Implementation", async () => {
|
||||
const implementation = await loadProgramBpf(
|
||||
ARTIFACTS_PATH,
|
||||
circleIntegration.upgradeAuthorityAddress(),
|
||||
);
|
||||
|
||||
localVariables.set("implementation", implementation);
|
||||
});
|
||||
|
||||
it("Invoke `upgrade_contract` on Forked Circle Integration", async () => {
|
||||
const implementation = localVariables.get("implementation") as anchor.web3.PublicKey;
|
||||
expect(localVariables.delete("implementation")).is.true;
|
||||
|
||||
const vaa = await postGovVaa(
|
||||
connection,
|
||||
payer,
|
||||
guardians,
|
||||
0n,
|
||||
{
|
||||
upgradeContract: {
|
||||
targetChain: 1,
|
||||
implementation,
|
||||
},
|
||||
},
|
||||
{
|
||||
coreBridgeAddress: WORMHOLE_CORE_BRIDGE_ADDRESS,
|
||||
},
|
||||
);
|
||||
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
});
|
||||
|
||||
await expectIxOk(connection, [ix], [payer]);
|
||||
});
|
||||
|
||||
it("Deploy Same Implementation and Invoke `upgrade_contract` with Another VAA", async () => {
|
||||
const implementation = await loadProgramBpf(
|
||||
ARTIFACTS_PATH,
|
||||
circleIntegration.upgradeAuthorityAddress(),
|
||||
);
|
||||
|
||||
const vaa = await postGovVaa(
|
||||
connection,
|
||||
payer,
|
||||
guardians,
|
||||
1n,
|
||||
{
|
||||
upgradeContract: {
|
||||
targetChain: 1,
|
||||
implementation,
|
||||
},
|
||||
},
|
||||
{
|
||||
coreBridgeAddress: WORMHOLE_CORE_BRIDGE_ADDRESS,
|
||||
},
|
||||
);
|
||||
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
});
|
||||
|
||||
await expectIxOk(connection, [ix], [payer]);
|
||||
|
||||
// Save for later.
|
||||
localVariables.set("vaa", vaa);
|
||||
});
|
||||
|
||||
it("Cannot Invoke `upgrade_contract` with Same VAA", async () => {
|
||||
const vaa = localVariables.get("vaa") as anchor.web3.PublicKey;
|
||||
expect(localVariables.delete("vaa")).is.true;
|
||||
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
});
|
||||
|
||||
// NOTE: The claim account created in the upgrade contract instruction doesn't trigger
|
||||
// the protection for a replay attack. The account data in the program data does. But
|
||||
// we will keep this test here just in case something changes in the future.
|
||||
await expectIxErr(connection, [ix], [payer], "invalid account data for instruction");
|
||||
});
|
||||
|
||||
it("Cannot Invoke `upgrade_contract` with Implementation Mismatch", async () => {
|
||||
const implementation = await loadProgramBpf(
|
||||
ARTIFACTS_PATH,
|
||||
circleIntegration.upgradeAuthorityAddress(),
|
||||
);
|
||||
const anotherImplementation = await loadProgramBpf(
|
||||
ARTIFACTS_PATH,
|
||||
circleIntegration.upgradeAuthorityAddress(),
|
||||
);
|
||||
|
||||
const vaa = await postGovVaa(
|
||||
connection,
|
||||
payer,
|
||||
guardians,
|
||||
0n,
|
||||
{
|
||||
upgradeContract: {
|
||||
targetChain: 1,
|
||||
implementation: anotherImplementation,
|
||||
},
|
||||
},
|
||||
{
|
||||
coreBridgeAddress: WORMHOLE_CORE_BRIDGE_ADDRESS,
|
||||
},
|
||||
);
|
||||
|
||||
// Create the upgrade instruction, but pass a different implementation.
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
buffer: implementation,
|
||||
});
|
||||
|
||||
await expectIxErr(connection, [ix], [payer], "Error Code: ImplementationMismatch");
|
||||
});
|
||||
|
||||
it("Cannot Invoke `upgrade_contract` with Invalid Governance Emitter", async () => {
|
||||
const implementation = await loadProgramBpf(
|
||||
ARTIFACTS_PATH,
|
||||
circleIntegration.upgradeAuthorityAddress(),
|
||||
);
|
||||
|
||||
// Create a bad governance emitter by using an invalid address.
|
||||
const invalidEmitter = new MockEmitter(
|
||||
circleIntegration.ID.toBuffer().toString("hex"),
|
||||
1,
|
||||
12121212,
|
||||
);
|
||||
|
||||
const vaa = await postGovVaa(
|
||||
connection,
|
||||
payer,
|
||||
guardians,
|
||||
2n,
|
||||
{
|
||||
upgradeContract: {
|
||||
targetChain: 1,
|
||||
implementation,
|
||||
},
|
||||
},
|
||||
{
|
||||
coreBridgeAddress: WORMHOLE_CORE_BRIDGE_ADDRESS,
|
||||
governanceEmitter: invalidEmitter,
|
||||
},
|
||||
);
|
||||
|
||||
// Create the upgrade instruction, but pass a different implementation.
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
buffer: implementation,
|
||||
});
|
||||
|
||||
await expectIxErr(connection, [ix], [payer], "Error Code: InvalidGovernanceEmitter");
|
||||
});
|
||||
|
||||
it("Cannot Invoke `upgrade_contract` with Governance For Another Chain", async () => {
|
||||
const implementation = await loadProgramBpf(
|
||||
ARTIFACTS_PATH,
|
||||
circleIntegration.upgradeAuthorityAddress(),
|
||||
);
|
||||
|
||||
const vaa = await postGovVaa(
|
||||
connection,
|
||||
payer,
|
||||
guardians,
|
||||
2n,
|
||||
{
|
||||
upgradeContract: {
|
||||
targetChain: 2,
|
||||
implementation,
|
||||
},
|
||||
},
|
||||
{
|
||||
coreBridgeAddress: WORMHOLE_CORE_BRIDGE_ADDRESS,
|
||||
},
|
||||
);
|
||||
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
});
|
||||
|
||||
await expectIxErr(connection, [ix], [payer], "Error Code: GovernanceForAnotherChain");
|
||||
});
|
||||
|
||||
it("Cannot Invoke `upgrade_contract` with Invalid Governance Action", async () => {
|
||||
const implementation = await loadProgramBpf(
|
||||
ARTIFACTS_PATH,
|
||||
circleIntegration.upgradeAuthorityAddress(),
|
||||
);
|
||||
|
||||
const vaa = await postGovVaa(
|
||||
connection,
|
||||
payer,
|
||||
guardians,
|
||||
2n,
|
||||
{
|
||||
registerEmitterAndDomain: {
|
||||
targetChain: 1,
|
||||
foreignChain: 2,
|
||||
foreignEmitter: Array.from(
|
||||
Buffer.from(
|
||||
"000000000000000000000000deadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
|
||||
"hex",
|
||||
),
|
||||
),
|
||||
cctpDomain: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
coreBridgeAddress: WORMHOLE_CORE_BRIDGE_ADDRESS,
|
||||
},
|
||||
);
|
||||
|
||||
const ix = await circleIntegration.upgradeContractIx({
|
||||
payer: payer.publicKey,
|
||||
vaa,
|
||||
buffer: implementation,
|
||||
});
|
||||
|
||||
await expectIxErr(connection, [ix], [payer], "Error Code: InvalidGovernanceAction");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "2yVjuQwpsvdsrywzsJJVs9Ueh4zayyo5DYJbBNc3DDpn",
|
||||
"account": {
|
||||
"lamports": 1057920,
|
||||
"data": [
|
||||
"AAAAAMbrG4wAAAAAgFEBAAoAAAAAAAAA",
|
||||
"base64"
|
||||
],
|
||||
"owner": "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 24
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "9bFNrXNb2WTx8fMHXCheaZqkLZ3YCCaiqTftHxeintHy",
|
||||
"account": {
|
||||
"lamports": 2350640070,
|
||||
"data": [
|
||||
"",
|
||||
"base64"
|
||||
],
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "DS7qfSAgYsonPpKoAjcGhX9VFjXdGkiHjEDkTidf8H2P",
|
||||
"account": {
|
||||
"lamports": 21141440,
|
||||
"data": [
|
||||
"AAAAAAEAAAC++kKdV80Yt/ik2RotqatK8F0PvkPJm2EAAAAA",
|
||||
"base64"
|
||||
],
|
||||
"owner": "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 36
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "6bi4JGDoRwUs9TYBuvoA7dUVyikTJDrJsJU1ew6KVLiu",
|
||||
"account": {
|
||||
"lamports": 1057920,
|
||||
"data": [
|
||||
"AAAAAMbrG4wAAAAAgFEBAAoAAAAAAAAA",
|
||||
"base64"
|
||||
],
|
||||
"owner": "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 24
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "7s3a1ycs16d6SNDumaRtjcoyMaTDZPavzgsmS3uUZYWX",
|
||||
"account": {
|
||||
"lamports": 2350640070,
|
||||
"data": [
|
||||
"",
|
||||
"base64"
|
||||
],
|
||||
"owner": "11111111111111111111111111111111",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "dxZtypiKT5D9LYzdPxjvSZER9MgYfeRVU5qpMTMTRs4",
|
||||
"account": {
|
||||
"lamports": 21141440,
|
||||
"data": [
|
||||
"AAAAAAEAAAC++kKdV80Yt/ik2RotqatK8F0PvkPJm2EAAAAA",
|
||||
"base64"
|
||||
],
|
||||
"owner": "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 36
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "BWrwSWjbikT3H7qHAkUEbLmwDQoB4ZDJ4wcSEhSPTZCu",
|
||||
"account": {
|
||||
"lamports": 2519520,
|
||||
"data": [
|
||||
"Ryi0jhPLI/wfOQgPIIpMNon4r0rVMO7Sy1fUtUxQmdUxE51OObvhOoDFz5C0iApooK3hQfndo8m3eRHbcLcd6T35aIm/9s3sgMXPkLSICmigreFB+d2jybd5Edtwtx3pPfloib/2zeyAxc+QtIgKaKCt4UH53aPJt3kR23C3Hek9+WiJv/bN7AAFAAAAAAAAAAEAAAABAAAAAAAAAAAAAAAAAAAAvvpCnVfNGLf4pNkaLamrSvBdD77QBwAAAAAAACYAAAAAAAAA/w==",
|
||||
"base64"
|
||||
],
|
||||
"owner": "CCTPmbSD7gX1bxKPAmg77w8oFzNFpaQiQUWD43TKaecd",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 234
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "Hazwi3jFQtLKc2ughi7HFXPkpDeso7DQaMR9Ks4afh3j",
|
||||
"account": {
|
||||
"lamports": 1197120,
|
||||
"data": [
|
||||
"aXOuIl/pivwAAAAAAAAAAAAAAAAAAAAA0MPaWPVTWBQrjT4GwcMMXGEU7+g=",
|
||||
"base64"
|
||||
],
|
||||
"owner": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 44
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "BWyFzH6LsnmDAaDWbGsriQ9SiiKq1CF6pbH4Ye3kzSBV",
|
||||
"account": {
|
||||
"lamports": 1197120,
|
||||
"data": [
|
||||
"aXOuIl/pivwAAAAAAAAAAAAAAAAAAAAA0MPaWPVTWBQrjT4GwcMMXGEU7+g=",
|
||||
"base64"
|
||||
],
|
||||
"owner": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 44
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "Afgq3BHEfCE7d78D2XE9Bfyu2ieDqvE24xX8KDwreBms",
|
||||
"account": {
|
||||
"lamports": 1649520,
|
||||
"data": [
|
||||
"ogTyNJPz3WCAxc+QtIgKaKCt4UH53aPJt3kR23C3Hek9+WiJv/bN7B85CA8gikw2ifivStUw7tLLV9S1TFCZ1TETnU45u+E6pl/JidtfXUJ1nzpUYFjvzc3AvzwYmActjrRd0dgFCM4AAAAA/Q==",
|
||||
"base64"
|
||||
],
|
||||
"owner": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 109
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "DBD8hAwLDRQkTsu6EqviaYNGKPnsAMmQonxf7AH8ZcFY",
|
||||
"account": {
|
||||
"lamports": 1405920,
|
||||
"data": [
|
||||
"eoVUPzmfq86Axc+QtIgKaKCt4UH53aPJt3kR23C3Hek9+WiJv/bN7IDFz5C0iApooK3hQfndo8m3eRHbcLcd6T35aIm/9s3sAP0=",
|
||||
"base64"
|
||||
],
|
||||
"owner": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 74
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "AEfKU8wHGtYgsXpymQ6e1cGHJJeKqCj95pw82iyRUKEs",
|
||||
"account": {
|
||||
"lamports": 2039280,
|
||||
"data": [
|
||||
"O0Qss5EhV/E6kz0BNCgtAytf/s0Botvxt3kGCN8ALqe06cUFm0yAK1JhEHKELuHdBTqROz8nrg0RaRNBnQEQ50g9+D6VVAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
|
||||
"base64"
|
||||
],
|
||||
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 165
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "4xt9P42CcMHXAgvemTnzineHp6owfGUcrg1xD9V7mdk1",
|
||||
"account": {
|
||||
"lamports": 1795680,
|
||||
"data": [
|
||||
"n4M6qsFUgLaJOQ9DWBY8Pr5GkgAm5md6yYsX7D3P0LYdBflhsqtwJDtELLORIVfxOpM9ATQoLQMrX/7NAaLb8bd5BgjfAC6nABCl1OgAAAABAAAAAAAAAAEAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAD//w==",
|
||||
"base64"
|
||||
],
|
||||
"owner": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 130
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"pubkey": "ADcG1d7znq6wR73BJgEh7dR4vTJcETLLyfXMNZjJVwk4",
|
||||
"account": {
|
||||
"lamports": 1426800,
|
||||
"data": [
|
||||
"EdYtsOWVxUcAAAAAAAAAAAAAAAAAAAAAB4Zcboe59wJVN34CSs5mMMHqo38649Wj+M9UXZAg4cVbgkofjwh29UJBwfc8LZK10/cIjv4=",
|
||||
"base64"
|
||||
],
|
||||
"owner": "CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3",
|
||||
"executable": false,
|
||||
"rentEpoch": 18446744073709551615,
|
||||
"space": 77
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue