From 426d09742ffa58521935e61917d65d67f9211927 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Mon, 7 Aug 2023 09:21:57 -0500 Subject: [PATCH 1/2] solana: various fixes to tbtc and gateway programs Co-authored-by: gator-boi --- cross-chain/solana/.gitignore | 1 + cross-chain/solana/Anchor.toml | 8 +- cross-chain/solana/Cargo.lock | 169 +++++++++-- cross-chain/solana/Makefile | 9 +- cross-chain/solana/programs/tbtc/Cargo.toml | 9 +- cross-chain/solana/programs/tbtc/src/error.rs | 37 ++- cross-chain/solana/programs/tbtc/src/event.rs | 2 +- cross-chain/solana/programs/tbtc/src/lib.rs | 2 + .../tbtc/src/processor/admin/add_guardian.rs | 27 +- .../tbtc/src/processor/admin/add_minter.rs | 24 +- .../tbtc/src/processor/admin/initialize.rs | 78 ++++- .../src/processor/admin/remove_guardian.rs | 32 ++- .../tbtc/src/processor/admin/remove_minter.rs | 32 ++- .../src/processor/admin/take_authority.rs | 24 +- .../solana/programs/tbtc/src/state/config.rs | 4 +- .../programs/tbtc/src/state/guardian_info.rs | 2 +- .../programs/tbtc/src/state/guardians.rs | 30 ++ .../solana/programs/tbtc/src/state/minters.rs | 30 ++ .../solana/programs/tbtc/src/state/mod.rs | 6 + .../programs/wormhole-gateway/Cargo.toml | 4 +- .../wormhole-gateway/src/constants.rs | 18 ++ .../programs/wormhole-gateway/src/error.rs | 29 +- .../programs/wormhole-gateway/src/event.rs | 34 +++ .../programs/wormhole-gateway/src/lib.rs | 22 +- .../admin/cancel_authority_change.rs | 22 ++ .../src/processor/admin/change_authority.rs | 23 ++ .../src/processor/{ => admin}/initialize.rs | 43 +-- .../src/processor/admin/mod.rs | 17 ++ .../src/processor/admin/take_authority.rs | 38 +++ .../{ => admin}/update_gateway_address.rs | 7 +- .../{ => admin}/update_minting_limit.rs | 5 + .../src/processor/deposit_wormhole_tbtc.rs | 18 +- .../wormhole-gateway/src/processor/mod.rs | 16 +- .../src/processor/receive_tbtc.rs | 266 +++++++++++++++--- .../src/processor/send_tbtc/gateway.rs | 35 ++- .../src/processor/send_tbtc/mod.rs | 34 ++- .../src/processor/send_tbtc/wrapped.rs | 106 +++---- .../wormhole-gateway/src/state/custodian.rs | 11 +- 38 files changed, 1051 insertions(+), 223 deletions(-) create mode 100644 cross-chain/solana/programs/tbtc/src/state/guardians.rs create mode 100644 cross-chain/solana/programs/tbtc/src/state/minters.rs create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/constants.rs create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/event.rs create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/processor/admin/cancel_authority_change.rs create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/processor/admin/change_authority.rs rename cross-chain/solana/programs/wormhole-gateway/src/processor/{ => admin}/initialize.rs (62%) create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/processor/admin/mod.rs create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/processor/admin/take_authority.rs rename cross-chain/solana/programs/wormhole-gateway/src/processor/{ => admin}/update_gateway_address.rs (88%) rename cross-chain/solana/programs/wormhole-gateway/src/processor/{ => admin}/update_minting_limit.rs (86%) diff --git a/cross-chain/solana/.gitignore b/cross-chain/solana/.gitignore index 3db15d86f..0f1813a21 100644 --- a/cross-chain/solana/.gitignore +++ b/cross-chain/solana/.gitignore @@ -1,5 +1,6 @@ .anchor +.prettierrc.json .DS_Store target **/*.rs.bk diff --git a/cross-chain/solana/Anchor.toml b/cross-chain/solana/Anchor.toml index 6025a94f1..052529539 100644 --- a/cross-chain/solana/Anchor.toml +++ b/cross-chain/solana/Anchor.toml @@ -11,7 +11,7 @@ members = [ [programs.localnet] tbtc = "HksEtDgsXJV1BqcuhzbLRTmXp5gHgHJktieJCtQd3pG" -wormhole-gateway = "8H9F5JGbEMyERycwaGuzLS5MQnV7dn2wm2h6egJ3Leiu" +wormhole_gateway = "8H9F5JGbEMyERycwaGuzLS5MQnV7dn2wm2h6egJ3Leiu" [registry] url = "https://api.apr.dev" @@ -61,10 +61,10 @@ filename = "tests/accounts/ethereum_token_bridge.json" address = "DapiQYH3BGonhN8cngWcXQ6SrqSm3cwysoznoHr6Sbsx" filename = "tests/accounts/token_bridge_config.json" -### Core Bridge -- Bridge -[[test.validator.clone]] +### Core Bridge -- Bridge Data +[[test.validator.account]] address = "2yVjuQwpsvdsrywzsJJVs9Ueh4zayyo5DYJbBNc3DDpn" -filename = "tests/accounts/core_bridge.json" +filename = "tests/accounts/core_bridge_data.json" ### Core Bridge -- Emitter Sequence (Token Bridge's) [[test.validator.account]] diff --git a/cross-chain/solana/Cargo.lock b/cross-chain/solana/Cargo.lock index b01fb264b..b9f9f8463 100644 --- a/cross-chain/solana/Cargo.lock +++ b/cross-chain/solana/Cargo.lock @@ -191,6 +191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78f860599da1c2354e7234c768783049eb42e2f54509ecfc942d2e0076a2da7b" dependencies = [ "anchor-lang", + "mpl-token-metadata", "solana-program", "spl-associated-token-account", "spl-token", @@ -1015,6 +1016,77 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mpl-token-auth-rules" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c376f2cc7dae80e2949cd6ca8a2420b3c61c1ecb7a275c6433d9a4d2d24f994d" +dependencies = [ + "borsh", + "bytemuck", + "mpl-token-metadata-context-derive 0.2.1", + "num-derive", + "num-traits", + "rmp-serde", + "serde", + "shank", + "solana-program", + "solana-zk-token-sdk", + "thiserror", +] + +[[package]] +name = "mpl-token-metadata" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e73b5df66f4e6f98606e3fb327cbc6a0dba8df11085246f2e766949acb96bb" +dependencies = [ + "arrayref", + "borsh", + "mpl-token-auth-rules", + "mpl-token-metadata-context-derive 0.3.0", + "mpl-utils", + "num-derive", + "num-traits", + "shank", + "solana-program", + "spl-associated-token-account", + "spl-token", + "thiserror", +] + +[[package]] +name = "mpl-token-metadata-context-derive" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12989bc45715b0ee91944855130131479f9c772e198a910c3eb0ea327d5bffc3" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mpl-token-metadata-context-derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a739019e11d93661a64ef5fe108ab17c79b35961e944442ff6efdd460ad01a" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mpl-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "822133b6cba8f9a43e5e0e189813be63dd795858f54155c729833be472ffdb51" +dependencies = [ + "arrayref", + "borsh", + "solana-program", + "spl-token", +] + [[package]] name = "num-derive" version = "0.3.3" @@ -1101,6 +1173,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "paste" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" + [[package]] name = "pbkdf2" version = "0.4.0" @@ -1308,6 +1386,28 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +[[package]] +name = "rmp" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44519172358fd6d58656c86ab8e7fbc9e1490c3e8f14d35ed78ca0dd07403c9f" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + +[[package]] +name = "rmp-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b13be192e0220b8afb7222aa5813cb62cc269ebb5cac346ca6487681d2913e" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -1433,6 +1533,40 @@ dependencies = [ "keccak", ] +[[package]] +name = "shank" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63e565b5e95ad88ab38f312e89444c749360641c509ef2de0093b49f55974a5" +dependencies = [ + "shank_macro", +] + +[[package]] +name = "shank_macro" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63927d22a1e8b74bda98cc6e151fcdf178b7abb0dc6c4f81e0bbf5ffe2fc4ec8" +dependencies = [ + "proc-macro2", + "quote", + "shank_macro_impl", + "syn 1.0.109", +] + +[[package]] +name = "shank_macro_impl" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce03403df682f80f4dc1efafa87a4d0cb89b03726d0565e6364bdca5b9a441" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "serde", + "syn 1.0.109", +] + [[package]] name = "signature" version = "1.6.4" @@ -1457,9 +1591,9 @@ checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "solana-frozen-abi" -version = "1.14.20" +version = "1.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a100b7fa8198c20354eb7256c0d9789107d8a62280221f3efe15f7c9dc4cec" +checksum = "225ac329a67b02e2ac4ae8010665ad4bb77b7db7fc8577b99e6746c7606072ee" dependencies = [ "ahash", "blake3", @@ -1491,9 +1625,9 @@ dependencies = [ [[package]] name = "solana-frozen-abi-macro" -version = "1.14.20" +version = "1.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f527f44601b35dd67d11bc72f2f7512976a466f9304ef574b87dac83ced8a42" +checksum = "52444c75e502210ef16edbf0e8d57c3945603899216cb144dfb86449d260aa30" dependencies = [ "proc-macro2", "quote", @@ -1503,9 +1637,9 @@ dependencies = [ [[package]] name = "solana-logger" -version = "1.14.20" +version = "1.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8632c8bc480bb5615b70a18b807ede73024aebc7761503ff86a70b7f4906ae47" +checksum = "9a1d78de034cab8726fe7863873addf0aa6a4bb7ece54b5706616bac739e28b4" dependencies = [ "env_logger", "lazy_static", @@ -1514,9 +1648,9 @@ dependencies = [ [[package]] name = "solana-program" -version = "1.14.20" +version = "1.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51ad5f48743ce505f6139a07e20aecdc689def12da7230fed661c2073ab97df8" +checksum = "f37355e56ce445ae981624d51b58a633be33bf476fd599722a4cfc8db98ef3cb" dependencies = [ "base64 0.13.1", "bincode", @@ -1563,9 +1697,9 @@ dependencies = [ [[package]] name = "solana-sdk" -version = "1.14.20" +version = "1.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c515a5a5a5cdc115044c33959eb4d091680f5e7ca8be9eb5218fb0c21bf3568" +checksum = "31e0955817486854951828071fb6b248633928a86e2f9fd8f69230f365d59f2e" dependencies = [ "assert_matches", "base64 0.13.1", @@ -1614,9 +1748,9 @@ dependencies = [ [[package]] name = "solana-sdk-macro" -version = "1.14.20" +version = "1.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bbc3ab3070c090e1a18fd5a0a07d729d0db2bc8524414dc3e16504286d38049" +checksum = "8a83905d4911d35e7367544909fa1ca2611c974e5584e3e82efb2313da69df96" dependencies = [ "bs58 0.4.0", "proc-macro2", @@ -1627,9 +1761,9 @@ dependencies = [ [[package]] name = "solana-zk-token-sdk" -version = "1.14.20" +version = "1.14.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d51d131cdefcb621a8034321ce487c4f788e813f81ce81e4f65eed8d4b4f2aa" +checksum = "fb0789e84a4e93ad101a67d59a83270c3bad001206f923ea97b1791f54c33c80" dependencies = [ "aes-gcm-siv", "arrayref", @@ -1748,6 +1882,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "mpl-token-metadata", "solana-program", ] @@ -2068,8 +2203,9 @@ dependencies = [ [[package]] name = "wormhole-anchor-sdk" -version = "0.1.0" -source = "git+https://github.com/wormhole-foundation/wormhole-scaffolding?rev=f8d5ba04bfd449ab3693b15c818fd3e85e30f758#f8d5ba04bfd449ab3693b15c818fd3e85e30f758" +version = "0.1.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1789eb9fd2113b6e2945cb67123b902141a9bfde1ec33762be58447eb2431f6" dependencies = [ "anchor-lang", "anchor-spl", @@ -2082,6 +2218,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "solana-program", "tbtc", "wormhole-anchor-sdk", ] diff --git a/cross-chain/solana/Makefile b/cross-chain/solana/Makefile index 6fe96802c..60201a688 100644 --- a/cross-chain/solana/Makefile +++ b/cross-chain/solana/Makefile @@ -1,8 +1,7 @@ - out_solana-devnet=artifacts-testnet out_mainnet=artifacts-mainnet -.PHONY: all clean build test +.PHONY: all clean build test lint all: test @@ -24,3 +23,9 @@ endif test: node_modules anchor test --arch sbf + +lint: + cargo fmt --check + cargo check --features "mainnet" --no-default-features + cargo check --features "solana-devnet" --no-default-features + cargo clippy --no-deps --all-targets -- -D warnings \ No newline at end of file diff --git a/cross-chain/solana/programs/tbtc/Cargo.toml b/cross-chain/solana/programs/tbtc/Cargo.toml index 18e88c859..b80c4641c 100644 --- a/cross-chain/solana/programs/tbtc/Cargo.toml +++ b/cross-chain/solana/programs/tbtc/Cargo.toml @@ -18,6 +18,9 @@ no-log-ix-name = [] cpi = ["no-entrypoint"] [dependencies] -anchor-lang = { version = "=0.28.0", features = ["derive", "init-if-needed"] } -anchor-spl = "=0.28.0" -solana-program = "=1.14.20" +anchor-lang = { version = "0.28.0", features = ["derive", "init-if-needed"] } +anchor-spl = { version = "0.28.0", features = ["metadata"] } + +solana-program = "=1.14" + +mpl-token-metadata = "1.13.1" diff --git a/cross-chain/solana/programs/tbtc/src/error.rs b/cross-chain/solana/programs/tbtc/src/error.rs index 5e1b279d5..433eb20eb 100644 --- a/cross-chain/solana/programs/tbtc/src/error.rs +++ b/cross-chain/solana/programs/tbtc/src/error.rs @@ -2,9 +2,36 @@ use anchor_lang::prelude::error_code; #[error_code] pub enum TbtcError { - IsPaused, - IsNotPaused, - IsNotAuthority, - IsNotPendingAuthority, - NoPendingAuthorityChange, + #[msg("Not valid authority to perform this action")] + IsNotAuthority = 0x20, + + #[msg("Not valid pending authority to take authority")] + IsNotPendingAuthority = 0x22, + + #[msg("No pending authority")] + NoPendingAuthorityChange = 0x24, + + #[msg("This address is already a guardian")] + GuardianAlreadyExists = 0x30, + + #[msg("This address is not a guardian")] + GuardianNonexistent = 0x32, + + #[msg("Caller is not a guardian")] + SignerNotGuardian = 0x34, + + #[msg("This address is already a minter")] + MinterAlreadyExists = 0x40, + + #[msg("This address is not a minter")] + MinterNonexistent = 0x42, + + #[msg("Caller is not a minter")] + SignerNotMinter = 0x44, + + #[msg("Program is paused")] + IsPaused = 0x50, + + #[msg("Program is not paused")] + IsNotPaused = 0x52, } diff --git a/cross-chain/solana/programs/tbtc/src/event.rs b/cross-chain/solana/programs/tbtc/src/event.rs index 6a385a50a..341f6156e 100644 --- a/cross-chain/solana/programs/tbtc/src/event.rs +++ b/cross-chain/solana/programs/tbtc/src/event.rs @@ -18,4 +18,4 @@ pub struct GuardianAdded { #[event] pub struct GuardianRemoved { pub guardian: Pubkey, -} \ No newline at end of file +} diff --git a/cross-chain/solana/programs/tbtc/src/lib.rs b/cross-chain/solana/programs/tbtc/src/lib.rs index f2604b0f9..8011c6cc2 100644 --- a/cross-chain/solana/programs/tbtc/src/lib.rs +++ b/cross-chain/solana/programs/tbtc/src/lib.rs @@ -5,6 +5,8 @@ pub use constants::*; pub mod error; +pub(crate) mod event; + mod processor; pub(crate) use processor::*; diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/add_guardian.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/add_guardian.rs index 422bac905..6cc51c2f6 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/add_guardian.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/add_guardian.rs @@ -1,6 +1,6 @@ use crate::{ error::TbtcError, - state::{Config, GuardianInfo}, + state::{Config, GuardianInfo, Guardians}, }; use anchor_lang::prelude::*; @@ -17,6 +17,16 @@ pub struct AddGuardian<'info> { #[account(mut)] authority: Signer<'info>, + #[account( + mut, + seeds = [Guardians::SEED_PREFIX], + bump = guardians.bump, + realloc = Guardians::compute_size(guardians.keys.len() + 1), + realloc::payer = authority, + realloc::zero = true, + )] + guardians: Account<'info, Guardians>, + #[account( init, payer = authority, @@ -26,18 +36,29 @@ pub struct AddGuardian<'info> { )] guardian_info: Account<'info, GuardianInfo>, - /// CHECK: Required authority to pause contract. This pubkey lives in `GuardianInfo`. + /// CHECK: Required authority to pause contract. This pubkey lives in `GuardianInfo` and + /// `Guardians`. guardian: AccountInfo<'info>, system_program: Program<'info, System>, } pub fn add_guardian(ctx: Context) -> Result<()> { + let guardian = ctx.accounts.guardian.key(); + + // Set account data. ctx.accounts.guardian_info.set_inner(GuardianInfo { - guardian: ctx.accounts.guardian.key(), bump: ctx.bumps["guardian_info"], + guardian, }); + // Push pubkey to guardians account. + ctx.accounts.guardians.push(guardian); + + // Update config. ctx.accounts.config.num_guardians += 1; + + emit!(crate::event::GuardianAdded { guardian }); + Ok(()) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs index ccb8620cb..a76cf2480 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/add_minter.rs @@ -1,6 +1,6 @@ use crate::{ error::TbtcError, - state::{Config, MinterInfo}, + state::{Config, MinterInfo, Minters}, }; use anchor_lang::prelude::*; @@ -17,6 +17,16 @@ pub struct AddMinter<'info> { #[account(mut)] authority: Signer<'info>, + #[account( + mut, + seeds = [Minters::SEED_PREFIX], + bump = minters.bump, + realloc = Minters::compute_size(minters.keys.len() + 1), + realloc::payer = authority, + realloc::zero = true, + )] + minters: Account<'info, Minters>, + #[account( init, payer = authority, @@ -33,11 +43,21 @@ pub struct AddMinter<'info> { } pub fn add_minter(ctx: Context) -> Result<()> { + let minter = ctx.accounts.minter.key(); + + // Set account data. ctx.accounts.minter_info.set_inner(MinterInfo { - minter: ctx.accounts.minter.key(), bump: ctx.bumps["minter_info"], + minter, }); + // Push pubkey to minters account. + ctx.accounts.minters.push(minter); + + // Update config. ctx.accounts.config.num_minters += 1; + + emit!(crate::event::MinterAdded { minter }); + Ok(()) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs index e32ebc5bb..d4cf6ecd7 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs @@ -1,6 +1,9 @@ -use crate::{constants::SEED_PREFIX_TBTC_MINT, state::Config}; +use crate::{ + constants::SEED_PREFIX_TBTC_MINT, + state::{Config, Guardians, Minters}, +}; use anchor_lang::prelude::*; -use anchor_spl::token; +use anchor_spl::{metadata, token}; #[derive(Accounts)] pub struct Initialize<'info> { @@ -11,7 +14,7 @@ pub struct Initialize<'info> { seeds = [SEED_PREFIX_TBTC_MINT], bump, payer = authority, - mint::decimals = 9, + mint::decimals = 8, mint::authority = config, )] mint: Account<'info, token::Mint>, @@ -25,14 +28,41 @@ pub struct Initialize<'info> { )] config: Account<'info, Config>, + #[account( + init, + payer = authority, + space = Guardians::compute_size(0), + seeds = [Guardians::SEED_PREFIX], + bump, + )] + guardians: Account<'info, Guardians>, + + #[account( + init, + payer = authority, + space = Minters::compute_size(0), + seeds = [Minters::SEED_PREFIX], + bump, + )] + minters: Account<'info, Minters>, + #[account(mut)] authority: Signer<'info>, + /// CHECK: This account is needed for the MPL Token Metadata program. + #[account(mut)] + tbtc_metadata: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the MPL Token Metadata program. + rent: UncheckedAccount<'info>, + + mpl_token_metadata_program: Program<'info, metadata::Metadata>, token_program: Program<'info, token::Token>, system_program: Program<'info, System>, } pub fn initialize(ctx: Context) -> Result<()> { + // Set Config account data. ctx.accounts.config.set_inner(Config { bump: ctx.bumps["config"], authority: ctx.accounts.authority.key(), @@ -43,5 +73,45 @@ pub fn initialize(ctx: Context) -> Result<()> { num_guardians: 0, paused: false, }); - Ok(()) + + // Set Guardians account data with empty vec. + ctx.accounts.guardians.set_inner(Guardians { + bump: ctx.bumps["guardians"], + keys: Vec::new(), + }); + + // Set Guardians account data with empty vec. + ctx.accounts.minters.set_inner(Minters { + bump: ctx.bumps["minters"], + keys: Vec::new(), + }); + + // Create metadata for tBTC. + metadata::create_metadata_accounts_v3( + CpiContext::new_with_signer( + ctx.accounts.mpl_token_metadata_program.to_account_info(), + metadata::CreateMetadataAccountsV3 { + metadata: ctx.accounts.tbtc_metadata.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + mint_authority: ctx.accounts.config.to_account_info(), + payer: ctx.accounts.authority.to_account_info(), + update_authority: ctx.accounts.config.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + rent: ctx.accounts.rent.to_account_info(), + }, + &[&[Config::SEED_PREFIX, &[ctx.bumps["config"]]]], + ), + mpl_token_metadata::state::DataV2 { + symbol: "tBTC".to_string(), + name: "tBTC v2".to_string(), + uri: "".to_string(), + seller_fee_basis_points: 0, + creators: None, + collection: None, + uses: None, + }, + true, + true, + None, + ) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/remove_guardian.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/remove_guardian.rs index 777edf865..06f45c3f6 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/remove_guardian.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/remove_guardian.rs @@ -1,6 +1,6 @@ use crate::{ error::TbtcError, - state::{Config, GuardianInfo}, + state::{Config, GuardianInfo, Guardians}, }; use anchor_lang::prelude::*; @@ -12,8 +12,19 @@ pub struct RemoveGuardian<'info> { )] config: Account<'info, Config>, + #[account(mut)] authority: Signer<'info>, + #[account( + mut, + seeds = [Guardians::SEED_PREFIX], + bump = guardians.bump, + realloc = Guardians::compute_size(guardians.keys.len().saturating_sub(1)), + realloc::payer = authority, + realloc::zero = true, + )] + guardians: Account<'info, Guardians>, + #[account( mut, has_one = guardian, @@ -25,9 +36,28 @@ pub struct RemoveGuardian<'info> { /// CHECK: Required authority to pause contract. This pubkey lives in `GuardianInfo`. guardian: AccountInfo<'info>, + + system_program: Program<'info, System>, } pub fn remove_guardian(ctx: Context) -> Result<()> { + let guardians: &mut Vec<_> = &mut ctx.accounts.guardians; + let removed = ctx.accounts.guardian.key(); + + // It is safe to unwrap because the key we are removing is guaranteed to exist since there is + // a guardian info account for it. + let index = guardians + .iter() + .position(|&guardian| guardian == removed) + .unwrap(); + + // Remove pubkey to guardians account. + guardians.swap_remove(index); + + // Update config. ctx.accounts.config.num_guardians -= 1; + + emit!(crate::event::GuardianRemoved { guardian: removed }); + Ok(()) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs index 83aa5a07d..a506bb1d5 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/remove_minter.rs @@ -1,6 +1,6 @@ use crate::{ error::TbtcError, - state::{Config, MinterInfo}, + state::{Config, MinterInfo, Minters}, }; use anchor_lang::prelude::*; @@ -14,8 +14,19 @@ pub struct RemoveMinter<'info> { )] config: Account<'info, Config>, + #[account(mut)] authority: Signer<'info>, + #[account( + mut, + seeds = [Minters::SEED_PREFIX], + bump = minters.bump, + realloc = Minters::compute_size(minters.keys.len().saturating_sub(1)), + realloc::payer = authority, + realloc::zero = true, + )] + minters: Account<'info, Minters>, + #[account( mut, has_one = minter, @@ -27,9 +38,28 @@ pub struct RemoveMinter<'info> { /// CHECK: Required authority to mint tokens. This pubkey lives in `MinterInfo`. minter: AccountInfo<'info>, + + system_program: Program<'info, System>, } pub fn remove_minter(ctx: Context) -> Result<()> { + let minters: &mut Vec<_> = &mut ctx.accounts.minters; + let removed = ctx.accounts.minter.key(); + + // It is safe to unwrap because the key we are removing is guaranteed to exist since there is + // a minter info account for it. + let index = minters + .iter() + .position(|&minter| minter == removed) + .unwrap(); + + // Remove pubkey to minters account. + minters.swap_remove(index); + + // Update config. ctx.accounts.config.num_minters -= 1; + + emit!(crate::event::MinterRemoved { minter: removed }); + Ok(()) } diff --git a/cross-chain/solana/programs/tbtc/src/processor/admin/take_authority.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/take_authority.rs index 292d2d726..4d9826a73 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/take_authority.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/take_authority.rs @@ -7,18 +7,32 @@ pub struct TakeAuthority<'info> { mut, seeds = [Config::SEED_PREFIX], bump, - constraint = config.pending_authority.is_some() @ TbtcError::NoPendingAuthorityChange )] config: Account<'info, Config>, - #[account( - constraint = pending_authority.key() == config.pending_authority.unwrap() @ TbtcError::IsNotPendingAuthority - )] pending_authority: Signer<'info>, } +impl<'info> TakeAuthority<'info> { + fn constraints(ctx: &Context) -> Result<()> { + match ctx.accounts.config.pending_authority { + Some(pending_authority) => { + require_keys_eq!( + pending_authority, + ctx.accounts.pending_authority.key(), + TbtcError::IsNotPendingAuthority + ); + + Ok(()) + } + None => err!(TbtcError::NoPendingAuthorityChange), + } + } +} + +#[access_control(TakeAuthority::constraints(&ctx))] pub fn take_authority(ctx: Context) -> Result<()> { ctx.accounts.config.authority = ctx.accounts.pending_authority.key(); ctx.accounts.config.pending_authority = None; Ok(()) -} \ No newline at end of file +} diff --git a/cross-chain/solana/programs/tbtc/src/state/config.rs b/cross-chain/solana/programs/tbtc/src/state/config.rs index 497200848..da0366f2c 100644 --- a/cross-chain/solana/programs/tbtc/src/state/config.rs +++ b/cross-chain/solana/programs/tbtc/src/state/config.rs @@ -14,8 +14,8 @@ pub struct Config { pub mint_bump: u8, // Admin info. - pub num_minters: u8, - pub num_guardians: u8, + pub num_minters: u32, + pub num_guardians: u32, pub paused: bool, } diff --git a/cross-chain/solana/programs/tbtc/src/state/guardian_info.rs b/cross-chain/solana/programs/tbtc/src/state/guardian_info.rs index 34f1492ba..a87c5cdbb 100644 --- a/cross-chain/solana/programs/tbtc/src/state/guardian_info.rs +++ b/cross-chain/solana/programs/tbtc/src/state/guardian_info.rs @@ -3,8 +3,8 @@ use anchor_lang::prelude::*; #[account] #[derive(Debug, InitSpace)] pub struct GuardianInfo { - pub guardian: Pubkey, pub bump: u8, + pub guardian: Pubkey, } impl GuardianInfo { diff --git a/cross-chain/solana/programs/tbtc/src/state/guardians.rs b/cross-chain/solana/programs/tbtc/src/state/guardians.rs new file mode 100644 index 000000000..bfacd6935 --- /dev/null +++ b/cross-chain/solana/programs/tbtc/src/state/guardians.rs @@ -0,0 +1,30 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Debug)] +pub struct Guardians { + pub bump: u8, + pub keys: Vec, +} + +impl Guardians { + pub const SEED_PREFIX: &'static [u8] = b"guardians"; + + pub(crate) fn compute_size(num_guardians: usize) -> usize { + 8 + 1 + 4 + num_guardians * 32 + } +} + +impl std::ops::Deref for Guardians { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.keys + } +} + +impl std::ops::DerefMut for Guardians { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.keys + } +} diff --git a/cross-chain/solana/programs/tbtc/src/state/minters.rs b/cross-chain/solana/programs/tbtc/src/state/minters.rs new file mode 100644 index 000000000..6974b10e4 --- /dev/null +++ b/cross-chain/solana/programs/tbtc/src/state/minters.rs @@ -0,0 +1,30 @@ +use anchor_lang::prelude::*; + +#[account] +#[derive(Debug)] +pub struct Minters { + pub bump: u8, + pub keys: Vec, +} + +impl Minters { + pub const SEED_PREFIX: &'static [u8] = b"minters"; + + pub(crate) fn compute_size(num_minters: usize) -> usize { + 8 + 1 + 4 + num_minters * 32 + } +} + +impl std::ops::Deref for Minters { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.keys + } +} + +impl std::ops::DerefMut for Minters { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.keys + } +} diff --git a/cross-chain/solana/programs/tbtc/src/state/mod.rs b/cross-chain/solana/programs/tbtc/src/state/mod.rs index dffd4099b..296352f95 100644 --- a/cross-chain/solana/programs/tbtc/src/state/mod.rs +++ b/cross-chain/solana/programs/tbtc/src/state/mod.rs @@ -4,5 +4,11 @@ pub use config::*; mod guardian_info; pub use guardian_info::*; +mod guardians; +pub use guardians::*; + mod minter_info; pub use minter_info::*; + +mod minters; +pub use minters::*; diff --git a/cross-chain/solana/programs/wormhole-gateway/Cargo.toml b/cross-chain/solana/programs/wormhole-gateway/Cargo.toml index 6df2961fc..b00b3ebb5 100644 --- a/cross-chain/solana/programs/wormhole-gateway/Cargo.toml +++ b/cross-chain/solana/programs/wormhole-gateway/Cargo.toml @@ -18,9 +18,11 @@ no-log-ix-name = [] cpi = ["no-entrypoint"] [dependencies] +wormhole-anchor-sdk = { version = "0.1.0-alpha.1", features = ["token-bridge"], default-features = false } + anchor-lang = { version = "0.28.0", features = ["init-if-needed"]} anchor-spl = "0.28.0" -wormhole-anchor-sdk = { git = "https://github.com/wormhole-foundation/wormhole-scaffolding", rev = "f8d5ba04bfd449ab3693b15c818fd3e85e30f758", features = ["token-bridge"], default-features = false } +solana-program = "=1.14" tbtc = { path = "../tbtc", features = ["cpi"] } \ No newline at end of file diff --git a/cross-chain/solana/programs/wormhole-gateway/src/constants.rs b/cross-chain/solana/programs/wormhole-gateway/src/constants.rs new file mode 100644 index 000000000..75ece6f5b --- /dev/null +++ b/cross-chain/solana/programs/wormhole-gateway/src/constants.rs @@ -0,0 +1,18 @@ +pub const TBTC_ETHEREUM_TOKEN_CHAIN: u16 = 2; + +#[cfg(feature = "mainnet")] +/// tBTC token address on the Ethereum Mainnet. +pub const TBTC_ETHEREUM_TOKEN_ADDRESS: [u8; 32] = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x18, 0x08, 0x4f, 0xba, 0x66, 0x6a, + 0x33, 0xd3, 0x75, 0x92, 0xfa, 0x26, 0x33, 0xfd, 0x49, 0xa7, 0x4d, 0xd9, 0x3a, 0x88, +]; + +#[cfg(feature = "solana-devnet")] +/// tBTC token address on the Ethereum Testnet (Goerli). +pub const TBTC_ETHEREUM_TOKEN_ADDRESS: [u8; 32] = [ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x67, 0x98, 0x74, 0xfb, 0xe6, 0xd4, + 0xe7, 0xcc, 0x54, 0xa5, 0x9e, 0x31, 0x5f, 0xf1, 0xeb, 0x26, 0x66, 0x86, 0xa9, 0x37, +]; + +/// A.K.A. b"msg". +pub const MSG_SEED_PREFIX: &[u8] = b"msg"; diff --git a/cross-chain/solana/programs/wormhole-gateway/src/error.rs b/cross-chain/solana/programs/wormhole-gateway/src/error.rs index 09a8b136c..c48f2760f 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/error.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/error.rs @@ -2,12 +2,18 @@ use anchor_lang::prelude::error_code; #[error_code] pub enum WormholeGatewayError { - #[msg("Cannot mint more than the minting limit.")] + #[msg("Cannot mint more than the minting limit")] MintingLimitExceeded = 0x10, - #[msg("Only custodian authority is permitted for this action.")] + #[msg("Only custodian authority is permitted for this action")] IsNotAuthority = 0x20, + #[msg("Not valid pending authority to take authority")] + IsNotPendingAuthority = 0x22, + + #[msg("No pending authority")] + NoPendingAuthorityChange = 0x24, + #[msg("0x0 recipient not allowed")] ZeroRecipient = 0x30, @@ -17,6 +23,21 @@ pub enum WormholeGatewayError { #[msg("Amount must not be 0")] ZeroAmount = 0x50, - #[msg("Amount too low to bridge")] - TruncatedZeroAmount = 0x60, + #[msg("Token Bridge transfer already redeemed")] + TransferAlreadyRedeemed = 0x70, + + #[msg("Token chain and address do not match Ethereum's tBTC")] + InvalidEthereumTbtc = 0x80, + + #[msg("No tBTC transferred")] + NoTbtcTransferred = 0x90, + + #[msg("0x0 receiver not allowed")] + RecipientZeroAddress = 0xa0, + + #[msg("Not enough minted by the gateway to satisfy sending tBTC")] + MintedAmountUnderflow = 0xb0, + + #[msg("Minted amount after deposit exceeds u64")] + MintedAmountOverflow = 0xb2, } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/event.rs b/cross-chain/solana/programs/wormhole-gateway/src/event.rs new file mode 100644 index 000000000..17a57b7f7 --- /dev/null +++ b/cross-chain/solana/programs/wormhole-gateway/src/event.rs @@ -0,0 +1,34 @@ +use anchor_lang::prelude::*; + +#[event] +pub struct WormholeTbtcReceived { + pub receiver: Pubkey, + pub amount: u64, +} + +#[event] +pub struct WormholeTbtcSent { + pub amount: u64, + pub recipient_chain: u16, + pub gateway: [u8; 32], + pub recipient: [u8; 32], + pub arbiter_fee: u64, + pub nonce: u32, +} + +#[event] +pub struct WormholeTbtcDeposited { + pub depositor: Pubkey, + pub amount: u64, +} + +#[event] +pub struct GatewayAddressUpdated { + pub chain: u16, + pub gateway: [u8; 32], +} + +#[event] +pub struct MintingLimitUpdated { + pub minting_limit: u64, +} diff --git a/cross-chain/solana/programs/wormhole-gateway/src/lib.rs b/cross-chain/solana/programs/wormhole-gateway/src/lib.rs index b58d6d681..65d5ca7c9 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/lib.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/lib.rs @@ -1,7 +1,11 @@ #![allow(clippy::result_large_err)] +pub mod constants; + pub mod error; +pub(crate) mod event; + mod processor; pub(crate) use processor::*; @@ -30,6 +34,18 @@ pub mod wormhole_gateway { processor::initialize(ctx, minting_limit) } + pub fn change_authority(ctx: Context) -> Result<()> { + processor::change_authority(ctx) + } + + pub fn cancel_authority_change(ctx: Context) -> Result<()> { + processor::cancel_authority_change(ctx) + } + + pub fn take_authority(ctx: Context) -> Result<()> { + processor::take_authority(ctx) + } + pub fn update_gateway_address( ctx: Context, args: UpdateGatewayAddressArgs, @@ -41,9 +57,9 @@ pub mod wormhole_gateway { processor::update_minting_limit(ctx, new_limit) } - // pub fn receive_tbtc(ctx: Context) -> Result<()> { - // processor::receive_tbtc(ctx) - // } + pub fn receive_tbtc(ctx: Context, message_hash: [u8; 32]) -> Result<()> { + processor::receive_tbtc(ctx, message_hash) + } pub fn send_tbtc_gateway( ctx: Context, diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/cancel_authority_change.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/cancel_authority_change.rs new file mode 100644 index 000000000..a428b64f6 --- /dev/null +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/cancel_authority_change.rs @@ -0,0 +1,22 @@ +use crate::{error::WormholeGatewayError, state::Custodian}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] + +pub struct CancelAuthorityChange<'info> { + #[account( + mut, + seeds = [Custodian::SEED_PREFIX], + bump, + has_one = authority @ WormholeGatewayError::IsNotAuthority, + constraint = custodian.pending_authority.is_some() @ WormholeGatewayError::NoPendingAuthorityChange + )] + custodian: Account<'info, Custodian>, + + authority: Signer<'info>, +} + +pub fn cancel_authority_change(ctx: Context) -> Result<()> { + ctx.accounts.custodian.pending_authority = None; + Ok(()) +} diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/change_authority.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/change_authority.rs new file mode 100644 index 000000000..9d77622f8 --- /dev/null +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/change_authority.rs @@ -0,0 +1,23 @@ +use crate::{error::WormholeGatewayError, state::Custodian}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct ChangeAuthority<'info> { + #[account( + mut, + seeds = [Custodian::SEED_PREFIX], + bump, + has_one = authority @ WormholeGatewayError::IsNotAuthority + )] + custodian: Account<'info, Custodian>, + + authority: Signer<'info>, + + /// CHECK: New authority. + new_authority: AccountInfo<'info>, +} + +pub fn change_authority(ctx: Context) -> Result<()> { + ctx.accounts.custodian.pending_authority = Some(ctx.accounts.new_authority.key()); + Ok(()) +} diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/initialize.rs similarity index 62% rename from cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs rename to cross-chain/solana/programs/wormhole-gateway/src/processor/admin/initialize.rs index f73831546..0b8c07d35 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/initialize.rs @@ -1,23 +1,11 @@ -use crate::state::Custodian; +use crate::{ + constants::{TBTC_ETHEREUM_TOKEN_ADDRESS, TBTC_ETHEREUM_TOKEN_CHAIN}, + state::Custodian, +}; use anchor_lang::prelude::*; use anchor_spl::token; use wormhole_anchor_sdk::token_bridge; -const TBTC_FOREIGN_TOKEN_CHAIN: u8 = 2; - -#[cfg(feature = "mainnet")] -const TBTC_FOREIGN_TOKEN_ADDRESS: [u8; 32] = [ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x18, 0x08, 0x4f, 0xbA, 0x66, 0x6a, - 0x33, 0xd3, 0x75, 0x92, 0xfA, 0x26, 0x33, 0xfD, 0x49, 0xa7, 0x4D, 0xD9, 0x3a, 0x88, -]; - -/// TODO: Fix this to reflect testnet contract address. -#[cfg(feature = "solana-devnet")] -const TBTC_FOREIGN_TOKEN_ADDRESS: [u8; 32] = [ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x18, 0x08, 0x4f, 0xbA, 0x66, 0x6a, - 0x33, 0xd3, 0x75, 0x92, 0xfA, 0x26, 0x33, 0xfD, 0x49, 0xa7, 0x4D, 0xD9, 0x3a, 0x88, -]; - #[derive(Accounts)] pub struct Initialize<'info> { #[account(mut)] @@ -45,10 +33,11 @@ pub struct Initialize<'info> { #[account( seeds = [ token_bridge::WrappedMint::SEED_PREFIX, - &TBTC_FOREIGN_TOKEN_CHAIN.to_be_bytes(), - TBTC_FOREIGN_TOKEN_ADDRESS.as_ref() + &TBTC_ETHEREUM_TOKEN_CHAIN.to_be_bytes(), + TBTC_ETHEREUM_TOKEN_ADDRESS.as_ref() ], - bump + bump, + seeds::program = token_bridge::program::ID )] wrapped_tbtc_mint: Account<'info, token::Mint>, @@ -56,7 +45,7 @@ pub struct Initialize<'info> { init, payer = authority, token::mint = wrapped_tbtc_mint, - token::authority = authority, + token::authority = custodian, seeds = [b"wrapped-token"], bump )] @@ -70,30 +59,22 @@ pub struct Initialize<'info> { )] token_bridge_sender: AccountInfo<'info>, - /// CHECK: This account is needed for the Token Bridge program. This PDA is specifically used to - /// sign for transferring via Token Bridge program with a message. - #[account( - seeds = [token_bridge::SEED_PREFIX_REDEEMER], - bump, - )] - token_bridge_redeemer: AccountInfo<'info>, - system_program: Program<'info, System>, token_program: Program<'info, token::Token>, } pub fn initialize(ctx: Context, minting_limit: u64) -> Result<()> { ctx.accounts.custodian.set_inner(Custodian { - bump: ctx.bumps["config"], + bump: ctx.bumps["custodian"], authority: ctx.accounts.authority.key(), + pending_authority: None, tbtc_mint: ctx.accounts.tbtc_mint.key(), wrapped_tbtc_mint: ctx.accounts.wrapped_tbtc_mint.key(), wrapped_tbtc_token: ctx.accounts.wrapped_tbtc_token.key(), token_bridge_sender: ctx.accounts.token_bridge_sender.key(), token_bridge_sender_bump: ctx.bumps["token_bridge_sender"], - token_bridge_redeemer: ctx.accounts.token_bridge_sender.key(), - token_bridge_redeemer_bump: ctx.bumps["token_bridge_redeemer"], minting_limit, + minted_amount: 0, }); Ok(()) diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/mod.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/mod.rs new file mode 100644 index 000000000..a4c9400e1 --- /dev/null +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/mod.rs @@ -0,0 +1,17 @@ +mod cancel_authority_change; +pub use cancel_authority_change::*; + +mod change_authority; +pub use change_authority::*; + +mod initialize; +pub use initialize::*; + +mod take_authority; +pub use take_authority::*; + +mod update_gateway_address; +pub use update_gateway_address::*; + +mod update_minting_limit; +pub use update_minting_limit::*; diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/take_authority.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/take_authority.rs new file mode 100644 index 000000000..e239b4dca --- /dev/null +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/take_authority.rs @@ -0,0 +1,38 @@ +use crate::{error::WormholeGatewayError, state::Custodian}; +use anchor_lang::prelude::*; + +#[derive(Accounts)] +pub struct TakeAuthority<'info> { + #[account( + mut, + seeds = [Custodian::SEED_PREFIX], + bump, + )] + custodian: Account<'info, Custodian>, + + pending_authority: Signer<'info>, +} + +impl<'info> TakeAuthority<'info> { + fn constraints(ctx: &Context) -> Result<()> { + match ctx.accounts.custodian.pending_authority { + Some(pending_authority) => { + require_keys_eq!( + pending_authority, + ctx.accounts.pending_authority.key(), + WormholeGatewayError::IsNotPendingAuthority + ); + + Ok(()) + } + None => err!(WormholeGatewayError::NoPendingAuthorityChange), + } + } +} + +#[access_control(TakeAuthority::constraints(&ctx))] +pub fn take_authority(ctx: Context) -> Result<()> { + ctx.accounts.custodian.authority = ctx.accounts.pending_authority.key(); + ctx.accounts.custodian.pending_authority = None; + Ok(()) +} diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/update_gateway_address.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/update_gateway_address.rs similarity index 88% rename from cross-chain/solana/programs/wormhole-gateway/src/processor/update_gateway_address.rs rename to cross-chain/solana/programs/wormhole-gateway/src/processor/admin/update_gateway_address.rs index 6fcfbfed2..7b62dd521 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/update_gateway_address.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/update_gateway_address.rs @@ -39,12 +39,17 @@ pub fn update_gateway_address( ctx: Context, args: UpdateGatewayAddressArgs, ) -> Result<()> { - let UpdateGatewayAddressArgs { address, .. } = args; + let UpdateGatewayAddressArgs { chain, address } = args; ctx.accounts.gateway_info.set_inner(GatewayInfo { bump: ctx.bumps["gateway_info"], address, }); + emit!(crate::event::GatewayAddressUpdated { + chain, + gateway: address + }); + Ok(()) } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/update_minting_limit.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/update_minting_limit.rs similarity index 86% rename from cross-chain/solana/programs/wormhole-gateway/src/processor/update_minting_limit.rs rename to cross-chain/solana/programs/wormhole-gateway/src/processor/admin/update_minting_limit.rs index b52b72df4..c86bc4194 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/update_minting_limit.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/admin/update_minting_limit.rs @@ -16,5 +16,10 @@ pub struct UpdateMintingLimit<'info> { pub fn update_minting_limit(ctx: Context, new_limit: u64) -> Result<()> { ctx.accounts.custodian.minting_limit = new_limit; + + emit!(crate::event::MintingLimitUpdated { + minting_limit: new_limit + }); + Ok(()) } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/deposit_wormhole_tbtc.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/deposit_wormhole_tbtc.rs index 28ccdc59a..a181c7889 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/deposit_wormhole_tbtc.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/deposit_wormhole_tbtc.rs @@ -7,6 +7,7 @@ use anchor_spl::token; pub struct DepositWormholeTbtc<'info> { /// NOTE: This account also acts as a minter for the TBTC program. #[account( + mut, seeds = [Custodian::SEED_PREFIX], bump = custodian.bump, has_one = wrapped_tbtc_token, @@ -51,7 +52,7 @@ pub struct DepositWormholeTbtc<'info> { tbtc_config: UncheckedAccount<'info>, /// CHECK: TBTC program requires this account. - minter_info: UncheckedAccount<'info>, + tbtc_minter_info: UncheckedAccount<'info>, token_program: Program<'info, token::Token>, tbtc_program: Program<'info, tbtc::Tbtc>, @@ -59,9 +60,15 @@ pub struct DepositWormholeTbtc<'info> { impl<'info> DepositWormholeTbtc<'info> { fn constraints(ctx: &Context, amount: u64) -> Result<()> { - require_gt!( + let updated_minted_amount = ctx + .accounts + .custodian + .minted_amount + .checked_add(amount) + .ok_or(WormholeGatewayError::MintedAmountOverflow)?; + require_gte!( ctx.accounts.custodian.minting_limit, - ctx.accounts.tbtc_mint.supply.saturating_add(amount), + updated_minted_amount, WormholeGatewayError::MintingLimitExceeded ); @@ -84,6 +91,9 @@ pub fn deposit_wormhole_tbtc(ctx: Context, amount: u64) -> amount, )?; + // Account for minted amount. + ctx.accounts.custodian.minted_amount += amount; + let custodian = &ctx.accounts.custodian; // Now mint. @@ -93,7 +103,7 @@ pub fn deposit_wormhole_tbtc(ctx: Context, amount: u64) -> tbtc::cpi::accounts::Mint { mint: ctx.accounts.tbtc_mint.to_account_info(), config: ctx.accounts.tbtc_config.to_account_info(), - minter_info: ctx.accounts.minter_info.to_account_info(), + minter_info: ctx.accounts.tbtc_minter_info.to_account_info(), minter: custodian.to_account_info(), recipient_token: ctx.accounts.recipient_token.to_account_info(), token_program: ctx.accounts.token_program.to_account_info(), diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/mod.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/mod.rs index 8948c9b82..2744a62ea 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/mod.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/mod.rs @@ -1,17 +1,11 @@ +mod admin; +pub use admin::*; + mod deposit_wormhole_tbtc; pub use deposit_wormhole_tbtc::*; -mod initialize; -pub use initialize::*; - -// mod receive_tbtc; -// pub use receive_tbtc::*; +mod receive_tbtc; +pub use receive_tbtc::*; mod send_tbtc; pub use send_tbtc::*; - -mod update_gateway_address; -pub use update_gateway_address::*; - -mod update_minting_limit; -pub use update_minting_limit::*; diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/receive_tbtc.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/receive_tbtc.rs index b3cc68d83..e5df5ab85 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/receive_tbtc.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/receive_tbtc.rs @@ -1,77 +1,251 @@ -use crate::state::Custodian; +use crate::{ + constants::{TBTC_ETHEREUM_TOKEN_ADDRESS, TBTC_ETHEREUM_TOKEN_CHAIN}, + error::WormholeGatewayError, + state::Custodian, +}; use anchor_lang::prelude::*; -use anchor_spl::token; +use anchor_spl::{associated_token, token}; +use wormhole_anchor_sdk::{ + token_bridge::{self, program::TokenBridge}, + wormhole::{self as core_bridge, program::Wormhole as CoreBridge}, +}; #[derive(Accounts)] +#[instruction(message_hash: [u8; 32])] pub struct ReceiveTbtc<'info> { #[account(mut)] payer: Signer<'info>, - /// NOTE: This account also acts as a minter for the TBTC program. #[account( + mut, seeds = [Custodian::SEED_PREFIX], bump = custodian.bump, has_one = wrapped_tbtc_token, has_one = wrapped_tbtc_mint, - has_one = tbtc_mint, + has_one = tbtc_mint )] custodian: Account<'info, Custodian>, - // TODO: posted_vaa + #[account( + seeds = [core_bridge::SEED_PREFIX_POSTED_VAA, &message_hash], + bump, + seeds::program = core_bridge_program + )] + posted_vaa: Box>>, + + /// CHECK: This claim account is created by the Token Bridge program when it redeems its inbound + /// transfer. By checking whether this account exists is a short-circuit way of bailing out + /// early if this transfer has already been redeemed (as opposed to letting the Token Bridge + /// instruction fail). #[account(mut)] - tbtc_mint: Account<'info, token::Mint>, + token_bridge_claim: AccountInfo<'info>, - /// CHECK: This account is needed fot the TBTC program. - tbtc_config: UncheckedAccount<'info>, + /// Custody account. + #[account(mut)] + wrapped_tbtc_token: Box>, - /// CHECK: This account is needed fot the TBTC program. - minter_info: UncheckedAccount<'info>, + /// This mint is owned by the Wormhole Token Bridge program. This PDA address is stored in the + /// custodian account. + #[account(mut)] + wrapped_tbtc_mint: Box>, + + #[account(mut)] + tbtc_mint: Box>, - // Use the associated token account for the recipient. + /// Token account for minted tBTC. + /// + /// NOTE: Because the recipient is encoded in the transfer message payload, we can check the + /// authority from the deserialized VAA. But we should still check whether the authority is the + /// zero address in access control. #[account( - associated_token::mint = tbtc_mint, - associated_token::authority = recipient, + mut, + token::mint = tbtc_mint, + token::authority = recipient, )] - pub recipient_account: Account<'info, token::TokenAccount>, + recipient_token: Box>, - /// CHECK: the recipient doesn't need to sign the mint, - /// and it doesn't conform to any specific rules. - /// Validating the recipient is the minter's responsibility. + /// CHECK: This account needs to be in the context in case an associated token account needs to + /// be created for him. + #[account(address = Pubkey::from(*posted_vaa.data().message()))] recipient: AccountInfo<'info>, + + /// CHECK: This account exists just in case the minting limit is breached after this transfer. + /// The gateway will create an associated token account for the recipient if it doesn't exist. + /// + /// NOTE: When the minting limit increases, the recipient can use this token account to mint + /// tBTC using the deposit_wormhole_tbtc instruction. + #[account( + mut, + address = associated_token::get_associated_token_address( + &recipient.key(), + &wrapped_tbtc_mint.key() + ), + )] + recipient_wrapped_token: AccountInfo<'info>, + + /// CHECK: This account is needed for the TBTC program. + tbtc_config: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the TBTC program. + tbtc_minter_info: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_config: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_registered_emitter: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_wrapped_asset: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + token_bridge_mint_authority: UncheckedAccount<'info>, + + /// CHECK: This account is needed for the Token Bridge program. + rent: UncheckedAccount<'info>, + + tbtc_program: Program<'info, tbtc::Tbtc>, + token_bridge_program: Program<'info, TokenBridge>, + core_bridge_program: Program<'info, CoreBridge>, + associated_token_program: Program<'info, associated_token::AssociatedToken>, + token_program: Program<'info, token::Token>, + system_program: Program<'info, System>, } -pub fn receive_tbtc(ctx: Context) -> Result<()> { - // get balance delta +impl<'info> ReceiveTbtc<'info> { + fn constraints(ctx: &Context) -> Result<()> { + // Check if transfer has already been claimed. + require!( + ctx.accounts.token_bridge_claim.data_is_empty(), + WormholeGatewayError::TransferAlreadyRedeemed + ); - let amount = _; + // Token info must match Ethereum's canonical tBTC token info. + let transfer = ctx.accounts.posted_vaa.data(); + require!( + transfer.token_chain() == TBTC_ETHEREUM_TOKEN_CHAIN + && *transfer.token_address() == TBTC_ETHEREUM_TOKEN_ADDRESS, + WormholeGatewayError::InvalidEthereumTbtc + ); - let minted_amount = ctx.accounts.wormhole_gateway.minted_amount; - let minting_limit = ctx.accounts.wormhole_gateway.minting_limit; + // There must be an encoded amount. + require_gt!( + transfer.amount(), + 0, + WormholeGatewayError::NoTbtcTransferred + ); - if (minted_amount + amount > minting_limit) { - // transfer bridge token - } else { - ctx.accounts.wormhole_gateway.minted_amount += amount; - - let seed_prefix = Config::SEED_PREFIX; - let key_seed = ctx.accounts.wormhole_gateway.key(); - let gateway_bump = ctx.accounts.wormhole_gateway.self_bump; - - let signer: &[&[&[u8]]] = &[&[seed_prefix, key_seed.as_ref(), &[gateway_bump]]]; - - let mint_cpi_ctx = CpiContext::new_with_signer( - ctx.accounts.tbtc.to_account_info(), - tbtc::Mint { - tbtc_mint: ctx.accounts.tbtc_mint.to_account_info(), - tbtc: ctx.accounts.tbtc.to_account_info(), - minter_info: ctx.accounts.minter_info.to_account_info(), - minter: ctx.accounts.wormhole_gateway.to_account_info(), - recipient_account: ctx.accounts.recipient_account.to_account_info(), - recipient: ctx.accounts.recipient.to_account_info(), - payer: ctx.accounts.payer.to_account_info(), - }, - signer, + // Recipient must not be zero address. + require_keys_neq!( + ctx.accounts.recipient.key(), + Pubkey::default(), + WormholeGatewayError::RecipientZeroAddress ); - tbtc::mint(mint_cpi_ctx, amount) + + Ok(()) + } +} + +#[access_control(ReceiveTbtc::constraints(&ctx))] +pub fn receive_tbtc(ctx: Context, _message_hash: [u8; 32]) -> Result<()> { + let wrapped_tbtc_token = &ctx.accounts.wrapped_tbtc_token; + let wrapped_tbtc_mint = &ctx.accounts.wrapped_tbtc_mint; + + // Redeem the token transfer. + token_bridge::complete_transfer_wrapped_with_payload(CpiContext::new_with_signer( + ctx.accounts.token_bridge_program.to_account_info(), + token_bridge::CompleteTransferWrappedWithPayload { + payer: ctx.accounts.payer.to_account_info(), + config: ctx.accounts.token_bridge_config.to_account_info(), + vaa: ctx.accounts.posted_vaa.to_account_info(), + claim: ctx.accounts.token_bridge_claim.to_account_info(), + foreign_endpoint: ctx + .accounts + .token_bridge_registered_emitter + .to_account_info(), + to: wrapped_tbtc_token.to_account_info(), + redeemer: ctx.accounts.custodian.to_account_info(), + wrapped_mint: wrapped_tbtc_mint.to_account_info(), + wrapped_metadata: ctx.accounts.token_bridge_wrapped_asset.to_account_info(), + mint_authority: ctx.accounts.token_bridge_mint_authority.to_account_info(), + rent: ctx.accounts.rent.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + wormhole_program: ctx.accounts.core_bridge_program.to_account_info(), + }, + &[&[ + token_bridge::SEED_PREFIX_REDEEMER, + &[ctx.accounts.custodian.bump], + ]], + ))?; + + // Because we are working with wrapped token amounts, we can take the amount as-is and determine + // whether to mint or transfer based on the minting limit. + let amount = ctx.accounts.posted_vaa.data().amount(); + let recipient = &ctx.accounts.recipient; + + emit!(crate::event::WormholeTbtcReceived { + receiver: recipient.key(), + amount + }); + + let updated_minted_amount = ctx.accounts.custodian.minted_amount.saturating_add(amount); + let custodian_seeds = &[Custodian::SEED_PREFIX, &[ctx.accounts.custodian.bump]]; + + // We send Wormhole tBTC OR mint canonical tBTC. We do not want to send dust. Sending Wormhole + // tBTC is an exceptional situation and we want to keep it simple. + if updated_minted_amount > ctx.accounts.custodian.minting_limit { + msg!("Insufficient minted amount. Sending Wormhole tBTC instead"); + + let ata = &ctx.accounts.recipient_wrapped_token; + + // Create associated token account for recipient if it doesn't exist already. + if ata.data_is_empty() { + associated_token::create(CpiContext::new( + ctx.accounts.associated_token_program.to_account_info(), + associated_token::Create { + payer: ctx.accounts.payer.to_account_info(), + associated_token: ata.to_account_info(), + authority: recipient.to_account_info(), + mint: wrapped_tbtc_mint.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + }, + ))?; + } + + // Finally transfer. + token::transfer( + CpiContext::new_with_signer( + ctx.accounts.token_program.to_account_info(), + token::Transfer { + from: wrapped_tbtc_token.to_account_info(), + to: ata.to_account_info(), + authority: ctx.accounts.custodian.to_account_info(), + }, + &[custodian_seeds], + ), + amount, + ) + } else { + // The function is non-reentrant given bridge.completeTransferWithPayload + // call that does not allow to use the same VAA again. + ctx.accounts.custodian.minted_amount = updated_minted_amount; + + tbtc::cpi::mint( + CpiContext::new_with_signer( + ctx.accounts.tbtc_program.to_account_info(), + tbtc::cpi::accounts::Mint { + mint: ctx.accounts.tbtc_mint.to_account_info(), + config: ctx.accounts.tbtc_config.to_account_info(), + minter_info: ctx.accounts.tbtc_minter_info.to_account_info(), + minter: ctx.accounts.custodian.to_account_info(), + recipient_token: ctx.accounts.recipient_token.to_account_info(), + token_program: ctx.accounts.token_program.to_account_info(), + }, + &[custodian_seeds], + ), + amount, + ) } } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/gateway.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/gateway.rs index babd8a490..0802a9e7c 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/gateway.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/gateway.rs @@ -1,4 +1,7 @@ -use crate::state::{Custodian, GatewayInfo}; +use crate::{ + constants::MSG_SEED_PREFIX, + state::{Custodian, GatewayInfo}, +}; use anchor_lang::prelude::*; use anchor_spl::token; use wormhole_anchor_sdk::{ @@ -10,6 +13,7 @@ use wormhole_anchor_sdk::{ #[instruction(args: SendTbtcGatewayArgs)] pub struct SendTbtcGateway<'info> { #[account( + mut, seeds = [Custodian::SEED_PREFIX], bump = custodian.bump, has_one = wrapped_tbtc_token, @@ -26,9 +30,11 @@ pub struct SendTbtcGateway<'info> { gateway_info: Account<'info, GatewayInfo>, /// Custody account. + #[account(mut)] wrapped_tbtc_token: Box>, /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] wrapped_tbtc_mint: UncheckedAccount<'info>, #[account(mut)] @@ -55,12 +61,15 @@ pub struct SendTbtcGateway<'info> { /// CHECK: This account is needed for the Token Bridge program. #[account(mut)] - core_bridge: UncheckedAccount<'info>, + core_bridge_data: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. #[account( mut, - seeds = [b"msg", &core_emitter_sequence.value().to_le_bytes()], + seeds = [ + MSG_SEED_PREFIX, + &core_emitter_sequence.value().to_le_bytes() + ], bump, )] core_message: AccountInfo<'info>, @@ -120,16 +129,17 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg } = args; let sender = &ctx.accounts.sender; - let custodian = &ctx.accounts.custodian; let wrapped_tbtc_token = &ctx.accounts.wrapped_tbtc_token; let token_bridge_transfer_authority = &ctx.accounts.token_bridge_transfer_authority; let token_program = &ctx.accounts.token_program; + let gateway = ctx.accounts.gateway_info.address; + // Prepare for wrapped tBTC transfer (this method also truncates the amount to prevent having to // handle dust since tBTC has >8 decimals). - let amount = super::burn_and_prepare_transfer( + super::burn_and_prepare_transfer( super::PrepareTransfer { - custodian, + custodian: &mut ctx.accounts.custodian, tbtc_mint: &ctx.accounts.tbtc_mint, sender_token: &ctx.accounts.sender_token, sender, @@ -138,8 +148,15 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg token_program, }, amount, + recipient_chain, + Some(gateway), + recipient, + None, // arbiter_fee + nonce, )?; + let custodian = &ctx.accounts.custodian; + // Finally transfer wrapped tBTC with the recipient encoded as this transfer's message. token_bridge::transfer_wrapped_with_payload( CpiContext::new_with_signer( @@ -152,7 +169,7 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg wrapped_mint: ctx.accounts.wrapped_tbtc_mint.to_account_info(), wrapped_metadata: ctx.accounts.token_bridge_wrapped_asset.to_account_info(), authority_signer: token_bridge_transfer_authority.to_account_info(), - wormhole_bridge: ctx.accounts.core_bridge.to_account_info(), + wormhole_bridge: ctx.accounts.core_bridge_data.to_account_info(), wormhole_message: ctx.accounts.core_message.to_account_info(), wormhole_emitter: ctx.accounts.token_bridge_core_emitter.to_account_info(), wormhole_sequence: ctx.accounts.core_emitter_sequence.to_account_info(), @@ -171,7 +188,7 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg &[ctx.accounts.custodian.token_bridge_sender_bump], ], &[ - b"msg", + MSG_SEED_PREFIX, &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), &[ctx.bumps["core_message"]], ], @@ -179,7 +196,7 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg ), nonce, amount, - ctx.accounts.gateway_info.address, + gateway, recipient_chain, recipient.to_vec(), &crate::ID, diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/mod.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/mod.rs index a1b89550f..aa79c836a 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/mod.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/mod.rs @@ -28,7 +28,7 @@ pub fn validate_send( } pub struct PrepareTransfer<'ctx, 'info> { - custodian: &'ctx Account<'info, Custodian>, + custodian: &'ctx mut Account<'info, Custodian>, tbtc_mint: &'ctx Account<'info, token::Mint>, sender_token: &'ctx Account<'info, token::TokenAccount>, sender: &'ctx Signer<'info>, @@ -37,7 +37,15 @@ pub struct PrepareTransfer<'ctx, 'info> { token_program: &'ctx Program<'info, token::Token>, } -pub fn burn_and_prepare_transfer(prepare_transfer: PrepareTransfer, amount: u64) -> Result { +pub fn burn_and_prepare_transfer( + prepare_transfer: PrepareTransfer, + amount: u64, + recipient_chain: u16, + gateway: Option<[u8; 32]>, + recipient: [u8; 32], + arbiter_fee: Option, + nonce: u32, +) -> Result<()> { let PrepareTransfer { custodian, tbtc_mint, @@ -48,8 +56,11 @@ pub fn burn_and_prepare_transfer(prepare_transfer: PrepareTransfer, amount: u64) token_program, } = prepare_transfer; - let truncated = 10 * (amount / 10); - require_gt!(truncated, 0, WormholeGatewayError::TruncatedZeroAmount); + // Account for burning tBTC. + custodian.minted_amount = custodian + .minted_amount + .checked_sub(amount) + .ok_or(WormholeGatewayError::MintedAmountUnderflow)?; // Burn TBTC mint. token::burn( @@ -64,6 +75,15 @@ pub fn burn_and_prepare_transfer(prepare_transfer: PrepareTransfer, amount: u64) amount, )?; + emit!(crate::event::WormholeTbtcSent { + amount, + recipient_chain, + gateway: gateway.unwrap_or_default(), + recipient, + arbiter_fee: arbiter_fee.unwrap_or_default(), + nonce + }); + // Delegate authority to Token Bridge's transfer authority. token::approve( CpiContext::new_with_signer( @@ -75,8 +95,6 @@ pub fn burn_and_prepare_transfer(prepare_transfer: PrepareTransfer, amount: u64) }, &[&[Custodian::SEED_PREFIX, &[custodian.bump]]], ), - truncated, - )?; - - Ok(truncated) + amount, + ) } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/wrapped.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/wrapped.rs index 37ba459c5..99f26a1b0 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/wrapped.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/wrapped.rs @@ -1,5 +1,5 @@ -use crate::state::Custodian; -use anchor_lang::{prelude::*, solana_program}; +use crate::{constants::MSG_SEED_PREFIX, state::Custodian}; +use anchor_lang::prelude::*; use anchor_spl::token; use wormhole_anchor_sdk::{ token_bridge::{self, program::TokenBridge}, @@ -10,6 +10,7 @@ use wormhole_anchor_sdk::{ #[instruction(args: SendTbtcWrappedArgs)] pub struct SendTbtcWrapped<'info> { #[account( + mut, seeds = [Custodian::SEED_PREFIX], bump = custodian.bump, has_one = wrapped_tbtc_token, @@ -19,9 +20,11 @@ pub struct SendTbtcWrapped<'info> { custodian: Account<'info, Custodian>, /// Custody account. + #[account(mut)] wrapped_tbtc_token: Box>, /// CHECK: This account is needed for the Token Bridge program. + #[account(mut)] wrapped_tbtc_mint: UncheckedAccount<'info>, #[account(mut)] @@ -48,13 +51,16 @@ pub struct SendTbtcWrapped<'info> { /// CHECK: This account is needed for the Token Bridge program. #[account(mut)] - core_bridge: UncheckedAccount<'info>, + core_bridge_data: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. #[account( mut, - seeds = [b"msg", &core_emitter_sequence.value().to_le_bytes()], - bump, + seeds = [ + MSG_SEED_PREFIX, + &core_emitter_sequence.value().to_le_bytes() + ], + bump )] core_message: AccountInfo<'info>, @@ -111,15 +117,14 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg } = args; let sender = &ctx.accounts.sender; - let custodian = &ctx.accounts.custodian; let wrapped_tbtc_token = &ctx.accounts.wrapped_tbtc_token; let token_bridge_transfer_authority = &ctx.accounts.token_bridge_transfer_authority; let token_program = &ctx.accounts.token_program; // Prepare for wrapped tBTC transfer. - let amount = super::burn_and_prepare_transfer( + super::burn_and_prepare_transfer( super::PrepareTransfer { - custodian, + custodian: &mut ctx.accounts.custodian, tbtc_mint: &ctx.accounts.tbtc_mint, sender_token: &ctx.accounts.sender_token, sender, @@ -128,52 +133,51 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg token_program, }, amount, + recipient_chain, + None, // gateway + recipient, + Some(arbiter_fee), + nonce, )?; - // Because the wormhole-anchor-sdk does not support relayable transfers (i.e. payload ID == 1), - // we need to construct the instruction from scratch and invoke it. - let ix = solana_program::instruction::Instruction { - program_id: ctx.accounts.token_bridge_program.key(), - accounts: vec![ - AccountMeta::new(sender.key(), true), - AccountMeta::new_readonly(ctx.accounts.token_bridge_config.key(), false), - AccountMeta::new(wrapped_tbtc_token.key(), false), - AccountMeta::new_readonly(custodian.key(), false), - AccountMeta::new(ctx.accounts.wrapped_tbtc_mint.key(), false), - AccountMeta::new_readonly(ctx.accounts.token_bridge_wrapped_asset.key(), false), - AccountMeta::new_readonly(token_bridge_transfer_authority.key(), false), - AccountMeta::new(ctx.accounts.core_bridge.key(), false), - AccountMeta::new(ctx.accounts.core_message.key(), true), - AccountMeta::new_readonly(ctx.accounts.token_bridge_core_emitter.key(), false), - AccountMeta::new(ctx.accounts.core_emitter_sequence.key(), false), - AccountMeta::new(ctx.accounts.core_fee_collector.key(), false), - AccountMeta::new_readonly(ctx.accounts.clock.key(), false), - AccountMeta::new_readonly(ctx.accounts.rent.key(), false), - AccountMeta::new_readonly(ctx.accounts.system_program.key(), false), - AccountMeta::new_readonly(ctx.accounts.core_bridge_program.key(), false), - AccountMeta::new_readonly(token_program.key(), false), - ], - data: token_bridge::Instruction::TransferWrapped { - batch_id: nonce, - amount, - fee: arbiter_fee, - recipient_address: recipient, - recipient_chain, - } - .try_to_vec()?, - }; - - solana_program::program::invoke_signed( - &ix, - &ctx.accounts.to_account_infos(), - &[ - &[Custodian::SEED_PREFIX, &[custodian.bump]], + let custodian = &ctx.accounts.custodian; + + // Finally transfer wrapped tBTC to the recipient. + token_bridge::transfer_wrapped( + CpiContext::new_with_signer( + ctx.accounts.token_bridge_program.to_account_info(), + token_bridge::TransferWrapped { + payer: sender.to_account_info(), + config: ctx.accounts.token_bridge_config.to_account_info(), + from: wrapped_tbtc_token.to_account_info(), + from_owner: custodian.to_account_info(), + wrapped_mint: ctx.accounts.wrapped_tbtc_mint.to_account_info(), + wrapped_metadata: ctx.accounts.token_bridge_wrapped_asset.to_account_info(), + authority_signer: token_bridge_transfer_authority.to_account_info(), + wormhole_bridge: ctx.accounts.core_bridge_data.to_account_info(), + wormhole_message: ctx.accounts.core_message.to_account_info(), + wormhole_emitter: ctx.accounts.token_bridge_core_emitter.to_account_info(), + wormhole_sequence: ctx.accounts.core_emitter_sequence.to_account_info(), + wormhole_fee_collector: ctx.accounts.core_fee_collector.to_account_info(), + clock: ctx.accounts.clock.to_account_info(), + rent: ctx.accounts.rent.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + token_program: token_program.to_account_info(), + wormhole_program: ctx.accounts.core_bridge_program.to_account_info(), + }, &[ - b"msg", - &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), - &[ctx.bumps["core_message"]], + &[Custodian::SEED_PREFIX, &[custodian.bump]], + &[ + MSG_SEED_PREFIX, + &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), + &[ctx.bumps["core_message"]], + ], ], - ], + ), + nonce, + amount, + arbiter_fee, + recipient, + recipient_chain, ) - .map_err(Into::into) } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs index 72882128d..15a545f43 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs @@ -1,22 +1,25 @@ use anchor_lang::prelude::*; +use wormhole_anchor_sdk::token_bridge; #[account] #[derive(Debug, InitSpace)] pub struct Custodian { pub bump: u8, pub authority: Pubkey, + pub pending_authority: Option, pub tbtc_mint: Pubkey, pub wrapped_tbtc_mint: Pubkey, pub wrapped_tbtc_token: Pubkey, pub token_bridge_sender: Pubkey, pub token_bridge_sender_bump: u8, - pub token_bridge_redeemer: Pubkey, - pub token_bridge_redeemer_bump: u8, - pub minting_limit: u64, + pub minted_amount: u64, } impl Custodian { - pub const SEED_PREFIX: &'static [u8] = b"custodian"; + /// Due to the Token Bridge requiring the redeemer PDA be the owner of the token account for + /// completing transfers with payload, we are conveniently having the Custodian's PDA address + /// derived as this redeemer. + pub const SEED_PREFIX: &'static [u8] = token_bridge::SEED_PREFIX_REDEEMER; } From 79885a19f219a2f5f4512fb4f76960f52fbb644b Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Mon, 7 Aug 2023 09:22:13 -0500 Subject: [PATCH 2/2] solana: fix tests to reflect program changes Co-authored-by: gator-boi --- cross-chain/solana/package-lock.json | 100 ++ cross-chain/solana/package.json | 1 + cross-chain/solana/tests/01__tbtc.ts | 1493 +++++++++-------- .../solana/tests/02__wormholeGateway.ts | 1294 +++++++++++++- ...core_bridge.json => core_bridge_data.json} | 0 .../tests/accounts/core_fee_collector.json | 2 +- cross-chain/solana/tests/helpers/consts.ts | 50 + cross-chain/solana/tests/helpers/index.ts | 2 + cross-chain/solana/tests/helpers/tbtc.ts | 510 ++++++ cross-chain/solana/tests/helpers/utils.ts | 327 +++- .../solana/tests/helpers/wormholeGateway.ts | 809 +++++++++ 11 files changed, 3758 insertions(+), 830 deletions(-) rename cross-chain/solana/tests/accounts/{core_bridge.json => core_bridge_data.json} (100%) create mode 100644 cross-chain/solana/tests/helpers/consts.ts create mode 100644 cross-chain/solana/tests/helpers/index.ts create mode 100644 cross-chain/solana/tests/helpers/tbtc.ts create mode 100644 cross-chain/solana/tests/helpers/wormholeGateway.ts diff --git a/cross-chain/solana/package-lock.json b/cross-chain/solana/package-lock.json index ddcd6cb2d..caf320a81 100644 --- a/cross-chain/solana/package-lock.json +++ b/cross-chain/solana/package-lock.json @@ -9,6 +9,7 @@ }, "devDependencies": { "@certusone/wormhole-sdk": "^0.9.22", + "@metaplex-foundation/mpl-token-metadata": "^2.13.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.77.3", "@types/bn.js": "^5.1.0", @@ -1602,6 +1603,99 @@ "rlp": "^2.2.3" } }, + "node_modules/@metaplex-foundation/beet": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet/-/beet-0.7.1.tgz", + "integrity": "sha512-hNCEnS2WyCiYyko82rwuISsBY3KYpe828ubsd2ckeqZr7tl0WVLivGkoyA/qdiaaHEBGdGl71OpfWa2rqL3DiA==", + "dev": true, + "dependencies": { + "ansicolors": "^0.3.2", + "bn.js": "^5.2.0", + "debug": "^4.3.3" + } + }, + "node_modules/@metaplex-foundation/beet-solana": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/beet-solana/-/beet-solana-0.4.0.tgz", + "integrity": "sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ==", + "dev": true, + "dependencies": { + "@metaplex-foundation/beet": ">=0.1.0", + "@solana/web3.js": "^1.56.2", + "bs58": "^5.0.0", + "debug": "^4.3.4" + } + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/base-x": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", + "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==", + "dev": true + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "dev": true, + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/@metaplex-foundation/beet-solana/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@metaplex-foundation/cusper": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz", + "integrity": "sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA==", + "dev": true + }, + "node_modules/@metaplex-foundation/mpl-token-metadata": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.13.0.tgz", + "integrity": "sha512-Fl/8I0L9rv4bKTV/RAl5YIbJe9SnQPInKvLz+xR1fEc4/VQkuCn3RPgypfUMEKWmCznzaw4sApDxy6CFS4qmJw==", + "dev": true, + "dependencies": { + "@metaplex-foundation/beet": "^0.7.1", + "@metaplex-foundation/beet-solana": "^0.4.0", + "@metaplex-foundation/cusper": "^0.0.2", + "@solana/spl-token": "^0.3.6", + "@solana/web3.js": "^1.66.2", + "bn.js": "^5.2.0", + "debug": "^4.3.4" + } + }, + "node_modules/@metaplex-foundation/mpl-token-metadata/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/@mysten/bcs": { "version": "0.7.1", "dev": true, @@ -2222,6 +2316,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/ansicolors": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", + "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", + "dev": true + }, "node_modules/anymatch": { "version": "3.1.3", "dev": true, diff --git a/cross-chain/solana/package.json b/cross-chain/solana/package.json index a18adfd59..ea15b6a98 100644 --- a/cross-chain/solana/package.json +++ b/cross-chain/solana/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@certusone/wormhole-sdk": "^0.9.22", + "@metaplex-foundation/mpl-token-metadata": "^2.13.0", "@solana/spl-token": "^0.3.8", "@solana/web3.js": "^1.77.3", "@types/bn.js": "^5.1.0", diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 0a70e3d93..7d3591ec0 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -1,363 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; -import { Program, AnchorError } from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; import * as spl from "@solana/spl-token"; -import * as web3 from '@solana/web3.js'; +import { assert, expect } from "chai"; import { Tbtc } from "../target/types/tbtc"; -import { expect } from 'chai'; -import { ASSOCIATED_PROGRAM_ID } from "@coral-xyz/anchor/dist/cjs/utils/token"; -import { transferLamports } from "./helpers/utils"; - -function maybeAuthorityAnd( - signer, - signers -) { - return signers.concat(signer instanceof (anchor.Wallet as any) ? [] : [signer]); -} - -async function setup( - program: Program, - authority -) { - const [config,] = getConfigPDA(program); - const [tbtcMintPDA, _] = getTokenPDA(program); - - await program.methods - .initialize() - .accounts({ - mint: tbtcMintPDA, - config, - authority: authority.publicKey - }) - .rpc(); -} - -async function checkState( - program: Program, - expectedAuthority, - expectedMinters, - expectedGuardians, - expectedTokensSupply -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - - expect(configState.authority).to.eql(expectedAuthority.publicKey); - expect(configState.numMinters).to.equal(expectedMinters); - expect(configState.numGuardians).to.equal(expectedGuardians); - - let tbtcMint = configState.mint; - - let mintState = await spl.getMint(program.provider.connection, tbtcMint); - - expect(mintState.supply).to.equal(BigInt(expectedTokensSupply)); -} - -async function changeAuthority( - program: Program, - authority, - newAuthority, -) { - const [config,] = getConfigPDA(program); - await program.methods - .changeAuthority() - .accounts({ - config, - authority: authority.publicKey, - newAuthority: newAuthority.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function takeAuthority( - program: Program, - newAuthority, -) { - const [config,] = getConfigPDA(program); - await program.methods - .takeAuthority() - .accounts({ - config, - pendingAuthority: newAuthority.publicKey, - }) - .signers(maybeAuthorityAnd(newAuthority, [])) - .rpc(); -} - -async function cancelAuthorityChange( - program: Program, - authority, -) { - const [config,] = getConfigPDA(program); - await program.methods - .cancelAuthorityChange() - .accounts({ - config, - authority: authority.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function checkPendingAuthority( - program: Program, - pendingAuthority, -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - expect(configState.pendingAuthority).to.eql(pendingAuthority.publicKey); -} - -async function checkNoPendingAuthority( - program: Program, -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - expect(configState.pendingAuthority).to.equal(null); -} - -async function checkPaused( - program: Program, - paused: boolean -) { - const [config,] = getConfigPDA(program); - let configState = await program.account.config.fetch(config); - expect(configState.paused).to.equal(paused); -} - - -function getConfigPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('config'), - ], - program.programId - ); -} - -function getTokenPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('tbtc-mint'), - ], - program.programId - ); -} - -function getMinterPDA( - program: Program, - minter -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('minter-info'), - minter.publicKey.toBuffer(), - ], - program.programId - ); -} - -async function addMinter( - program: Program, - authority, - minter, - payer -): Promise { - const [config,] = getConfigPDA(program); - const [minterInfoPDA, _] = getMinterPDA(program, minter); - await program.methods - .addMinter() - .accounts({ - config, - authority: authority.publicKey, - minter: minter.publicKey, - minterInfo: minterInfoPDA, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); - return minterInfoPDA; -} - -async function checkMinter( - program: Program, - minter -) { - const [minterInfoPDA, bump] = getMinterPDA(program, minter); - let minterInfo = await program.account.minterInfo.fetch(minterInfoPDA); - - expect(minterInfo.minter).to.eql(minter.publicKey); - expect(minterInfo.bump).to.equal(bump); -} - -async function removeMinter( - program: Program, - authority, - minter, - minterInfo -) { - const [config,] = getConfigPDA(program); - await program.methods - .removeMinter() - .accounts({ - config, - authority: authority.publicKey, - minterInfo: minterInfo, - minter: minter.publicKey - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -function getGuardianPDA( - program: Program, - guardian -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('guardian-info'), - guardian.publicKey.toBuffer(), - ], - program.programId - ); -} - -async function addGuardian( - program: Program, - authority, - guardian, - payer -): Promise { - const [config,] = getConfigPDA(program); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardian); - await program.methods - .addGuardian() - .accounts({ - config, - authority: authority.publicKey, - guardianInfo: guardianInfoPDA, - guardian: guardian.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); - return guardianInfoPDA; -} - -async function checkGuardian( - program: Program, - guardian -) { - const [guardianInfoPDA, bump] = getGuardianPDA(program, guardian); - let guardianInfo = await program.account.guardianInfo.fetch(guardianInfoPDA); - - expect(guardianInfo.guardian).to.eql(guardian.publicKey); - expect(guardianInfo.bump).to.equal(bump); -} - -async function removeGuardian( - program: Program, - authority, - guardian, - guardianInfo -) { - const [config,] = getConfigPDA(program); - await program.methods - .removeGuardian() - .accounts({ - config, - authority: authority.publicKey, - guardianInfo: guardianInfo, - guardian: guardian.publicKey - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function pause( - program: Program, - guardian -) { - const [config,] = getConfigPDA(program); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardian); - await program.methods - .pause() - .accounts({ - config, - guardianInfo: guardianInfoPDA, - guardian: guardian.publicKey - }) - .signers([guardian]) - .rpc(); -} - -async function unpause( - program: Program, - authority -) { - const [config,] = getConfigPDA(program); - await program.methods - .unpause() - .accounts({ - config, - authority: authority.publicKey - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function mint( - program: Program, - minter, - minterInfoPDA, - recipient, - amount, - payer, -) { - const connection = program.provider.connection; - - const [config,] = getConfigPDA(program); - const [tbtcMintPDA, _] = getTokenPDA(program); - const recipientToken = spl.getAssociatedTokenAddressSync(tbtcMintPDA, recipient.publicKey); - - const tokenData = await spl.getAccount(connection, recipientToken).catch((err) => { - if (err instanceof spl.TokenAccountNotFoundError) { - return null; - } else { - throw err; - }; - }); - - if (tokenData === null) { - const tx = await web3.sendAndConfirmTransaction( - connection, - new web3.Transaction().add( - spl.createAssociatedTokenAccountIdempotentInstruction( - payer.publicKey, - recipientToken, - recipient.publicKey, - tbtcMintPDA, - ) - ), - [payer.payer] - ); - } - - - await program.methods - .mint(new anchor.BN(amount)) - .accounts({ - mint: tbtcMintPDA, - config, - minterInfo: minterInfoPDA, - minter: minter.publicKey, - recipientToken, - }) - .signers(maybeAuthorityAnd(payer, [minter])) - .rpc(); -} +import * as tbtc from "./helpers/tbtc"; +import { + expectIxFail, + expectIxSuccess, + getOrCreateAta, + getTokenBalance, + sleep, + transferLamports, +} from "./helpers/utils"; describe("tbtc", () => { // Configure the client to use the local cluster. @@ -365,348 +19,805 @@ describe("tbtc", () => { const program = anchor.workspace.Tbtc as Program; - const authority = (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet; + const authority = ( + (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet + ).payer; const newAuthority = anchor.web3.Keypair.generate(); - const minterKeys = anchor.web3.Keypair.generate(); - const minter2Keys = anchor.web3.Keypair.generate(); - const impostorKeys = anchor.web3.Keypair.generate(); - const guardianKeys = anchor.web3.Keypair.generate(); - const guardian2Keys = anchor.web3.Keypair.generate(); - - const recipientKeys = anchor.web3.Keypair.generate(); - - it('setup', async () => { - await setup(program, authority); - await checkState(program, authority, 0, 0, 0); - }); - - it('change authority', async () => { - await checkState(program, authority, 0, 0, 0); - await checkNoPendingAuthority(program); - try { - await cancelAuthorityChange(program, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('NoPendingAuthorityChange'); - expect(err.program.equals(program.programId)).is.true; - } - try { - await takeAuthority(program, newAuthority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('NoPendingAuthorityChange'); - expect(err.program.equals(program.programId)).is.true; - } - - await changeAuthority(program, authority, newAuthority); - await checkPendingAuthority(program, newAuthority); - await takeAuthority(program, newAuthority); - await checkNoPendingAuthority(program); - await checkState(program, newAuthority, 0, 0, 0); - await changeAuthority(program, newAuthority, authority.payer); - try { - await takeAuthority(program, impostorKeys); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPendingAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - try { - await takeAuthority(program, newAuthority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPendingAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - try { - await cancelAuthorityChange(program, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - await takeAuthority(program, authority); - - await checkState(program, authority, 0, 0, 0); - }) - - it('add minter', async () => { - await checkState(program, authority, 0, 0, 0); - await addMinter(program, authority, minterKeys, authority); - await checkMinter(program, minterKeys); - await checkState(program, authority, 1, 0, 0); - - // Transfer lamports to imposter. - await transferLamports(program.provider.connection, authority.payer, impostorKeys.publicKey, 1000000000); - // await web3.sendAndConfirmTransaction( - // program.provider.connection, - // new web3.Transaction().add( - // web3.SystemProgram.transfer({ - // fromPubkey: authority.publicKey, - // toPubkey: impostorKeys.publicKey, - // lamports: 1000000000, - // }) - // ), - // [authority.payer] - // ); - - try { - await addMinter(program, impostorKeys, minter2Keys, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('mint', async () => { - await checkState(program, authority, 1, 0, 0); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - - // await setupMint(program, authority, recipientKeys); - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 1000, authority); - - await checkState(program, authority, 1, 0, 1000); - - // // Burn for next test. - // const ix = spl.createBurnCheckedInstruction( - // account, // PublicKey of Owner's Associated Token Account - // new PublicKey(MINT_ADDRESS), // Public Key of the Token Mint Address - // WALLET.publicKey, // Public Key of Owner's Wallet - // BURN_QUANTITY * (10**MINT_DECIMALS), // Number of tokens to burn - // MINT_DECIMALS // Number of Decimals of the Token Mint - // ) - - }); - - it('won\'t mint', async () => { - await checkState(program, authority, 1, 0, 1000); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - - // await setupMint(program, authority, recipientKeys); - - try { - await mint(program, impostorKeys, minterInfoPDA, recipientKeys, 1000, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('use two minters', async () => { - await checkState(program, authority, 1, 0, 1000); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - const minter2InfoPDA = await addMinter(program, authority, minter2Keys, authority); - await checkMinter(program, minter2Keys); - await checkState(program, authority, 2, 0, 1000); - // await setupMint(program, authority, recipientKeys); - - // cannot mint with wrong keys - try { - await mint(program, minter2Keys, minterInfoPDA, recipientKeys, 1000, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } - - // cannot remove minter with wrong keys - try { - await removeMinter(program, authority, minter2Keys, minterInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } - - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 500, authority); - await checkState(program, authority, 2, 0, 1500); - }); - - it('remove minter', async () => { - await checkState(program, authority, 2, 0, 1500); - const [minter2InfoPDA, _] = getMinterPDA(program, minter2Keys); - await checkMinter(program, minter2Keys); - await removeMinter(program, authority, minter2Keys, minter2InfoPDA); - await checkState(program, authority, 1, 0, 1500); + const minter = anchor.web3.Keypair.generate(); + const anotherMinter = anchor.web3.Keypair.generate(); + const imposter = anchor.web3.Keypair.generate(); + const guardian = anchor.web3.Keypair.generate(); + const anotherGuardian = anchor.web3.Keypair.generate(); + + const recipient = anchor.web3.Keypair.generate(); + const txPayer = anchor.web3.Keypair.generate(); + + it("set up payers", async () => { + await transferLamports(authority, newAuthority.publicKey, 10000000000); + await transferLamports(authority, imposter.publicKey, 10000000000); + await transferLamports(authority, recipient.publicKey, 10000000000); + await transferLamports(authority, txPayer.publicKey, 10000000000); }); - it('won\'t remove minter', async () => { - await checkState(program, authority, 1, 0, 1500); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); - await checkMinter(program, minterKeys); - - try { - await removeMinter(program, impostorKeys, minterKeys, minterInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - - await removeMinter(program, authority, minterKeys, minterInfoPDA); - await checkState(program, authority, 0, 0, 1500); - - try { - await removeMinter(program, authority, minterKeys, minterInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('AccountNotInitialized'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('add guardian', async () => { - await checkState(program, authority, 0, 0, 1500); - await addGuardian(program, authority, guardianKeys, authority); - await checkGuardian(program, guardianKeys); - await checkState(program, authority, 0, 1, 1500); - - try { - await addGuardian(program, impostorKeys, guardian2Keys, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - }); - - it('remove guardian', async () => { - await checkState(program, authority, 0, 1, 1500); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardianKeys); - await checkGuardian(program, guardianKeys); - - try { - await removeGuardian(program, impostorKeys, guardianKeys, guardianInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); - expect(err.program.equals(program.programId)).is.true; - } - - await removeGuardian(program, authority, guardianKeys, guardianInfoPDA); - await checkState(program, authority, 0, 0, 1500); - - try { - await removeGuardian(program, authority, guardianKeys, guardianInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('AccountNotInitialized'); - expect(err.program.equals(program.programId)).is.true; - } + it("initialize", async () => { + const ix = await tbtc.initializeIx({ authority: authority.publicKey }); + await expectIxSuccess([ix], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); }); - it('pause', async () => { - await checkState(program, authority, 0, 0, 1500); - await addGuardian(program, authority, guardianKeys, authority); - await checkPaused(program, false); - await pause(program, guardianKeys); - await checkPaused(program, true); + describe("authority changes", () => { + it("cannot cancel authority if no pending", async () => { + const failedCancelIx = await tbtc.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [failedCancelIx], + [authority], + "NoPendingAuthorityChange" + ); + }); + + it("cannot take authority if no pending", async () => { + const failedTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedTakeIx], + [newAuthority], + "NoPendingAuthorityChange" + ); + }); + + it("change authority to new authority", async () => { + const changeIx = await tbtc.changeAuthorityIx({ + authority: authority.publicKey, + newAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([changeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: newAuthority.publicKey, + }); + }); + + it("take as new authority", async () => { + // Bug in validator? Need to wait a bit for new blockhash. + await sleep(10000); + + const takeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([takeIx], [newAuthority]); + await tbtc.checkConfig({ + authority: newAuthority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); + }); + + it("change pending authority back to original authority", async () => { + const changeBackIx = await tbtc.changeAuthorityIx({ + authority: newAuthority.publicKey, + newAuthority: authority.publicKey, + }); + await expectIxSuccess([changeBackIx], [newAuthority]); + await tbtc.checkConfig({ + authority: newAuthority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: authority.publicKey, + }); + }); + + it("cannot take as signers that are not pending authority", async () => { + const failedImposterTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: imposter.publicKey, + }); + await expectIxFail( + [failedImposterTakeIx], + [imposter], + "IsNotPendingAuthority" + ); + + const failedNewAuthorityTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedNewAuthorityTakeIx], + [newAuthority], + "IsNotPendingAuthority" + ); + }); + + it("cannot cancel as someone else", async () => { + const anotherFailedCancelIx = await tbtc.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [anotherFailedCancelIx], + [authority], + "IsNotAuthority" + ); + }); + + it("finally take as authority", async () => { + const anotherTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: authority.publicKey, + }); + await expectIxSuccess([anotherTakeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); + }); }); - it('unpause', async () => { - await checkState(program, authority, 0, 1, 1500); - await checkPaused(program, true); - await unpause(program, authority); - await checkPaused(program, false); - - try { - await unpause(program, authority); - - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPaused'); - expect(err.program.equals(program.programId)).is.true; - } + describe("minting", () => { + it("cannot add minter without authority", async () => { + const cannotAddMinterIx = await tbtc.addMinterIx({ + authority: imposter.publicKey, + minter: minter.publicKey, + }); + await expectIxFail([cannotAddMinterIx], [imposter], "IsNotAuthority"); + }); + + it("add minter", async () => { + const mustBeNull = await tbtc + .checkMinterInfo(minter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(0), + paused: false, + pendingAuthority: null, + }); + await tbtc.checkMinterInfo(minter.publicKey); + }); + + it("mint", async () => { + const amount = BigInt(1000); + + const recipientToken = await getOrCreateAta( + authority, + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(0)); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1000), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(amount); + }); + + it("cannot mint without minter", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const cannotMintIx = await tbtc.mintIx( + { + minter: imposter.publicKey, + recipientToken, + }, + new anchor.BN(420) + ); + await expectIxFail( + [cannotMintIx], + [txPayer, imposter], + "AccountNotInitialized" + ); + + // Now try with actual minter's info account. + const minterInfo = tbtc.getMinterInfoPDA(minter.publicKey); + + const cannotMintAgainIx = await tbtc.mintIx( + { + minterInfo, + minter: imposter.publicKey, + recipientToken, + }, + new anchor.BN(420) + ); + await expectIxFail( + [cannotMintAgainIx], + [txPayer, imposter], + "ConstraintSeeds" + ); + }); + + it("add another minter", async () => { + const mustBeNull = await tbtc + .checkMinterInfo(anotherMinter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 2, + numGuardians: 0, + supply: BigInt(1000), + paused: false, + pendingAuthority: null, + }); + await tbtc.checkMinterInfo(anotherMinter.publicKey); + }); + + it("cannot remove minter with wrong key", async () => { + const minterInfo = tbtc.getMinterInfoPDA(minter.publicKey); + const cannotRemoveIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minterInfo, + minter: anotherMinter.publicKey, + }); + await expectIxFail([cannotRemoveIx], [authority], "ConstraintSeeds"); + }); + + it("mint with another minter", async () => { + const amount = BigInt(500); + + const recipientToken = await spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(1000)); + + const mintIx = await tbtc.mintIx( + { + minter: anotherMinter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, anotherMinter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 2, + numGuardians: 0, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("cannot remove minter without authority", async () => { + const cannotRemoveIx = await tbtc.removeMinterIx({ + authority: imposter.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxFail([cannotRemoveIx], [imposter], "IsNotAuthority"); + }); + + it("remove minter", async () => { + const removeIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkMinterInfo(anotherMinter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + }); + + it("cannot remove same minter again", async () => { + const cannotRemoveIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: anotherMinter.publicKey, + }); + await expectIxFail( + [cannotRemoveIx], + [authority], + "AccountNotInitialized" + ); + }); + + it("remove last minter", async () => { + const removeIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkMinterInfo(minter.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "minter info found"); + }); }); - it('won\'t mint when paused', async () => { - await checkState(program, authority, 0, 1, 1500); - const minterInfoPDA = await addMinter(program, authority, minterKeys, authority); - await pause(program, guardianKeys); - // await setupMint(program, authority, recipientKeys); - - try { - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 1000, authority); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsPaused'); - expect(err.program.equals(program.programId)).is.true; - } - - await unpause(program, authority); - await checkPaused(program, false); - }) - - it('use two guardians', async () => { - await checkState(program, authority, 1, 1, 1500); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardianKeys); - await checkGuardian(program, guardianKeys); - await addGuardian(program, authority, guardian2Keys, authority); - await checkGuardian(program, guardian2Keys); - - await pause(program, guardianKeys); - - try { - await pause(program, guardian2Keys); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsPaused'); - expect(err.program.equals(program.programId)).is.true; - } - - await unpause(program, authority); - await pause(program, guardian2Keys); - await checkPaused(program, true); - await unpause(program, authority); - - // cannot remove guardian with wrong keys - try { - await removeGuardian(program, authority, guardian2Keys, guardianInfoPDA); - chai.assert(false, "should've failed but didn't"); - } catch (_err) { - expect(_err).to.be.instanceOf(AnchorError); - const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); - expect(err.program.equals(program.programId)).is.true; - } + describe("guardians", () => { + it("cannot add guardian without authority", async () => { + const cannotAddIx = await tbtc.addGuardianIx({ + authority: imposter.publicKey, + guardian: guardian.publicKey, + }); + await expectIxFail([cannotAddIx], [imposter], "IsNotAuthority"); + }); + + it("add guardian", async () => { + const mustBeNull = await tbtc + .checkGuardianInfo(guardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + + const addIx = await tbtc.addGuardianIx({ + authority: authority.publicKey, + guardian: guardian.publicKey, + }); + await expectIxSuccess([addIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 1, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + await tbtc.checkGuardianInfo(guardian.publicKey); + }); + + it("cannot pause without guardian", async () => { + const cannotPauseIx = await tbtc.pauseIx({ + guardian: imposter.publicKey, + }); + await expectIxFail( + [cannotPauseIx], + [txPayer, imposter], + "AccountNotInitialized" + ); + + // Now try with actual guardian's info account. + const guardianInfo = tbtc.getGuardianInfoPDA(guardian.publicKey); + + const cannotPauseAgainIx = await tbtc.pauseIx({ + guardianInfo, + guardian: imposter.publicKey, + }); + await expectIxFail( + [cannotPauseAgainIx], + [txPayer, imposter], + "ConstraintSeeds" + ); + }); + + it("add minter and mint", async () => { + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1500), + paused: false, + pendingAuthority: null, + }); + + const amount = BigInt(100); + + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(1500)); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1600), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("pause", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: guardian.publicKey, + }); + await expectIxSuccess([pauseIx], [txPayer, guardian]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1600), + paused: true, + pendingAuthority: null, + }); + }); + + it("cannot mint while paused", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(100) + ); + await expectIxFail([mintIx], [txPayer, minter], "IsPaused"); + }); + + it("add another guardian", async () => { + const mustBeNull = await tbtc + .checkGuardianInfo(anotherGuardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + + const addIx = await tbtc.addGuardianIx({ + authority: authority.publicKey, + guardian: anotherGuardian.publicKey, + }); + await expectIxSuccess([addIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1600), + paused: true, + pendingAuthority: null, + }); + await tbtc.checkGuardianInfo(anotherGuardian.publicKey); + }); + + it("cannot pause again", async () => { + const cannotPauseIx = await tbtc.pauseIx({ + guardian: anotherGuardian.publicKey, + }); + await expectIxFail( + [cannotPauseIx], + [txPayer, anotherGuardian], + "IsPaused" + ); + }); + + it("unpause", async () => { + const unpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxSuccess([unpauseIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1600), + paused: false, + pendingAuthority: null, + }); + }); + + it("cannot unpause again", async () => { + const cannotUnpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [cannotUnpauseIx], + [txPayer, authority], + "IsNotPaused" + ); + }); + + it("mint while unpaused", async () => { + const amount = BigInt(200); + + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + const recipientBefore = await getTokenBalance(recipientToken); + expect(recipientBefore).to.equal(BigInt(1600)); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1800), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("pause as another guardian", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: anotherGuardian.publicKey, + }); + await expectIxSuccess([pauseIx], [txPayer, anotherGuardian]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 2, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + }); + + it("cannot mint again while paused", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(100) + ); + await expectIxFail([mintIx], [txPayer, minter], "IsPaused"); + }); + + it("cannot remove guardian without authority", async () => { + const cannotRemoveIx = await tbtc.removeGuardianIx({ + authority: imposter.publicKey, + guardian: anotherGuardian.publicKey, + }); + await expectIxFail([cannotRemoveIx], [imposter], "IsNotAuthority"); + }); + + it("cannot remove guardian with mismatched info", async () => { + const guardianInfo = tbtc.getGuardianInfoPDA(anotherGuardian.publicKey); + const cannotRemoveIx = await tbtc.removeGuardianIx({ + authority: authority.publicKey, + guardianInfo, + guardian: guardian.publicKey, + }); + await expectIxFail([cannotRemoveIx], [authority], "ConstraintSeeds"); + }); + + it("remove guardian", async () => { + const removeIx = await tbtc.removeGuardianIx({ + authority: authority.publicKey, + guardian: anotherGuardian.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkGuardianInfo(anotherGuardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + }); + + it("unpause", async () => { + const unpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxSuccess([unpauseIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1800), + paused: false, + pendingAuthority: null, + }); + }); + + it("cannot pause with removed guardian", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: anotherGuardian.publicKey, + }); + await expectIxFail( + [pauseIx], + [txPayer, anotherGuardian], + "AccountNotInitialized" + ); + }); + + it("pause and remove last guardian", async () => { + const pauseIx = await tbtc.pauseIx({ + guardian: guardian.publicKey, + }); + await expectIxSuccess([pauseIx], [txPayer, guardian]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 1, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + + const removeIx = await tbtc.removeGuardianIx({ + authority: authority.publicKey, + guardian: guardian.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1800), + paused: true, + pendingAuthority: null, + }); + const mustBeNull = await tbtc + .checkGuardianInfo(guardian.publicKey) + .catch((_) => null); + assert(mustBeNull === null, "guardian info found"); + }); + + it("cannot mint yet again", async () => { + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(100) + ); + await expectIxFail([mintIx], [txPayer, minter], "IsPaused"); + }); + + it("unpause without any guardians then mint", async () => { + const unpauseIx = await tbtc.unpauseIx({ + authority: authority.publicKey, + }); + await expectIxSuccess([unpauseIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(1800), + paused: false, + pendingAuthority: null, + }); + + const recipientToken = spl.getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient.publicKey + ); + + const amount = BigInt(200); + + const recipientBefore = await getTokenBalance(recipientToken); + const mintIx = await tbtc.mintIx( + { + minter: minter.publicKey, + recipientToken, + }, + new anchor.BN(amount.toString()) + ); + await expectIxSuccess([mintIx], [txPayer, minter]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + + const recipientAfter = await getTokenBalance(recipientToken); + expect(recipientAfter).to.equal(recipientBefore + amount); + }); + + it("remove minter", async () => { + const removeIx = await tbtc.removeMinterIx({ + authority: authority.publicKey, + minter: minter.publicKey, + }); + await expectIxSuccess([removeIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + }); }); }); diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 69eb14fe3..e5641ddea 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -1,116 +1,1232 @@ -import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock"; -import * as tokenBridge from "@certusone/wormhole-sdk/lib/cjs/solana/tokenBridge"; -import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; -import { parseTokenTransferVaa, postVaaSolana, redeemOnSolana, tryNativeToHexString } from "@certusone/wormhole-sdk"; +import { MockEthereumTokenBridge } from "@certusone/wormhole-sdk/lib/cjs/mock"; import * as anchor from "@coral-xyz/anchor"; import { Program } from "@coral-xyz/anchor"; -import * as spl from "@solana/spl-token"; -import { expect } from 'chai'; +import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; +import { expect } from "chai"; import { WormholeGateway } from "../target/types/wormhole_gateway"; -import { generatePayer, getOrCreateTokenAccount } from "./helpers/utils"; -import { web3 } from "@coral-xyz/anchor"; +import { + ETHEREUM_TOKEN_BRIDGE_ADDRESS, + WORMHOLE_GATEWAY_PROGRAM_ID, + WRAPPED_TBTC_MINT, + ethereumGatewaySendTbtc, + expectIxFail, + expectIxSuccess, + generatePayer, + getOrCreateAta, + preloadWrappedTbtc, + transferLamports, +} from "./helpers"; +import * as tbtc from "./helpers/tbtc"; +import * as wormholeGateway from "./helpers/wormholeGateway"; -const SOLANA_CORE_BRIDGE_ADDRESS = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"; -const SOLANA_TOKEN_BRIDGE_ADDRESS = "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"; -const ETHEREUM_TOKEN_BRIDGE_ADDRESS = "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"; -const ETHEREUM_TBTC_ADDRESS = "0x18084fbA666a33d37592fA2633fD49a74DD93a88"; - -const GUARDIAN_SET_INDEX = 3; - -function getCustodianPDA( +async function setup( program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('custodian'), - ], - program.programId - ); -} + authority, + mintingLimit: bigint +) { + const custodian = wormholeGateway.getCustodianPDA(); + const tbtcMint = tbtc.getMintPDA(); + const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); + const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); + await program.methods + .initialize(new anchor.BN(mintingLimit.toString())) + .accounts({ + authority: authority.publicKey, + custodian, + tbtcMint, + wrappedTbtcMint: WRAPPED_TBTC_MINT, + wrappedTbtcToken: gatewayWrappedTbtcToken, + tokenBridgeSender, + }) + .rpc(); +} describe("wormhole-gateway", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); + // Initialize anchor program. const program = anchor.workspace.WormholeGateway as Program; const connection = program.provider.connection; - const authority = (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet; + const custodian = wormholeGateway.getCustodianPDA(); + const tbtcMint = tbtc.getMintPDA(); + const tbtcConfig = tbtc.getConfigPDA(); + const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); + const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); + const tokenBridgeRedeemer = wormholeGateway.getTokenBridgeRedeemerPDA(); + + const authority = ( + (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet + ).payer; const newAuthority = anchor.web3.Keypair.generate(); const minterKeys = anchor.web3.Keypair.generate(); const minter2Keys = anchor.web3.Keypair.generate(); - const impostorKeys = anchor.web3.Keypair.generate(); + const imposter = anchor.web3.Keypair.generate(); const guardianKeys = anchor.web3.Keypair.generate(); const guardian2Keys = anchor.web3.Keypair.generate(); - const recipientKeys = anchor.web3.Keypair.generate(); - - const ethereumTokenBridge = new mock.MockEthereumTokenBridge(ETHEREUM_TOKEN_BRIDGE_ADDRESS); - - it('check core bridge and token bridge', async () => { - // Check core bridge guardian set. - const guardianSetData = await coreBridge.getGuardianSet(connection, SOLANA_CORE_BRIDGE_ADDRESS, GUARDIAN_SET_INDEX); - expect(guardianSetData.keys).has.length(1); - - // Set up new wallet - const payer = await generatePayer(connection, authority.payer); - - // Check wrapped tBTC mint. - const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey(SOLANA_TOKEN_BRIDGE_ADDRESS, 2, ETHEREUM_TBTC_ADDRESS); - const mintData = await spl.getMint(connection, wrappedTbtcMint); - expect(mintData.decimals).to.equal(8); - expect(mintData.supply).to.equal(BigInt(90)); - - const wrappedTbtcToken = await getOrCreateTokenAccount(connection, payer, wrappedTbtcMint, payer.publicKey); - - // Bridge tbtc to token account. - const published = ethereumTokenBridge.publishTransferTokens( - tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), - 2, - BigInt("100000000000"), - 1, - wrappedTbtcToken.address.toBuffer().toString("hex"), - BigInt(0), - 0, - 0 - ); - - const signedVaa = await mockSignAndPostVaa(connection, payer, published); - - const tx = await redeemOnSolana( - connection, - SOLANA_CORE_BRIDGE_ADDRESS, - SOLANA_TOKEN_BRIDGE_ADDRESS, - payer.publicKey, - signedVaa, - ); - await web3.sendAndConfirmTransaction(connection, tx, [payer]); + const recipient = anchor.web3.Keypair.generate(); + const txPayer = anchor.web3.Keypair.generate(); + + const commonTokenOwner = anchor.web3.Keypair.generate(); + + // Mock foreign emitter. + const ethereumTokenBridge = new MockEthereumTokenBridge( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); + + it("set up payers", async () => { + await transferLamports(authority, newAuthority.publicKey, 10000000000); + await transferLamports(authority, imposter.publicKey, 10000000000); + await transferLamports(authority, recipient.publicKey, 10000000000); + await transferLamports(authority, txPayer.publicKey, 10000000000); + await transferLamports(authority, commonTokenOwner.publicKey, 10000000000); }); - it('setup', async () => { - // await setup(program, authority); - // await checkState(program, authority, 0, 0, 0); + describe("setup", () => { + it("initialize", async () => { + // Max amount of TBTC that can be minted. + const mintingLimit = BigInt(10000); + + // Initialize the program. + await setup(program, authority, mintingLimit); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit, + pendingAuthority: null, + }); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + + // Also set up common token account. + await getOrCreateAta( + authority, + tbtc.getMintPDA(), + commonTokenOwner.publicKey + ); + + // Give the impostor some lamports. + await transferLamports(authority, imposter.publicKey, 100000000000); + }); }); -}); -async function mockSignAndPostVaa(connection: web3.Connection, payer: web3.Keypair, published: Buffer) { - const guardians = new mock.MockGuardians( - GUARDIAN_SET_INDEX, - ["cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"] - ); + describe("authority changes", () => { + it("cannot cancel authority if no pending", async () => { + const failedCancelIx = await wormholeGateway.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [failedCancelIx], + [authority], + "NoPendingAuthorityChange" + ); + }); - // Add guardian signature. - const signedVaa = guardians.addSignatures(published, [0]); + it("cannot take authority if no pending", async () => { + const failedTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedTakeIx], + [newAuthority], + "NoPendingAuthorityChange" + ); + }); - // Verify and post VAA. - await postVaaSolana(connection, - new NodeWallet(payer).signTransaction, - SOLANA_CORE_BRIDGE_ADDRESS, - payer.publicKey, - signedVaa - ); + it("change authority to new authority", async () => { + const changeIx = await wormholeGateway.changeAuthorityIx({ + authority: authority.publicKey, + newAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([changeIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: newAuthority.publicKey, + }); + }); - return signedVaa; -} + it("take as new authority", async () => { + // Bug in validator? Need to wait a bit for new blockhash. + //await sleep(10000); + + const takeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxSuccess([takeIx], [newAuthority]); + await wormholeGateway.checkCustodian({ + authority: newAuthority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: null, + }); + }); + + it("change pending authority back to original authority", async () => { + const changeBackIx = await wormholeGateway.changeAuthorityIx({ + authority: newAuthority.publicKey, + newAuthority: authority.publicKey, + }); + await expectIxSuccess([changeBackIx], [newAuthority]); + await wormholeGateway.checkCustodian({ + authority: newAuthority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: authority.publicKey, + }); + }); + + it("cannot take as signers that are not pending authority", async () => { + const failedImposterTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: imposter.publicKey, + }); + await expectIxFail( + [failedImposterTakeIx], + [imposter], + "IsNotPendingAuthority" + ); + + const failedNewAuthorityTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedNewAuthorityTakeIx], + [newAuthority], + "IsNotPendingAuthority" + ); + }); + + it("cannot cancel as someone else", async () => { + const anotherFailedCancelIx = + await wormholeGateway.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [anotherFailedCancelIx], + [authority], + "IsNotAuthority" + ); + }); + + it("finally take as authority", async () => { + const anotherTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: authority.publicKey, + }); + await expectIxSuccess([anotherTakeIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: BigInt(10000), + pendingAuthority: null, + }); + }); + }); + + describe("minting limit", () => { + it("update minting limit", async () => { + // Update minting limit as authority. + const newLimit = BigInt(20000); + const ix = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([ix], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + }); + + it("cannot update minting limit (not authority)", async () => { + // Only the authority can update the minting limit. + const newLimit = BigInt(69000); + const failingIx = await wormholeGateway.updateMintingLimitIx( + { + authority: imposter.publicKey, + }, + newLimit + ); + await expectIxFail([failingIx], [imposter], "IsNotAuthority"); + }); + }); + + describe("gateway address", () => { + const chain = 2; + + it("gateway does not exist", async () => { + // demonstrate gateway address does not exist + const gatewayInfo = await connection.getAccountInfo( + wormholeGateway.getGatewayInfoPDA(chain) + ); + expect(gatewayInfo).is.null; + }); + + it("set initial gateway address", async () => { + // Make new gateway. + const firstAddress = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const firstIx = await wormholeGateway.updateGatewayAddress( + { + authority: authority.publicKey, + }, + { chain, address: firstAddress } + ); + await expectIxSuccess([firstIx], [authority]); + await wormholeGateway.checkGateway(chain, firstAddress); + }); + + it("update gateway address", async () => { + // Update gateway. + const goodAddress = Array.from(ethereumTokenBridge.address); + const secondIx = await wormholeGateway.updateGatewayAddress( + { + authority: authority.publicKey, + }, + { chain, address: goodAddress } + ); + await expectIxSuccess([secondIx], [authority]); + await wormholeGateway.checkGateway(chain, goodAddress); + }); + + it("cannot update gateway address (not authority)", async () => { + // Only the authority can update the gateway address. + const goodAddress = Array.from(ethereumTokenBridge.address); + const failingIx = await wormholeGateway.updateGatewayAddress( + { + authority: imposter.publicKey, + }, + { chain, address: goodAddress } + ); + await expectIxFail([failingIx], [imposter], "IsNotAuthority"); + }); + }); + + describe("deposit wrapped tbtc", () => { + it("cannot deposit wrapped tbtc (custodian not a minter)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + const depositAmount = BigInt(500); + + // Attempt to deposit before the custodian is a minter. + const ix = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + depositAmount + ); + await expectIxFail([ix], [payer], "AccountNotInitialized"); + }); + + it("deposit wrapped tokens", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + const depositAmount = BigInt(500); + + const ix = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + depositAmount + ); + + // Add custodian as minter. + const addMinterIx = await tbtc.addMinterIx({ + authority: authority.publicKey, + minter: custodian, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + + // Check token account balances before deposit. + const [wrappedBefore, tbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientWrappedToken), + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + await expectIxSuccess([ix], [payer]); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 1, + numGuardians: 0, + supply: BigInt(2500), + paused: false, + pendingAuthority: null, + }); + + const [wrappedAfter, tbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientWrappedToken), + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount after. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore + depositAmount); + + // Check balance change. + expect(wrappedAfter.amount).to.equal( + wrappedBefore.amount - depositAmount + ); + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + depositAmount); + expect(gatewayAfter.amount).to.equal( + gatewayBefore.amount + depositAmount + ); + }); + + it("cannot deposit wrapped tbtc (minting limit exceeded)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + // Cannot deposit past minting limit. + const failingIx = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + BigInt(50000) + ); + await expectIxFail([failingIx], [payer], "MintingLimitExceeded"); + }); + + it("deposit wrapped tbtc after increasing mint limit", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Check wrapped tBTC mint. + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey + ); + + const recipientToken = await getOrCreateAta( + payer, + tbtcMint, + payer.publicKey + ); + + // Check minted amount before deposit. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + const depositAmount = BigInt(50000); + + // Cannot deposit past minting limit. + const failingIx = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + depositAmount + ); + + // Will succeed if minting limit is increased. + const newLimit = BigInt(70000); + const updateLimitIx = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([updateLimitIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + await expectIxSuccess([failingIx], [payer]); + + // Check minted amount after. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore + depositAmount); + }); + }); + + describe("receive tbtc", () => { + let replayVaa; + + it("receive tbtc", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + // Get minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + const [tbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxSuccess([ix], [payer]); + + const [tbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check balance change. + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + sentAmount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + sentAmount); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore + sentAmount); + + // Save vaa. + replayVaa = signedVaa; + }); + + it("cannot receive tbtc (vaa already redeemed)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + replayVaa + ); + + // Cannot receive tbtc again. + await expectIxFail([ix], [payer], "TransferAlreadyRedeemed"); + }); + + it("receive wrapped tbtc (ata doesn't exist)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + const recipientWrappedToken = getAssociatedTokenAddressSync( + WRAPPED_TBTC_MINT, + recipient + ); + + // Get minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Verify that the wrapped token account doesn't exist yet. + try { + await getAccount(connection, recipientWrappedToken); + } catch (e: any) { + expect(e.toString()).to.equal("TokenAccountNotFoundError"); + } + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + // Create transfer VAA. + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + // Set the mint limit to a value smaller than sentAmount. + const newLimit = sentAmount - BigInt(69); + const updateLimitIx = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([updateLimitIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + + // Balance check before receiving wrapped tbtc. We can't + // check the balance of the recipient's wrapped tbtc yet, + // since the contract will create the ATA. + const [tbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxSuccess([ix], [payer]); + + // Check token accounts after receiving wrapped tbtc. We should + // be able to fetch the recipient's wrapped tbtc now. + const [tbtcAfter, wrappedTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, recipientWrappedToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore); + + // Check balance change. + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount); + expect(wrappedTbtcAfter.amount).to.equal(sentAmount); + }); + + it("receive wrapped tbtc (ata exists)", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + const recipientWrappedToken = await getOrCreateAta( + payer, + WRAPPED_TBTC_MINT, + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + // Get minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Create transfer VAA. + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + // Set the mint limit to a value smaller than sentAmount. + const newLimit = sentAmount - BigInt(69); + const updateLimitIx = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit + ); + await expectIxSuccess([updateLimitIx], [authority]); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); + + // Balance check before receiving wrapped tbtc. If this + // line successfully executes, then the recipient's + // wrapped tbtc account already exists. + const [tbtcBefore, wrappedTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, recipientWrappedToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + const ix = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxSuccess([ix], [payer]); + + // Check token accounts after receiving wrapped tbtc. + const [tbtcAfter, wrappedTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, recipientWrappedToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore); + + // Check balance change. + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount); + expect(wrappedTbtcAfter.amount).to.equal( + wrappedTbtcBefore.amount + sentAmount + ); + }); + + it("cannot receive non-tbtc transfers", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient, + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH address + 69 // hehe + ); + + const failingIx = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxFail([failingIx], [payer], "InvalidEthereumTbtc"); + }); + + it("cannot receive zero-amount tbtc transfers", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + const sentAmount = BigInt(0); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + const failingIx = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken, + recipient, + }, + signedVaa + ); + await expectIxFail([failingIx], [payer], "NoTbtcTransferred"); + }); + + it("cannot receive tbtc transfer with zero address as recipient", async () => { + // Set up new wallet + const payer = await generatePayer(authority); + + // Use common token account. Set the recipient to the zero address. + const recipient = PublicKey.default; + const defaultTokenAccount = await getOrCreateAta( + payer, + tbtc.getMintPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + const sentAmount = BigInt(100); + const signedVaa = await ethereumGatewaySendTbtc( + payer, + ethereumTokenBridge, + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient + ); + + const failingIx = await wormholeGateway.receiveTbtcIx( + { + payer: payer.publicKey, + recipientToken: defaultTokenAccount, + recipient, + }, + signedVaa + ); + await expectIxFail([failingIx], [payer], "RecipientZeroAddress"); + }); + }); + + describe("send tbtc", () => { + it("send tbtc to gateway", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Check token accounts. + const [senderTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // This should work. + const sendAmount = BigInt(2000); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxSuccess([ix], [commonTokenOwner]); + + // Check token accounts after sending tbtc. + const [senderTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore - sendAmount); + + // Check balance change. + expect(senderTbtcAfter.amount).to.equal( + senderTbtcBefore.amount - sendAmount + ); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - sendAmount); + }); + + it("cannot send tbtc to gateway (insufficient wrapped balance)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Check token accounts. + const gatewayWrappedBalance = await getAccount( + connection, + gatewayWrappedTbtcToken + ); + + // Try an amount that won't work. + const sendAmount = gatewayWrappedBalance.amount + BigInt(69); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "NotEnoughWrappedTbtc"); + }); + + it("cannot send tbtc to gateway (zero amount)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Try an amount that won't work. + const sendAmount = BigInt(0); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroAmount"); + }); + + it("cannot send tbtc to gateway (recipient is zero address)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32)); // empty buffer + const nonce = 420; + + const sendAmount = BigInt(69); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroRecipient"); + }); + + it("cannot send tbtc to gateway (invalid target gateway)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 69; // bad gateway + const recipient = Array.from(Buffer.alloc(32)); // empty buffer + const nonce = 420; + + const sendAmount = BigInt(69); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "AccountNotInitialized"); + }); + }); + + describe("send wrapped tbtc", () => { + it("send wrapped tbtc", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Check token accounts. + const [senderTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount before. + const mintedAmountBefore = await wormholeGateway.getMintedAmount(); + + // Get destination gateway. + const recipientChain = 69; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // This should work. + const sendAmount = BigInt(2000); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxSuccess([ix], [commonTokenOwner]); + + // Check token accounts after sending tbtc. + const [senderTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check minted amount. + const mintedAmountAfter = await wormholeGateway.getMintedAmount(); + expect(mintedAmountAfter).to.equal(mintedAmountBefore - sendAmount); + + // Check balance change. + expect(senderTbtcAfter.amount).to.equal( + senderTbtcBefore.amount - sendAmount + ); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - sendAmount); + }); + + it("cannot send wrapped tbtc (insufficient wrapped balance)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Check token accounts. + const gatewayWrappedBalance = await getAccount( + connection, + gatewayWrappedTbtcToken + ); + + // Try an amount that won't work. + const sendAmount = gatewayWrappedBalance.amount + BigInt(69); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "NotEnoughWrappedTbtc"); + }); + + it("cannot send wrapped tbtc(zero amount)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // Try an amount that won't work. + const sendAmount = BigInt(0); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroAmount"); + }); + + it("cannot send wrapped tbtc (recipient is zero address)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); + + // Get destination gateway. + const recipientChain = 2; + const recipient = Array.from(Buffer.alloc(32)); // empty buffer + const nonce = 420; + + const sendAmount = BigInt(69); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(sendAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxFail([ix], [commonTokenOwner], "ZeroRecipient"); + }); + }); +}); diff --git a/cross-chain/solana/tests/accounts/core_bridge.json b/cross-chain/solana/tests/accounts/core_bridge_data.json similarity index 100% rename from cross-chain/solana/tests/accounts/core_bridge.json rename to cross-chain/solana/tests/accounts/core_bridge_data.json diff --git a/cross-chain/solana/tests/accounts/core_fee_collector.json b/cross-chain/solana/tests/accounts/core_fee_collector.json index 6f355d442..beae11950 100644 --- a/cross-chain/solana/tests/accounts/core_fee_collector.json +++ b/cross-chain/solana/tests/accounts/core_fee_collector.json @@ -1,7 +1,7 @@ { "pubkey": "9bFNrXNb2WTx8fMHXCheaZqkLZ3YCCaiqTftHxeintHy", "account": { - "lamports": 86533780, + "lamports": 86533880, "data": [ "", "base64" diff --git a/cross-chain/solana/tests/helpers/consts.ts b/cross-chain/solana/tests/helpers/consts.ts new file mode 100644 index 000000000..748be2689 --- /dev/null +++ b/cross-chain/solana/tests/helpers/consts.ts @@ -0,0 +1,50 @@ +import { PublicKey } from "@solana/web3.js"; + +export const TBTC_PROGRAM_ID = new PublicKey( + "HksEtDgsXJV1BqcuhzbLRTmXp5gHgHJktieJCtQd3pG" +); +export const WORMHOLE_GATEWAY_PROGRAM_ID = new PublicKey( + "8H9F5JGbEMyERycwaGuzLS5MQnV7dn2wm2h6egJ3Leiu" +); + +export const CORE_BRIDGE_PROGRAM_ID = new PublicKey( + "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" +); +export const TOKEN_BRIDGE_PROGRAM_ID = new PublicKey( + "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb" +); + +export const ETHEREUM_TOKEN_BRIDGE_ADDRESS = + "0x3ee18B2214AFF97000D974cf647E7C347E8fa585"; +export const ETHEREUM_TBTC_ADDRESS = + "0x18084fbA666a33d37592fA2633fD49a74DD93a88"; + +export const GUARDIAN_SET_INDEX = 3; +export const GUARDIAN_DEVNET_PRIVATE_KEYS = [ + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", +]; + +// relevant core bridge PDAs +export const CORE_BRIDGE_DATA = new PublicKey( + "2yVjuQwpsvdsrywzsJJVs9Ueh4zayyo5DYJbBNc3DDpn" +); +export const CORE_EMITTER_SEQUENCE = new PublicKey( + "GF2ghkjwsR9CHkGk1RvuZrApPZGBZynxMm817VNi51Nf" +); +export const CORE_FEE_COLLECTOR = new PublicKey( + "9bFNrXNb2WTx8fMHXCheaZqkLZ3YCCaiqTftHxeintHy" +); + +// relevant token bridge PDAs +export const WRAPPED_TBTC_MINT = new PublicKey( + "25rXTx9zDZcHyTav5sRqM6YBvTGu9pPH9yv83uAEqbgG" +); +export const WRAPPED_TBTC_ASSET = new PublicKey( + "5LEUZpBxUQmoxoNGqmYmFEGAPDuhWbAY5CGt519UixLo" +); +export const ETHEREUM_ENDPOINT = new PublicKey( + "DujfLgMKW71CT2W8pxknf42FT86VbcK5PjQ6LsutjWKC" +); +export const TOKEN_BRIDGE_CONFIG = new PublicKey( + "DapiQYH3BGonhN8cngWcXQ6SrqSm3cwysoznoHr6Sbsx" +); diff --git a/cross-chain/solana/tests/helpers/index.ts b/cross-chain/solana/tests/helpers/index.ts new file mode 100644 index 000000000..cb1b44601 --- /dev/null +++ b/cross-chain/solana/tests/helpers/index.ts @@ -0,0 +1,2 @@ +export * from "./consts"; +export * from "./utils"; \ No newline at end of file diff --git a/cross-chain/solana/tests/helpers/tbtc.ts b/cross-chain/solana/tests/helpers/tbtc.ts new file mode 100644 index 000000000..7302c3ed3 --- /dev/null +++ b/cross-chain/solana/tests/helpers/tbtc.ts @@ -0,0 +1,510 @@ +import { BN, Program, Wallet, workspace } from "@coral-xyz/anchor"; +import { getMint } from "@solana/spl-token"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { config, expect } from "chai"; +import { Tbtc } from "../../target/types/tbtc"; +import { TBTC_PROGRAM_ID } from "./consts"; +import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"; + +export function getConfigPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("config")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getMintPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("tbtc-mint")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getTbtcMetadataPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [ + Buffer.from("metadata"), + METADATA_PROGRAM_ID.toBuffer(), + getMintPDA().toBuffer(), + ], + METADATA_PROGRAM_ID + )[0]; +} + +export function getMinterInfoPDA(minter: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("minter-info"), minter.toBuffer()], + TBTC_PROGRAM_ID + )[0]; +} + +export function getGuardianInfoPDA(guardian: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("guardian-info"), guardian.toBuffer()], + TBTC_PROGRAM_ID + )[0]; +} + +export function getGuardiansPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("guardians")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getMintersPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("minters")], + TBTC_PROGRAM_ID + )[0]; +} + +export async function getConfigData() { + const program = workspace.Tbtc as Program; + const config = getConfigPDA(); + return program.account.config.fetch(config); +} + +export async function checkConfig(expected: { + authority: PublicKey; + numMinters: number; + numGuardians: number; + supply: bigint; + paused: boolean; + pendingAuthority: PublicKey | null; +}) { + let { + authority, + numMinters, + numGuardians, + supply, + paused, + pendingAuthority, + } = expected; + const program = workspace.Tbtc as Program; + const configState = await getConfigData(); + + expect(configState.authority).to.eql(authority); + expect(configState.numMinters).to.equal(numMinters); + expect(configState.numGuardians).to.equal(numGuardians); + expect(configState.paused).to.equal(paused); + expect(configState.pendingAuthority).to.eql(pendingAuthority); + + const mintState = await getMint( + program.provider.connection, + configState.mint + ); + expect(mintState.supply).to.equal(supply); + + const guardians = getGuardiansPDA(); + const guardiansState = await program.account.guardians.fetch(guardians); + expect(guardiansState.keys).has.length(numGuardians); + + const minters = getMintersPDA(); + const mintersState = await program.account.minters.fetch(minters); + expect(mintersState.keys).has.length(numMinters); +} + +export async function getMinterInfo(minter: PublicKey) { + const program = workspace.Tbtc as Program; + const minterInfoPDA = getMinterInfoPDA(minter); + return program.account.minterInfo.fetch(minterInfoPDA); +} + +export async function checkMinterInfo(minter: PublicKey) { + const minterInfo = await getMinterInfo(minter); + expect(minterInfo.minter).to.eql(minter); +} + +export async function getGuardianInfo(guardian: PublicKey) { + const program = workspace.Tbtc as Program; + const guardianInfoPDA = getGuardianInfoPDA(guardian); + return program.account.guardianInfo.fetch(guardianInfoPDA); +} + +export async function checkGuardianInfo(guardian: PublicKey) { + let guardianInfo = await getGuardianInfo(guardian); + expect(guardianInfo.guardian).to.eql(guardian); +} + +type AddGuardianContext = { + config?: PublicKey; + authority: PublicKey; + guardians?: PublicKey; + guardianInfo?: PublicKey; + guardian: PublicKey; +}; + +export async function addGuardianIx( + accounts: AddGuardianContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, guardians, guardianInfo, guardian } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardians === undefined) { + guardians = getGuardiansPDA(); + } + + if (guardianInfo === undefined) { + guardianInfo = getGuardianInfoPDA(guardian); + } + + return program.methods + .addGuardian() + .accounts({ + config, + authority, + guardians, + guardianInfo, + guardian, + }) + .instruction(); +} + +type AddMinterContext = { + config?: PublicKey; + authority: PublicKey; + minters?: PublicKey; + minterInfo?: PublicKey; + minter: PublicKey; +}; + +export async function addMinterIx( + accounts: AddMinterContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, minters, minterInfo, minter } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (minters === undefined) { + minters = getMintersPDA(); + } + + if (minterInfo === undefined) { + minterInfo = getMinterInfoPDA(minter); + } + + return program.methods + .addMinter() + .accounts({ + config, + authority, + minters, + minterInfo, + minter, + }) + .instruction(); +} + +type CancelAuthorityChange = { + config?: PublicKey; + authority: PublicKey; +}; + +export async function cancelAuthorityChangeIx( + accounts: CancelAuthorityChange +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .cancelAuthorityChange() + .accounts({ + config, + authority, + }) + .instruction(); +} + +type ChangeAuthorityContext = { + config?: PublicKey; + authority: PublicKey; + newAuthority: PublicKey; +}; + +export async function changeAuthorityIx( + accounts: ChangeAuthorityContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, newAuthority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .changeAuthority() + .accounts({ + config, + authority, + newAuthority, + }) + .instruction(); +} + +type InitializeContext = { + mint?: PublicKey; + config?: PublicKey; + guardians?: PublicKey; + minters?: PublicKey; + authority: PublicKey; + tbtcMetadata?: PublicKey; + mplTokenMetadataProgram?: PublicKey; +}; + +export async function initializeIx( + accounts: InitializeContext +): Promise { + const program = workspace.Tbtc as Program; + + let { + mint, + config, + guardians, + minters, + authority, + tbtcMetadata, + mplTokenMetadataProgram, + } = accounts; + + if (mint === undefined) { + mint = getMintPDA(); + } + + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardians === undefined) { + guardians = getGuardiansPDA(); + } + + if (minters === undefined) { + minters = getMintersPDA(); + } + + if (tbtcMetadata === undefined) { + tbtcMetadata = getTbtcMetadataPDA(); + } + + if (mplTokenMetadataProgram === undefined) { + mplTokenMetadataProgram = METADATA_PROGRAM_ID; + } + + return program.methods + .initialize() + .accounts({ + mint, + config, + guardians, + minters, + authority, + tbtcMetadata, + mplTokenMetadataProgram, + }) + .instruction(); +} + +type PauseContext = { + config?: PublicKey; + guardianInfo?: PublicKey; + guardian: PublicKey; +}; + +export async function pauseIx( + accounts: PauseContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, guardianInfo, guardian } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardianInfo === undefined) { + guardianInfo = getGuardianInfoPDA(guardian); + } + + return program.methods + .pause() + .accounts({ + config, + guardianInfo, + guardian, + }) + .instruction(); +} + +type RemoveGuardianContext = { + config?: PublicKey; + authority: PublicKey; + guardians?: PublicKey; + guardianInfo?: PublicKey; + guardian: PublicKey; +}; + +export async function removeGuardianIx( + accounts: RemoveGuardianContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, guardians, guardianInfo, guardian } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (guardians === undefined) { + guardians = getGuardiansPDA(); + } + + if (guardianInfo === undefined) { + guardianInfo = getGuardianInfoPDA(guardian); + } + + return program.methods + .removeGuardian() + .accounts({ + config, + authority, + guardians, + guardianInfo, + guardian, + }) + .instruction(); +} + +type RemoveMinterContext = { + config?: PublicKey; + authority: PublicKey; + minters?: PublicKey; + minterInfo?: PublicKey; + minter: PublicKey; +}; + +export async function removeMinterIx( + accounts: RemoveMinterContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority, minters, minterInfo, minter } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + if (minters === undefined) { + minters = getMintersPDA(); + } + + if (minterInfo === undefined) { + minterInfo = getMinterInfoPDA(minter); + } + + return program.methods + .removeMinter() + .accounts({ + config, + authority, + minters, + minterInfo, + minter, + }) + .instruction(); +} + +type TakeAuthorityContext = { + config?: PublicKey; + pendingAuthority: PublicKey; +}; + +export async function takeAuthorityIx( + accounts: TakeAuthorityContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, pendingAuthority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .takeAuthority() + .accounts({ + config, + pendingAuthority, + }) + .instruction(); +} + +type UnpauseContext = { + config?: PublicKey; + authority: PublicKey; +}; + +export async function unpauseIx( + accounts: UnpauseContext +): Promise { + const program = workspace.Tbtc as Program; + + let { config, authority } = accounts; + if (config === undefined) { + config = getConfigPDA(); + } + + return program.methods + .unpause() + .accounts({ + config, + authority, + }) + .instruction(); +} + +type MintContext = { + mint?: PublicKey; + config?: PublicKey; + minterInfo?: PublicKey; + minter: PublicKey; + recipientToken: PublicKey; +}; + +export async function mintIx( + accounts: MintContext, + amount: BN +): Promise { + const program = workspace.Tbtc as Program; + + let { mint, config, minterInfo, minter, recipientToken } = accounts; + if (mint === undefined) { + mint = getMintPDA(); + } + + if (config === undefined) { + config = getConfigPDA(); + } + + if (minterInfo === undefined) { + minterInfo = getMinterInfoPDA(minter); + } + + return program.methods + .mint(amount) + .accounts({ + mint, + config, + minterInfo, + minter, + recipientToken, + }) + .instruction(); +} diff --git a/cross-chain/solana/tests/helpers/utils.ts b/cross-chain/solana/tests/helpers/utils.ts index b87a0a948..40980e668 100644 --- a/cross-chain/solana/tests/helpers/utils.ts +++ b/cross-chain/solana/tests/helpers/utils.ts @@ -1,53 +1,282 @@ -import { web3 } from "@coral-xyz/anchor"; -import { Account, TokenAccountNotFoundError, createAssociatedTokenAccountIdempotentInstruction, getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; -import { Connection, Keypair, PublicKey, SystemProgram, Transaction, sendAndConfirmTransaction } from "@solana/web3.js"; - -export async function transferLamports(connection: web3.Connection, fromSigner: web3.Keypair, toPubkey: web3.PublicKey, lamports: number) { - return sendAndConfirmTransaction( - connection, - new Transaction().add( - SystemProgram.transfer({ - fromPubkey: fromSigner.publicKey, - toPubkey, - lamports, - }) - ), - [fromSigner] +import { + postVaaSolana, + redeemOnSolana, + tryNativeToHexString, +} from "@certusone/wormhole-sdk"; +import { + MockEthereumTokenBridge, + MockGuardians, +} from "@certusone/wormhole-sdk/lib/cjs/mock"; +import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; +import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; +import { Program, web3, workspace } from "@coral-xyz/anchor"; +import { + Account, + TokenAccountNotFoundError, + createAssociatedTokenAccountIdempotentInstruction, + getAccount, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { + Keypair, + PublicKey, + SystemProgram, + Transaction, + TransactionInstruction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { assert, expect } from "chai"; +import { WormholeGateway } from "../../target/types/wormhole_gateway"; // This is only here to hack a connection. +import { + CORE_BRIDGE_PROGRAM_ID, + ETHEREUM_TBTC_ADDRESS, + GUARDIAN_DEVNET_PRIVATE_KEYS, + GUARDIAN_SET_INDEX, + TOKEN_BRIDGE_PROGRAM_ID, + WRAPPED_TBTC_MINT, +} from "./consts"; + +export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export async function transferLamports( + fromSigner: web3.Keypair, + toPubkey: web3.PublicKey, + lamports: number +) { + const program = workspace.WormholeGateway as Program; + return sendAndConfirmTransaction( + program.provider.connection, + new Transaction().add( + SystemProgram.transfer({ + fromPubkey: fromSigner.publicKey, + toPubkey, + lamports, + }) + ), + [fromSigner] + ); +} + +export async function generatePayer(funder: Keypair, lamports?: number) { + const newPayer = Keypair.generate(); + await transferLamports( + funder, + newPayer.publicKey, + lamports === undefined ? 1000000000 : lamports + ); + return newPayer; +} + +export async function getOrCreateAta( + payer: Keypair, + mint: PublicKey, + owner: PublicKey +) { + const program = workspace.WormholeGateway as Program; + const connection = program.provider.connection; + + const token = getAssociatedTokenAddressSync(mint, owner); + const tokenData: Account = await getAccount(connection, token).catch( + (err) => { + if (err instanceof TokenAccountNotFoundError) { + return null; + } else { + throw err; + } + } + ); + + if (tokenData === null) { + await web3.sendAndConfirmTransaction( + connection, + new web3.Transaction().add( + createAssociatedTokenAccountIdempotentInstruction( + payer.publicKey, + token, + owner, + mint + ) + ), + [payer] ); + } + + return token; +} + +export async function getTokenBalance(token: PublicKey): Promise { + const program = workspace.WormholeGateway as Program; + return getAccount(program.provider.connection, token).then( + (account) => account.amount + ); +} + +export async function preloadWrappedTbtc( + payer: Keypair, + ethereumTokenBridge: MockEthereumTokenBridge, + amount: bigint, + tokenOwner: PublicKey +) { + const program = workspace.WormholeGateway as Program; + const connection = program.provider.connection; + + const wrappedTbtcToken = await getOrCreateAta( + payer, + WRAPPED_TBTC_MINT, + tokenOwner + ); + + // Bridge tbtc to token account. + const published = ethereumTokenBridge.publishTransferTokens( + tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), + 2, + amount, + 1, + wrappedTbtcToken.toBuffer().toString("hex"), + BigInt(0), + 0, + 0 + ); + + const signedVaa = await mockSignAndPostVaa(payer, published); + + const tx = await redeemOnSolana( + connection, + CORE_BRIDGE_PROGRAM_ID, + TOKEN_BRIDGE_PROGRAM_ID, + payer.publicKey, + signedVaa + ); + await web3.sendAndConfirmTransaction(connection, tx, [payer]); + + return wrappedTbtcToken; +} + +export async function mockSignAndPostVaa( + payer: web3.Keypair, + published: Buffer +) { + const program = workspace.WormholeGateway as Program; + + const guardians = new MockGuardians(GUARDIAN_SET_INDEX, [ + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", + ]); + + // Add guardian signature. + const signedVaa = guardians.addSignatures(published, [0]); + + // Verify and post VAA. + await postVaaSolana( + program.provider.connection, + new NodeWallet(payer).signTransaction, + CORE_BRIDGE_PROGRAM_ID, + payer.publicKey, + signedVaa + ); + + return signedVaa; +} + +export async function ethereumGatewaySendTbtc( + payer: web3.Keypair, + ethereumTokenBridge: MockEthereumTokenBridge, + amount: bigint, + fromGateway: number[], + toGateway: PublicKey, + recipient: PublicKey, + tokenAddress?: string, + tokenChain?: number +) { + const program = workspace.WormholeGateway as Program; + + const published = ethereumTokenBridge.publishTransferTokensWithPayload( + tryNativeToHexString(tokenAddress ?? ETHEREUM_TBTC_ADDRESS, "ethereum"), + tokenChain ?? 2, + amount, + 1, + toGateway.toBuffer().toString("hex"), + Buffer.from(fromGateway), + recipient.toBuffer(), + 0, + 0 + ); + + const guardians = new MockGuardians( + GUARDIAN_SET_INDEX, + GUARDIAN_DEVNET_PRIVATE_KEYS + ); + + // Add guardian signature. + const signedVaa = guardians.addSignatures(published, [0]); + + // Verify and post VAA. + await postVaaSolana( + program.provider.connection, + new NodeWallet(payer).signTransaction, + CORE_BRIDGE_PROGRAM_ID, + payer.publicKey, + signedVaa + ); + + return signedVaa; } -export async function generatePayer(connection: web3.Connection, payer: Keypair, lamports?: number) { - const newPayer = Keypair.generate(); - await transferLamports(connection, payer, newPayer.publicKey, lamports === undefined ? 1000000000 : lamports); - return newPayer; -} - -export async function getOrCreateTokenAccount(connection: Connection, payer: Keypair, mint: PublicKey, owner: PublicKey) { - const token = getAssociatedTokenAddressSync(mint, owner); - const tokenData: Account = await getAccount(connection, token).catch((err) => { - if (err instanceof TokenAccountNotFoundError) { - return null; - } else { - throw err; - }; - }); - - if (tokenData === null) { - await web3.sendAndConfirmTransaction( - connection, - new web3.Transaction().add( - createAssociatedTokenAccountIdempotentInstruction( - payer.publicKey, - token, - owner, - mint, - ) - ), - [payer] - ); - - return getAccount(connection, token); - } else { - return tokenData; +export async function expectIxSuccess( + ixes: TransactionInstruction[], + signers: Keypair[] +) { + const program = workspace.WormholeGateway as Program; + await sendAndConfirmTransaction( + program.provider.connection, + new Transaction().add(...ixes), + signers + ).catch((err) => { + if (err.logs !== undefined) { + console.log(err.logs); } -} \ No newline at end of file + throw err; + }); +} + +export async function expectIxFail( + ixes: TransactionInstruction[], + signers: Keypair[], + errorMessage: string +) { + const program = workspace.WormholeGateway as Program; + try { + const txSig = await sendAndConfirmTransaction( + program.provider.connection, + new Transaction().add(...ixes), + signers + ); + assert(false, `transaction should have failed: ${txSig}`); + } catch (err) { + if (err.logs === undefined) { + throw err; + } + const logs: string[] = err.logs; + expect(logs.join("\n")).includes(errorMessage); + } +} + +export function getTokenBridgeCoreEmitter() { + const [tokenBridgeCoreEmitter] = PublicKey.findProgramAddressSync( + [Buffer.from("emitter")], + TOKEN_BRIDGE_PROGRAM_ID + ); + + return tokenBridgeCoreEmitter; +} + +export async function getTokenBridgeSequence() { + const program = workspace.WormholeGateway as Program; + const emitter = getTokenBridgeCoreEmitter(); + return coreBridge + .getSequenceTracker( + program.provider.connection, + emitter, + CORE_BRIDGE_PROGRAM_ID + ) + .then((tracker) => tracker.sequence); +} diff --git a/cross-chain/solana/tests/helpers/wormholeGateway.ts b/cross-chain/solana/tests/helpers/wormholeGateway.ts new file mode 100644 index 000000000..efb3f06d6 --- /dev/null +++ b/cross-chain/solana/tests/helpers/wormholeGateway.ts @@ -0,0 +1,809 @@ +import { parseVaa } from "@certusone/wormhole-sdk"; +import * as tokenBridge from "@certusone/wormhole-sdk/lib/cjs/solana/tokenBridge"; +import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; +import { BN, Program, workspace } from "@coral-xyz/anchor"; +import { getAssociatedTokenAddressSync } from "@solana/spl-token"; +import { + PublicKey, + SYSVAR_CLOCK_PUBKEY, + SYSVAR_RENT_PUBKEY, + TransactionInstruction, +} from "@solana/web3.js"; +import { expect } from "chai"; +import { WormholeGateway } from "../../target/types/wormhole_gateway"; +import { + CORE_BRIDGE_DATA, + CORE_BRIDGE_PROGRAM_ID, + ETHEREUM_ENDPOINT, + TBTC_PROGRAM_ID, + TOKEN_BRIDGE_PROGRAM_ID, + WORMHOLE_GATEWAY_PROGRAM_ID, + WRAPPED_TBTC_ASSET, + WRAPPED_TBTC_MINT, +} from "./consts"; +import * as tbtc from "./tbtc"; +import { getTokenBridgeCoreEmitter, getTokenBridgeSequence } from "./utils"; + +export function getCustodianPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("redeemer")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getCoreMessagePDA(sequence: bigint): PublicKey { + const encodedSequence = Buffer.alloc(8); + encodedSequence.writeBigUInt64LE(sequence); + return PublicKey.findProgramAddressSync( + [Buffer.from("msg"), encodedSequence], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getGatewayInfoPDA(targetChain: number): PublicKey { + const encodedChain = Buffer.alloc(2); + encodedChain.writeUInt16LE(targetChain); + return PublicKey.findProgramAddressSync( + [Buffer.from("gateway-info"), encodedChain], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getWrappedTbtcTokenPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("wrapped-token")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getTokenBridgeSenderPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("sender")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getTokenBridgeRedeemerPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("redeemer")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export async function getCustodianData() { + const program = workspace.WormholeGateway as Program; + const custodian = getCustodianPDA(); + return program.account.custodian.fetch(custodian); +} + +export async function checkCustodian(expected: { + authority: PublicKey; + mintingLimit: bigint; + pendingAuthority: PublicKey | null; +}) { + let { authority, mintingLimit, pendingAuthority } = expected; + const custodianState = await getCustodianData(); + + expect(custodianState.mintingLimit.eq(new BN(mintingLimit.toString()))).to.be + .true; + expect(custodianState.authority).to.eql(authority); + expect(custodianState.pendingAuthority).to.eql(pendingAuthority); +} + +export async function getMintedAmount(): Promise { + const custodianState = await getCustodianData(); + return BigInt(custodianState.mintedAmount.toString()); +} + +export async function getGatewayInfo(chain: number) { + const program = workspace.WormholeGateway as Program; + const gatewayInfo = getGatewayInfoPDA(chain); + return program.account.gatewayInfo.fetch(gatewayInfo); +} + +export async function checkGateway(chain: number, expectedAddress: number[]) { + const gatewayInfoState = await getGatewayInfo(chain); + expect(gatewayInfoState.address).to.eql(expectedAddress); +} + +type CancelAuthorityChange = { + custodian?: PublicKey; + authority: PublicKey; +}; + +export async function cancelAuthorityChangeIx( + accounts: CancelAuthorityChange +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, authority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .cancelAuthorityChange() + .accounts({ + custodian, + authority, + }) + .instruction(); +} + +type ChangeAuthorityContext = { + custodian?: PublicKey; + authority: PublicKey; + newAuthority: PublicKey; +}; + +export async function changeAuthorityIx( + accounts: ChangeAuthorityContext +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, authority, newAuthority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .changeAuthority() + .accounts({ + custodian, + authority, + newAuthority, + }) + .instruction(); +} + +type TakeAuthorityContext = { + custodian?: PublicKey; + pendingAuthority: PublicKey; +}; + +export async function takeAuthorityIx( + accounts: TakeAuthorityContext +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, pendingAuthority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .takeAuthority() + .accounts({ + custodian, + pendingAuthority, + }) + .instruction(); +} + +type UpdateMintingLimitContext = { + custodian?: PublicKey; + authority: PublicKey; +}; + +export async function updateMintingLimitIx( + accounts: UpdateMintingLimitContext, + amount: bigint +): Promise { + const program = workspace.WormholeGateway as Program; + + let { custodian, authority } = accounts; + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + return program.methods + .updateMintingLimit(new BN(amount.toString())) + .accounts({ + custodian, + authority, + }) + .instruction(); +} + +type UpdateGatewayAddressContext = { + custodian?: PublicKey; + gatewayInfo?: PublicKey; + authority: PublicKey; +}; + +type UpdateGatewayAddressArgs = { + chain: number; + address: number[]; +}; + +export async function updateGatewayAddress( + accounts: UpdateGatewayAddressContext, + args: UpdateGatewayAddressArgs +) { + const program = workspace.WormholeGateway as Program; + let { custodian, gatewayInfo, authority } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (gatewayInfo === undefined) { + gatewayInfo = getGatewayInfoPDA(args.chain); + } + + return program.methods + .updateGatewayAddress(args) + .accounts({ + custodian, + gatewayInfo, + authority, + }) + .instruction(); +} + +type DepositWormholeTbtcContext = { + custodian?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + recipientWrappedToken: PublicKey; + recipientToken: PublicKey; + recipient: PublicKey; + tbtcConfig?: PublicKey; + tbtcMinterInfo?: PublicKey; + tbtcProgram?: PublicKey; +}; + +export async function depositWormholeTbtcIx( + accounts: DepositWormholeTbtcContext, + amount: bigint +): Promise { + const program = workspace.WormholeGateway as Program; + let { + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientWrappedToken, + recipientToken, + recipient, + tbtcConfig, + tbtcMinterInfo, + tbtcProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (tbtcConfig === undefined) { + tbtcConfig = tbtc.getConfigPDA(); + } + + if (tbtcMinterInfo === undefined) { + tbtcMinterInfo = tbtc.getMinterInfoPDA(custodian); + } + + if (tbtcProgram === undefined) { + tbtcProgram = TBTC_PROGRAM_ID; + } + + return program.methods + .depositWormholeTbtc(new BN(amount.toString())) + .accounts({ + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientWrappedToken, + recipientToken, + recipient, + tbtcConfig, + tbtcMinterInfo, + tbtcProgram, + }) + .instruction(); +} + +type ReceiveTbtcContext = { + payer: PublicKey; + custodian?: PublicKey; + postedVaa?: PublicKey; + tokenBridgeClaim?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + recipientToken: PublicKey; + recipient: PublicKey; + recipientWrappedToken?: PublicKey; + tbtcConfig?: PublicKey; + tbtcMinterInfo?: PublicKey; + tokenBridgeConfig?: PublicKey; + tokenBridgeRegisteredEmitter?: PublicKey; + //tokenBridgeRedeemer?: PublicKey; + tokenBridgeWrappedAsset?: PublicKey; + tokenBridgeMintAuthority?: PublicKey; + rent?: PublicKey; + tbtcProgram?: PublicKey; + tokenBridgeProgram?: PublicKey; + coreBridgeProgram?: PublicKey; +}; + +export async function receiveTbtcIx( + accounts: ReceiveTbtcContext, + signedVaa: Buffer +): Promise { + const parsed = parseVaa(signedVaa); + + const program = workspace.WormholeGateway as Program; + let { + payer, + custodian, + postedVaa, + tokenBridgeClaim, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientToken, + recipient, + recipientWrappedToken, + tbtcConfig, + tbtcMinterInfo, + tokenBridgeConfig, + tokenBridgeRegisteredEmitter, + //tokenBridgeRedeemer, + tokenBridgeWrappedAsset, + tokenBridgeMintAuthority, + rent, + tbtcProgram, + tokenBridgeProgram, + coreBridgeProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (postedVaa === undefined) { + postedVaa = coreBridge.derivePostedVaaKey( + CORE_BRIDGE_PROGRAM_ID, + parsed.hash + ); + } + + if (tokenBridgeClaim === undefined) { + tokenBridgeClaim = coreBridge.deriveClaimKey( + TOKEN_BRIDGE_PROGRAM_ID, + parsed.emitterAddress, + parsed.emitterChain, + parsed.sequence + ); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (recipientWrappedToken == undefined) { + recipientWrappedToken = getAssociatedTokenAddressSync( + wrappedTbtcMint, + recipient + ); + } + + if (tbtcConfig === undefined) { + tbtcConfig = tbtc.getConfigPDA(); + } + + if (tbtcMinterInfo === undefined) { + tbtcMinterInfo = tbtc.getMinterInfoPDA(custodian); + } + + if (tokenBridgeConfig === undefined) { + tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (tokenBridgeRegisteredEmitter === undefined) { + tokenBridgeRegisteredEmitter = ETHEREUM_ENDPOINT; + } + + // if (tokenBridgeRedeemer === undefined) { + // tokenBridgeRedeemer = tokenBridge.deriveRedeemerAccountKey( + // WORMHOLE_GATEWAY_PROGRAM_ID + // ); + // } + + if (tokenBridgeWrappedAsset === undefined) { + tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET; + } + + if (tokenBridgeMintAuthority === undefined) { + tokenBridgeMintAuthority = tokenBridge.deriveMintAuthorityKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (rent === undefined) { + rent = SYSVAR_RENT_PUBKEY; + } + + if (tbtcProgram === undefined) { + tbtcProgram = TBTC_PROGRAM_ID; + } + + if (tokenBridgeProgram === undefined) { + tokenBridgeProgram = TOKEN_BRIDGE_PROGRAM_ID; + } + + if (coreBridgeProgram === undefined) { + coreBridgeProgram = CORE_BRIDGE_PROGRAM_ID; + } + + return program.methods + .receiveTbtc(Array.from(parsed.hash)) + .accounts({ + payer, + custodian, + postedVaa, + tokenBridgeClaim, + wrappedTbtcToken, + tbtcMint, + recipientToken, + recipient, + recipientWrappedToken, + tbtcConfig, + tbtcMinterInfo, + wrappedTbtcMint, + tokenBridgeConfig, + tokenBridgeRegisteredEmitter, + //tokenBridgeRedeemer, + tokenBridgeWrappedAsset, + tokenBridgeMintAuthority, + rent, + tbtcProgram, + tokenBridgeProgram, + coreBridgeProgram, + }) + .instruction(); +} + +type SendTbtcGatewayContext = { + custodian?: PublicKey; + gatewayInfo?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + senderToken: PublicKey; + sender: PublicKey; + tokenBridgeConfig?: PublicKey; + tokenBridgeWrappedAsset?: PublicKey; + tokenBridgeTransferAuthority?: PublicKey; + coreBridgeData?: PublicKey; + coreMessage?: PublicKey; + tokenBridgeCoreEmitter?: PublicKey; + coreEmitterSequence?: PublicKey; + coreFeeCollector?: PublicKey; + clock?: PublicKey; + tokenBridgeSender?: PublicKey; + rent?: PublicKey; + tokenBridgeProgram?: PublicKey; + coreBridgeProgram?: PublicKey; +}; + +type SendTbtcGatewayArgs = { + amount: BN; + recipientChain: number; + recipient: number[]; + nonce: number; +}; + +export async function sendTbtcGatewayIx( + accounts: SendTbtcGatewayContext, + args: SendTbtcGatewayArgs +): Promise { + const program = workspace.WormholeGateway as Program; + let { + custodian, + gatewayInfo, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + tokenBridgeSender, + rent, + tokenBridgeProgram, + coreBridgeProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (gatewayInfo === undefined) { + gatewayInfo = getGatewayInfoPDA(args.recipientChain); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (tokenBridgeConfig === undefined) { + tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (tokenBridgeWrappedAsset === undefined) { + tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET; + } + + if (tokenBridgeTransferAuthority === undefined) { + tokenBridgeTransferAuthority = tokenBridge.deriveAuthoritySignerKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (coreBridgeData === undefined) { + coreBridgeData = CORE_BRIDGE_DATA; + } + + if (coreMessage === undefined) { + const sequence = await getTokenBridgeSequence(); + coreMessage = getCoreMessagePDA(sequence); + } + + if (tokenBridgeCoreEmitter === undefined) { + tokenBridgeCoreEmitter = getTokenBridgeCoreEmitter(); + } + + if (coreEmitterSequence === undefined) { + coreEmitterSequence = coreBridge.deriveEmitterSequenceKey( + tokenBridgeCoreEmitter, + CORE_BRIDGE_PROGRAM_ID + ); + } + + if (coreFeeCollector === undefined) { + coreFeeCollector = coreBridge.deriveFeeCollectorKey(CORE_BRIDGE_PROGRAM_ID); + } + + if (clock === undefined) { + clock = SYSVAR_CLOCK_PUBKEY; + } + + if (tokenBridgeSender === undefined) { + tokenBridgeSender = tokenBridge.deriveSenderAccountKey( + WORMHOLE_GATEWAY_PROGRAM_ID + ); + } + + if (rent === undefined) { + rent = SYSVAR_RENT_PUBKEY; + } + + if (tokenBridgeProgram === undefined) { + tokenBridgeProgram = TOKEN_BRIDGE_PROGRAM_ID; + } + + if (coreBridgeProgram === undefined) { + coreBridgeProgram = CORE_BRIDGE_PROGRAM_ID; + } + + return program.methods + .sendTbtcGateway(args) + .accounts({ + custodian, + gatewayInfo, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + tokenBridgeSender, + rent, + tokenBridgeProgram, + coreBridgeProgram, + }) + .instruction(); +} + +type SendTbtcWrappedContext = { + custodian?: PublicKey; + wrappedTbtcToken?: PublicKey; + wrappedTbtcMint?: PublicKey; + tbtcMint?: PublicKey; + senderToken: PublicKey; + sender: PublicKey; + tokenBridgeConfig?: PublicKey; + tokenBridgeWrappedAsset?: PublicKey; + tokenBridgeTransferAuthority?: PublicKey; + coreBridgeData?: PublicKey; + coreMessage?: PublicKey; + tokenBridgeCoreEmitter?: PublicKey; + coreEmitterSequence?: PublicKey; + coreFeeCollector?: PublicKey; + clock?: PublicKey; + rent?: PublicKey; + tokenBridgeProgram?: PublicKey; + coreBridgeProgram?: PublicKey; +}; + +type SendTbtcWrappedArgs = { + amount: BN; + recipientChain: number; + recipient: number[]; + arbiterFee: BN; + nonce: number; +}; + +export async function sendTbtcWrappedIx( + accounts: SendTbtcWrappedContext, + args: SendTbtcWrappedArgs +): Promise { + const program = workspace.WormholeGateway as Program; + let { + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + rent, + tokenBridgeProgram, + coreBridgeProgram, + } = accounts; + + if (custodian === undefined) { + custodian = getCustodianPDA(); + } + + if (wrappedTbtcToken === undefined) { + wrappedTbtcToken = getWrappedTbtcTokenPDA(); + } + + if (wrappedTbtcMint === undefined) { + wrappedTbtcMint = WRAPPED_TBTC_MINT; + } + + if (tbtcMint === undefined) { + tbtcMint = tbtc.getMintPDA(); + } + + if (tokenBridgeConfig === undefined) { + tokenBridgeConfig = tokenBridge.deriveTokenBridgeConfigKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (tokenBridgeWrappedAsset === undefined) { + tokenBridgeWrappedAsset = WRAPPED_TBTC_ASSET; + } + + if (tokenBridgeTransferAuthority === undefined) { + tokenBridgeTransferAuthority = tokenBridge.deriveAuthoritySignerKey( + TOKEN_BRIDGE_PROGRAM_ID + ); + } + + if (coreBridgeData === undefined) { + coreBridgeData = CORE_BRIDGE_DATA; + } + + if (coreMessage === undefined) { + const sequence = await getTokenBridgeSequence(); + coreMessage = getCoreMessagePDA(sequence); + } + + if (tokenBridgeCoreEmitter === undefined) { + tokenBridgeCoreEmitter = getTokenBridgeCoreEmitter(); + } + + if (coreEmitterSequence === undefined) { + coreEmitterSequence = coreBridge.deriveEmitterSequenceKey( + tokenBridgeCoreEmitter, + CORE_BRIDGE_PROGRAM_ID + ); + } + + if (coreFeeCollector === undefined) { + coreFeeCollector = coreBridge.deriveFeeCollectorKey(CORE_BRIDGE_PROGRAM_ID); + } + + if (clock === undefined) { + clock = SYSVAR_CLOCK_PUBKEY; + } + + if (rent === undefined) { + rent = SYSVAR_RENT_PUBKEY; + } + + if (tokenBridgeProgram === undefined) { + tokenBridgeProgram = TOKEN_BRIDGE_PROGRAM_ID; + } + + if (coreBridgeProgram === undefined) { + coreBridgeProgram = CORE_BRIDGE_PROGRAM_ID; + } + + return program.methods + .sendTbtcWrapped(args) + .accounts({ + custodian, + wrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + senderToken, + sender, + tokenBridgeConfig, + tokenBridgeWrappedAsset, + tokenBridgeTransferAuthority, + coreBridgeData, + coreMessage, + tokenBridgeCoreEmitter, + coreEmitterSequence, + coreFeeCollector, + clock, + rent, + tokenBridgeProgram, + coreBridgeProgram, + }) + .instruction(); +}