From 426d09742ffa58521935e61917d65d67f9211927 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Mon, 7 Aug 2023 09:21:57 -0500 Subject: [PATCH] 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; }