From 05a01b286518cd7a76ef8d265ad98bce6eb8ca9b Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Wed, 2 Aug 2023 15:15:52 +0100 Subject: [PATCH 01/38] Move tbtc PDA functions to helpers --- cross-chain/solana/tests/01__tbtc.ts | 47 +--------------- .../solana/tests/helpers/tbtcHelpers.ts | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 46 deletions(-) create mode 100644 cross-chain/solana/tests/helpers/tbtcHelpers.ts diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 0a70e3d93..28b13556e 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -6,6 +6,7 @@ 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"; +import { getConfigPDA, getTokenPDA, getMinterPDA, getGuardianPDA } from "./helpers/tbtcHelpers"; function maybeAuthorityAnd( signer, @@ -126,40 +127,7 @@ async function checkPaused( } -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, @@ -212,19 +180,6 @@ async function removeMinter( .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, diff --git a/cross-chain/solana/tests/helpers/tbtcHelpers.ts b/cross-chain/solana/tests/helpers/tbtcHelpers.ts new file mode 100644 index 000000000..db05c47cf --- /dev/null +++ b/cross-chain/solana/tests/helpers/tbtcHelpers.ts @@ -0,0 +1,54 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import * as web3 from '@solana/web3.js'; +import { Tbtc } from "../../target/types/tbtc"; + +export function getConfigPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('config'), + ], + program.programId + ); +} + +export function getTokenPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('tbtc-mint'), + ], + program.programId + ); +} + +export function getMinterPDA( + program: Program, + minter +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('minter-info'), + minter.publicKey.toBuffer(), + ], + program.programId + ); +} + + +export function getGuardianPDA( + program: Program, + guardian +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('guardian-info'), + guardian.publicKey.toBuffer(), + ], + program.programId + ); +} + \ No newline at end of file From 97e13f0d25011e4282044559b704c281ca3d5fa5 Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Wed, 2 Aug 2023 16:37:58 +0100 Subject: [PATCH 02/38] Add wormhole gateway setup test --- .../src/processor/initialize.rs | 7 +- .../solana/tests/02__wormholeGateway.ts | 57 +++++++++++++--- .../tests/helpers/wormholeGatewayHelpers.ts | 67 +++++++++++++++++++ 3 files changed, 118 insertions(+), 13 deletions(-) create mode 100644 cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs index f73831546..540270f2e 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; use anchor_spl::token; use wormhole_anchor_sdk::token_bridge; -const TBTC_FOREIGN_TOKEN_CHAIN: u8 = 2; +const TBTC_FOREIGN_TOKEN_CHAIN: u16 = 2; #[cfg(feature = "mainnet")] const TBTC_FOREIGN_TOKEN_ADDRESS: [u8; 32] = [ @@ -48,7 +48,8 @@ pub struct Initialize<'info> { &TBTC_FOREIGN_TOKEN_CHAIN.to_be_bytes(), TBTC_FOREIGN_TOKEN_ADDRESS.as_ref() ], - bump + bump, + seeds::program = token_bridge::program::ID )] wrapped_tbtc_mint: Account<'info, token::Mint>, @@ -84,7 +85,7 @@ pub struct Initialize<'info> { 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(), tbtc_mint: ctx.accounts.tbtc_mint.key(), wrapped_tbtc_mint: ctx.accounts.wrapped_tbtc_mint.key(), diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 69eb14fe3..47390f8ae 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -9,7 +9,10 @@ import * as spl from "@solana/spl-token"; import { expect } from 'chai'; import { WormholeGateway } from "../target/types/wormhole_gateway"; import { generatePayer, getOrCreateTokenAccount } from "./helpers/utils"; +import { getCustodianPDA, getTokenBridgeRedeemerPDA, getTokenBridgeSenderPDA, getWrappedTbtcTokenPDA } from "./helpers/wormholeGatewayHelpers"; +import { getConfigPDA, getTokenPDA, getMinterPDA, getGuardianPDA } from "./helpers/tbtcHelpers"; import { web3 } from "@coral-xyz/anchor"; +import { Tbtc } from "../target/types/tbtc"; const SOLANA_CORE_BRIDGE_ADDRESS = "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"; const SOLANA_TOKEN_BRIDGE_ADDRESS = "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb"; @@ -18,17 +21,49 @@ 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 - ); + tbtc: Program, + authority, + mintingLimit: number +) { + const [custodian,] = getCustodianPDA(program); + const [tbtcMint,] = getTokenPDA(tbtc); + const [gatewayWrappedTbtcToken,] = getWrappedTbtcTokenPDA(program); + const [tokenBridgeSender,] = getTokenBridgeSenderPDA(program); + const [tokenBridgeRedeemer,] = getTokenBridgeRedeemerPDA(program); + + const connection = program.provider.connection; + + const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey(SOLANA_TOKEN_BRIDGE_ADDRESS, 2, ETHEREUM_TBTC_ADDRESS); + + await program.methods + .initialize(new anchor.BN(mintingLimit)) + .accounts({ + authority: authority.publicKey, + custodian, + tbtcMint, + wrappedTbtcMint, + wrappedTbtcToken: gatewayWrappedTbtcToken, + tokenBridgeSender, + tokenBridgeRedeemer, + }) + .rpc(); } +async function checkState( + program: Program, + expectedAuthority, + expectedMintingLimit, + // expectedMintedAmount, +) { + const [custodian,] = getCustodianPDA(program); + let custodianState = await program.account.custodian.fetch(custodian); + + expect(custodianState.mintingLimit.eq(new anchor.BN(expectedMintingLimit))).to.be.true; + expect(custodianState.authority).to.eql(expectedAuthority.publicKey); +} describe("wormhole-gateway", () => { // Configure the client to use the local cluster. @@ -37,6 +72,8 @@ describe("wormhole-gateway", () => { const program = anchor.workspace.WormholeGateway as Program; const connection = program.provider.connection; + const tbtcProgram = anchor.workspace.Tbtc as Program; + const authority = (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet; const newAuthority = anchor.web3.Keypair.generate(); const minterKeys = anchor.web3.Keypair.generate(); @@ -90,8 +127,8 @@ describe("wormhole-gateway", () => { }); it('setup', async () => { - // await setup(program, authority); - // await checkState(program, authority, 0, 0, 0); + await setup(program, tbtcProgram, authority, 1000); + await checkState(program, authority, 1000); }); }); diff --git a/cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts b/cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts new file mode 100644 index 000000000..90d2ee31d --- /dev/null +++ b/cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts @@ -0,0 +1,67 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import * as web3 from '@solana/web3.js'; +import { WormholeGateway } from "../../target/types/wormhole_gateway"; + +export function getCustodianPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('custodian'), + ], + program.programId + ); +} + +export function getGatewayInfoPDA( + program: Program, + targetChain +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('gateway-info'), + toBytesLE(targetChain), + ], + program.programId + ); +} + +export function getWrappedTbtcTokenPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('wrapped-token'), + ], + program.programId + ); +} + +export function getTokenBridgeSenderPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('sender'), + ], + program.programId + ); +} + +export function getTokenBridgeRedeemerPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('redeemer'), + ], + program.programId + ); +} + +function toBytesLE(x): Buffer { + const buf = Buffer.alloc(2); + buf.writeUint16LE(x); + return buf; +} \ No newline at end of file From 79728bca9e3697256899b3fb6e3054a40d9ef01f Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Wed, 2 Aug 2023 17:31:43 +0100 Subject: [PATCH 03/38] Add test for depositing wrapped tokens --- cross-chain/solana/tests/01__tbtc.ts | 72 +++--------------- .../solana/tests/02__wormholeGateway.ts | 76 +++++++++++++++++-- .../solana/tests/helpers/tbtcHelpers.ts | 53 ++++++++++++- 3 files changed, 131 insertions(+), 70 deletions(-) diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 28b13556e..b71473f87 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -6,14 +6,7 @@ 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"; -import { getConfigPDA, getTokenPDA, getMinterPDA, getGuardianPDA } from "./helpers/tbtcHelpers"; - -function maybeAuthorityAnd( - signer, - signers -) { - return signers.concat(signer instanceof (anchor.Wallet as any) ? [] : [signer]); -} +import { maybeAuthorityAnd, getConfigPDA, getTokenPDA, getMinterPDA, getGuardianPDA, checkState, addMinter } from "./helpers/tbtcHelpers"; async function setup( program: Program, @@ -32,26 +25,6 @@ async function setup( .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, @@ -127,34 +100,11 @@ async function checkPaused( } - - -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); + const [minterInfoPDA, bump] = getMinterPDA(program, minter.publicKey); let minterInfo = await program.account.minterInfo.fetch(minterInfoPDA); expect(minterInfo.minter).to.eql(minter.publicKey); @@ -397,7 +347,7 @@ describe("tbtc", () => { it('add minter', async () => { await checkState(program, authority, 0, 0, 0); - await addMinter(program, authority, minterKeys, authority); + await addMinter(program, authority, minterKeys.publicKey); await checkMinter(program, minterKeys); await checkState(program, authority, 1, 0, 0); @@ -416,7 +366,7 @@ describe("tbtc", () => { // ); try { - await addMinter(program, impostorKeys, minter2Keys, authority); + await addMinter(program, impostorKeys, minter2Keys.publicKey); chai.assert(false, "should've failed but didn't"); } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); @@ -428,7 +378,7 @@ describe("tbtc", () => { it('mint', async () => { await checkState(program, authority, 1, 0, 0); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); + const [minterInfoPDA, _] = getMinterPDA(program, minterKeys.publicKey); await checkMinter(program, minterKeys); // await setupMint(program, authority, recipientKeys); @@ -449,7 +399,7 @@ describe("tbtc", () => { it('won\'t mint', async () => { await checkState(program, authority, 1, 0, 1000); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); + const [minterInfoPDA, _] = getMinterPDA(program, minterKeys.publicKey); await checkMinter(program, minterKeys); // await setupMint(program, authority, recipientKeys); @@ -467,9 +417,9 @@ describe("tbtc", () => { it('use two minters', async () => { await checkState(program, authority, 1, 0, 1000); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); + const [minterInfoPDA, _] = getMinterPDA(program, minterKeys.publicKey); await checkMinter(program, minterKeys); - const minter2InfoPDA = await addMinter(program, authority, minter2Keys, authority); + const minter2InfoPDA = await addMinter(program, authority, minter2Keys.publicKey); await checkMinter(program, minter2Keys); await checkState(program, authority, 2, 0, 1000); // await setupMint(program, authority, recipientKeys); @@ -502,7 +452,7 @@ describe("tbtc", () => { it('remove minter', async () => { await checkState(program, authority, 2, 0, 1500); - const [minter2InfoPDA, _] = getMinterPDA(program, minter2Keys); + const [minter2InfoPDA, _] = getMinterPDA(program, minter2Keys.publicKey); await checkMinter(program, minter2Keys); await removeMinter(program, authority, minter2Keys, minter2InfoPDA); await checkState(program, authority, 1, 0, 1500); @@ -510,7 +460,7 @@ describe("tbtc", () => { it('won\'t remove minter', async () => { await checkState(program, authority, 1, 0, 1500); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys); + const [minterInfoPDA, _] = getMinterPDA(program, minterKeys.publicKey); await checkMinter(program, minterKeys); try { @@ -611,7 +561,7 @@ describe("tbtc", () => { it('won\'t mint when paused', async () => { await checkState(program, authority, 0, 1, 1500); - const minterInfoPDA = await addMinter(program, authority, minterKeys, authority); + const minterInfoPDA = await addMinter(program, authority, minterKeys.publicKey); await pause(program, guardianKeys); // await setupMint(program, authority, recipientKeys); diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 47390f8ae..cf07e7c8e 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -10,7 +10,7 @@ import { expect } from 'chai'; import { WormholeGateway } from "../target/types/wormhole_gateway"; import { generatePayer, getOrCreateTokenAccount } from "./helpers/utils"; import { getCustodianPDA, getTokenBridgeRedeemerPDA, getTokenBridgeSenderPDA, getWrappedTbtcTokenPDA } from "./helpers/wormholeGatewayHelpers"; -import { getConfigPDA, getTokenPDA, getMinterPDA, getGuardianPDA } from "./helpers/tbtcHelpers"; +import * as tbtc from "./helpers/tbtcHelpers"; import { web3 } from "@coral-xyz/anchor"; import { Tbtc } from "../target/types/tbtc"; @@ -24,18 +24,16 @@ const GUARDIAN_SET_INDEX = 3; async function setup( program: Program, - tbtc: Program, + tbtcProgram: Program, authority, mintingLimit: number ) { const [custodian,] = getCustodianPDA(program); - const [tbtcMint,] = getTokenPDA(tbtc); + const [tbtcMint,] = tbtc.getTokenPDA(tbtcProgram); const [gatewayWrappedTbtcToken,] = getWrappedTbtcTokenPDA(program); const [tokenBridgeSender,] = getTokenBridgeSenderPDA(program); const [tokenBridgeRedeemer,] = getTokenBridgeRedeemerPDA(program); - const connection = program.provider.connection; - const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey(SOLANA_TOKEN_BRIDGE_ADDRESS, 2, ETHEREUM_TBTC_ADDRESS); await program.methods @@ -74,6 +72,13 @@ describe("wormhole-gateway", () => { const tbtcProgram = anchor.workspace.Tbtc as Program; + const [custodian,] = getCustodianPDA(program); + const [tbtcMint,] = tbtc.getTokenPDA(tbtcProgram); + const [tbtcConfig,] = tbtc.getConfigPDA(tbtcProgram); + const [gatewayWrappedTbtcToken,] = getWrappedTbtcTokenPDA(program); + const [tokenBridgeSender,] = getTokenBridgeSenderPDA(program); + const [tokenBridgeRedeemer,] = getTokenBridgeRedeemerPDA(program); + const authority = (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet; const newAuthority = anchor.web3.Keypair.generate(); const minterKeys = anchor.web3.Keypair.generate(); @@ -127,9 +132,66 @@ describe("wormhole-gateway", () => { }); it('setup', async () => { - await setup(program, tbtcProgram, authority, 1000); - await checkState(program, authority, 1000); + await setup(program, tbtcProgram, authority, 10000); + await checkState(program, authority, 10000); + await tbtc.checkState(tbtcProgram, authority, 1, 2, 1500); }); + + it('deposit wrapped tokens', async () => { + const [custodian,] = getCustodianPDA(program); + const minterInfo = await tbtc.addMinter(tbtcProgram, authority, custodian); + + // 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 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 recipientToken = await getOrCreateTokenAccount(connection, payer, tbtcMint, payer.publicKey); + + await program.methods + .depositWormholeTbtc(new anchor.BN(500)) + .accounts({ + custodian, + wrappedTbtcToken: gatewayWrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientWrappedToken: wrappedTbtcToken.address, + recipientToken: recipientToken.address, + recipient: payer.publicKey, + tbtcConfig, + minterInfo, + tbtcProgram: tbtcProgram.programId, + }) + .signers(tbtc.maybeAuthorityAnd(payer, [])) + .rpc(); + + await tbtc.checkState(tbtcProgram, authority, 2, 2, 2000); + }) }); async function mockSignAndPostVaa(connection: web3.Connection, payer: web3.Keypair, published: Buffer) { diff --git a/cross-chain/solana/tests/helpers/tbtcHelpers.ts b/cross-chain/solana/tests/helpers/tbtcHelpers.ts index db05c47cf..19a06deaa 100644 --- a/cross-chain/solana/tests/helpers/tbtcHelpers.ts +++ b/cross-chain/solana/tests/helpers/tbtcHelpers.ts @@ -1,7 +1,16 @@ import * as anchor 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 { Tbtc } from "../../target/types/tbtc"; +import { expect } from 'chai'; + +export function maybeAuthorityAnd( + signer, + signers + ) { + return signers.concat(signer instanceof (anchor.Wallet as any) ? [] : [signer]); + } export function getConfigPDA( program: Program, @@ -32,7 +41,7 @@ export function getMinterPDA( return web3.PublicKey.findProgramAddressSync( [ Buffer.from('minter-info'), - minter.publicKey.toBuffer(), + minter.toBuffer(), ], program.programId ); @@ -51,4 +60,44 @@ export function getGuardianPDA( program.programId ); } - \ No newline at end of file + +export 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)); + } + +export async function addMinter( + program: Program, + authority, + minter + ): Promise { + const [config,] = getConfigPDA(program); + const [minterInfoPDA, _] = getMinterPDA(program, minter); + await program.methods + .addMinter() + .accounts({ + config, + authority: authority.publicKey, + minter, + minterInfo: minterInfoPDA, + }) + .signers(maybeAuthorityAnd(authority, [])) + .rpc(); + return minterInfoPDA; + } \ No newline at end of file From 1887888e3eb030a25c056779b2b33b0a66888c4f Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Wed, 2 Aug 2023 17:44:27 +0100 Subject: [PATCH 04/38] Add minor tests --- .../solana/tests/02__wormholeGateway.ts | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index cf07e7c8e..9b4d712a7 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -4,7 +4,7 @@ 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 * as anchor from "@coral-xyz/anchor"; -import { Program } from "@coral-xyz/anchor"; +import { Program, AnchorError } from "@coral-xyz/anchor"; import * as spl from "@solana/spl-token"; import { expect } from 'chai'; import { WormholeGateway } from "../target/types/wormhole_gateway"; @@ -137,6 +137,17 @@ describe("wormhole-gateway", () => { await tbtc.checkState(tbtcProgram, authority, 1, 2, 1500); }); + it('update minting limit', async () => { + await program.methods + .updateMintingLimit(new anchor.BN(20000)) + .accounts({ + custodian, + authority: authority.publicKey + }) + .rpc(); + await checkState(program, authority, 20000); + }); + it('deposit wrapped tokens', async () => { const [custodian,] = getCustodianPDA(program); const minterInfo = await tbtc.addMinter(tbtcProgram, authority, custodian); @@ -191,7 +202,32 @@ describe("wormhole-gateway", () => { .rpc(); await tbtc.checkState(tbtcProgram, authority, 2, 2, 2000); - }) + + try { + await program.methods + .depositWormholeTbtc(new anchor.BN(50000)) + .accounts({ + custodian, + wrappedTbtcToken: gatewayWrappedTbtcToken, + wrappedTbtcMint, + tbtcMint, + recipientWrappedToken: wrappedTbtcToken.address, + recipientToken: recipientToken.address, + recipient: payer.publicKey, + tbtcConfig, + minterInfo, + tbtcProgram: tbtcProgram.programId, + }) + .signers(tbtc.maybeAuthorityAnd(payer, [])) + .rpc(); + 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('MintingLimitExceeded'); + expect(err.program.equals(program.programId)).is.true; + } + }); }); async function mockSignAndPostVaa(connection: web3.Connection, payer: web3.Keypair, published: Buffer) { From a3bf2e97c3b18b73409b550f55277afd77ddb754 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 08:56:10 -0500 Subject: [PATCH 05/38] solana: lint --- cross-chain/solana/Makefile | 7 +++++-- .../programs/tbtc/src/processor/admin/take_authority.rs | 2 +- .../programs/wormhole-gateway/src/processor/initialize.rs | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cross-chain/solana/Makefile b/cross-chain/solana/Makefile index 6fe96802c..e232a79f1 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,7 @@ endif test: node_modules anchor test --arch sbf + +lint: + cargo fmt --check + cargo clippy --no-deps -- -D warnings \ No newline at end of file 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..f5c8122e0 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 @@ -21,4 +21,4 @@ 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/wormhole-gateway/src/processor/initialize.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs index f73831546..11db8c1e2 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs @@ -7,15 +7,15 @@ 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, + 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, + 0x33, 0xd3, 0x75, 0x92, 0xfa, 0x26, 0x33, 0xfD, 0x49, 0xa7, 0x4d, 0xd9, 0x3a, 0x88, ]; #[derive(Accounts)] From cbf3b8119e037a46754e97c812cb1839221fc491 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 10:29:57 -0500 Subject: [PATCH 06/38] solana: fix wrapped mint pda --- .../programs/wormhole-gateway/src/processor/initialize.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs index 11db8c1e2..81523e7a4 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs @@ -3,7 +3,7 @@ use anchor_lang::prelude::*; use anchor_spl::token; use wormhole_anchor_sdk::token_bridge; -const TBTC_FOREIGN_TOKEN_CHAIN: u8 = 2; +const TBTC_FOREIGN_TOKEN_CHAIN: u16 = 2; #[cfg(feature = "mainnet")] const TBTC_FOREIGN_TOKEN_ADDRESS: [u8; 32] = [ @@ -48,7 +48,8 @@ pub struct Initialize<'info> { &TBTC_FOREIGN_TOKEN_CHAIN.to_be_bytes(), TBTC_FOREIGN_TOKEN_ADDRESS.as_ref() ], - bump + bump, + seeds::program = token_bridge::program::ID )] wrapped_tbtc_mint: Account<'info, token::Mint>, From eb5cb5b1beada75b3341c8cdc91ef50b1acb2ae1 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 12:59:08 -0500 Subject: [PATCH 07/38] solana: add receive_tbtc; add minted_amount --- .../wormhole-gateway/src/constants.rs | 14 + .../programs/wormhole-gateway/src/error.rs | 23 +- .../programs/wormhole-gateway/src/lib.rs | 8 +- .../src/processor/deposit_wormhole_tbtc.rs | 8 +- .../src/processor/initialize.rs | 25 +- .../wormhole-gateway/src/processor/mod.rs | 4 +- .../src/processor/receive_tbtc.rs | 258 ++++++++++++++---- .../src/processor/send_tbtc/gateway.rs | 9 +- .../src/processor/send_tbtc/mod.rs | 8 +- .../src/processor/send_tbtc/wrapped.rs | 9 +- .../wormhole-gateway/src/state/custodian.rs | 1 + 11 files changed, 283 insertions(+), 84 deletions(-) create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/constants.rs 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..31180515f --- /dev/null +++ b/cross-chain/solana/programs/wormhole-gateway/src/constants.rs @@ -0,0 +1,14 @@ +pub const TBTC_ETHEREUM_TOKEN_CHAIN: u16 = 2; + +#[cfg(feature = "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, +]; + +/// TODO: Fix this to reflect testnet contract address. +#[cfg(feature = "solana-devnet")] +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, +]; diff --git a/cross-chain/solana/programs/wormhole-gateway/src/error.rs b/cross-chain/solana/programs/wormhole-gateway/src/error.rs index 09a8b136c..f1ced8941 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/error.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/error.rs @@ -8,15 +8,30 @@ pub enum WormholeGatewayError { #[msg("Only custodian authority is permitted for this action.")] IsNotAuthority = 0x20, - #[msg("0x0 recipient not allowed")] + #[msg("0x0 recipient not allowed.")] ZeroRecipient = 0x30, - #[msg("Not enough wormhole tBTC in the gateway to bridge")] + #[msg("Not enough wormhole tBTC in the gateway to bridge.")] NotEnoughWrappedTbtc = 0x40, - #[msg("Amount must not be 0")] + #[msg("Amount must not be 0.")] ZeroAmount = 0x50, - #[msg("Amount too low to bridge")] + #[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, } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/lib.rs b/cross-chain/solana/programs/wormhole-gateway/src/lib.rs index b58d6d681..6d525a270 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/lib.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/lib.rs @@ -1,5 +1,7 @@ #![allow(clippy::result_large_err)] +pub mod constants; + pub mod error; mod processor; @@ -41,9 +43,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/deposit_wormhole_tbtc.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/deposit_wormhole_tbtc.rs index 28ccdc59a..92a498595 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 @@ -59,9 +59,9 @@ pub struct DepositWormholeTbtc<'info> { impl<'info> DepositWormholeTbtc<'info> { fn constraints(ctx: &Context, amount: u64) -> Result<()> { - require_gt!( + require_gte!( ctx.accounts.custodian.minting_limit, - ctx.accounts.tbtc_mint.supply.saturating_add(amount), + ctx.accounts.custodian.minted_amount.saturating_add(amount), WormholeGatewayError::MintingLimitExceeded ); @@ -84,6 +84,10 @@ pub fn deposit_wormhole_tbtc(ctx: Context, amount: u64) -> amount, )?; + // Account for minted amount. + ctx.accounts.custodian.minted_amount = + ctx.accounts.custodian.minted_amount.saturating_add(amount); + let custodian = &ctx.accounts.custodian; // Now mint. diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs index 81523e7a4..e2c8400bf 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/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: u16 = 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,8 +33,8 @@ 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, seeds::program = token_bridge::program::ID @@ -95,6 +83,7 @@ pub fn initialize(ctx: Context, minting_limit: u64) -> Result<()> { 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/mod.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/mod.rs index 8948c9b82..5023cbdf1 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/mod.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/mod.rs @@ -4,8 +4,8 @@ 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::*; 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..49187710e 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,243 @@ -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 = token_bridge_redeemer, )] custodian: Account<'info, Custodian>, - // TODO: posted_vaa - #[account(mut)] - tbtc_mint: Account<'info, token::Mint>, + #[account( + seeds = [core_bridge::SEED_PREFIX_POSTED_VAA, &message_hash], + bump, + seeds::program = core_bridge_program + )] + posted_vaa: Box>>, - /// CHECK: This account is needed fot the TBTC program. - tbtc_config: UncheckedAccount<'info>, + /// 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). + token_bridge_claim: AccountInfo<'info>, - /// CHECK: This account is needed fot the TBTC program. - minter_info: UncheckedAccount<'info>, + /// Custody account. + wrapped_tbtc_token: Box>, - // Use the associated token account for the recipient. + #[account(mut)] + tbtc_mint: Box>, + + /// 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. + wrapped_tbtc_mint: 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_redeemer: 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>, + associated_token_program: Program<'info, associated_token::AssociatedToken>, + token_bridge_program: Program<'info, TokenBridge>, + core_bridge_program: Program<'info, CoreBridge>, + 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_gte!( + 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.token_bridge_redeemer.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.token_bridge_redeemer_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 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 { + 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: ctx.accounts.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..8e81142af 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 @@ -55,7 +55,7 @@ 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( @@ -120,7 +120,6 @@ 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; @@ -129,7 +128,7 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg // handle dust since tBTC has >8 decimals). let amount = 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, @@ -140,6 +139,8 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg amount, )?; + 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 +153,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(), 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..5996e879b 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>, @@ -51,6 +51,12 @@ pub fn burn_and_prepare_transfer(prepare_transfer: PrepareTransfer, amount: u64) let truncated = 10 * (amount / 10); require_gt!(truncated, 0, WormholeGatewayError::TruncatedZeroAmount); + // Account for burning tBTC. + custodian + .minted_amount + .checked_sub(truncated) + .ok_or(WormholeGatewayError::MintedAmountUnderflow)?; + // Burn TBTC mint. token::burn( CpiContext::new( 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..f6d9ca734 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 @@ -48,7 +48,7 @@ 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( @@ -111,7 +111,6 @@ 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; @@ -119,7 +118,7 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg // Prepare for wrapped tBTC transfer. let amount = 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, @@ -130,6 +129,8 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg amount, )?; + let custodian = &ctx.accounts.custodian; + // 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 { @@ -142,7 +143,7 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg 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_bridge_data.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), 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..cd792c027 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs @@ -15,6 +15,7 @@ pub struct Custodian { pub token_bridge_redeemer_bump: u8, pub minting_limit: u64, + pub minted_amount: u64, } impl Custodian { From ae5b9b5f8d1ed999e36cd7e3d87396db52bc2ea2 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 14:21:31 -0500 Subject: [PATCH 08/38] solana: clean up errors --- cross-chain/solana/programs/tbtc/src/error.rs | 37 ++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/cross-chain/solana/programs/tbtc/src/error.rs b/cross-chain/solana/programs/tbtc/src/error.rs index 5e1b279d5..3cf0253fd 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("This address is already a minter.")] + MinterAlreadyExists = 0x10, + + #[msg("This address is not a minter.")] + MinterNonexistent = 0x12, + + #[msg("This address is already a guardian.")] + GuardianAlreadyExists = 0x20, + + #[msg("This address is not a guardian.")] + GuardianNonexistent = 0x22, + + #[msg("Caller is not a guardian.")] + SignerNotGuardian = 0x30, + + #[msg("Caller is not a minter.")] + SignerNotMinter = 0x32, + + #[msg("Program is paused.")] + IsPaused = 0x40, + + #[msg("Program is not paused.")] + IsNotPaused = 0x42, + + #[msg("Not valid authority to perform this action.")] + IsNotAuthority = 0x50, + + #[msg("Not valid pending authority to take authority.")] + IsNotPendingAuthority = 0x52, + + #[msg("No pending authority.")] + NoPendingAuthorityChange = 0x54, } From 2506576071fe596e952b3edbe549b3915bb2e1ce Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 15:28:42 -0500 Subject: [PATCH 09/38] solana: add guardians acct for indexing --- .../tbtc/src/processor/admin/add_guardian.rs | 25 ++++++++++++-- .../tbtc/src/processor/admin/initialize.rs | 20 ++++++++++- .../src/processor/admin/remove_guardian.rs | 33 +++++++++++++++++-- .../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/mod.rs | 3 ++ cross-chain/solana/tests/01__tbtc.ts | 23 ++++++++++++- 8 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 cross-chain/solana/programs/tbtc/src/state/guardians.rs 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..34bd26417 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,27 @@ 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; + 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..0ee7dc4db 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs @@ -1,4 +1,7 @@ -use crate::{constants::SEED_PREFIX_TBTC_MINT, state::Config}; +use crate::{ + constants::SEED_PREFIX_TBTC_MINT, + state::{Config, Guardians}, +}; use anchor_lang::prelude::*; use anchor_spl::token; @@ -25,6 +28,15 @@ 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(mut)] authority: Signer<'info>, @@ -43,5 +55,11 @@ pub fn initialize(ctx: Context) -> Result<()> { num_guardians: 0, paused: false, }); + + ctx.accounts.guardians.set_inner(Guardians { + bump: ctx.bumps["guardians"], + keys: Vec::new(), + }); + Ok(()) } 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..8642d9222 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,25 @@ 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<()> { - ctx.accounts.config.num_guardians -= 1; - Ok(()) + let guardians: &mut Vec<_> = &mut ctx.accounts.guardians; + match guardians + .iter() + .position(|&guardian| guardian == ctx.accounts.guardian.key()) + { + Some(index) => { + // Remove pubkey to guardians account. + guardians.swap_remove(index); + + // Update config. + ctx.accounts.config.num_guardians -= 1; + + Ok(()) + } + None => err!(TbtcError::GuardianNonexistent), + } } 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/mod.rs b/cross-chain/solana/programs/tbtc/src/state/mod.rs index dffd4099b..6b3503155 100644 --- a/cross-chain/solana/programs/tbtc/src/state/mod.rs +++ b/cross-chain/solana/programs/tbtc/src/state/mod.rs @@ -4,5 +4,8 @@ pub use config::*; mod guardian_info; pub use guardian_info::*; +mod guardians; +pub use guardians::*; + mod minter_info; pub use minter_info::*; diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 0a70e3d93..77adf4bbb 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -19,6 +19,7 @@ async function setup( authority ) { const [config,] = getConfigPDA(program); + const [guardians,] = getGuardiansPDA(program); const [tbtcMintPDA, _] = getTokenPDA(program); await program.methods @@ -26,6 +27,7 @@ async function setup( .accounts({ mint: tbtcMintPDA, config, + guardians, authority: authority.publicKey }) .rpc(); @@ -50,6 +52,10 @@ async function checkState( let mintState = await spl.getMint(program.provider.connection, tbtcMint); expect(mintState.supply).to.equal(BigInt(expectedTokensSupply)); + + const [guardians,] = getGuardiansPDA(program); + let guardiansState = await program.account.guardians.fetch(guardians); + expect(guardiansState.keys).has.length(expectedGuardians); } async function changeAuthority( @@ -85,7 +91,7 @@ async function takeAuthority( } async function cancelAuthorityChange( - program: Program, + program: Program, authority, ) { const [config,] = getConfigPDA(program); @@ -148,6 +154,17 @@ function getTokenPDA( ); } +function getGuardiansPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('guardians'), + ], + program.programId + ); +} + function getMinterPDA( program: Program, minter @@ -232,12 +249,14 @@ async function addGuardian( payer ): Promise { const [config,] = getConfigPDA(program); + const [guardians,] = getGuardiansPDA(program); const [guardianInfoPDA, _] = getGuardianPDA(program, guardian); await program.methods .addGuardian() .accounts({ config, authority: authority.publicKey, + guardians, guardianInfo: guardianInfoPDA, guardian: guardian.publicKey, }) @@ -264,11 +283,13 @@ async function removeGuardian( guardianInfo ) { const [config,] = getConfigPDA(program); + const [guardians,] = getGuardiansPDA(program); await program.methods .removeGuardian() .accounts({ config, authority: authority.publicKey, + guardians, guardianInfo: guardianInfo, guardian: guardian.publicKey }) From ca5b8b38d514bee9a937f9ae59affe3b73b3a098 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 16:29:37 -0500 Subject: [PATCH 10/38] solana: add minters acct for indexing --- .../tbtc/src/processor/admin/add_minter.rs | 21 ++++++++++-- .../tbtc/src/processor/admin/initialize.rs | 16 ++++++++- .../tbtc/src/processor/admin/remove_minter.rs | 33 +++++++++++++++++-- .../solana/programs/tbtc/src/state/minters.rs | 30 +++++++++++++++++ .../solana/programs/tbtc/src/state/mod.rs | 3 ++ cross-chain/solana/tests/01__tbtc.ts | 22 +++++++++++++ 6 files changed, 119 insertions(+), 6 deletions(-) create mode 100644 cross-chain/solana/programs/tbtc/src/state/minters.rs 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..c107bd3ae 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,18 @@ 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; 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 0ee7dc4db..81453a5dc 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,6 @@ use crate::{ constants::SEED_PREFIX_TBTC_MINT, - state::{Config, Guardians}, + state::{Config, Guardians, Minters}, }; use anchor_lang::prelude::*; use anchor_spl::token; @@ -37,6 +37,15 @@ pub struct Initialize<'info> { )] 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>, @@ -61,5 +70,10 @@ pub fn initialize(ctx: Context) -> Result<()> { keys: Vec::new(), }); + ctx.accounts.minters.set_inner(Minters { + bump: ctx.bumps["minters"], + keys: Vec::new(), + }); + 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..202f1ef42 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,25 @@ 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<()> { - ctx.accounts.config.num_minters -= 1; - Ok(()) + let minters: &mut Vec<_> = &mut ctx.accounts.minters; + match minters + .iter() + .position(|&minter| minter == ctx.accounts.minter.key()) + { + Some(index) => { + // Remove pubkey to minters account. + minters.swap_remove(index); + + // Update config. + ctx.accounts.config.num_minters -= 1; + + Ok(()) + } + None => err!(TbtcError::GuardianNonexistent), + } } 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 6b3503155..296352f95 100644 --- a/cross-chain/solana/programs/tbtc/src/state/mod.rs +++ b/cross-chain/solana/programs/tbtc/src/state/mod.rs @@ -9,3 +9,6 @@ pub use guardians::*; mod minter_info; pub use minter_info::*; + +mod minters; +pub use minters::*; diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 77adf4bbb..9f7f8cd8e 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -20,6 +20,7 @@ async function setup( ) { const [config,] = getConfigPDA(program); const [guardians,] = getGuardiansPDA(program); + const [minters,] = getMintersPDA(program); const [tbtcMintPDA, _] = getTokenPDA(program); await program.methods @@ -28,6 +29,7 @@ async function setup( mint: tbtcMintPDA, config, guardians, + minters, authority: authority.publicKey }) .rpc(); @@ -56,6 +58,10 @@ async function checkState( const [guardians,] = getGuardiansPDA(program); let guardiansState = await program.account.guardians.fetch(guardians); expect(guardiansState.keys).has.length(expectedGuardians); + + const [minters,] = getMintersPDA(program); + let mintersState = await program.account.minters.fetch(minters); + expect(mintersState.keys).has.length(expectedMinters); } async function changeAuthority( @@ -165,6 +171,18 @@ function getGuardiansPDA( ); } + +function getMintersPDA( + program: Program, +): [anchor.web3.PublicKey, number] { + return web3.PublicKey.findProgramAddressSync( + [ + Buffer.from('minters'), + ], + program.programId + ); +} + function getMinterPDA( program: Program, minter @@ -185,12 +203,14 @@ async function addMinter( payer ): Promise { const [config,] = getConfigPDA(program); + const [minters,] = getMintersPDA(program); const [minterInfoPDA, _] = getMinterPDA(program, minter); await program.methods .addMinter() .accounts({ config, authority: authority.publicKey, + minters, minter: minter.publicKey, minterInfo: minterInfoPDA, }) @@ -217,11 +237,13 @@ async function removeMinter( minterInfo ) { const [config,] = getConfigPDA(program); + const [minters,] = getMintersPDA(program); await program.methods .removeMinter() .accounts({ config, authority: authority.publicKey, + minters, minterInfo: minterInfo, minter: minter.publicKey }) From d4683b7494753cb775c8bc1c67720c007ae472e2 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 16:49:58 -0500 Subject: [PATCH 11/38] solana: add events --- cross-chain/solana/programs/tbtc/src/lib.rs | 2 ++ .../tbtc/src/processor/admin/add_guardian.rs | 2 ++ .../tbtc/src/processor/admin/add_minter.rs | 3 ++ .../src/processor/admin/remove_guardian.rs | 8 ++--- .../tbtc/src/processor/admin/remove_minter.rs | 8 ++--- .../programs/wormhole-gateway/src/event.rs | 34 +++++++++++++++++++ .../programs/wormhole-gateway/src/lib.rs | 2 ++ .../src/processor/receive_tbtc.rs | 8 ++++- .../src/processor/send_tbtc/gateway.rs | 13 ++++++- .../src/processor/send_tbtc/wrapped.rs | 9 +++++ .../src/processor/update_gateway_address.rs | 7 +++- .../src/processor/update_minting_limit.rs | 5 +++ 12 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 cross-chain/solana/programs/wormhole-gateway/src/event.rs 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 34bd26417..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 @@ -58,5 +58,7 @@ pub fn add_guardian(ctx: Context) -> Result<()> { // 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 c107bd3ae..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 @@ -56,5 +56,8 @@ pub fn add_minter(ctx: Context) -> Result<()> { // 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/remove_guardian.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/remove_guardian.rs index 8642d9222..cd3bbe301 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 @@ -42,10 +42,8 @@ pub struct RemoveGuardian<'info> { pub fn remove_guardian(ctx: Context) -> Result<()> { let guardians: &mut Vec<_> = &mut ctx.accounts.guardians; - match guardians - .iter() - .position(|&guardian| guardian == ctx.accounts.guardian.key()) - { + let removed = ctx.accounts.guardian.key(); + match guardians.iter().position(|&guardian| guardian == removed) { Some(index) => { // Remove pubkey to guardians account. guardians.swap_remove(index); @@ -53,6 +51,8 @@ pub fn remove_guardian(ctx: Context) -> Result<()> { // Update config. ctx.accounts.config.num_guardians -= 1; + emit!(crate::event::GuardianRemoved { guardian: removed }); + Ok(()) } None => err!(TbtcError::GuardianNonexistent), 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 202f1ef42..6f6eeaf50 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 @@ -44,10 +44,8 @@ pub struct RemoveMinter<'info> { pub fn remove_minter(ctx: Context) -> Result<()> { let minters: &mut Vec<_> = &mut ctx.accounts.minters; - match minters - .iter() - .position(|&minter| minter == ctx.accounts.minter.key()) - { + let removed = ctx.accounts.minter.key(); + match minters.iter().position(|&minter| minter == removed) { Some(index) => { // Remove pubkey to minters account. minters.swap_remove(index); @@ -55,6 +53,8 @@ pub fn remove_minter(ctx: Context) -> Result<()> { // Update config. ctx.accounts.config.num_minters -= 1; + emit!(crate::event::MinterRemoved { minter: removed }); + Ok(()) } None => err!(TbtcError::GuardianNonexistent), 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 6d525a270..39b3fb5f9 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/lib.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/lib.rs @@ -4,6 +4,8 @@ pub mod constants; pub mod error; +pub(crate) mod event; + mod processor; pub(crate) use processor::*; 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 49187710e..b23269187 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 @@ -182,6 +182,12 @@ pub fn receive_tbtc(ctx: Context, _message_hash: [u8; 32]) -> Resul // 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]]; @@ -198,7 +204,7 @@ pub fn receive_tbtc(ctx: Context, _message_hash: [u8; 32]) -> Resul associated_token::Create { payer: ctx.accounts.payer.to_account_info(), associated_token: ata.to_account_info(), - authority: ctx.accounts.recipient.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(), 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 8e81142af..acbd4346c 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 @@ -139,6 +139,17 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg amount, )?; + let gateway = ctx.accounts.gateway_info.address; + + emit!(crate::event::WormholeTbtcSent { + amount, + recipient_chain, + gateway, + recipient, + arbiter_fee: Default::default(), + nonce + }); + let custodian = &ctx.accounts.custodian; // Finally transfer wrapped tBTC with the recipient encoded as this transfer's message. @@ -180,7 +191,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/wrapped.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/wrapped.rs index f6d9ca734..0be9a0207 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 @@ -129,6 +129,15 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg amount, )?; + emit!(crate::event::WormholeTbtcSent { + amount, + recipient_chain, + gateway: Default::default(), + recipient, + arbiter_fee, + nonce + }); + let custodian = &ctx.accounts.custodian; // Because the wormhole-anchor-sdk does not support relayable transfers (i.e. payload ID == 1), diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/update_gateway_address.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/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/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/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/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(()) } From f2ff01531cabede43189ba44eca39ae95143e502 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 20:34:08 -0500 Subject: [PATCH 12/38] solana: fix decimals == 8 --- cross-chain/solana/programs/tbtc/src/event.rs | 2 +- .../tbtc/src/processor/admin/initialize.rs | 2 +- .../programs/wormhole-gateway/src/error.rs | 3 -- .../src/processor/send_tbtc/gateway.rs | 18 +++++------ .../src/processor/send_tbtc/mod.rs | 30 +++++++++++++------ .../src/processor/send_tbtc/wrapped.rs | 14 ++++----- 6 files changed, 35 insertions(+), 34 deletions(-) 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/processor/admin/initialize.rs b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs index 81453a5dc..d36c49a9c 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs @@ -14,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>, diff --git a/cross-chain/solana/programs/wormhole-gateway/src/error.rs b/cross-chain/solana/programs/wormhole-gateway/src/error.rs index f1ced8941..a607e79b8 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/error.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/error.rs @@ -17,9 +17,6 @@ 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, 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 acbd4346c..13fac606f 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 @@ -124,9 +124,11 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg 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: &mut ctx.accounts.custodian, tbtc_mint: &ctx.accounts.tbtc_mint, @@ -137,18 +139,12 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg token_program, }, amount, - )?; - - let gateway = ctx.accounts.gateway_info.address; - - emit!(crate::event::WormholeTbtcSent { - amount, recipient_chain, - gateway, + Some(gateway), recipient, - arbiter_fee: Default::default(), - nonce - }); + None, // arbiter_fee + nonce, + )?; let custodian = &ctx.accounts.custodian; 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 5996e879b..f7bfc1e3f 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 @@ -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,13 +56,10 @@ 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 - .checked_sub(truncated) + .checked_sub(amount) .ok_or(WormholeGatewayError::MintedAmountUnderflow)?; // Burn TBTC mint. @@ -70,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( @@ -81,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 0be9a0207..71bacbf57 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 @@ -116,7 +116,7 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg 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: &mut ctx.accounts.custodian, tbtc_mint: &ctx.accounts.tbtc_mint, @@ -127,16 +127,12 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg token_program, }, amount, - )?; - - emit!(crate::event::WormholeTbtcSent { - amount, recipient_chain, - gateway: Default::default(), + None, // gateway recipient, - arbiter_fee, - nonce - }); + Some(arbiter_fee), + nonce, + )?; let custodian = &ctx.accounts.custodian; From 4d91dc7143119028cdc438f17d3e461620664555 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Wed, 2 Aug 2023 20:37:32 -0500 Subject: [PATCH 13/38] solana: simplify remove logic --- .../src/processor/admin/remove_guardian.rs | 25 +++++++++++-------- .../tbtc/src/processor/admin/remove_minter.rs | 25 +++++++++++-------- 2 files changed, 28 insertions(+), 22 deletions(-) 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 cd3bbe301..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 @@ -43,18 +43,21 @@ pub struct RemoveGuardian<'info> { pub fn remove_guardian(ctx: Context) -> Result<()> { let guardians: &mut Vec<_> = &mut ctx.accounts.guardians; let removed = ctx.accounts.guardian.key(); - match guardians.iter().position(|&guardian| guardian == removed) { - Some(index) => { - // Remove pubkey to guardians account. - guardians.swap_remove(index); - // Update config. - ctx.accounts.config.num_guardians -= 1; + // 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(); - emit!(crate::event::GuardianRemoved { guardian: removed }); + // Remove pubkey to guardians account. + guardians.swap_remove(index); - Ok(()) - } - None => err!(TbtcError::GuardianNonexistent), - } + // 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 6f6eeaf50..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 @@ -45,18 +45,21 @@ pub struct RemoveMinter<'info> { pub fn remove_minter(ctx: Context) -> Result<()> { let minters: &mut Vec<_> = &mut ctx.accounts.minters; let removed = ctx.accounts.minter.key(); - match minters.iter().position(|&minter| minter == removed) { - Some(index) => { - // Remove pubkey to minters account. - minters.swap_remove(index); - // Update config. - ctx.accounts.config.num_minters -= 1; + // 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(); - emit!(crate::event::MinterRemoved { minter: removed }); + // Remove pubkey to minters account. + minters.swap_remove(index); - Ok(()) - } - None => err!(TbtcError::GuardianNonexistent), - } + // Update config. + ctx.accounts.config.num_minters -= 1; + + emit!(crate::event::MinterRemoved { minter: removed }); + + Ok(()) } From 0e0608f028e799bfc8ec0d67fdb8710623a2beca Mon Sep 17 00:00:00 2001 From: Promethea Raschke Date: Thu, 3 Aug 2023 13:26:58 +0100 Subject: [PATCH 14/38] Fix test errors --- cross-chain/solana/tests/helpers/tbtcHelpers.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/cross-chain/solana/tests/helpers/tbtcHelpers.ts b/cross-chain/solana/tests/helpers/tbtcHelpers.ts index 70b57bc9a..a0918c1d1 100644 --- a/cross-chain/solana/tests/helpers/tbtcHelpers.ts +++ b/cross-chain/solana/tests/helpers/tbtcHelpers.ts @@ -101,7 +101,15 @@ export async function checkState( let mintState = await spl.getMint(program.provider.connection, tbtcMint); expect(mintState.supply).to.equal(BigInt(expectedTokensSupply)); - } + + const [guardians,] = getGuardiansPDA(program); + let guardiansState = await program.account.guardians.fetch(guardians); + expect(guardiansState.keys).has.length(expectedGuardians); + + const [minters,] = getMintersPDA(program); + let mintersState = await program.account.minters.fetch(minters); + expect(mintersState.keys).has.length(expectedMinters); +} export async function addMinter( program: Program, @@ -109,12 +117,14 @@ export async function addMinter( minter ): Promise { const [config,] = getConfigPDA(program); + const [minters,] = getMintersPDA(program); const [minterInfoPDA, _] = getMinterPDA(program, minter); await program.methods .addMinter() .accounts({ config, authority: authority.publicKey, + minters, minter, minterInfo: minterInfoPDA, }) From 0ac930e0eaba1b0164fb5107bf03424f0d044779 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Thu, 3 Aug 2023 09:04:48 -0500 Subject: [PATCH 15/38] solana: fix program name --- cross-chain/solana/Anchor.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cross-chain/solana/Anchor.toml b/cross-chain/solana/Anchor.toml index 6025a94f1..0801ceacc 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" From 753c7eadcc8e2e3dce92813f381e5e9136d71b2f Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Thu, 3 Aug 2023 12:51:43 -0500 Subject: [PATCH 16/38] solana: clean up test dir --- cross-chain/solana/.gitignore | 1 + cross-chain/solana/Anchor.toml | 4 +- cross-chain/solana/tests/01__tbtc.ts | 359 ++++++++++-------- .../solana/tests/02__wormholeGateway.ts | 218 ++++++----- ...core_bridge.json => core_bridge_data.json} | 0 cross-chain/solana/tests/helpers/consts.ts | 50 +++ cross-chain/solana/tests/helpers/index.ts | 2 + cross-chain/solana/tests/helpers/tbtc.ts | 108 ++++++ .../solana/tests/helpers/tbtcHelpers.ts | 134 ------- cross-chain/solana/tests/helpers/utils.ts | 235 +++++++++--- .../solana/tests/helpers/wormholeGateway.ts | 39 ++ .../tests/helpers/wormholeGatewayHelpers.ts | 67 ---- 12 files changed, 689 insertions(+), 528 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 delete mode 100644 cross-chain/solana/tests/helpers/tbtcHelpers.ts create mode 100644 cross-chain/solana/tests/helpers/wormholeGateway.ts delete mode 100644 cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts 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 0801ceacc..127ca5360 100644 --- a/cross-chain/solana/Anchor.toml +++ b/cross-chain/solana/Anchor.toml @@ -61,10 +61,10 @@ filename = "tests/accounts/ethereum_token_bridge.json" address = "DapiQYH3BGonhN8cngWcXQ6SrqSm3cwysoznoHr6Sbsx" filename = "tests/accounts/token_bridge_config.json" -### Core Bridge -- Bridge +### Core Bridge -- Bridge Data [[test.validator.clone]] 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/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index 6bc20e51b..d40a34df1 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -1,21 +1,27 @@ import * as anchor from "@coral-xyz/anchor"; -import { Program, AnchorError } from "@coral-xyz/anchor"; +import { AnchorError, Program } from "@coral-xyz/anchor"; import * as spl from "@solana/spl-token"; -import * as web3 from '@solana/web3.js'; +import * as web3 from "@solana/web3.js"; +import { 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 { + addMinter, + checkState, + getConfigPDA, + getGuardianPDA, + getGuardiansPDA, + getMinterPDA, + getMintersPDA, + getTokenPDA, + maybeAuthorityAnd, +} from "./helpers/tbtc"; import { transferLamports } from "./helpers/utils"; -import { maybeAuthorityAnd, getConfigPDA, getTokenPDA, getMinterPDA, getGuardianPDA, getMintersPDA, getGuardiansPDA, checkState, addMinter } from "./helpers/tbtcHelpers"; -async function setup( - program: Program, - authority -) { - const [config,] = getConfigPDA(program); - const [guardians,] = getGuardiansPDA(program); - const [minters,] = getMintersPDA(program); - const [tbtcMintPDA, _] = getTokenPDA(program); +async function setup(program: Program, authority) { + const config = getConfigPDA(); + const guardians = getGuardiansPDA(); + const minters = getMintersPDA(); + const tbtcMintPDA = getTokenPDA(); await program.methods .initialize() @@ -24,7 +30,7 @@ async function setup( config, guardians, minters, - authority: authority.publicKey + authority: authority.publicKey, }) .rpc(); } @@ -32,9 +38,9 @@ async function setup( async function changeAuthority( program: Program, authority, - newAuthority, + newAuthority ) { - const [config,] = getConfigPDA(program); + const config = getConfigPDA(); await program.methods .changeAuthority() .accounts({ @@ -46,11 +52,8 @@ async function changeAuthority( .rpc(); } -async function takeAuthority( - program: Program, - newAuthority, -) { - const [config,] = getConfigPDA(program); +async function takeAuthority(program: Program, newAuthority) { + const config = getConfigPDA(); await program.methods .takeAuthority() .accounts({ @@ -61,11 +64,8 @@ async function takeAuthority( .rpc(); } -async function cancelAuthorityChange( - program: Program, - authority, -) { - const [config,] = getConfigPDA(program); +async function cancelAuthorityChange(program: Program, authority) { + const config = getConfigPDA(); await program.methods .cancelAuthorityChange() .accounts({ @@ -76,41 +76,29 @@ async function cancelAuthorityChange( .rpc(); } -async function checkPendingAuthority( - program: Program, - pendingAuthority, -) { - const [config,] = getConfigPDA(program); +async function checkPendingAuthority(program: Program, pendingAuthority) { + const config = getConfigPDA(); let configState = await program.account.config.fetch(config); expect(configState.pendingAuthority).to.eql(pendingAuthority.publicKey); } -async function checkNoPendingAuthority( - program: Program, -) { - const [config,] = getConfigPDA(program); +async function checkNoPendingAuthority(program: Program) { + const config = getConfigPDA(); 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); +async function checkPaused(program: Program, paused: boolean) { + const config = getConfigPDA(); let configState = await program.account.config.fetch(config); expect(configState.paused).to.equal(paused); } -async function checkMinter( - program: Program, - minter -) { - const [minterInfoPDA, bump] = getMinterPDA(program, minter.publicKey); +async function checkMinter(program: Program, minter) { + const minterInfoPDA = getMinterPDA(minter.publicKey); let minterInfo = await program.account.minterInfo.fetch(minterInfoPDA); expect(minterInfo.minter).to.eql(minter.publicKey); - expect(minterInfo.bump).to.equal(bump); } async function removeMinter( @@ -119,8 +107,8 @@ async function removeMinter( minter, minterInfo ) { - const [config,] = getConfigPDA(program); - const [minters,] = getMintersPDA(program); + const config = getConfigPDA(); + const minters = getMintersPDA(); await program.methods .removeMinter() .accounts({ @@ -128,7 +116,7 @@ async function removeMinter( authority: authority.publicKey, minters, minterInfo: minterInfo, - minter: minter.publicKey + minter: minter.publicKey, }) .signers(maybeAuthorityAnd(authority, [])) .rpc(); @@ -140,9 +128,9 @@ async function addGuardian( guardian, payer ): Promise { - const [config,] = getConfigPDA(program); - const [guardians,] = getGuardiansPDA(program); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardian); + const config = getConfigPDA(); + const guardians = getGuardiansPDA(); + const guardianInfoPDA = getGuardianPDA(guardian); await program.methods .addGuardian() .accounts({ @@ -157,15 +145,11 @@ async function addGuardian( return guardianInfoPDA; } -async function checkGuardian( - program: Program, - guardian -) { - const [guardianInfoPDA, bump] = getGuardianPDA(program, guardian); +async function checkGuardian(program: Program, guardian) { + const guardianInfoPDA = getGuardianPDA(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( @@ -174,8 +158,8 @@ async function removeGuardian( guardian, guardianInfo ) { - const [config,] = getConfigPDA(program); - const [guardians,] = getGuardiansPDA(program); + const config = getConfigPDA(); + const guardians = getGuardiansPDA(); await program.methods .removeGuardian() .accounts({ @@ -183,39 +167,33 @@ async function removeGuardian( authority: authority.publicKey, guardians, guardianInfo: guardianInfo, - guardian: guardian.publicKey + guardian: guardian.publicKey, }) .signers(maybeAuthorityAnd(authority, [])) .rpc(); } -async function pause( - program: Program, - guardian -) { - const [config,] = getConfigPDA(program); - const [guardianInfoPDA, _] = getGuardianPDA(program, guardian); +async function pause(program: Program, guardian) { + const config = getConfigPDA(); + const guardianInfoPDA = getGuardianPDA(guardian); await program.methods .pause() .accounts({ config, guardianInfo: guardianInfoPDA, - guardian: guardian.publicKey + guardian: guardian.publicKey, }) .signers([guardian]) .rpc(); } -async function unpause( - program: Program, - authority -) { - const [config,] = getConfigPDA(program); +async function unpause(program: Program, authority) { + const config = getConfigPDA(); await program.methods .unpause() .accounts({ config, - authority: authority.publicKey + authority: authority.publicKey, }) .signers(maybeAuthorityAnd(authority, [])) .rpc(); @@ -227,21 +205,26 @@ async function mint( minterInfoPDA, recipient, amount, - payer, + 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; - }; - }); + const config = getConfigPDA(); + const tbtcMintPDA = getTokenPDA(); + 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( @@ -251,14 +234,13 @@ async function mint( payer.publicKey, recipientToken, recipient.publicKey, - tbtcMintPDA, + tbtcMintPDA ) ), [payer.payer] ); } - await program.methods .mint(new anchor.BN(amount)) .accounts({ @@ -278,7 +260,8 @@ 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; const newAuthority = anchor.web3.Keypair.generate(); const minterKeys = anchor.web3.Keypair.generate(); const minter2Keys = anchor.web3.Keypair.generate(); @@ -288,13 +271,13 @@ describe("tbtc", () => { const recipientKeys = anchor.web3.Keypair.generate(); - it('setup', async () => { + it("setup", async () => { await setup(program, authority); - await checkState(program, authority, 0, 0, 0); + await checkState(authority, 0, 0, 0); }); - it('change authority', async () => { - await checkState(program, authority, 0, 0, 0); + it("change authority", async () => { + await checkState(authority, 0, 0, 0); await checkNoPendingAuthority(program); try { await cancelAuthorityChange(program, authority); @@ -302,7 +285,7 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('NoPendingAuthorityChange'); + expect(err.error.errorCode.code).to.equal("NoPendingAuthorityChange"); expect(err.program.equals(program.programId)).is.true; } try { @@ -311,7 +294,7 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('NoPendingAuthorityChange'); + expect(err.error.errorCode.code).to.equal("NoPendingAuthorityChange"); expect(err.program.equals(program.programId)).is.true; } @@ -319,7 +302,7 @@ describe("tbtc", () => { await checkPendingAuthority(program, newAuthority); await takeAuthority(program, newAuthority); await checkNoPendingAuthority(program); - await checkState(program, newAuthority, 0, 0, 0); + await checkState(newAuthority, 0, 0, 0); await changeAuthority(program, newAuthority, authority.payer); try { await takeAuthority(program, impostorKeys); @@ -327,7 +310,7 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPendingAuthority'); + expect(err.error.errorCode.code).to.equal("IsNotPendingAuthority"); expect(err.program.equals(program.programId)).is.true; } try { @@ -336,7 +319,7 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPendingAuthority'); + expect(err.error.errorCode.code).to.equal("IsNotPendingAuthority"); expect(err.program.equals(program.programId)).is.true; } try { @@ -345,22 +328,27 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); + 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); - }) + await checkState(authority, 0, 0, 0); + }); - it('add minter', async () => { - await checkState(program, authority, 0, 0, 0); - await addMinter(program, authority, minterKeys.publicKey); + it("add minter", async () => { + await checkState(authority, 0, 0, 0); + await addMinter(authority, minterKeys.publicKey); await checkMinter(program, minterKeys); - await checkState(program, authority, 1, 0, 0); + await checkState(authority, 1, 0, 0); // Transfer lamports to imposter. - await transferLamports(program.provider.connection, authority.payer, impostorKeys.publicKey, 1000000000); + await transferLamports( + program.provider.connection, + authority.payer, + impostorKeys.publicKey, + 1000000000 + ); // await web3.sendAndConfirmTransaction( // program.provider.connection, // new web3.Transaction().add( @@ -374,25 +362,32 @@ describe("tbtc", () => { // ); try { - await addMinter(program, impostorKeys, minter2Keys.publicKey); + await addMinter(impostorKeys, minter2Keys.publicKey); 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.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.publicKey); + it("mint", async () => { + await checkState(authority, 1, 0, 0); + const minterInfoPDA = getMinterPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); // await setupMint(program, authority, recipientKeys); - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 1000, authority); + await mint( + program, + minterKeys, + minterInfoPDA, + recipientKeys, + 1000, + authority + ); - await checkState(program, authority, 1, 0, 1000); + await checkState(authority, 1, 0, 1000); // // Burn for next test. // const ix = spl.createBurnCheckedInstruction( @@ -402,44 +397,57 @@ describe("tbtc", () => { // 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.publicKey); + it("won't mint", async () => { + await checkState(authority, 1, 0, 1000); + const minterInfoPDA = getMinterPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); // await setupMint(program, authority, recipientKeys); try { - await mint(program, impostorKeys, minterInfoPDA, recipientKeys, 1000, authority); + 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.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.publicKey); + it("use two minters", async () => { + await checkState(authority, 1, 0, 1000); + const minterInfoPDA = getMinterPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); - const minter2InfoPDA = await addMinter(program, authority, minter2Keys.publicKey); + const minter2InfoPDA = await addMinter(authority, minter2Keys.publicKey); await checkMinter(program, minter2Keys); - await checkState(program, authority, 2, 0, 1000); + await checkState(authority, 2, 0, 1000); // await setupMint(program, authority, recipientKeys); // cannot mint with wrong keys try { - await mint(program, minter2Keys, minterInfoPDA, recipientKeys, 1000, authority); + 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.error.errorCode.code).to.equal("ConstraintSeeds"); expect(err.program.equals(program.programId)).is.true; } @@ -450,25 +458,32 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); + 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); + await mint( + program, + minterKeys, + minterInfoPDA, + recipientKeys, + 500, + authority + ); + await checkState(authority, 2, 0, 1500); }); - it('remove minter', async () => { - await checkState(program, authority, 2, 0, 1500); - const [minter2InfoPDA, _] = getMinterPDA(program, minter2Keys.publicKey); + it("remove minter", async () => { + await checkState(authority, 2, 0, 1500); + const minter2InfoPDA = getMinterPDA(minter2Keys.publicKey); await checkMinter(program, minter2Keys); await removeMinter(program, authority, minter2Keys, minter2InfoPDA); - await checkState(program, authority, 1, 0, 1500); + await checkState(authority, 1, 0, 1500); }); - it('won\'t remove minter', async () => { - await checkState(program, authority, 1, 0, 1500); - const [minterInfoPDA, _] = getMinterPDA(program, minterKeys.publicKey); + it("won't remove minter", async () => { + await checkState(authority, 1, 0, 1500); + const minterInfoPDA = getMinterPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); try { @@ -477,12 +492,12 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); + 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); + await checkState(authority, 0, 0, 1500); try { await removeMinter(program, authority, minterKeys, minterInfoPDA); @@ -490,16 +505,16 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('AccountNotInitialized'); + 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); + it("add guardian", async () => { + await checkState(authority, 0, 0, 1500); await addGuardian(program, authority, guardianKeys, authority); await checkGuardian(program, guardianKeys); - await checkState(program, authority, 0, 1, 1500); + await checkState(authority, 0, 1, 1500); try { await addGuardian(program, impostorKeys, guardian2Keys, authority); @@ -507,28 +522,33 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotAuthority'); + 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); + it("remove guardian", async () => { + await checkState(authority, 0, 1, 1500); + const guardianInfoPDA = getGuardianPDA(guardianKeys); await checkGuardian(program, guardianKeys); try { - await removeGuardian(program, impostorKeys, guardianKeys, guardianInfoPDA); + 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.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); + await checkState(authority, 0, 0, 1500); try { await removeGuardian(program, authority, guardianKeys, guardianInfoPDA); @@ -536,21 +556,21 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('AccountNotInitialized'); + expect(err.error.errorCode.code).to.equal("AccountNotInitialized"); expect(err.program.equals(program.programId)).is.true; } }); - it('pause', async () => { - await checkState(program, authority, 0, 0, 1500); + it("pause", async () => { + await checkState(authority, 0, 0, 1500); await addGuardian(program, authority, guardianKeys, authority); await checkPaused(program, false); await pause(program, guardianKeys); await checkPaused(program, true); }); - it('unpause', async () => { - await checkState(program, authority, 0, 1, 1500); + it("unpause", async () => { + await checkState(authority, 0, 1, 1500); await checkPaused(program, true); await unpause(program, authority); await checkPaused(program, false); @@ -562,34 +582,41 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsNotPaused'); + expect(err.error.errorCode.code).to.equal("IsNotPaused"); expect(err.program.equals(program.programId)).is.true; } }); - it('won\'t mint when paused', async () => { - await checkState(program, authority, 0, 1, 1500); - const minterInfoPDA = await addMinter(program, authority, minterKeys.publicKey); + it("won't mint when paused", async () => { + await checkState(authority, 0, 1, 1500); + const minterInfoPDA = await addMinter(authority, minterKeys.publicKey); await pause(program, guardianKeys); // await setupMint(program, authority, recipientKeys); try { - await mint(program, minterKeys, minterInfoPDA, recipientKeys, 1000, authority); + 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.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); + it("use two guardians", async () => { + await checkState(authority, 1, 1, 1500); + const guardianInfoPDA = getGuardianPDA(guardianKeys); await checkGuardian(program, guardianKeys); await addGuardian(program, authority, guardian2Keys, authority); await checkGuardian(program, guardian2Keys); @@ -602,7 +629,7 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('IsPaused'); + expect(err.error.errorCode.code).to.equal("IsPaused"); expect(err.program.equals(program.programId)).is.true; } @@ -618,7 +645,7 @@ describe("tbtc", () => { } catch (_err) { expect(_err).to.be.instanceOf(AnchorError); const err: AnchorError = _err; - expect(err.error.errorCode.code).to.equal('ConstraintSeeds'); + expect(err.error.errorCode.code).to.equal("ConstraintSeeds"); expect(err.program.equals(program.programId)).is.true; } }); diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 9b4d712a7..964e46260 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -1,26 +1,32 @@ -import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock"; +import { redeemOnSolana, tryNativeToHexString } from "@certusone/wormhole-sdk"; +import { MockEthereumTokenBridge } 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 * as anchor from "@coral-xyz/anchor"; -import { Program, AnchorError } from "@coral-xyz/anchor"; -import * as spl from "@solana/spl-token"; -import { expect } from 'chai'; -import { WormholeGateway } from "../target/types/wormhole_gateway"; -import { generatePayer, getOrCreateTokenAccount } from "./helpers/utils"; -import { getCustodianPDA, getTokenBridgeRedeemerPDA, getTokenBridgeSenderPDA, getWrappedTbtcTokenPDA } from "./helpers/wormholeGatewayHelpers"; -import * as tbtc from "./helpers/tbtcHelpers"; -import { web3 } from "@coral-xyz/anchor"; +import { AnchorError, Program, web3 } from "@coral-xyz/anchor"; +import { getMint } from "@solana/spl-token"; +import { expect } from "chai"; import { Tbtc } from "../target/types/tbtc"; - -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; - +import { WormholeGateway } from "../target/types/wormhole_gateway"; +import { + ETHEREUM_TBTC_ADDRESS, + ETHEREUM_TOKEN_BRIDGE_ADDRESS, + GUARDIAN_SET_INDEX, + SOLANA_CORE_BRIDGE_ADDRESS, + SOLANA_TOKEN_BRIDGE_ADDRESS, + WRAPPED_TBTC_MINT, + generatePayer, + getOrCreateTokenAccount, + mockSignAndPostVaa, + preloadWrappedTbtc, +} from "./helpers"; +import * as tbtc from "./helpers/tbtc"; +import { + getCustodianPDA, + getTokenBridgeRedeemerPDA, + getTokenBridgeSenderPDA, + getWrappedTbtcTokenPDA, +} from "./helpers/wormholeGateway"; async function setup( program: Program, @@ -28,14 +34,18 @@ async function setup( authority, mintingLimit: number ) { - const [custodian,] = getCustodianPDA(program); - const [tbtcMint,] = tbtc.getTokenPDA(tbtcProgram); - const [gatewayWrappedTbtcToken,] = getWrappedTbtcTokenPDA(program); - const [tokenBridgeSender,] = getTokenBridgeSenderPDA(program); - const [tokenBridgeRedeemer,] = getTokenBridgeRedeemerPDA(program); - - const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey(SOLANA_TOKEN_BRIDGE_ADDRESS, 2, ETHEREUM_TBTC_ADDRESS); - + const custodian = getCustodianPDA(); + const tbtcMint = tbtc.getTokenPDA(); + const gatewayWrappedTbtcToken = getWrappedTbtcTokenPDA(); + const tokenBridgeSender = getTokenBridgeSenderPDA(); + const tokenBridgeRedeemer = getTokenBridgeRedeemerPDA(); + + const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey( + SOLANA_TOKEN_BRIDGE_ADDRESS, + 2, + ETHEREUM_TBTC_ADDRESS + ); + await program.methods .initialize(new anchor.BN(mintingLimit)) .accounts({ @@ -53,13 +63,14 @@ async function setup( async function checkState( program: Program, expectedAuthority, - expectedMintingLimit, + expectedMintingLimit // expectedMintedAmount, ) { - const [custodian,] = getCustodianPDA(program); + const custodian = getCustodianPDA(); let custodianState = await program.account.custodian.fetch(custodian); - expect(custodianState.mintingLimit.eq(new anchor.BN(expectedMintingLimit))).to.be.true; + expect(custodianState.mintingLimit.eq(new anchor.BN(expectedMintingLimit))).to + .be.true; expect(custodianState.authority).to.eql(expectedAuthority.publicKey); } @@ -72,14 +83,15 @@ describe("wormhole-gateway", () => { const tbtcProgram = anchor.workspace.Tbtc as Program; - const [custodian,] = getCustodianPDA(program); - const [tbtcMint,] = tbtc.getTokenPDA(tbtcProgram); - const [tbtcConfig,] = tbtc.getConfigPDA(tbtcProgram); - const [gatewayWrappedTbtcToken,] = getWrappedTbtcTokenPDA(program); - const [tokenBridgeSender,] = getTokenBridgeSenderPDA(program); - const [tokenBridgeRedeemer,] = getTokenBridgeRedeemerPDA(program); + const custodian = getCustodianPDA(); + const tbtcMint = tbtc.getTokenPDA(); + const tbtcConfig = tbtc.getConfigPDA(); + const gatewayWrappedTbtcToken = getWrappedTbtcTokenPDA(); + const tokenBridgeSender = getTokenBridgeSenderPDA(); + const tokenBridgeRedeemer = getTokenBridgeRedeemerPDA(); - const authority = (program.provider as anchor.AnchorProvider).wallet as anchor.Wallet; + const authority = (program.provider as anchor.AnchorProvider) + .wallet as anchor.Wallet; const newAuthority = anchor.web3.Keypair.generate(); const minterKeys = anchor.web3.Keypair.generate(); const minter2Keys = anchor.web3.Keypair.generate(); @@ -89,23 +101,38 @@ describe("wormhole-gateway", () => { const recipientKeys = anchor.web3.Keypair.generate(); - const ethereumTokenBridge = new mock.MockEthereumTokenBridge(ETHEREUM_TOKEN_BRIDGE_ADDRESS); + const ethereumTokenBridge = new MockEthereumTokenBridge( + ETHEREUM_TOKEN_BRIDGE_ADDRESS + ); - it('check core bridge and token bridge', async () => { + 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); + 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); + const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey( + SOLANA_TOKEN_BRIDGE_ADDRESS, + 2, + ETHEREUM_TBTC_ADDRESS + ); + const mintData = await 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); + const wrappedTbtcToken = await getOrCreateTokenAccount( + connection, + payer, + wrappedTbtcMint, + payer.publicKey + ); // Bridge tbtc to token account. const published = ethereumTokenBridge.publishTransferTokens( @@ -126,72 +153,59 @@ describe("wormhole-gateway", () => { SOLANA_CORE_BRIDGE_ADDRESS, SOLANA_TOKEN_BRIDGE_ADDRESS, payer.publicKey, - signedVaa, + signedVaa ); await web3.sendAndConfirmTransaction(connection, tx, [payer]); }); - it('setup', async () => { + it("setup", async () => { await setup(program, tbtcProgram, authority, 10000); await checkState(program, authority, 10000); - await tbtc.checkState(tbtcProgram, authority, 1, 2, 1500); + await tbtc.checkState(authority, 1, 2, 1500); }); - it('update minting limit', async () => { + it("update minting limit", async () => { await program.methods .updateMintingLimit(new anchor.BN(20000)) .accounts({ custodian, - authority: authority.publicKey + authority: authority.publicKey, }) .rpc(); await checkState(program, authority, 20000); }); - it('deposit wrapped tokens', async () => { - const [custodian,] = getCustodianPDA(program); - const minterInfo = await tbtc.addMinter(tbtcProgram, authority, custodian); + it("deposit wrapped tokens", async () => { + const custodian = getCustodianPDA(); + const minterInfo = await tbtc.addMinter(authority, custodian); // 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 wrappedTbtcToken = await getOrCreateTokenAccount(connection, payer, wrappedTbtcMint, payer.publicKey); - - // Bridge tbtc to token account. - const published = ethereumTokenBridge.publishTransferTokens( - tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), - 2, + const wrappedTbtcToken = await preloadWrappedTbtc( + connection, + payer, + ethereumTokenBridge, BigInt("100000000000"), - 1, - wrappedTbtcToken.address.toBuffer().toString("hex"), - BigInt(0), - 0, - 0 + payer.publicKey ); - const signedVaa = await mockSignAndPostVaa(connection, payer, published); - - const tx = await redeemOnSolana( + const recipientToken = await getOrCreateTokenAccount( connection, - SOLANA_CORE_BRIDGE_ADDRESS, - SOLANA_TOKEN_BRIDGE_ADDRESS, - payer.publicKey, - signedVaa, + payer, + tbtcMint, + payer.publicKey ); - await web3.sendAndConfirmTransaction(connection, tx, [payer]); - - const recipientToken = await getOrCreateTokenAccount(connection, payer, tbtcMint, payer.publicKey); await program.methods .depositWormholeTbtc(new anchor.BN(500)) .accounts({ custodian, wrappedTbtcToken: gatewayWrappedTbtcToken, - wrappedTbtcMint, + wrappedTbtcMint: WRAPPED_TBTC_MINT, tbtcMint, - recipientWrappedToken: wrappedTbtcToken.address, + recipientWrappedToken: wrappedTbtcToken, recipientToken: recipientToken.address, recipient: payer.publicKey, tbtcConfig, @@ -201,51 +215,31 @@ describe("wormhole-gateway", () => { .signers(tbtc.maybeAuthorityAnd(payer, [])) .rpc(); - await tbtc.checkState(tbtcProgram, authority, 2, 2, 2000); + await tbtc.checkState(authority, 2, 2, 2000); try { await program.methods - .depositWormholeTbtc(new anchor.BN(50000)) - .accounts({ - custodian, - wrappedTbtcToken: gatewayWrappedTbtcToken, - wrappedTbtcMint, - tbtcMint, - recipientWrappedToken: wrappedTbtcToken.address, - recipientToken: recipientToken.address, - recipient: payer.publicKey, - tbtcConfig, - minterInfo, - tbtcProgram: tbtcProgram.programId, - }) - .signers(tbtc.maybeAuthorityAnd(payer, [])) - .rpc(); + .depositWormholeTbtc(new anchor.BN(50000)) + .accounts({ + custodian, + wrappedTbtcToken: gatewayWrappedTbtcToken, + wrappedTbtcMint: WRAPPED_TBTC_MINT, + tbtcMint, + recipientWrappedToken: wrappedTbtcToken, + recipientToken: recipientToken.address, + recipient: payer.publicKey, + tbtcConfig, + minterInfo, + tbtcProgram: tbtcProgram.programId, + }) + .signers(tbtc.maybeAuthorityAnd(payer, [])) + .rpc(); 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('MintingLimitExceeded'); + expect(err.error.errorCode.code).to.equal("MintingLimitExceeded"); expect(err.program.equals(program.programId)).is.true; } }); }); - -async function mockSignAndPostVaa(connection: web3.Connection, payer: web3.Keypair, published: Buffer) { - const guardians = new mock.MockGuardians( - GUARDIAN_SET_INDEX, - ["cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0"] - ); - - // Add guardian signature. - const signedVaa = guardians.addSignatures(published, [0]); - - // Verify and post VAA. - await postVaaSolana(connection, - new NodeWallet(payer).signTransaction, - SOLANA_CORE_BRIDGE_ADDRESS, - payer.publicKey, - signedVaa - ); - - return signedVaa; -} 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/helpers/consts.ts b/cross-chain/solana/tests/helpers/consts.ts new file mode 100644 index 000000000..e27570ec2 --- /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 SOLANA_CORE_BRIDGE_ADDRESS = new PublicKey( + "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" +); +export const SOLANA_TOKEN_BRIDGE_ADDRESS = 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..93cd9194e --- /dev/null +++ b/cross-chain/solana/tests/helpers/tbtc.ts @@ -0,0 +1,108 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { getMint } from "@solana/spl-token"; +import { PublicKey } from "@solana/web3.js"; +import { expect } from "chai"; +import { Tbtc } from "../../target/types/tbtc"; +import { TBTC_PROGRAM_ID } from "./consts"; + +export function maybeAuthorityAnd(signer, signers) { + return signers.concat( + signer instanceof (anchor.Wallet as any) ? [] : [signer] + ); +} + +export function getConfigPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("config")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getTokenPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("tbtc-mint")], + TBTC_PROGRAM_ID + )[0]; +} + +export function getMinterPDA(minter: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("minter-info"), minter.toBuffer()], + TBTC_PROGRAM_ID + )[0]; +} + +export function getGuardianPDA(guardian): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("guardian-info"), guardian.publicKey.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 checkState( + expectedAuthority, + expectedMinters: number, + expectedGuardians: number, + expectedTokensSupply +) { + const program = anchor.workspace.Tbtc as Program; + + const config = getConfigPDA(); + 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 getMint(program.provider.connection, tbtcMint); + + expect(mintState.supply).to.equal(BigInt(expectedTokensSupply)); + + const guardians = getGuardiansPDA(); + let guardiansState = await program.account.guardians.fetch(guardians); + expect(guardiansState.keys).has.length(expectedGuardians); + + const minters = getMintersPDA(); + let mintersState = await program.account.minters.fetch(minters); + expect(mintersState.keys).has.length(expectedMinters); +} + +export async function addMinter( + authority, + minter +): Promise { + const program = anchor.workspace.Tbtc as Program; + + const config = getConfigPDA(); + const minters = getMintersPDA(); + const minterInfoPDA = getMinterPDA(minter); + await program.methods + .addMinter() + .accounts({ + config, + authority: authority.publicKey, + minters, + minter, + minterInfo: minterInfoPDA, + }) + .signers(maybeAuthorityAnd(authority, [])) + .rpc(); + return minterInfoPDA; +} diff --git a/cross-chain/solana/tests/helpers/tbtcHelpers.ts b/cross-chain/solana/tests/helpers/tbtcHelpers.ts deleted file mode 100644 index a0918c1d1..000000000 --- a/cross-chain/solana/tests/helpers/tbtcHelpers.ts +++ /dev/null @@ -1,134 +0,0 @@ -import * as anchor 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 { Tbtc } from "../../target/types/tbtc"; -import { expect } from 'chai'; - -export function maybeAuthorityAnd( - signer, - signers - ) { - return signers.concat(signer instanceof (anchor.Wallet as any) ? [] : [signer]); - } - -export function getConfigPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('config'), - ], - program.programId - ); -} - -export function getTokenPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('tbtc-mint'), - ], - program.programId - ); -} - -export function getMinterPDA( - program: Program, - minter -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('minter-info'), - minter.toBuffer(), - ], - program.programId - ); -} - -export function getGuardianPDA( - program: Program, - guardian -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('guardian-info'), - guardian.publicKey.toBuffer(), - ], - program.programId - ); -} - -export function getGuardiansPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('guardians'), - ], - program.programId - ); -} - -export function getMintersPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('minters'), - ], - program.programId - ); -} - -export 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)); - - const [guardians,] = getGuardiansPDA(program); - let guardiansState = await program.account.guardians.fetch(guardians); - expect(guardiansState.keys).has.length(expectedGuardians); - - const [minters,] = getMintersPDA(program); - let mintersState = await program.account.minters.fetch(minters); - expect(mintersState.keys).has.length(expectedMinters); -} - -export async function addMinter( - program: Program, - authority, - minter - ): Promise { - const [config,] = getConfigPDA(program); - const [minters,] = getMintersPDA(program); - const [minterInfoPDA, _] = getMinterPDA(program, minter); - await program.methods - .addMinter() - .accounts({ - config, - authority: authority.publicKey, - minters, - minter, - minterInfo: minterInfoPDA, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); - return minterInfoPDA; - } \ No newline at end of file diff --git a/cross-chain/solana/tests/helpers/utils.ts b/cross-chain/solana/tests/helpers/utils.ts index b87a0a948..901f2f980 100644 --- a/cross-chain/solana/tests/helpers/utils.ts +++ b/cross-chain/solana/tests/helpers/utils.ts @@ -1,53 +1,194 @@ +import { + MockEthereumTokenBridge, + MockGuardians, +} from "@certusone/wormhole-sdk/lib/cjs/mock"; 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 { + Account, + TokenAccountNotFoundError, + createAssociatedTokenAccountIdempotentInstruction, + getAccount, + getAssociatedTokenAddressSync, +} from "@solana/spl-token"; +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { + ETHEREUM_TBTC_ADDRESS, + GUARDIAN_SET_INDEX, + SOLANA_CORE_BRIDGE_ADDRESS, + SOLANA_TOKEN_BRIDGE_ADDRESS, + WRAPPED_TBTC_MINT, +} from "./consts"; +import { + postVaaSolana, + redeemOnSolana, + tryNativeToHexString, +} from "@certusone/wormhole-sdk"; +import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; + +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] + ); } -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 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 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; + } } -} \ No newline at end of file + ); + + 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 preloadWrappedTbtc( + connection: Connection, + payer: Keypair, + ethereumTokenBridge: MockEthereumTokenBridge, + amount: bigint, + tokenOwner: PublicKey +) { + const wrappedTbtcToken = await getOrCreateTokenAccount( + connection, + payer, + WRAPPED_TBTC_MINT, + tokenOwner + ); + + // Bridge tbtc to token account. + const published = ethereumTokenBridge.publishTransferTokens( + tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), + 2, + amount, + 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]); + + return wrappedTbtcToken.address; +} + +export async function mockSignAndPostVaa( + connection: web3.Connection, + payer: web3.Keypair, + published: Buffer +) { + const guardians = new MockGuardians(GUARDIAN_SET_INDEX, [ + "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", + ]); + + // Add guardian signature. + const signedVaa = guardians.addSignatures(published, [0]); + + // Verify and post VAA. + await postVaaSolana( + connection, + new NodeWallet(payer).signTransaction, + SOLANA_CORE_BRIDGE_ADDRESS, + payer.publicKey, + signedVaa + ); + + return signedVaa; +} + +// export function ethereumGatewaySendTbtc( +// ethereumTokenBridge: MockEthereumTokenBridge, +// amount: bigint, +// recipient: Buffer +// ) { +// const wrappedTbtcMint = getWrappedTbtcMintPDA(); +// const custodianWrappedTbtcToken = getWrappedTbtcTokenPDA; +// const published = ethereumTokenBridge.publishTransferTokens( +// tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), +// 2, +// BigInt("100000000000"), +// 1, +// wrappedTbtcToken.address.toBuffer().toString("hex"), +// BigInt(0), +// 0, +// 0 +// ); + +// const guardians = new mock.MockGuardians(GUARDIAN_SET_INDEX, [ +// "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", +// ]); + +// // Add guardian signature. +// const signedVaa = guardians.addSignatures(published, [0]); +// } diff --git a/cross-chain/solana/tests/helpers/wormholeGateway.ts b/cross-chain/solana/tests/helpers/wormholeGateway.ts new file mode 100644 index 000000000..747da5a10 --- /dev/null +++ b/cross-chain/solana/tests/helpers/wormholeGateway.ts @@ -0,0 +1,39 @@ +import { PublicKey } from "@solana/web3.js"; +import { WORMHOLE_GATEWAY_PROGRAM_ID } from "./consts"; + +export function getCustodianPDA(): PublicKey { + return PublicKey.findProgramAddressSync( + [Buffer.from("custodian")], + WORMHOLE_GATEWAY_PROGRAM_ID + )[0]; +} + +export function getGatewayInfoPDA(targetChain): 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]; +} diff --git a/cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts b/cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts deleted file mode 100644 index 90d2ee31d..000000000 --- a/cross-chain/solana/tests/helpers/wormholeGatewayHelpers.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as anchor from "@coral-xyz/anchor"; -import { Program } from "@coral-xyz/anchor"; -import * as web3 from '@solana/web3.js'; -import { WormholeGateway } from "../../target/types/wormhole_gateway"; - -export function getCustodianPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('custodian'), - ], - program.programId - ); -} - -export function getGatewayInfoPDA( - program: Program, - targetChain -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('gateway-info'), - toBytesLE(targetChain), - ], - program.programId - ); -} - -export function getWrappedTbtcTokenPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('wrapped-token'), - ], - program.programId - ); -} - -export function getTokenBridgeSenderPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('sender'), - ], - program.programId - ); -} - -export function getTokenBridgeRedeemerPDA( - program: Program, -): [anchor.web3.PublicKey, number] { - return web3.PublicKey.findProgramAddressSync( - [ - Buffer.from('redeemer'), - ], - program.programId - ); -} - -function toBytesLE(x): Buffer { - const buf = Buffer.alloc(2); - buf.writeUint16LE(x); - return buf; -} \ No newline at end of file From c5d0770b5051e3d7034be35f94c80477edb96f15 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Thu, 3 Aug 2023 14:53:30 -0500 Subject: [PATCH 17/38] solana: add checked math; fix error msgs --- cross-chain/solana/programs/tbtc/src/error.rs | 22 +++++++++--------- .../programs/wormhole-gateway/src/error.rs | 23 +++++++++++-------- .../src/processor/deposit_wormhole_tbtc.rs | 11 ++++++--- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/cross-chain/solana/programs/tbtc/src/error.rs b/cross-chain/solana/programs/tbtc/src/error.rs index 3cf0253fd..7db624a97 100644 --- a/cross-chain/solana/programs/tbtc/src/error.rs +++ b/cross-chain/solana/programs/tbtc/src/error.rs @@ -2,36 +2,36 @@ use anchor_lang::prelude::error_code; #[error_code] pub enum TbtcError { - #[msg("This address is already a minter.")] + #[msg("This address is already a minter")] MinterAlreadyExists = 0x10, - #[msg("This address is not a minter.")] + #[msg("This address is not a minter")] MinterNonexistent = 0x12, - #[msg("This address is already a guardian.")] + #[msg("This address is already a guardian")] GuardianAlreadyExists = 0x20, - #[msg("This address is not a guardian.")] + #[msg("This address is not a guardian")] GuardianNonexistent = 0x22, - #[msg("Caller is not a guardian.")] + #[msg("Caller is not a guardian")] SignerNotGuardian = 0x30, - #[msg("Caller is not a minter.")] + #[msg("Caller is not a minter")] SignerNotMinter = 0x32, - #[msg("Program is paused.")] + #[msg("Program is paused")] IsPaused = 0x40, - #[msg("Program is not paused.")] + #[msg("Program is not paused")] IsNotPaused = 0x42, - #[msg("Not valid authority to perform this action.")] + #[msg("Not valid authority to perform this action")] IsNotAuthority = 0x50, - #[msg("Not valid pending authority to take authority.")] + #[msg("Not valid pending authority to take authority")] IsNotPendingAuthority = 0x52, - #[msg("No pending authority.")] + #[msg("No pending authority")] NoPendingAuthorityChange = 0x54, } diff --git a/cross-chain/solana/programs/wormhole-gateway/src/error.rs b/cross-chain/solana/programs/wormhole-gateway/src/error.rs index a607e79b8..972c548f8 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/error.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/error.rs @@ -2,33 +2,36 @@ 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("0x0 recipient not allowed.")] + #[msg("0x0 recipient not allowed")] ZeroRecipient = 0x30, - #[msg("Not enough wormhole tBTC in the gateway to bridge.")] + #[msg("Not enough wormhole tBTC in the gateway to bridge")] NotEnoughWrappedTbtc = 0x40, - #[msg("Amount must not be 0.")] + #[msg("Amount must not be 0")] ZeroAmount = 0x50, - #[msg("Token Bridge transfer already redeemed.")] + #[msg("Token Bridge transfer already redeemed")] TransferAlreadyRedeemed = 0x70, - #[msg("Token chain and address do not match Ethereum's tBTC.")] + #[msg("Token chain and address do not match Ethereum's tBTC")] InvalidEthereumTbtc = 0x80, - #[msg("No tBTC transferred.")] + #[msg("No tBTC transferred")] NoTbtcTransferred = 0x90, - #[msg("0x0 receiver not allowed.")] + #[msg("0x0 receiver not allowed")] RecipientZeroAddress = 0xa0, - #[msg("Not enough minted by the gateway to satisfy sending tBTC.")] + #[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/processor/deposit_wormhole_tbtc.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/deposit_wormhole_tbtc.rs index 92a498595..f8888ef2a 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 @@ -59,9 +59,15 @@ pub struct DepositWormholeTbtc<'info> { impl<'info> DepositWormholeTbtc<'info> { fn constraints(ctx: &Context, amount: u64) -> Result<()> { + 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.custodian.minted_amount.saturating_add(amount), + updated_minted_amount, WormholeGatewayError::MintingLimitExceeded ); @@ -85,8 +91,7 @@ pub fn deposit_wormhole_tbtc(ctx: Context, amount: u64) -> )?; // Account for minted amount. - ctx.accounts.custodian.minted_amount = - ctx.accounts.custodian.minted_amount.saturating_add(amount); + ctx.accounts.custodian.minted_amount += amount; let custodian = &ctx.accounts.custodian; From f6e0bc2a68d3d09bdf258a6e53b67783f55e67b9 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Thu, 3 Aug 2023 17:44:51 -0500 Subject: [PATCH 18/38] solana: fix various program bugs; add tests --- .../src/processor/deposit_wormhole_tbtc.rs | 4 +- .../src/processor/initialize.rs | 21 +- .../src/processor/receive_tbtc.rs | 22 +- .../src/processor/send_tbtc/gateway.rs | 4 + .../src/processor/send_tbtc/wrapped.rs | 2 + .../wormhole-gateway/src/state/custodian.rs | 9 +- cross-chain/solana/tests/01__tbtc.ts | 21 +- .../solana/tests/02__wormholeGateway.ts | 390 ++++++---- cross-chain/solana/tests/helpers/consts.ts | 4 +- cross-chain/solana/tests/helpers/tbtc.ts | 20 +- cross-chain/solana/tests/helpers/utils.ts | 187 +++-- .../solana/tests/helpers/wormholeGateway.ts | 710 +++++++++++++++++- 12 files changed, 1127 insertions(+), 267 deletions(-) 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 f8888ef2a..ecfa92492 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 @@ -51,7 +51,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>, @@ -102,7 +102,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/initialize.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs index f9545fdf5..67e51d1d5 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs @@ -45,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 )] @@ -59,14 +59,13 @@ 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>, - + // /// 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>, } @@ -80,8 +79,8 @@ pub fn initialize(ctx: Context, minting_limit: u64) -> Result<()> { 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"], + // token_bridge_redeemer: ctx.accounts.token_bridge_redeemer.key(), + // token_bridge_redeemer_bump: ctx.bumps["token_bridge_redeemer"], minting_limit, minted_amount: 0, }); 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 b23269187..ce183fae9 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 @@ -23,7 +23,7 @@ pub struct ReceiveTbtc<'info> { has_one = wrapped_tbtc_token, has_one = wrapped_tbtc_mint, has_one = tbtc_mint, - has_one = token_bridge_redeemer, + //has_one = token_bridge_redeemer, )] custodian: Account<'info, Custodian>, @@ -38,11 +38,18 @@ pub struct ReceiveTbtc<'info> { /// 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)] token_bridge_claim: AccountInfo<'info>, /// Custody account. + #[account(mut)] wrapped_tbtc_token: Box>, + /// 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>, @@ -83,9 +90,6 @@ pub struct ReceiveTbtc<'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. - wrapped_tbtc_mint: UncheckedAccount<'info>, - /// CHECK: This account is needed for the Token Bridge program. token_bridge_config: UncheckedAccount<'info>, @@ -93,7 +97,7 @@ pub struct ReceiveTbtc<'info> { token_bridge_registered_emitter: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. - token_bridge_redeemer: UncheckedAccount<'info>, + //token_bridge_redeemer: UncheckedAccount<'info>, /// CHECK: This account is needed for the Token Bridge program. token_bridge_wrapped_asset: UncheckedAccount<'info>, @@ -105,9 +109,9 @@ pub struct ReceiveTbtc<'info> { rent: UncheckedAccount<'info>, tbtc_program: Program<'info, tbtc::Tbtc>, - associated_token_program: Program<'info, associated_token::AssociatedToken>, 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>, } @@ -164,7 +168,7 @@ pub fn receive_tbtc(ctx: Context, _message_hash: [u8; 32]) -> Resul .token_bridge_registered_emitter .to_account_info(), to: wrapped_tbtc_token.to_account_info(), - redeemer: ctx.accounts.token_bridge_redeemer.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(), @@ -175,7 +179,7 @@ pub fn receive_tbtc(ctx: Context, _message_hash: [u8; 32]) -> Resul }, &[&[ token_bridge::SEED_PREFIX_REDEEMER, - &[ctx.accounts.custodian.token_bridge_redeemer_bump], + &[ctx.accounts.custodian.bump], ]], ))?; @@ -195,6 +199,8 @@ pub fn receive_tbtc(ctx: Context, _message_hash: [u8; 32]) -> Resul // 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. 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 13fac606f..9fdbf0d02 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 @@ -16,6 +16,7 @@ pub struct SendTbtcGateway<'info> { has_one = wrapped_tbtc_mint, has_one = tbtc_mint, has_one = token_bridge_sender, + // has_one = tbtc_minter_info, TODO: add this guy to custodian )] custodian: Account<'info, Custodian>, @@ -26,9 +27,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)] @@ -149,6 +152,7 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg let custodian = &ctx.accounts.custodian; // Finally transfer wrapped tBTC with the recipient encoded as this transfer's message. + // TODO: fix bug here: InvalidSigner(GZqbpJ4J1d4TwEG76fnQk48za4JE2FA13qaqWF8h1rvs) token_bridge::transfer_wrapped_with_payload( CpiContext::new_with_signer( ctx.accounts.token_bridge_program.to_account_info(), 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 71bacbf57..61c939b28 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 @@ -19,9 +19,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)] 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 cd792c027..c3e38396a 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +use wormhole_anchor_sdk::token_bridge; #[account] #[derive(Debug, InitSpace)] @@ -11,13 +12,13 @@ pub struct Custodian { 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 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"; + /// TODO: This is an undesirable pattern in the Token Bridge due to how transfers are redeemed. + pub const SEED_PREFIX: &'static [u8] = token_bridge::SEED_PREFIX_REDEEMER; } diff --git a/cross-chain/solana/tests/01__tbtc.ts b/cross-chain/solana/tests/01__tbtc.ts index d40a34df1..b9beb26b2 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -10,7 +10,7 @@ import { getConfigPDA, getGuardianPDA, getGuardiansPDA, - getMinterPDA, + getMinterInfoPDA, getMintersPDA, getTokenPDA, maybeAuthorityAnd, @@ -95,7 +95,7 @@ async function checkPaused(program: Program, paused: boolean) { } async function checkMinter(program: Program, minter) { - const minterInfoPDA = getMinterPDA(minter.publicKey); + const minterInfoPDA = getMinterInfoPDA(minter.publicKey); let minterInfo = await program.account.minterInfo.fetch(minterInfoPDA); expect(minterInfo.minter).to.eql(minter.publicKey); @@ -343,12 +343,7 @@ describe("tbtc", () => { await checkState(authority, 1, 0, 0); // Transfer lamports to imposter. - await transferLamports( - program.provider.connection, - authority.payer, - impostorKeys.publicKey, - 1000000000 - ); + await transferLamports(authority.payer, impostorKeys.publicKey, 1000000000); // await web3.sendAndConfirmTransaction( // program.provider.connection, // new web3.Transaction().add( @@ -374,7 +369,7 @@ describe("tbtc", () => { it("mint", async () => { await checkState(authority, 1, 0, 0); - const minterInfoPDA = getMinterPDA(minterKeys.publicKey); + const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); // await setupMint(program, authority, recipientKeys); @@ -401,7 +396,7 @@ describe("tbtc", () => { it("won't mint", async () => { await checkState(authority, 1, 0, 1000); - const minterInfoPDA = getMinterPDA(minterKeys.publicKey); + const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); // await setupMint(program, authority, recipientKeys); @@ -426,7 +421,7 @@ describe("tbtc", () => { it("use two minters", async () => { await checkState(authority, 1, 0, 1000); - const minterInfoPDA = getMinterPDA(minterKeys.publicKey); + const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); const minter2InfoPDA = await addMinter(authority, minter2Keys.publicKey); await checkMinter(program, minter2Keys); @@ -475,7 +470,7 @@ describe("tbtc", () => { it("remove minter", async () => { await checkState(authority, 2, 0, 1500); - const minter2InfoPDA = getMinterPDA(minter2Keys.publicKey); + const minter2InfoPDA = getMinterInfoPDA(minter2Keys.publicKey); await checkMinter(program, minter2Keys); await removeMinter(program, authority, minter2Keys, minter2InfoPDA); await checkState(authority, 1, 0, 1500); @@ -483,7 +478,7 @@ describe("tbtc", () => { it("won't remove minter", async () => { await checkState(authority, 1, 0, 1500); - const minterInfoPDA = getMinterPDA(minterKeys.publicKey); + const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); await checkMinter(program, minterKeys); try { diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 964e46260..22b2a8219 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -3,77 +3,61 @@ import { MockEthereumTokenBridge } 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 * as anchor from "@coral-xyz/anchor"; -import { AnchorError, Program, web3 } from "@coral-xyz/anchor"; -import { getMint } from "@solana/spl-token"; +import { AnchorError, Program } from "@coral-xyz/anchor"; +import { + getAccount, + getAssociatedTokenAddressSync, + getMint, +} from "@solana/spl-token"; import { expect } from "chai"; -import { Tbtc } from "../target/types/tbtc"; import { WormholeGateway } from "../target/types/wormhole_gateway"; import { ETHEREUM_TBTC_ADDRESS, ETHEREUM_TOKEN_BRIDGE_ADDRESS, GUARDIAN_SET_INDEX, - SOLANA_CORE_BRIDGE_ADDRESS, - SOLANA_TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_PROGRAM_ID, + TOKEN_BRIDGE_PROGRAM_ID, + TBTC_PROGRAM_ID, + WORMHOLE_GATEWAY_PROGRAM_ID, WRAPPED_TBTC_MINT, + expectIxFail, + expectIxSuccess, generatePayer, - getOrCreateTokenAccount, + getOrCreateAta, mockSignAndPostVaa, preloadWrappedTbtc, + ethereumGatewaySendTbtc, + transferLamports, + getTokenBridgeSequence, } from "./helpers"; import * as tbtc from "./helpers/tbtc"; -import { - getCustodianPDA, - getTokenBridgeRedeemerPDA, - getTokenBridgeSenderPDA, - getWrappedTbtcTokenPDA, -} from "./helpers/wormholeGateway"; +import * as wormholeGateway from "./helpers/wormholeGateway"; async function setup( program: Program, - tbtcProgram: Program, authority, - mintingLimit: number + mintingLimit: bigint ) { - const custodian = getCustodianPDA(); + const custodian = wormholeGateway.getCustodianPDA(); const tbtcMint = tbtc.getTokenPDA(); - const gatewayWrappedTbtcToken = getWrappedTbtcTokenPDA(); - const tokenBridgeSender = getTokenBridgeSenderPDA(); - const tokenBridgeRedeemer = getTokenBridgeRedeemerPDA(); - - const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey( - SOLANA_TOKEN_BRIDGE_ADDRESS, - 2, - ETHEREUM_TBTC_ADDRESS - ); + const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); + const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); + //const tokenBridgeRedeemer = wormholeGateway.getTokenBridgeRedeemerPDA(); await program.methods - .initialize(new anchor.BN(mintingLimit)) + .initialize(new anchor.BN(mintingLimit.toString())) .accounts({ authority: authority.publicKey, custodian, tbtcMint, - wrappedTbtcMint, + wrappedTbtcMint: WRAPPED_TBTC_MINT, wrappedTbtcToken: gatewayWrappedTbtcToken, tokenBridgeSender, - tokenBridgeRedeemer, + //tokenBridgeRedeemer, }) .rpc(); } -async function checkState( - program: Program, - expectedAuthority, - expectedMintingLimit - // expectedMintedAmount, -) { - const custodian = getCustodianPDA(); - let custodianState = await program.account.custodian.fetch(custodian); - - expect(custodianState.mintingLimit.eq(new anchor.BN(expectedMintingLimit))).to - .be.true; - expect(custodianState.authority).to.eql(expectedAuthority.publicKey); -} - describe("wormhole-gateway", () => { // Configure the client to use the local cluster. anchor.setProvider(anchor.AnchorProvider.env()); @@ -81,17 +65,16 @@ describe("wormhole-gateway", () => { const program = anchor.workspace.WormholeGateway as Program; const connection = program.provider.connection; - const tbtcProgram = anchor.workspace.Tbtc as Program; - - const custodian = getCustodianPDA(); + const custodian = wormholeGateway.getCustodianPDA(); const tbtcMint = tbtc.getTokenPDA(); const tbtcConfig = tbtc.getConfigPDA(); - const gatewayWrappedTbtcToken = getWrappedTbtcTokenPDA(); - const tokenBridgeSender = getTokenBridgeSenderPDA(); - const tokenBridgeRedeemer = getTokenBridgeRedeemerPDA(); + const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); + const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); + const tokenBridgeRedeemer = wormholeGateway.getTokenBridgeRedeemerPDA(); - 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(); @@ -101,145 +84,244 @@ describe("wormhole-gateway", () => { const recipientKeys = anchor.web3.Keypair.generate(); + const commonTokenOwner = anchor.web3.Keypair.generate(); + const ethereumTokenBridge = new 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 + it("setup", async () => { + const mintingLimit = BigInt(10000); + await setup(program, authority, mintingLimit); + await wormholeGateway.checkState(authority.publicKey, mintingLimit); + await tbtc.checkState(authority, 1, 2, 1500); + + // Also set up common token account. + await transferLamports(authority, commonTokenOwner.publicKey, 100000000000); + await getOrCreateAta( + authority, + tbtc.getTokenPDA(), + commonTokenOwner.publicKey + ); + }); + + it("update minting limit", async () => { + const newLimit = BigInt(20000); + const ix = await wormholeGateway.updateMintingLimitIx( + { + authority: authority.publicKey, + }, + newLimit ); - expect(guardianSetData.keys).has.length(1); + await expectIxSuccess([ix], [authority]); + await wormholeGateway.checkState(authority.publicKey, newLimit); + }); + + it("deposit wrapped tokens", async () => { + const custodian = wormholeGateway.getCustodianPDA(); + // TODO: cannot deposit if custodian isn't minter yet. + + // Add custodian as minter. + await tbtc.addMinter(authority, custodian); // Set up new wallet - const payer = await generatePayer(connection, authority.payer); + const payer = await generatePayer(authority); // Check wrapped tBTC mint. - const wrappedTbtcMint = tokenBridge.deriveWrappedMintKey( - SOLANA_TOKEN_BRIDGE_ADDRESS, - 2, - ETHEREUM_TBTC_ADDRESS + const recipientWrappedToken = await preloadWrappedTbtc( + payer, + ethereumTokenBridge, + BigInt("100000000000"), + payer.publicKey ); - const mintData = await getMint(connection, wrappedTbtcMint); - expect(mintData.decimals).to.equal(8); - expect(mintData.supply).to.equal(BigInt(90)); - const wrappedTbtcToken = await getOrCreateTokenAccount( - connection, + const recipientToken = await getOrCreateAta( payer, - wrappedTbtcMint, + tbtcMint, 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 [wrappedBefore, tbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, recipientWrappedToken), + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + const depositAmount = BigInt(500); + + const ix = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + depositAmount ); + await expectIxSuccess([ix], [payer]); + await tbtc.checkState(authority, 2, 2, 2000); - const signedVaa = await mockSignAndPostVaa(connection, payer, published); + const [wrappedAfter, tbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientWrappedToken), + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); - const tx = await redeemOnSolana( - connection, - SOLANA_CORE_BRIDGE_ADDRESS, - SOLANA_TOKEN_BRIDGE_ADDRESS, - payer.publicKey, - signedVaa + // 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); + + // Cannot deposit past minting limit. + const failingIx = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + BigInt(50000) ); - await web3.sendAndConfirmTransaction(connection, tx, [payer]); - }); + await expectIxFail([failingIx], [payer], "MintingLimitExceeded"); - it("setup", async () => { - await setup(program, tbtcProgram, authority, 10000); - await checkState(program, authority, 10000); - await tbtc.checkState(authority, 1, 2, 1500); + // 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.checkState(authority.publicKey, newLimit); + await expectIxSuccess([failingIx], [payer]); }); - it("update minting limit", async () => { - await program.methods - .updateMintingLimit(new anchor.BN(20000)) - .accounts({ - custodian, + it("update gateway address", async () => { + const chain = 2; + + // demonstrate gateway address does not exist + const gatewayInfo = await connection.getAccountInfo( + wormholeGateway.getGatewayInfoPDA(chain) + ); + expect(gatewayInfo).is.null; + + // Make new gateway. + const firstAddress = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const firstIx = await wormholeGateway.updateGatewayAddress( + { authority: authority.publicKey, - }) - .rpc(); - await checkState(program, authority, 20000); - }); + }, + { chain, address: firstAddress } + ); + await expectIxSuccess([firstIx], [authority]); + await wormholeGateway.checkGateway(chain, firstAddress); - it("deposit wrapped tokens", async () => { - const custodian = getCustodianPDA(); - const minterInfo = await tbtc.addMinter(authority, custodian); + // 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("receive tbtc", async () => { // Set up new wallet - const payer = await generatePayer(connection, authority.payer); + const payer = await generatePayer(authority); - // Check wrapped tBTC mint. - const wrappedTbtcToken = await preloadWrappedTbtc( - connection, + // Use common token account. + const recipient = commonTokenOwner.publicKey; + const recipientToken = getAssociatedTokenAddressSync( + tbtc.getTokenPDA(), + recipient + ); + + // Get foreign gateway. + const fromGateway = await wormholeGateway + .getGatewayInfo(2) + .then((info) => info.address); + + const sentAmount = BigInt(5000); + const signedVaa = await ethereumGatewaySendTbtc( payer, ethereumTokenBridge, - BigInt("100000000000"), - payer.publicKey + sentAmount, + fromGateway, + WORMHOLE_GATEWAY_PROGRAM_ID, + recipient ); - const recipientToken = await getOrCreateTokenAccount( - connection, - payer, - tbtcMint, - payer.publicKey + 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]); - await program.methods - .depositWormholeTbtc(new anchor.BN(500)) - .accounts({ - custodian, - wrappedTbtcToken: gatewayWrappedTbtcToken, - wrappedTbtcMint: WRAPPED_TBTC_MINT, - tbtcMint, - recipientWrappedToken: wrappedTbtcToken, - recipientToken: recipientToken.address, - recipient: payer.publicKey, - tbtcConfig, - minterInfo, - tbtcProgram: tbtcProgram.programId, - }) - .signers(tbtc.maybeAuthorityAnd(payer, [])) - .rpc(); + const [tbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, recipientToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); - await tbtc.checkState(authority, 2, 2, 2000); + // TODO: compare balances + }); + + it("send tbtc to gateway", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getTokenPDA(), + sender + ); + + // Check token account. + const gatewayBefore = await getAccount(connection, gatewayWrappedTbtcToken); - try { - await program.methods - .depositWormholeTbtc(new anchor.BN(50000)) - .accounts({ - custodian, - wrappedTbtcToken: gatewayWrappedTbtcToken, - wrappedTbtcMint: WRAPPED_TBTC_MINT, - tbtcMint, - recipientWrappedToken: wrappedTbtcToken, - recipientToken: recipientToken.address, - recipient: payer.publicKey, - tbtcConfig, - minterInfo, - tbtcProgram: tbtcProgram.programId, - }) - .signers(tbtc.maybeAuthorityAnd(payer, [])) - .rpc(); - 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("MintingLimitExceeded"); - expect(err.program.equals(program.programId)).is.true; - } + // 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 badAmount = BigInt(123000); + const badIx = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(badAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxFail([badIx], [commonTokenOwner], "NotEnoughWrappedTbtc"); + + // // This should work. + const goodAmount = BigInt(2000); + const ix = await wormholeGateway.sendTbtcGatewayIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(goodAmount.toString()), + recipientChain, + recipient, + nonce, + } + ); + await expectIxSuccess([ix], [commonTokenOwner]); }); }); diff --git a/cross-chain/solana/tests/helpers/consts.ts b/cross-chain/solana/tests/helpers/consts.ts index e27570ec2..748be2689 100644 --- a/cross-chain/solana/tests/helpers/consts.ts +++ b/cross-chain/solana/tests/helpers/consts.ts @@ -7,10 +7,10 @@ export const WORMHOLE_GATEWAY_PROGRAM_ID = new PublicKey( "8H9F5JGbEMyERycwaGuzLS5MQnV7dn2wm2h6egJ3Leiu" ); -export const SOLANA_CORE_BRIDGE_ADDRESS = new PublicKey( +export const CORE_BRIDGE_PROGRAM_ID = new PublicKey( "worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth" ); -export const SOLANA_TOKEN_BRIDGE_ADDRESS = new PublicKey( +export const TOKEN_BRIDGE_PROGRAM_ID = new PublicKey( "wormDTUJ6AWPNvk59vGQbDvGJmqbDTdgWgAqcLBCgUb" ); diff --git a/cross-chain/solana/tests/helpers/tbtc.ts b/cross-chain/solana/tests/helpers/tbtc.ts index 93cd9194e..4f918a62c 100644 --- a/cross-chain/solana/tests/helpers/tbtc.ts +++ b/cross-chain/solana/tests/helpers/tbtc.ts @@ -1,5 +1,4 @@ -import * as anchor from "@coral-xyz/anchor"; -import { Program } from "@coral-xyz/anchor"; +import { Program, Wallet, workspace } from "@coral-xyz/anchor"; import { getMint } from "@solana/spl-token"; import { PublicKey } from "@solana/web3.js"; import { expect } from "chai"; @@ -7,9 +6,7 @@ import { Tbtc } from "../../target/types/tbtc"; import { TBTC_PROGRAM_ID } from "./consts"; export function maybeAuthorityAnd(signer, signers) { - return signers.concat( - signer instanceof (anchor.Wallet as any) ? [] : [signer] - ); + return signers.concat(signer instanceof (Wallet as any) ? [] : [signer]); } export function getConfigPDA(): PublicKey { @@ -26,7 +23,7 @@ export function getTokenPDA(): PublicKey { )[0]; } -export function getMinterPDA(minter: PublicKey): PublicKey { +export function getMinterInfoPDA(minter: PublicKey): PublicKey { return PublicKey.findProgramAddressSync( [Buffer.from("minter-info"), minter.toBuffer()], TBTC_PROGRAM_ID @@ -60,7 +57,7 @@ export async function checkState( expectedGuardians: number, expectedTokensSupply ) { - const program = anchor.workspace.Tbtc as Program; + const program = workspace.Tbtc as Program; const config = getConfigPDA(); let configState = await program.account.config.fetch(config); @@ -84,15 +81,12 @@ export async function checkState( expect(mintersState.keys).has.length(expectedMinters); } -export async function addMinter( - authority, - minter -): Promise { - const program = anchor.workspace.Tbtc as Program; +export async function addMinter(authority, minter): Promise { + const program = workspace.Tbtc as Program; const config = getConfigPDA(); const minters = getMintersPDA(); - const minterInfoPDA = getMinterPDA(minter); + const minterInfoPDA = getMinterInfoPDA(minter); await program.methods .addMinter() .accounts({ diff --git a/cross-chain/solana/tests/helpers/utils.ts b/cross-chain/solana/tests/helpers/utils.ts index 901f2f980..a71044a2e 100644 --- a/cross-chain/solana/tests/helpers/utils.ts +++ b/cross-chain/solana/tests/helpers/utils.ts @@ -2,7 +2,7 @@ import { MockEthereumTokenBridge, MockGuardians, } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { web3 } from "@coral-xyz/anchor"; +import { Idl, Program, web3, workspace } from "@coral-xyz/anchor"; import { Account, TokenAccountNotFoundError, @@ -16,14 +16,16 @@ import { PublicKey, SystemProgram, Transaction, + TransactionInstruction, sendAndConfirmTransaction, } from "@solana/web3.js"; import { ETHEREUM_TBTC_ADDRESS, GUARDIAN_SET_INDEX, - SOLANA_CORE_BRIDGE_ADDRESS, - SOLANA_TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_PROGRAM_ID, + TOKEN_BRIDGE_PROGRAM_ID, WRAPPED_TBTC_MINT, + GUARDIAN_DEVNET_PRIVATE_KEYS, } from "./consts"; import { postVaaSolana, @@ -31,15 +33,17 @@ import { tryNativeToHexString, } from "@certusone/wormhole-sdk"; import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; +import { expect } from "chai"; +import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -export async function transferLamports( - connection: web3.Connection, +export async function transferLamports( fromSigner: web3.Keypair, toPubkey: web3.PublicKey, lamports: number ) { + const program = workspace.WormholeGateway as Program; return sendAndConfirmTransaction( - connection, + program.provider.connection, new Transaction().add( SystemProgram.transfer({ fromPubkey: fromSigner.publicKey, @@ -51,27 +55,27 @@ export async function transferLamports( ); } -export async function generatePayer( - connection: web3.Connection, - payer: Keypair, +export async function generatePayer( + funder: Keypair, lamports?: number ) { const newPayer = Keypair.generate(); - await transferLamports( - connection, - payer, + await transferLamports( + funder, newPayer.publicKey, lamports === undefined ? 1000000000 : lamports ); return newPayer; } -export async function getOrCreateTokenAccount( - connection: Connection, +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) => { @@ -96,22 +100,21 @@ export async function getOrCreateTokenAccount( ), [payer] ); - - return getAccount(connection, token); - } else { - return tokenData; } + + return token; } -export async function preloadWrappedTbtc( - connection: Connection, +export async function preloadWrappedTbtc( payer: Keypair, ethereumTokenBridge: MockEthereumTokenBridge, amount: bigint, tokenOwner: PublicKey ) { - const wrappedTbtcToken = await getOrCreateTokenAccount( - connection, + const program = workspace.WormholeGateway as Program; + const connection = program.provider.connection; + + const wrappedTbtcToken = await getOrCreateAta( payer, WRAPPED_TBTC_MINT, tokenOwner @@ -123,31 +126,32 @@ export async function preloadWrappedTbtc( 2, amount, 1, - wrappedTbtcToken.address.toBuffer().toString("hex"), + wrappedTbtcToken.toBuffer().toString("hex"), BigInt(0), 0, 0 ); - const signedVaa = await mockSignAndPostVaa(connection, payer, published); + const signedVaa = await mockSignAndPostVaa(payer, published); const tx = await redeemOnSolana( connection, - SOLANA_CORE_BRIDGE_ADDRESS, - SOLANA_TOKEN_BRIDGE_ADDRESS, + CORE_BRIDGE_PROGRAM_ID, + TOKEN_BRIDGE_PROGRAM_ID, payer.publicKey, signedVaa ); await web3.sendAndConfirmTransaction(connection, tx, [payer]); - return wrappedTbtcToken.address; + return wrappedTbtcToken; } -export async function mockSignAndPostVaa( - connection: web3.Connection, +export async function mockSignAndPostVaa( payer: web3.Keypair, published: Buffer ) { + const program = workspace.WormholeGateway as Program; + const guardians = new MockGuardians(GUARDIAN_SET_INDEX, [ "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", ]); @@ -157,9 +161,51 @@ export async function mockSignAndPostVaa( // Verify and post VAA. await postVaaSolana( - connection, + 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 +) { + const program = workspace.WormholeGateway as Program; + + const published = ethereumTokenBridge.publishTransferTokensWithPayload( + tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), + 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, - SOLANA_CORE_BRIDGE_ADDRESS, + CORE_BRIDGE_PROGRAM_ID, payer.publicKey, signedVaa ); @@ -167,28 +213,57 @@ export async function mockSignAndPostVaa( return signedVaa; } -// export function ethereumGatewaySendTbtc( -// ethereumTokenBridge: MockEthereumTokenBridge, -// amount: bigint, -// recipient: Buffer -// ) { -// const wrappedTbtcMint = getWrappedTbtcMintPDA(); -// const custodianWrappedTbtcToken = getWrappedTbtcTokenPDA; -// const published = ethereumTokenBridge.publishTransferTokens( -// tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), -// 2, -// BigInt("100000000000"), -// 1, -// wrappedTbtcToken.address.toBuffer().toString("hex"), -// BigInt(0), -// 0, -// 0 -// ); - -// const guardians = new mock.MockGuardians(GUARDIAN_SET_INDEX, [ -// "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", -// ]); - -// // Add guardian signature. -// const signedVaa = guardians.addSignatures(published, [0]); -// } +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) => { + console.log(err.logs); + 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 + ); + chai.assert(false, `transaction should have failed: ${txSig}`); + } catch (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 index 747da5a10..54a89dd46 100644 --- a/cross-chain/solana/tests/helpers/wormholeGateway.ts +++ b/cross-chain/solana/tests/helpers/wormholeGateway.ts @@ -1,14 +1,46 @@ -import { PublicKey } from "@solana/web3.js"; -import { WORMHOLE_GATEWAY_PROGRAM_ID } from "./consts"; +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("custodian")], + [Buffer.from("redeemer")], WORMHOLE_GATEWAY_PROGRAM_ID )[0]; } -export function getGatewayInfoPDA(targetChain): PublicKey { +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( @@ -37,3 +69,673 @@ export function getTokenBridgeRedeemerPDA(): PublicKey { 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 checkState( + expectedAuthority: PublicKey, + expectedMintingLimit: bigint +) { + const custodianState = await getCustodianData(); + + expect( + custodianState.mintingLimit.eq(new BN(expectedMintingLimit.toString())) + ).to.be.true; + expect(custodianState.authority).to.eql(expectedAuthority); +} + +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 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.getTokenPDA(); + } + + 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.getTokenPDA(); + } + + 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.getTokenPDA(); + } + + 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; +// 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 SendTbtcWrappedArgs = { +// 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.getTokenPDA(); +// } + +// 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 (tokenBridgeCoreEmitter === undefined) { +// [tokenBridgeCoreEmitter] = PublicKey.findProgramAddressSync( +// [Buffer.from("emitter")], +// TOKEN_BRIDGE_PROGRAM_ID +// ); +// } + +// 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(); +// } From 628d4e6ca14422070b98a23e14da7a1b5c17839d Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 10:55:04 -0500 Subject: [PATCH 19/38] solana: fix test accounts --- cross-chain/solana/Anchor.toml | 2 +- cross-chain/solana/tests/accounts/core_fee_collector.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cross-chain/solana/Anchor.toml b/cross-chain/solana/Anchor.toml index 127ca5360..052529539 100644 --- a/cross-chain/solana/Anchor.toml +++ b/cross-chain/solana/Anchor.toml @@ -62,7 +62,7 @@ address = "DapiQYH3BGonhN8cngWcXQ6SrqSm3cwysoznoHr6Sbsx" filename = "tests/accounts/token_bridge_config.json" ### Core Bridge -- Bridge Data -[[test.validator.clone]] +[[test.validator.account]] address = "2yVjuQwpsvdsrywzsJJVs9Ueh4zayyo5DYJbBNc3DDpn" filename = "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" From fc4d0202660850cab1ebdf3ad136dbec3d4dbec9 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 10:58:15 -0500 Subject: [PATCH 20/38] fix cpi; remove comments --- cross-chain/solana/Cargo.lock | 2 +- .../programs/wormhole-gateway/Cargo.toml | 2 +- .../src/processor/send_tbtc/gateway.rs | 4 +- .../src/processor/send_tbtc/wrapped.rs | 82 +++++++++---------- .../wormhole-gateway/src/state/custodian.rs | 4 +- 5 files changed, 45 insertions(+), 49 deletions(-) diff --git a/cross-chain/solana/Cargo.lock b/cross-chain/solana/Cargo.lock index b01fb264b..203a7e571 100644 --- a/cross-chain/solana/Cargo.lock +++ b/cross-chain/solana/Cargo.lock @@ -2069,7 +2069,7 @@ dependencies = [ [[package]] name = "wormhole-anchor-sdk" version = "0.1.0" -source = "git+https://github.com/wormhole-foundation/wormhole-scaffolding?rev=f8d5ba04bfd449ab3693b15c818fd3e85e30f758#f8d5ba04bfd449ab3693b15c818fd3e85e30f758" +source = "git+https://github.com/wormhole-foundation/wormhole-scaffolding?rev=81e6801bd9224958614504ff18914c220e9fdaf2#81e6801bd9224958614504ff18914c220e9fdaf2" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/cross-chain/solana/programs/wormhole-gateway/Cargo.toml b/cross-chain/solana/programs/wormhole-gateway/Cargo.toml index 6df2961fc..5655c5c0c 100644 --- a/cross-chain/solana/programs/wormhole-gateway/Cargo.toml +++ b/cross-chain/solana/programs/wormhole-gateway/Cargo.toml @@ -21,6 +21,6 @@ cpi = ["no-entrypoint"] 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 } +wormhole-anchor-sdk = { git = "https://github.com/wormhole-foundation/wormhole-scaffolding", rev = "81e6801bd9224958614504ff18914c220e9fdaf2", features = ["token-bridge"], default-features = false } tbtc = { path = "../tbtc", features = ["cpi"] } \ No newline at end of file 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 9fdbf0d02..5cd18da57 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 @@ -16,7 +16,6 @@ pub struct SendTbtcGateway<'info> { has_one = wrapped_tbtc_mint, has_one = tbtc_mint, has_one = token_bridge_sender, - // has_one = tbtc_minter_info, TODO: add this guy to custodian )] custodian: Account<'info, Custodian>, @@ -152,7 +151,6 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg let custodian = &ctx.accounts.custodian; // Finally transfer wrapped tBTC with the recipient encoded as this transfer's message. - // TODO: fix bug here: InvalidSigner(GZqbpJ4J1d4TwEG76fnQk48za4JE2FA13qaqWF8h1rvs) token_bridge::transfer_wrapped_with_payload( CpiContext::new_with_signer( ctx.accounts.token_bridge_program.to_account_info(), @@ -177,7 +175,6 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg wormhole_program: ctx.accounts.core_bridge_program.to_account_info(), }, &[ - &[Custodian::SEED_PREFIX, &[custodian.bump]], &[ token_bridge::SEED_PREFIX_SENDER, &[ctx.accounts.custodian.token_bridge_sender_bump], @@ -187,6 +184,7 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), &[ctx.bumps["core_message"]], ], + &[Custodian::SEED_PREFIX, &[custodian.bump]], ], ), nonce, 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 61c939b28..791bacef8 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 @@ -138,50 +138,46 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg let custodian = &ctx.accounts.custodian; - // 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_data.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]], + // 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"]], + &[ + token_bridge::SEED_PREFIX_SENDER, + &[ctx.accounts.custodian.token_bridge_sender_bump], + ], + &[ + b"msg", + &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), + &[ctx.bumps["core_message"]], + ], + &[Custodian::SEED_PREFIX, &[custodian.bump]], ], - ], + ), + 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 c3e38396a..8e2ea0d8b 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs @@ -19,6 +19,8 @@ pub struct Custodian { } impl Custodian { - /// TODO: This is an undesirable pattern in the Token Bridge due to how transfers are redeemed. + /// 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 25c8c4363da81d1e00763fb6f6b7eedc24a6ceb2 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 10:58:31 -0500 Subject: [PATCH 21/38] solana: add wrapped tbc test --- .../solana/tests/02__wormholeGateway.ts | 35 +- .../solana/tests/helpers/wormholeGateway.ts | 315 +++++++++--------- 2 files changed, 185 insertions(+), 165 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 22b2a8219..fce8d2d9b 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -53,7 +53,6 @@ async function setup( wrappedTbtcMint: WRAPPED_TBTC_MINT, wrappedTbtcToken: gatewayWrappedTbtcToken, tokenBridgeSender, - //tokenBridgeRedeemer, }) .rpc(); } @@ -324,4 +323,38 @@ describe("wormhole-gateway", () => { ); await expectIxSuccess([ix], [commonTokenOwner]); }); + + it("send wrapped tbtc", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getTokenPDA(), + sender + ); + + // Check token account. + const gatewayBefore = await getAccount(connection, gatewayWrappedTbtcToken); + + // Get destination gateway. + const recipientChain = 69; + const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); + const nonce = 420; + + // // This should work. + const goodAmount = BigInt(2000); + const ix = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(goodAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxSuccess([ix], [commonTokenOwner]); + }); }); diff --git a/cross-chain/solana/tests/helpers/wormholeGateway.ts b/cross-chain/solana/tests/helpers/wormholeGateway.ts index 54a89dd46..025df24f1 100644 --- a/cross-chain/solana/tests/helpers/wormholeGateway.ts +++ b/cross-chain/solana/tests/helpers/wormholeGateway.ts @@ -575,167 +575,154 @@ export async function sendTbtcGatewayIx( .instruction(); } -// type SendTbtcWrappedContext = { -// 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 SendTbtcWrappedArgs = { -// 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.getTokenPDA(); -// } - -// 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 (tokenBridgeCoreEmitter === undefined) { -// [tokenBridgeCoreEmitter] = PublicKey.findProgramAddressSync( -// [Buffer.from("emitter")], -// TOKEN_BRIDGE_PROGRAM_ID -// ); -// } - -// 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.getTokenPDA(); + } + + 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(); +} From 531c5855fa6eb7990266f508ff870dd0791ca905 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 11:19:42 -0500 Subject: [PATCH 22/38] solana: fix wormhole-anchor-sdk == 0.1.0-alpha.1 --- cross-chain/solana/Cargo.lock | 5 +++-- cross-chain/solana/programs/wormhole-gateway/Cargo.toml | 2 +- .../wormhole-gateway/src/processor/send_tbtc/wrapped.rs | 8 ++------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/cross-chain/solana/Cargo.lock b/cross-chain/solana/Cargo.lock index 203a7e571..809f12fa3 100644 --- a/cross-chain/solana/Cargo.lock +++ b/cross-chain/solana/Cargo.lock @@ -2068,8 +2068,9 @@ dependencies = [ [[package]] name = "wormhole-anchor-sdk" -version = "0.1.0" -source = "git+https://github.com/wormhole-foundation/wormhole-scaffolding?rev=81e6801bd9224958614504ff18914c220e9fdaf2#81e6801bd9224958614504ff18914c220e9fdaf2" +version = "0.1.0-alpha.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1789eb9fd2113b6e2945cb67123b902141a9bfde1ec33762be58447eb2431f6" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/cross-chain/solana/programs/wormhole-gateway/Cargo.toml b/cross-chain/solana/programs/wormhole-gateway/Cargo.toml index 5655c5c0c..b26b89a13 100644 --- a/cross-chain/solana/programs/wormhole-gateway/Cargo.toml +++ b/cross-chain/solana/programs/wormhole-gateway/Cargo.toml @@ -21,6 +21,6 @@ cpi = ["no-entrypoint"] 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 = "81e6801bd9224958614504ff18914c220e9fdaf2", features = ["token-bridge"], default-features = false } +wormhole-anchor-sdk = { version = "0.1.0-alpha.1", features = ["token-bridge"], default-features = false } tbtc = { path = "../tbtc", features = ["cpi"] } \ No newline at end of file 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 791bacef8..7fe40a5d2 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 anchor_lang::prelude::*; use anchor_spl::token; use wormhole_anchor_sdk::{ token_bridge::{self, program::TokenBridge}, @@ -162,16 +162,12 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg wormhole_program: ctx.accounts.core_bridge_program.to_account_info(), }, &[ - &[ - token_bridge::SEED_PREFIX_SENDER, - &[ctx.accounts.custodian.token_bridge_sender_bump], - ], + &[Custodian::SEED_PREFIX, &[custodian.bump]], &[ b"msg", &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), &[ctx.bumps["core_message"]], ], - &[Custodian::SEED_PREFIX, &[custodian.bump]], ], ), nonce, From 5a161eee3384f9b620eb3ba9f7157cd78d028567 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 11:26:44 -0500 Subject: [PATCH 23/38] solana: remove commented out code --- .../wormhole-gateway/src/processor/initialize.rs | 9 --------- .../programs/wormhole-gateway/src/state/custodian.rs | 2 -- 2 files changed, 11 deletions(-) diff --git a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs index 67e51d1d5..0028cfeff 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/processor/initialize.rs @@ -59,13 +59,6 @@ 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>, } @@ -79,8 +72,6 @@ pub fn initialize(ctx: Context, minting_limit: u64) -> Result<()> { 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_redeemer.key(), - // token_bridge_redeemer_bump: ctx.bumps["token_bridge_redeemer"], minting_limit, minted_amount: 0, }); 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 8e2ea0d8b..8f2a4385e 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs @@ -12,8 +12,6 @@ pub struct Custodian { 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, } From fa10d6d78c22237eef86423947d5237d73b4a838 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 11:49:06 -0500 Subject: [PATCH 24/38] solana: add minter not intialized test for tbtc deposits --- .../solana/tests/02__wormholeGateway.ts | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index fce8d2d9b..e456c1486 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -42,7 +42,6 @@ async function setup( const tbtcMint = tbtc.getTokenPDA(); const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); - //const tokenBridgeRedeemer = wormholeGateway.getTokenBridgeRedeemerPDA(); await program.methods .initialize(new anchor.BN(mintingLimit.toString())) @@ -61,6 +60,7 @@ 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; @@ -85,12 +85,16 @@ describe("wormhole-gateway", () => { const commonTokenOwner = anchor.web3.Keypair.generate(); + // Mock foreign emitter. const ethereumTokenBridge = new MockEthereumTokenBridge( ETHEREUM_TOKEN_BRIDGE_ADDRESS ); it("setup", async () => { + // Max amount of TBTC that can be minted. const mintingLimit = BigInt(10000); + + // Initialize the program. await setup(program, authority, mintingLimit); await wormholeGateway.checkState(authority.publicKey, mintingLimit); await tbtc.checkState(authority, 1, 2, 1500); @@ -118,11 +122,7 @@ describe("wormhole-gateway", () => { it("deposit wrapped tokens", async () => { const custodian = wormholeGateway.getCustodianPDA(); - // TODO: cannot deposit if custodian isn't minter yet. - - // Add custodian as minter. - await tbtc.addMinter(authority, custodian); - + // Set up new wallet const payer = await generatePayer(authority); @@ -139,15 +139,10 @@ describe("wormhole-gateway", () => { tbtcMint, payer.publicKey ); - - const [wrappedBefore, tbtcBefore, gatewayBefore] = await Promise.all([ - getAccount(connection, recipientWrappedToken), - getAccount(connection, recipientToken), - getAccount(connection, gatewayWrappedTbtcToken), - ]); - + const depositAmount = BigInt(500); + // Attempt to deposit before the custodian is a minter. const ix = await wormholeGateway.depositWormholeTbtcIx( { recipientWrappedToken, @@ -156,6 +151,18 @@ describe("wormhole-gateway", () => { }, depositAmount ); + await expectIxFail([ix], [payer], "AccountNotInitialized"); + + // Add custodian as minter. + await tbtc.addMinter(authority, custodian); + + // 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.checkState(authority, 2, 2, 2000); From 31e245cbebe45e11e026c595795869b97abbf8a2 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 12:29:37 -0500 Subject: [PATCH 25/38] solana: add balance checks to gateway tests --- .../solana/tests/02__wormholeGateway.ts | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index e456c1486..668b73426 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -279,7 +279,9 @@ describe("wormhole-gateway", () => { getAccount(connection, gatewayWrappedTbtcToken), ]); - // TODO: compare balances + // Check balance change. + expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + sentAmount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + sentAmount); }); it("send tbtc to gateway", async () => { @@ -290,8 +292,11 @@ describe("wormhole-gateway", () => { sender ); - // Check token account. - const gatewayBefore = await getAccount(connection, gatewayWrappedTbtcToken); + // Check token accounts. + const [senderTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); // Get destination gateway. const recipientChain = 2; @@ -314,7 +319,7 @@ describe("wormhole-gateway", () => { ); await expectIxFail([badIx], [commonTokenOwner], "NotEnoughWrappedTbtc"); - // // This should work. + // This should work. const goodAmount = BigInt(2000); const ix = await wormholeGateway.sendTbtcGatewayIx( { @@ -329,6 +334,16 @@ describe("wormhole-gateway", () => { } ); await expectIxSuccess([ix], [commonTokenOwner]); + + // Check token accounts after sending tbtc. + const [senderTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check balance change. + expect(senderTbtcAfter.amount).to.equal(senderTbtcBefore.amount - goodAmount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - goodAmount); }); it("send wrapped tbtc", async () => { @@ -339,15 +354,35 @@ describe("wormhole-gateway", () => { sender ); - // Check token account. - const gatewayBefore = await getAccount(connection, gatewayWrappedTbtcToken); + // Check token accounts. + const [senderTbtcBefore, gatewayBefore] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); // Get destination gateway. const recipientChain = 69; const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); const nonce = 420; - // // This should work. + // Try an amount that won't work. + const badAmount = BigInt(123000); + const badIx = await wormholeGateway.sendTbtcWrappedIx( + { + senderToken, + sender, + }, + { + amount: new anchor.BN(badAmount.toString()), + recipientChain, + recipient, + arbiterFee: new anchor.BN(0), + nonce, + } + ); + await expectIxFail([badIx], [commonTokenOwner], "NotEnoughWrappedTbtc"); + + // This should work. const goodAmount = BigInt(2000); const ix = await wormholeGateway.sendTbtcWrappedIx( { @@ -363,5 +398,15 @@ describe("wormhole-gateway", () => { } ); await expectIxSuccess([ix], [commonTokenOwner]); + + // Check token accounts after sending tbtc. + const [senderTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // Check balance change. + expect(senderTbtcAfter.amount).to.equal(senderTbtcBefore.amount - goodAmount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - goodAmount); }); }); From 64d1dc1c8030531cd9af7225fd2bd9c5c7a97b71 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 15:10:43 -0500 Subject: [PATCH 26/38] solana: add positive and negative tests for receiving tbtc --- .../src/processor/receive_tbtc.rs | 8 +- .../solana/tests/02__wormholeGateway.ts | 286 +++++++++++++++++- cross-chain/solana/tests/helpers/utils.ts | 8 +- 3 files changed, 291 insertions(+), 11 deletions(-) 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 ce183fae9..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 @@ -22,8 +22,7 @@ pub struct ReceiveTbtc<'info> { bump = custodian.bump, has_one = wrapped_tbtc_token, has_one = wrapped_tbtc_mint, - has_one = tbtc_mint, - //has_one = token_bridge_redeemer, + has_one = tbtc_mint )] custodian: Account<'info, Custodian>, @@ -96,9 +95,6 @@ pub struct ReceiveTbtc<'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_redeemer: UncheckedAccount<'info>, - /// CHECK: This account is needed for the Token Bridge program. token_bridge_wrapped_asset: UncheckedAccount<'info>, @@ -133,7 +129,7 @@ impl<'info> ReceiveTbtc<'info> { ); // There must be an encoded amount. - require_gte!( + require_gt!( transfer.amount(), 0, WormholeGatewayError::NoTbtcTransferred diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 668b73426..727c5e1d6 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -32,6 +32,7 @@ import { } from "./helpers"; import * as tbtc from "./helpers/tbtc"; import * as wormholeGateway from "./helpers/wormholeGateway"; +import { PublicKey } from "@solana/web3.js"; async function setup( program: Program, @@ -106,9 +107,13 @@ describe("wormhole-gateway", () => { tbtc.getTokenPDA(), commonTokenOwner.publicKey ); + + // Give the impostor some lamports. + await transferLamports(authority, impostorKeys.publicKey, 100000000000); }); it("update minting limit", async () => { + // Update minting limit as authority. const newLimit = BigInt(20000); const ix = await wormholeGateway.updateMintingLimitIx( { @@ -118,11 +123,19 @@ describe("wormhole-gateway", () => { ); await expectIxSuccess([ix], [authority]); await wormholeGateway.checkState(authority.publicKey, newLimit); + + // Only the authority can update the minting limit. + const failingIx = await wormholeGateway.updateMintingLimitIx( + { + authority: impostorKeys.publicKey, + }, + newLimit + BigInt(1) + ); + await expectIxFail([failingIx], [impostorKeys], "IsNotAuthority"); + await wormholeGateway.checkState(authority.publicKey, newLimit); }); it("deposit wrapped tokens", async () => { - const custodian = wormholeGateway.getCustodianPDA(); - // Set up new wallet const payer = await generatePayer(authority); @@ -282,6 +295,275 @@ describe("wormhole-gateway", () => { // Check balance change. expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + sentAmount); expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + sentAmount); + + // 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.getTokenPDA(), + recipient + ); + const recipientWrappedToken = getAssociatedTokenAddressSync( + WRAPPED_TBTC_MINT, + recipient + ); + + // 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.checkState(authority.publicKey, newLimit); + + // 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 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.getTokenPDA(), + recipient + ); + const recipientWrappedToken = await getOrCreateAta( + payer, + WRAPPED_TBTC_MINT, + recipient + ); + + // 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.checkState(authority.publicKey, newLimit); + + // 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 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.getTokenPDA(), + 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.getTokenPDA(), + 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.getTokenPDA(), 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"); }); it("send tbtc to gateway", async () => { diff --git a/cross-chain/solana/tests/helpers/utils.ts b/cross-chain/solana/tests/helpers/utils.ts index a71044a2e..18e2a8e1b 100644 --- a/cross-chain/solana/tests/helpers/utils.ts +++ b/cross-chain/solana/tests/helpers/utils.ts @@ -177,13 +177,15 @@ export async function ethereumGatewaySendTbtc( amount: bigint, fromGateway: number[], toGateway: PublicKey, - recipient: PublicKey + recipient: PublicKey, + tokenAddress?: string, + tokenChain?: number ) { const program = workspace.WormholeGateway as Program; const published = ethereumTokenBridge.publishTransferTokensWithPayload( - tryNativeToHexString(ETHEREUM_TBTC_ADDRESS, "ethereum"), - 2, + tryNativeToHexString(tokenAddress ?? ETHEREUM_TBTC_ADDRESS, "ethereum"), + tokenChain ?? 2, amount, 1, toGateway.toBuffer().toString("hex"), From 8fe2c508dc98e3174f8b1c31893eb0a7b829963a Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 15:23:07 -0500 Subject: [PATCH 27/38] solana: add negative outbound transfer tests --- .../solana/tests/02__wormholeGateway.ts | 83 +++++++++++++++---- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 727c5e1d6..b792253f4 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -583,49 +583,96 @@ describe("wormhole-gateway", () => { // Get destination gateway. const recipientChain = 2; const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const nonce = 420; + const nonce = 420; - // Try an amount that won't work. - const badAmount = BigInt(123000); - const badIx = await wormholeGateway.sendTbtcGatewayIx( + // This should work. + const sendAmount = BigInt(2000); + const ix = await wormholeGateway.sendTbtcGatewayIx( { senderToken, sender, }, { - amount: new anchor.BN(badAmount.toString()), + amount: new anchor.BN(sendAmount.toString()), recipientChain, recipient, nonce, } ); - await expectIxFail([badIx], [commonTokenOwner], "NotEnoughWrappedTbtc"); + await expectIxSuccess([ix], [commonTokenOwner]); - // This should work. - const goodAmount = BigInt(2000); + // Check token accounts after sending tbtc. + const [senderTbtcAfter, gatewayAfter] = await Promise.all([ + getAccount(connection, senderToken), + getAccount(connection, gatewayWrappedTbtcToken), + ]); + + // 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.getTokenPDA(), + 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(goodAmount.toString()), + amount: new anchor.BN(sendAmount.toString()), recipientChain, recipient, nonce, } ); - await expectIxSuccess([ix], [commonTokenOwner]); + await expectIxFail([ix], [commonTokenOwner], "NotEnoughWrappedTbtc"); + }); - // Check token accounts after sending tbtc. - const [senderTbtcAfter, gatewayAfter] = await Promise.all([ - getAccount(connection, senderToken), - getAccount(connection, gatewayWrappedTbtcToken), - ]); + it("cannot send tbtc to gateway (zero amount)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getTokenPDA(), + sender + ); - // Check balance change. - expect(senderTbtcAfter.amount).to.equal(senderTbtcBefore.amount - goodAmount); - expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - goodAmount); + // 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("send wrapped tbtc", async () => { From 2900c639e088d2d5d2e2c6fefd72db1611a946a9 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 12:06:48 -0500 Subject: [PATCH 28/38] solana: fix Makefile --- cross-chain/solana/Makefile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cross-chain/solana/Makefile b/cross-chain/solana/Makefile index e232a79f1..60201a688 100644 --- a/cross-chain/solana/Makefile +++ b/cross-chain/solana/Makefile @@ -26,4 +26,6 @@ test: node_modules lint: cargo fmt --check - cargo clippy --no-deps -- -D warnings \ No newline at end of file + 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 From c59d7c1df14a478a23687e27bc5361372a73c688 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 15:25:25 -0500 Subject: [PATCH 29/38] solana: add tbtc metadata creation --- cross-chain/solana/Cargo.lock | 135 ++++++++++++++++++ cross-chain/solana/programs/tbtc/Cargo.toml | 7 +- .../tbtc/src/processor/admin/initialize.rs | 42 +++++- .../src/processor/send_tbtc/gateway.rs | 2 +- 4 files changed, 181 insertions(+), 5 deletions(-) diff --git a/cross-chain/solana/Cargo.lock b/cross-chain/solana/Cargo.lock index 809f12fa3..414b454c2 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" @@ -1748,6 +1882,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "mpl-token-metadata", "solana-program", ] diff --git a/cross-chain/solana/programs/tbtc/Cargo.toml b/cross-chain/solana/programs/tbtc/Cargo.toml index 18e88c859..7d82bc736 100644 --- a/cross-chain/solana/programs/tbtc/Cargo.toml +++ b/cross-chain/solana/programs/tbtc/Cargo.toml @@ -19,5 +19,8 @@ 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-spl = { version = "=0.28.0", features = ["metadata"] } + +solana-program = "1.14.20" + +mpl-token-metadata = "1.13.1" 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 d36c49a9c..d4cf6ecd7 100644 --- a/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs +++ b/cross-chain/solana/programs/tbtc/src/processor/admin/initialize.rs @@ -3,7 +3,7 @@ use crate::{ state::{Config, Guardians, Minters}, }; use anchor_lang::prelude::*; -use anchor_spl::token; +use anchor_spl::{metadata, token}; #[derive(Accounts)] pub struct Initialize<'info> { @@ -49,11 +49,20 @@ pub struct Initialize<'info> { #[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(), @@ -65,15 +74,44 @@ pub fn initialize(ctx: Context) -> Result<()> { paused: false, }); + // 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(), }); - Ok(()) + // 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/wormhole-gateway/src/processor/send_tbtc/gateway.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/gateway.rs index 5cd18da57..bdac3890f 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 @@ -175,6 +175,7 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg wormhole_program: ctx.accounts.core_bridge_program.to_account_info(), }, &[ + &[Custodian::SEED_PREFIX, &[custodian.bump]], &[ token_bridge::SEED_PREFIX_SENDER, &[ctx.accounts.custodian.token_bridge_sender_bump], @@ -184,7 +185,6 @@ pub fn send_tbtc_gateway(ctx: Context, args: SendTbtcGatewayArg &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), &[ctx.bumps["core_message"]], ], - &[Custodian::SEED_PREFIX, &[custodian.bump]], ], ), nonce, From 67dad3068b7fd93175ec2a7575f0f66602388155 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 15:26:10 -0500 Subject: [PATCH 30/38] solana: refactor tbtc tests --- cross-chain/solana/package-lock.json | 100 ++ cross-chain/solana/package.json | 1 + cross-chain/solana/tests/01__tbtc.ts | 1408 +++++++++-------- .../solana/tests/02__wormholeGateway.ts | 113 +- cross-chain/solana/tests/helpers/tbtc.ts | 486 +++++- cross-chain/solana/tests/helpers/utils.ts | 83 +- .../solana/tests/helpers/wormholeGateway.ts | 8 +- 7 files changed, 1467 insertions(+), 732 deletions(-) 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 b9beb26b2..7d3591ec0 100644 --- a/cross-chain/solana/tests/01__tbtc.ts +++ b/cross-chain/solana/tests/01__tbtc.ts @@ -1,258 +1,17 @@ import * as anchor from "@coral-xyz/anchor"; -import { AnchorError, Program } 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 { expect } from "chai"; +import { assert, expect } from "chai"; import { Tbtc } from "../target/types/tbtc"; +import * as tbtc from "./helpers/tbtc"; import { - addMinter, - checkState, - getConfigPDA, - getGuardianPDA, - getGuardiansPDA, - getMinterInfoPDA, - getMintersPDA, - getTokenPDA, - maybeAuthorityAnd, -} from "./helpers/tbtc"; -import { transferLamports } from "./helpers/utils"; - -async function setup(program: Program, authority) { - const config = getConfigPDA(); - const guardians = getGuardiansPDA(); - const minters = getMintersPDA(); - const tbtcMintPDA = getTokenPDA(); - - await program.methods - .initialize() - .accounts({ - mint: tbtcMintPDA, - config, - guardians, - minters, - authority: authority.publicKey, - }) - .rpc(); -} - -async function changeAuthority( - program: Program, - authority, - newAuthority -) { - const config = getConfigPDA(); - 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(); - await program.methods - .takeAuthority() - .accounts({ - config, - pendingAuthority: newAuthority.publicKey, - }) - .signers(maybeAuthorityAnd(newAuthority, [])) - .rpc(); -} - -async function cancelAuthorityChange(program: Program, authority) { - const config = getConfigPDA(); - await program.methods - .cancelAuthorityChange() - .accounts({ - config, - authority: authority.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function checkPendingAuthority(program: Program, pendingAuthority) { - const config = getConfigPDA(); - let configState = await program.account.config.fetch(config); - expect(configState.pendingAuthority).to.eql(pendingAuthority.publicKey); -} - -async function checkNoPendingAuthority(program: Program) { - const config = getConfigPDA(); - let configState = await program.account.config.fetch(config); - expect(configState.pendingAuthority).to.equal(null); -} - -async function checkPaused(program: Program, paused: boolean) { - const config = getConfigPDA(); - let configState = await program.account.config.fetch(config); - expect(configState.paused).to.equal(paused); -} - -async function checkMinter(program: Program, minter) { - const minterInfoPDA = getMinterInfoPDA(minter.publicKey); - let minterInfo = await program.account.minterInfo.fetch(minterInfoPDA); - - expect(minterInfo.minter).to.eql(minter.publicKey); -} - -async function removeMinter( - program: Program, - authority, - minter, - minterInfo -) { - const config = getConfigPDA(); - const minters = getMintersPDA(); - await program.methods - .removeMinter() - .accounts({ - config, - authority: authority.publicKey, - minters, - minterInfo: minterInfo, - minter: minter.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function addGuardian( - program: Program, - authority, - guardian, - payer -): Promise { - const config = getConfigPDA(); - const guardians = getGuardiansPDA(); - const guardianInfoPDA = getGuardianPDA(guardian); - await program.methods - .addGuardian() - .accounts({ - config, - authority: authority.publicKey, - guardians, - guardianInfo: guardianInfoPDA, - guardian: guardian.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); - return guardianInfoPDA; -} - -async function checkGuardian(program: Program, guardian) { - const guardianInfoPDA = getGuardianPDA(guardian); - let guardianInfo = await program.account.guardianInfo.fetch(guardianInfoPDA); - - expect(guardianInfo.guardian).to.eql(guardian.publicKey); -} - -async function removeGuardian( - program: Program, - authority, - guardian, - guardianInfo -) { - const config = getConfigPDA(); - const guardians = getGuardiansPDA(); - await program.methods - .removeGuardian() - .accounts({ - config, - authority: authority.publicKey, - guardians, - guardianInfo: guardianInfo, - guardian: guardian.publicKey, - }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); -} - -async function pause(program: Program, guardian) { - const config = getConfigPDA(); - const guardianInfoPDA = getGuardianPDA(guardian); - await program.methods - .pause() - .accounts({ - config, - guardianInfo: guardianInfoPDA, - guardian: guardian.publicKey, - }) - .signers([guardian]) - .rpc(); -} - -async function unpause(program: Program, authority) { - const config = getConfigPDA(); - 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(); - const tbtcMintPDA = getTokenPDA(); - 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(); -} + expectIxFail, + expectIxSuccess, + getOrCreateAta, + getTokenBalance, + sleep, + transferLamports, +} from "./helpers/utils"; describe("tbtc", () => { // Configure the client to use the local cluster. @@ -260,388 +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(authority, 0, 0, 0); + 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("change authority", async () => { - await checkState(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(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(authority, 0, 0, 0); + 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("add minter", async () => { - await checkState(authority, 0, 0, 0); - await addMinter(authority, minterKeys.publicKey); - await checkMinter(program, minterKeys); - await checkState(authority, 1, 0, 0); - - // Transfer lamports to imposter. - await transferLamports(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(impostorKeys, minter2Keys.publicKey); - 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; - } - }); + 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("mint", async () => { - await checkState(authority, 1, 0, 0); - const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); - await checkMinter(program, minterKeys); - - // await setupMint(program, authority, recipientKeys); - await mint( - program, - minterKeys, - minterInfoPDA, - recipientKeys, - 1000, - authority - ); - - await checkState(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("cannot take authority if no pending", async () => { + const failedTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedTakeIx], + [newAuthority], + "NoPendingAuthorityChange" + ); + }); - it("won't mint", async () => { - await checkState(authority, 1, 0, 1000); - const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); - 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("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("use two minters", async () => { - await checkState(authority, 1, 0, 1000); - const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); - await checkMinter(program, minterKeys); - const minter2InfoPDA = await addMinter(authority, minter2Keys.publicKey); - await checkMinter(program, minter2Keys); - await checkState(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(authority, 2, 0, 1500); - }); + 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("remove minter", async () => { - await checkState(authority, 2, 0, 1500); - const minter2InfoPDA = getMinterInfoPDA(minter2Keys.publicKey); - await checkMinter(program, minter2Keys); - await removeMinter(program, authority, minter2Keys, minter2InfoPDA); - await checkState(authority, 1, 0, 1500); - }); + 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("won't remove minter", async () => { - await checkState(authority, 1, 0, 1500); - const minterInfoPDA = getMinterInfoPDA(minterKeys.publicKey); - 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(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("cannot take as signers that are not pending authority", async () => { + const failedImposterTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: imposter.publicKey, + }); + await expectIxFail( + [failedImposterTakeIx], + [imposter], + "IsNotPendingAuthority" + ); - it("add guardian", async () => { - await checkState(authority, 0, 0, 1500); - await addGuardian(program, authority, guardianKeys, authority); - await checkGuardian(program, guardianKeys); - await checkState(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; - } - }); + const failedNewAuthorityTakeIx = await tbtc.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedNewAuthorityTakeIx], + [newAuthority], + "IsNotPendingAuthority" + ); + }); - it("remove guardian", async () => { - await checkState(authority, 0, 1, 1500); - const guardianInfoPDA = getGuardianPDA(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(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("cannot cancel as someone else", async () => { + const anotherFailedCancelIx = await tbtc.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [anotherFailedCancelIx], + [authority], + "IsNotAuthority" + ); + }); - it("pause", async () => { - await checkState(authority, 0, 0, 1500); - await addGuardian(program, authority, guardianKeys, authority); - await checkPaused(program, false); - await pause(program, guardianKeys); - await checkPaused(program, true); + 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(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); - it("won't mint when paused", async () => { - await checkState(authority, 0, 1, 1500); - const minterInfoPDA = await addMinter(authority, minterKeys.publicKey); - 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); + 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("use two guardians", async () => { - await checkState(authority, 1, 1, 1500); - const guardianInfoPDA = getGuardianPDA(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 b792253f4..021dc290c 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -40,7 +40,7 @@ async function setup( mintingLimit: bigint ) { const custodian = wormholeGateway.getCustodianPDA(); - const tbtcMint = tbtc.getTokenPDA(); + const tbtcMint = tbtc.getMintPDA(); const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); @@ -66,7 +66,7 @@ describe("wormhole-gateway", () => { const connection = program.provider.connection; const custodian = wormholeGateway.getCustodianPDA(); - const tbtcMint = tbtc.getTokenPDA(); + const tbtcMint = tbtc.getMintPDA(); const tbtcConfig = tbtc.getConfigPDA(); const gatewayWrappedTbtcToken = wormholeGateway.getWrappedTbtcTokenPDA(); const tokenBridgeSender = wormholeGateway.getTokenBridgeSenderPDA(); @@ -86,7 +86,7 @@ describe("wormhole-gateway", () => { const commonTokenOwner = anchor.web3.Keypair.generate(); - // Mock foreign emitter. + // Mock foreign emitter. const ethereumTokenBridge = new MockEthereumTokenBridge( ETHEREUM_TOKEN_BRIDGE_ADDRESS ); @@ -98,13 +98,20 @@ describe("wormhole-gateway", () => { // Initialize the program. await setup(program, authority, mintingLimit); await wormholeGateway.checkState(authority.publicKey, mintingLimit); - await tbtc.checkState(authority, 1, 2, 1500); + await tbtc.checkConfig({ + authority: authority.publicKey, + numMinters: 0, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); // Also set up common token account. await transferLamports(authority, commonTokenOwner.publicKey, 100000000000); await getOrCreateAta( authority, - tbtc.getTokenPDA(), + tbtc.getMintPDA(), commonTokenOwner.publicKey ); @@ -113,7 +120,7 @@ describe("wormhole-gateway", () => { }); it("update minting limit", async () => { - // Update minting limit as authority. + // Update minting limit as authority. const newLimit = BigInt(20000); const ix = await wormholeGateway.updateMintingLimitIx( { @@ -152,7 +159,7 @@ describe("wormhole-gateway", () => { tbtcMint, payer.publicKey ); - + const depositAmount = BigInt(500); // Attempt to deposit before the custodian is a minter. @@ -167,7 +174,19 @@ describe("wormhole-gateway", () => { await expectIxFail([ix], [payer], "AccountNotInitialized"); // Add custodian as minter. - await tbtc.addMinter(authority, custodian); + 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([ @@ -177,7 +196,14 @@ describe("wormhole-gateway", () => { ]); await expectIxSuccess([ix], [payer]); - await tbtc.checkState(authority, 2, 2, 2000); + 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), @@ -253,7 +279,7 @@ describe("wormhole-gateway", () => { // Use common token account. const recipient = commonTokenOwner.publicKey; const recipientToken = getAssociatedTokenAddressSync( - tbtc.getTokenPDA(), + tbtc.getMintPDA(), recipient ); @@ -296,18 +322,18 @@ describe("wormhole-gateway", () => { expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + sentAmount); expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + sentAmount); - // Cannot receive tbtc again. + // 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.getTokenPDA(), + tbtc.getMintPDA(), recipient ); const recipientWrappedToken = getAssociatedTokenAddressSync( @@ -315,12 +341,12 @@ describe("wormhole-gateway", () => { recipient ); - // Verify that the wrapped token account doesn't exist yet. + // 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 @@ -350,7 +376,7 @@ describe("wormhole-gateway", () => { await wormholeGateway.checkState(authority.publicKey, newLimit); // Balance check before receiving wrapped tbtc. We can't - // check the balance of the recipient's wrapped tbtc yet, + // 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), @@ -367,7 +393,7 @@ describe("wormhole-gateway", () => { ); await expectIxSuccess([ix], [payer]); - // Check token accounts after receiving wrapped tbtc. We should + // 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), @@ -384,11 +410,11 @@ describe("wormhole-gateway", () => { 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.getTokenPDA(), + tbtc.getMintPDA(), recipient ); const recipientWrappedToken = await getOrCreateAta( @@ -424,7 +450,7 @@ describe("wormhole-gateway", () => { await expectIxSuccess([updateLimitIx], [authority]); await wormholeGateway.checkState(authority.publicKey, newLimit); - // Balance check before receiving wrapped tbtc. If this + // 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([ @@ -443,7 +469,7 @@ describe("wormhole-gateway", () => { ); await expectIxSuccess([ix], [payer]); - // Check token accounts after receiving wrapped tbtc. + // Check token accounts after receiving wrapped tbtc. const [tbtcAfter, wrappedTbtcAfter, gatewayAfter] = await Promise.all([ getAccount(connection, recipientToken), getAccount(connection, recipientWrappedToken), @@ -453,7 +479,9 @@ describe("wormhole-gateway", () => { // 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); + expect(wrappedTbtcAfter.amount).to.equal( + wrappedTbtcBefore.amount + sentAmount + ); }); it("cannot receive non-tbtc transfers", async () => { @@ -463,7 +491,7 @@ describe("wormhole-gateway", () => { // Use common token account. const recipient = commonTokenOwner.publicKey; const recipientToken = getAssociatedTokenAddressSync( - tbtc.getTokenPDA(), + tbtc.getMintPDA(), recipient ); @@ -502,7 +530,7 @@ describe("wormhole-gateway", () => { // Use common token account. const recipient = commonTokenOwner.publicKey; const recipientToken = getAssociatedTokenAddressSync( - tbtc.getTokenPDA(), + tbtc.getMintPDA(), recipient ); @@ -538,7 +566,11 @@ describe("wormhole-gateway", () => { // Use common token account. Set the recipient to the zero address. const recipient = PublicKey.default; - const defaultTokenAccount = await getOrCreateAta(payer, tbtc.getTokenPDA(), recipient); + const defaultTokenAccount = await getOrCreateAta( + payer, + tbtc.getMintPDA(), + recipient + ); // Get foreign gateway. const fromGateway = await wormholeGateway @@ -559,7 +591,7 @@ describe("wormhole-gateway", () => { { payer: payer.publicKey, recipientToken: defaultTokenAccount, - recipient + recipient, }, signedVaa ); @@ -570,7 +602,7 @@ describe("wormhole-gateway", () => { // Use common token account. const sender = commonTokenOwner.publicKey; const senderToken = getAssociatedTokenAddressSync( - tbtc.getTokenPDA(), + tbtc.getMintPDA(), sender ); @@ -583,7 +615,7 @@ describe("wormhole-gateway", () => { // Get destination gateway. const recipientChain = 2; const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const nonce = 420; + const nonce = 420; // This should work. const sendAmount = BigInt(2000); @@ -601,14 +633,16 @@ describe("wormhole-gateway", () => { ); await expectIxSuccess([ix], [commonTokenOwner]); - // Check token accounts after sending tbtc. + // Check token accounts after sending tbtc. const [senderTbtcAfter, gatewayAfter] = await Promise.all([ getAccount(connection, senderToken), getAccount(connection, gatewayWrappedTbtcToken), ]); // Check balance change. - expect(senderTbtcAfter.amount).to.equal(senderTbtcBefore.amount - sendAmount); + expect(senderTbtcAfter.amount).to.equal( + senderTbtcBefore.amount - sendAmount + ); expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - sendAmount); }); @@ -616,17 +650,20 @@ describe("wormhole-gateway", () => { // Use common token account. const sender = commonTokenOwner.publicKey; const senderToken = getAssociatedTokenAddressSync( - tbtc.getTokenPDA(), + tbtc.getMintPDA(), sender ); // Get destination gateway. const recipientChain = 2; const recipient = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const nonce = 420; + const nonce = 420; // Check token accounts. - const gatewayWrappedBalance = await getAccount(connection, gatewayWrappedTbtcToken); + const gatewayWrappedBalance = await getAccount( + connection, + gatewayWrappedTbtcToken + ); // Try an amount that won't work. const sendAmount = gatewayWrappedBalance.amount + BigInt(69); @@ -649,7 +686,7 @@ describe("wormhole-gateway", () => { // Use common token account. const sender = commonTokenOwner.publicKey; const senderToken = getAssociatedTokenAddressSync( - tbtc.getTokenPDA(), + tbtc.getMintPDA(), sender ); @@ -679,7 +716,7 @@ describe("wormhole-gateway", () => { // Use common token account. const sender = commonTokenOwner.publicKey; const senderToken = getAssociatedTokenAddressSync( - tbtc.getTokenPDA(), + tbtc.getMintPDA(), sender ); @@ -728,14 +765,16 @@ describe("wormhole-gateway", () => { ); await expectIxSuccess([ix], [commonTokenOwner]); - // Check token accounts after sending tbtc. + // Check token accounts after sending tbtc. const [senderTbtcAfter, gatewayAfter] = await Promise.all([ getAccount(connection, senderToken), getAccount(connection, gatewayWrappedTbtcToken), ]); // Check balance change. - expect(senderTbtcAfter.amount).to.equal(senderTbtcBefore.amount - goodAmount); + expect(senderTbtcAfter.amount).to.equal( + senderTbtcBefore.amount - goodAmount + ); expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - goodAmount); }); }); diff --git a/cross-chain/solana/tests/helpers/tbtc.ts b/cross-chain/solana/tests/helpers/tbtc.ts index 4f918a62c..7302c3ed3 100644 --- a/cross-chain/solana/tests/helpers/tbtc.ts +++ b/cross-chain/solana/tests/helpers/tbtc.ts @@ -1,13 +1,10 @@ -import { Program, Wallet, workspace } from "@coral-xyz/anchor"; +import { BN, Program, Wallet, workspace } from "@coral-xyz/anchor"; import { getMint } from "@solana/spl-token"; -import { PublicKey } from "@solana/web3.js"; -import { expect } from "chai"; +import { PublicKey, TransactionInstruction } from "@solana/web3.js"; +import { config, expect } from "chai"; import { Tbtc } from "../../target/types/tbtc"; import { TBTC_PROGRAM_ID } from "./consts"; - -export function maybeAuthorityAnd(signer, signers) { - return signers.concat(signer instanceof (Wallet as any) ? [] : [signer]); -} +import { PROGRAM_ID as METADATA_PROGRAM_ID } from "@metaplex-foundation/mpl-token-metadata"; export function getConfigPDA(): PublicKey { return PublicKey.findProgramAddressSync( @@ -16,13 +13,24 @@ export function getConfigPDA(): PublicKey { )[0]; } -export function getTokenPDA(): PublicKey { +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()], @@ -30,9 +38,9 @@ export function getMinterInfoPDA(minter: PublicKey): PublicKey { )[0]; } -export function getGuardianPDA(guardian): PublicKey { +export function getGuardianInfoPDA(guardian: PublicKey): PublicKey { return PublicKey.findProgramAddressSync( - [Buffer.from("guardian-info"), guardian.publicKey.toBuffer()], + [Buffer.from("guardian-info"), guardian.toBuffer()], TBTC_PROGRAM_ID )[0]; } @@ -51,52 +59,452 @@ export function getMintersPDA(): PublicKey { )[0]; } -export async function checkState( - expectedAuthority, - expectedMinters: number, - expectedGuardians: number, - expectedTokensSupply -) { +export async function getConfigData() { const program = workspace.Tbtc as Program; - const config = getConfigPDA(); - 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); + return program.account.config.fetch(config); +} - let tbtcMint = configState.mint; +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(); - let mintState = await getMint(program.provider.connection, tbtcMint); + 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); - expect(mintState.supply).to.equal(BigInt(expectedTokensSupply)); + const mintState = await getMint( + program.provider.connection, + configState.mint + ); + expect(mintState.supply).to.equal(supply); const guardians = getGuardiansPDA(); - let guardiansState = await program.account.guardians.fetch(guardians); - expect(guardiansState.keys).has.length(expectedGuardians); + const guardiansState = await program.account.guardians.fetch(guardians); + expect(guardiansState.keys).has.length(numGuardians); const minters = getMintersPDA(); - let mintersState = await program.account.minters.fetch(minters); - expect(mintersState.keys).has.length(expectedMinters); + const mintersState = await program.account.minters.fetch(minters); + expect(mintersState.keys).has.length(numMinters); } -export async function addMinter(authority, minter): Promise { +export async function getMinterInfo(minter: PublicKey) { const program = workspace.Tbtc as Program; - - const config = getConfigPDA(); - const minters = getMintersPDA(); const minterInfoPDA = getMinterInfoPDA(minter); - await program.methods + 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: authority.publicKey, + 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, - minterInfo: minterInfoPDA, + recipientToken, }) - .signers(maybeAuthorityAnd(authority, [])) - .rpc(); - return minterInfoPDA; + .instruction(); } diff --git a/cross-chain/solana/tests/helpers/utils.ts b/cross-chain/solana/tests/helpers/utils.ts index 18e2a8e1b..40980e668 100644 --- a/cross-chain/solana/tests/helpers/utils.ts +++ b/cross-chain/solana/tests/helpers/utils.ts @@ -1,8 +1,15 @@ +import { + postVaaSolana, + redeemOnSolana, + tryNativeToHexString, +} from "@certusone/wormhole-sdk"; import { MockEthereumTokenBridge, MockGuardians, } from "@certusone/wormhole-sdk/lib/cjs/mock"; -import { Idl, Program, web3, workspace } from "@coral-xyz/anchor"; +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, @@ -11,7 +18,6 @@ import { getAssociatedTokenAddressSync, } from "@solana/spl-token"; import { - Connection, Keypair, PublicKey, SystemProgram, @@ -19,29 +25,25 @@ import { 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, - CORE_BRIDGE_PROGRAM_ID, TOKEN_BRIDGE_PROGRAM_ID, WRAPPED_TBTC_MINT, - GUARDIAN_DEVNET_PRIVATE_KEYS, } from "./consts"; -import { - postVaaSolana, - redeemOnSolana, - tryNativeToHexString, -} from "@certusone/wormhole-sdk"; -import { NodeWallet } from "@certusone/wormhole-sdk/lib/cjs/solana"; -import { expect } from "chai"; -import * as coreBridge from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole"; -export async function transferLamports( +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; + const program = workspace.WormholeGateway as Program; return sendAndConfirmTransaction( program.provider.connection, new Transaction().add( @@ -55,12 +57,9 @@ export async function transferLamports( ); } -export async function generatePayer( - funder: Keypair, - lamports?: number -) { +export async function generatePayer(funder: Keypair, lamports?: number) { const newPayer = Keypair.generate(); - await transferLamports( + await transferLamports( funder, newPayer.publicKey, lamports === undefined ? 1000000000 : lamports @@ -68,12 +67,12 @@ export async function generatePayer( return newPayer; } -export async function getOrCreateAta( +export async function getOrCreateAta( payer: Keypair, mint: PublicKey, owner: PublicKey ) { - const program = workspace.WormholeGateway as Program; + const program = workspace.WormholeGateway as Program; const connection = program.provider.connection; const token = getAssociatedTokenAddressSync(mint, owner); @@ -105,16 +104,23 @@ export async function getOrCreateAta( return token; } -export async function preloadWrappedTbtc( +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 program = workspace.WormholeGateway as Program; const connection = program.provider.connection; - const wrappedTbtcToken = await getOrCreateAta( + const wrappedTbtcToken = await getOrCreateAta( payer, WRAPPED_TBTC_MINT, tokenOwner @@ -146,11 +152,11 @@ export async function preloadWrappedTbtc( return wrappedTbtcToken; } -export async function mockSignAndPostVaa( +export async function mockSignAndPostVaa( payer: web3.Keypair, published: Buffer ) { - const program = workspace.WormholeGateway as Program; + const program = workspace.WormholeGateway as Program; const guardians = new MockGuardians(GUARDIAN_SET_INDEX, [ "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0", @@ -171,7 +177,7 @@ export async function mockSignAndPostVaa( return signedVaa; } -export async function ethereumGatewaySendTbtc( +export async function ethereumGatewaySendTbtc( payer: web3.Keypair, ethereumTokenBridge: MockEthereumTokenBridge, amount: bigint, @@ -181,7 +187,7 @@ export async function ethereumGatewaySendTbtc( tokenAddress?: string, tokenChain?: number ) { - const program = workspace.WormholeGateway as Program; + const program = workspace.WormholeGateway as Program; const published = ethereumTokenBridge.publishTransferTokensWithPayload( tryNativeToHexString(tokenAddress ?? ETHEREUM_TBTC_ADDRESS, "ethereum"), @@ -215,35 +221,40 @@ export async function ethereumGatewaySendTbtc( return signedVaa; } -export async function expectIxSuccess( +export async function expectIxSuccess( ixes: TransactionInstruction[], signers: Keypair[] ) { - const program = workspace.WormholeGateway as Program; + const program = workspace.WormholeGateway as Program; await sendAndConfirmTransaction( program.provider.connection, new Transaction().add(...ixes), signers ).catch((err) => { - console.log(err.logs); + if (err.logs !== undefined) { + console.log(err.logs); + } throw err; }); } -export async function expectIxFail( +export async function expectIxFail( ixes: TransactionInstruction[], signers: Keypair[], errorMessage: string ) { - const program = workspace.WormholeGateway as Program; + const program = workspace.WormholeGateway as Program; try { const txSig = await sendAndConfirmTransaction( program.provider.connection, new Transaction().add(...ixes), signers ); - chai.assert(false, `transaction should have failed: ${txSig}`); + 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); } @@ -258,8 +269,8 @@ export function getTokenBridgeCoreEmitter() { return tokenBridgeCoreEmitter; } -export async function getTokenBridgeSequence() { - const program = workspace.WormholeGateway as Program; +export async function getTokenBridgeSequence() { + const program = workspace.WormholeGateway as Program; const emitter = getTokenBridgeCoreEmitter(); return coreBridge .getSequenceTracker( diff --git a/cross-chain/solana/tests/helpers/wormholeGateway.ts b/cross-chain/solana/tests/helpers/wormholeGateway.ts index 025df24f1..7666d69c9 100644 --- a/cross-chain/solana/tests/helpers/wormholeGateway.ts +++ b/cross-chain/solana/tests/helpers/wormholeGateway.ts @@ -204,7 +204,7 @@ export async function depositWormholeTbtcIx( } if (tbtcMint === undefined) { - tbtcMint = tbtc.getTokenPDA(); + tbtcMint = tbtc.getMintPDA(); } if (tbtcConfig === undefined) { @@ -320,7 +320,7 @@ export async function receiveTbtcIx( } if (tbtcMint === undefined) { - tbtcMint = tbtc.getTokenPDA(); + tbtcMint = tbtc.getMintPDA(); } if (recipientWrappedToken == undefined) { @@ -483,7 +483,7 @@ export async function sendTbtcGatewayIx( } if (tbtcMint === undefined) { - tbtcMint = tbtc.getTokenPDA(); + tbtcMint = tbtc.getMintPDA(); } if (tokenBridgeConfig === undefined) { @@ -643,7 +643,7 @@ export async function sendTbtcWrappedIx( } if (tbtcMint === undefined) { - tbtcMint = tbtc.getTokenPDA(); + tbtcMint = tbtc.getMintPDA(); } if (tokenBridgeConfig === undefined) { From 0d71328b37462e1cbd7884c015e54d059bf6db58 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 15:43:50 -0500 Subject: [PATCH 31/38] solana: add negative outbound wrapped transfer tests --- .../solana/tests/02__wormholeGateway.ts | 144 +++++++++++++++--- 1 file changed, 127 insertions(+), 17 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 021dc290c..a2e3f069b 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -712,6 +712,35 @@ describe("wormhole-gateway", () => { 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("send wrapped tbtc", async () => { // Use common token account. const sender = commonTokenOwner.publicKey; @@ -731,50 +760,131 @@ describe("wormhole-gateway", () => { 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 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 badAmount = BigInt(123000); - const badIx = await wormholeGateway.sendTbtcWrappedIx( + const sendAmount = gatewayWrappedBalance.amount + BigInt(69); + const ix = await wormholeGateway.sendTbtcWrappedIx( { senderToken, sender, }, { - amount: new anchor.BN(badAmount.toString()), + amount: new anchor.BN(sendAmount.toString()), recipientChain, recipient, arbiterFee: new anchor.BN(0), nonce, } ); - await expectIxFail([badIx], [commonTokenOwner], "NotEnoughWrappedTbtc"); + await expectIxFail([ix], [commonTokenOwner], "NotEnoughWrappedTbtc"); + }); - // This should work. - const goodAmount = BigInt(2000); + 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(goodAmount.toString()), + amount: new anchor.BN(sendAmount.toString()), recipientChain, recipient, arbiterFee: new anchor.BN(0), nonce, } ); - await expectIxSuccess([ix], [commonTokenOwner]); + await expectIxFail([ix], [commonTokenOwner], "ZeroAmount"); + }); - // Check token accounts after sending tbtc. - const [senderTbtcAfter, gatewayAfter] = await Promise.all([ - getAccount(connection, senderToken), - getAccount(connection, gatewayWrappedTbtcToken), - ]); + it("cannot send wrapped tbtc (recipient is zero address)", async () => { + // Use common token account. + const sender = commonTokenOwner.publicKey; + const senderToken = getAssociatedTokenAddressSync( + tbtc.getMintPDA(), + sender + ); - // Check balance change. - expect(senderTbtcAfter.amount).to.equal( - senderTbtcBefore.amount - goodAmount + // 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, + } ); - expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - goodAmount); + await expectIxFail([ix], [commonTokenOwner], "ZeroRecipient"); }); }); From ddad9e97a178b8d9f05ffb5b2c841bc0bcb3dac7 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 15:49:25 -0500 Subject: [PATCH 32/38] solana: add invalid outbound gateway test --- .../solana/tests/02__wormholeGateway.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index a2e3f069b..b8b9d85f0 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -741,6 +741,35 @@ describe("wormhole-gateway", () => { 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"); + }); + it("send wrapped tbtc", async () => { // Use common token account. const sender = commonTokenOwner.publicKey; From f5672ceb0c65886586ac5a5f24389494c2bf2f73 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 16:03:08 -0500 Subject: [PATCH 33/38] solana: start gateway test cleanup --- .../solana/tests/02__wormholeGateway.ts | 1606 +++++++++-------- 1 file changed, 810 insertions(+), 796 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index b8b9d85f0..69a6f337f 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -91,829 +91,843 @@ describe("wormhole-gateway", () => { ETHEREUM_TOKEN_BRIDGE_ADDRESS ); - it("setup", async () => { - // Max amount of TBTC that can be minted. - const mintingLimit = BigInt(10000); - - // Initialize the program. - await setup(program, authority, mintingLimit); - await wormholeGateway.checkState(authority.publicKey, mintingLimit); - await tbtc.checkConfig({ - authority: authority.publicKey, - numMinters: 0, - numGuardians: 0, - supply: BigInt(2000), - paused: false, - pendingAuthority: null, - }); - - // Also set up common token account. - await transferLamports(authority, commonTokenOwner.publicKey, 100000000000); - await getOrCreateAta( - authority, - tbtc.getMintPDA(), - commonTokenOwner.publicKey - ); - - // Give the impostor some lamports. - await transferLamports(authority, impostorKeys.publicKey, 100000000000); - }); - - it("update minting limit", async () => { - // Update minting limit as authority. - const newLimit = BigInt(20000); - const ix = await wormholeGateway.updateMintingLimitIx( - { + 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.checkState(authority.publicKey, mintingLimit); + await tbtc.checkConfig({ authority: authority.publicKey, - }, - newLimit - ); - await expectIxSuccess([ix], [authority]); - await wormholeGateway.checkState(authority.publicKey, newLimit); - - // Only the authority can update the minting limit. - const failingIx = await wormholeGateway.updateMintingLimitIx( - { - authority: impostorKeys.publicKey, - }, - newLimit + BigInt(1) - ); - await expectIxFail([failingIx], [impostorKeys], "IsNotAuthority"); - await wormholeGateway.checkState(authority.publicKey, newLimit); - }); - - 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 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"); - - // Add custodian as minter. - const addMinterIx = await tbtc.addMinterIx({ - authority: authority.publicKey, - minter: custodian, + numMinters: 0, + numGuardians: 0, + supply: BigInt(2000), + paused: false, + pendingAuthority: null, + }); + + // Also set up common token account. + await transferLamports(authority, commonTokenOwner.publicKey, 100000000000); + await getOrCreateAta( + authority, + tbtc.getMintPDA(), + commonTokenOwner.publicKey + ); + + // Give the impostor some lamports. + await transferLamports(authority, impostorKeys.publicKey, 100000000000); }); - await expectIxSuccess([addMinterIx], [authority]); - await tbtc.checkConfig({ - authority: authority.publicKey, - numMinters: 1, - numGuardians: 0, - supply: BigInt(2000), - paused: false, - 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.checkState(authority.publicKey, newLimit); }); - // 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, + 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: impostorKeys.publicKey, + }, + newLimit + ); + await expectIxFail([failingIx], [impostorKeys], "IsNotAuthority"); }); - - const [wrappedAfter, tbtcAfter, gatewayAfter] = await Promise.all([ - getAccount(connection, recipientWrappedToken), - getAccount(connection, recipientToken), - getAccount(connection, gatewayWrappedTbtcToken), - ]); - - // 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); - - // Cannot deposit past minting limit. - const failingIx = await wormholeGateway.depositWormholeTbtcIx( - { - recipientWrappedToken, - recipientToken, - recipient: payer.publicKey, - }, - BigInt(50000) - ); - await expectIxFail([failingIx], [payer], "MintingLimitExceeded"); - - // 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.checkState(authority.publicKey, newLimit); - await expectIxSuccess([failingIx], [payer]); }); - it("update gateway address", async () => { + describe("gateway address", () => { const chain = 2; - // demonstrate gateway address does not exist - const gatewayInfo = await connection.getAccountInfo( - wormholeGateway.getGatewayInfoPDA(chain) - ); - expect(gatewayInfo).is.null; + 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); + }); - // Make new gateway. - const firstAddress = Array.from(Buffer.alloc(32, "deadbeef", "hex")); - const firstIx = await wormholeGateway.updateGatewayAddress( - { + 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); + }); + }); + + describe("other", () => { + 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 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"); + + // Add custodian as minter. + const addMinterIx = await tbtc.addMinterIx({ authority: authority.publicKey, - }, - { chain, address: firstAddress } - ); - await expectIxSuccess([firstIx], [authority]); - await wormholeGateway.checkGateway(chain, firstAddress); - - // Update gateway. - const goodAddress = Array.from(ethereumTokenBridge.address); - const secondIx = await wormholeGateway.updateGatewayAddress( - { + minter: custodian, + }); + await expectIxSuccess([addMinterIx], [authority]); + await tbtc.checkConfig({ authority: authority.publicKey, - }, - { chain, address: goodAddress } - ); - await expectIxSuccess([secondIx], [authority]); - await wormholeGateway.checkGateway(chain, goodAddress); - }); + 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 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); + + // Cannot deposit past minting limit. + const failingIx = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + BigInt(50000) + ); + await expectIxFail([failingIx], [payer], "MintingLimitExceeded"); + + // 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.checkState(authority.publicKey, newLimit); + await expectIxSuccess([failingIx], [payer]); + }); + + 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); + + 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); + + // Cannot receive tbtc again. + await expectIxFail([ix], [payer], "TransferAlreadyRedeemed"); + }); - 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); - - 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); - - // 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 + ); + + // Verify that the wrapped token account doesn't exist yet. + try { + await getAccount(connection, recipientWrappedToken); + } catch (e: any) { + expect(e.toString()).to.equal("TokenAccountNotFoundError"); + } - 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 - ); - - // 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.checkState(authority.publicKey, newLimit); - - // 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 balance change. - expect(tbtcAfter.amount).to.equal(tbtcBefore.amount); - expect(gatewayAfter.amount).to.equal(gatewayBefore.amount); - expect(wrappedTbtcAfter.amount).to.equal(sentAmount); - }); + // 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.checkState(authority.publicKey, newLimit); + + // 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 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); - - // 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.checkState(authority.publicKey, newLimit); - - // 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 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("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); + + // 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.checkState(authority.publicKey, newLimit); + + // 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 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, + 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, - }, - signedVaa - ); - await expectIxFail([failingIx], [payer], "InvalidEthereumTbtc"); - }); + "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 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"); - }); + 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"); + }); - 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), - ]); - - // 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 balance change. - expect(senderTbtcAfter.amount).to.equal( - senderTbtcBefore.amount - sendAmount - ); - expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - sendAmount); - }); + 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), + ]); + + // 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 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 (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 (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 (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"); - }); + 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"); + }); - 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), - ]); - - // 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 balance change. - expect(senderTbtcAfter.amount).to.equal( - senderTbtcBefore.amount - sendAmount - ); - expect(gatewayAfter.amount).to.equal(gatewayBefore.amount - sendAmount); - }); + 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), + ]); + + // 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 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 (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(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"); + 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"); + }); }); }); From fdd0ae31ca0897d542fff32de793afc7335cda5d Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 16:05:46 -0500 Subject: [PATCH 34/38] solana: add change authority to gateway --- cross-chain/solana/programs/tbtc/src/error.rs | 38 ++++---- .../src/processor/admin/take_authority.rs | 22 ++++- .../programs/wormhole-gateway/src/error.rs | 6 ++ .../programs/wormhole-gateway/src/lib.rs | 12 +++ .../admin/cancel_authority_change.rs | 22 +++++ .../src/processor/admin/change_authority.rs | 23 +++++ .../src/processor/{ => admin}/initialize.rs | 1 + .../src/processor/admin/mod.rs | 17 ++++ .../src/processor/admin/take_authority.rs | 38 ++++++++ .../{ => admin}/update_gateway_address.rs | 0 .../{ => admin}/update_minting_limit.rs | 0 .../wormhole-gateway/src/processor/mod.rs | 12 +-- .../wormhole-gateway/src/state/custodian.rs | 1 + .../solana/tests/helpers/wormholeGateway.ts | 92 +++++++++++++++++-- 14 files changed, 244 insertions(+), 40 deletions(-) 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 (98%) 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 (100%) rename cross-chain/solana/programs/wormhole-gateway/src/processor/{ => admin}/update_minting_limit.rs (100%) diff --git a/cross-chain/solana/programs/tbtc/src/error.rs b/cross-chain/solana/programs/tbtc/src/error.rs index 7db624a97..433eb20eb 100644 --- a/cross-chain/solana/programs/tbtc/src/error.rs +++ b/cross-chain/solana/programs/tbtc/src/error.rs @@ -2,36 +2,36 @@ use anchor_lang::prelude::error_code; #[error_code] pub enum TbtcError { - #[msg("This address is already a minter")] - MinterAlreadyExists = 0x10, + #[msg("Not valid authority to perform this action")] + IsNotAuthority = 0x20, - #[msg("This address is not a minter")] - MinterNonexistent = 0x12, + #[msg("Not valid pending authority to take authority")] + IsNotPendingAuthority = 0x22, + + #[msg("No pending authority")] + NoPendingAuthorityChange = 0x24, #[msg("This address is already a guardian")] - GuardianAlreadyExists = 0x20, + GuardianAlreadyExists = 0x30, #[msg("This address is not a guardian")] - GuardianNonexistent = 0x22, + GuardianNonexistent = 0x32, #[msg("Caller is not a guardian")] - SignerNotGuardian = 0x30, + 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 = 0x32, + SignerNotMinter = 0x44, #[msg("Program is paused")] - IsPaused = 0x40, + IsPaused = 0x50, #[msg("Program is not paused")] - IsNotPaused = 0x42, - - #[msg("Not valid authority to perform this action")] - IsNotAuthority = 0x50, - - #[msg("Not valid pending authority to take authority")] - IsNotPendingAuthority = 0x52, - - #[msg("No pending authority")] - NoPendingAuthorityChange = 0x54, + IsNotPaused = 0x52, } 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 f5c8122e0..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,16 +7,30 @@ 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; diff --git a/cross-chain/solana/programs/wormhole-gateway/src/error.rs b/cross-chain/solana/programs/wormhole-gateway/src/error.rs index 972c548f8..c48f2760f 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/error.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/error.rs @@ -8,6 +8,12 @@ pub enum WormholeGatewayError { #[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, diff --git a/cross-chain/solana/programs/wormhole-gateway/src/lib.rs b/cross-chain/solana/programs/wormhole-gateway/src/lib.rs index 39b3fb5f9..65d5ca7c9 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/lib.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/lib.rs @@ -34,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, 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 98% 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 0028cfeff..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 @@ -67,6 +67,7 @@ pub fn initialize(ctx: Context, minting_limit: u64) -> Result<()> { ctx.accounts.custodian.set_inner(Custodian { 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(), 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 100% 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 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 100% 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 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 5023cbdf1..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 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/state/custodian.rs b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs index 8f2a4385e..15a545f43 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/state/custodian.rs @@ -6,6 +6,7 @@ use wormhole_anchor_sdk::token_bridge; pub struct Custodian { pub bump: u8, pub authority: Pubkey, + pub pending_authority: Option, pub tbtc_mint: Pubkey, pub wrapped_tbtc_mint: Pubkey, diff --git a/cross-chain/solana/tests/helpers/wormholeGateway.ts b/cross-chain/solana/tests/helpers/wormholeGateway.ts index 7666d69c9..8241b8ccb 100644 --- a/cross-chain/solana/tests/helpers/wormholeGateway.ts +++ b/cross-chain/solana/tests/helpers/wormholeGateway.ts @@ -76,16 +76,18 @@ export async function getCustodianData() { return program.account.custodian.fetch(custodian); } -export async function checkState( - expectedAuthority: PublicKey, - expectedMintingLimit: bigint -) { +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(expectedMintingLimit.toString())) - ).to.be.true; - expect(custodianState.authority).to.eql(expectedAuthority); + 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 getGatewayInfo(chain: number) { @@ -99,6 +101,80 @@ export async function checkGateway(chain: number, expectedAddress: number[]) { 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; From 8a4a0b0dc8d5e1f12005f349d0689834657a5e6a Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 16:12:26 -0500 Subject: [PATCH 35/38] solana: add gateway authority tests --- .../solana/tests/02__wormholeGateway.ts | 179 ++++++++++++++++-- 1 file changed, 160 insertions(+), 19 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index 69a6f337f..f9a0f7610 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -78,11 +78,12 @@ describe("wormhole-gateway", () => { 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 recipient = anchor.web3.Keypair.generate(); + const txPayer = anchor.web3.Keypair.generate(); const commonTokenOwner = anchor.web3.Keypair.generate(); @@ -91,14 +92,26 @@ describe("wormhole-gateway", () => { 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); + }); + describe("setup", () => { - it("initialize", async () => { + 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.checkState(authority.publicKey, mintingLimit); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit, + pendingAuthority: null, + }); await tbtc.checkConfig({ authority: authority.publicKey, numMinters: 0, @@ -109,7 +122,6 @@ describe("wormhole-gateway", () => { }); // Also set up common token account. - await transferLamports(authority, commonTokenOwner.publicKey, 100000000000); await getOrCreateAta( authority, tbtc.getMintPDA(), @@ -117,11 +129,120 @@ describe("wormhole-gateway", () => { ); // Give the impostor some lamports. - await transferLamports(authority, impostorKeys.publicKey, 100000000000); + await transferLamports(authority, imposter.publicKey, 100000000000); }); - }); + }); - describe("minting limit", () => { + describe("authority changes", () => { + it("cannot cancel authority if no pending", async () => { + const failedCancelIx = await wormholeGateway.cancelAuthorityChangeIx({ + authority: authority.publicKey, + }); + await expectIxFail( + [failedCancelIx], + [authority], + "NoPendingAuthorityChange" + ); + }); + + it("cannot take authority if no pending", async () => { + const failedTakeIx = await wormholeGateway.takeAuthorityIx({ + pendingAuthority: newAuthority.publicKey, + }); + await expectIxFail( + [failedTakeIx], + [newAuthority], + "NoPendingAuthorityChange" + ); + }); + + 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, + }); + }); + + 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); @@ -132,7 +253,11 @@ describe("wormhole-gateway", () => { newLimit ); await expectIxSuccess([ix], [authority]); - await wormholeGateway.checkState(authority.publicKey, newLimit); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); }); it("cannot update minting limit (not authority)", async () => { @@ -140,11 +265,11 @@ describe("wormhole-gateway", () => { const newLimit = BigInt(69000); const failingIx = await wormholeGateway.updateMintingLimitIx( { - authority: impostorKeys.publicKey, + authority: imposter.publicKey, }, newLimit ); - await expectIxFail([failingIx], [impostorKeys], "IsNotAuthority"); + await expectIxFail([failingIx], [imposter], "IsNotAuthority"); }); }); @@ -160,7 +285,7 @@ describe("wormhole-gateway", () => { }); it("set initial gateway address", async () => { - // Make new gateway. + // Make new gateway. const firstAddress = Array.from(Buffer.alloc(32, "deadbeef", "hex")); const firstIx = await wormholeGateway.updateGatewayAddress( { @@ -184,7 +309,7 @@ describe("wormhole-gateway", () => { await expectIxSuccess([secondIx], [authority]); await wormholeGateway.checkGateway(chain, goodAddress); }); - }); + }); describe("other", () => { it("deposit wrapped tokens", async () => { @@ -257,9 +382,13 @@ describe("wormhole-gateway", () => { ]); // Check balance change. - expect(wrappedAfter.amount).to.equal(wrappedBefore.amount - depositAmount); + expect(wrappedAfter.amount).to.equal( + wrappedBefore.amount - depositAmount + ); expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + depositAmount); - expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + depositAmount); + expect(gatewayAfter.amount).to.equal( + gatewayBefore.amount + depositAmount + ); // Cannot deposit past minting limit. const failingIx = await wormholeGateway.depositWormholeTbtcIx( @@ -281,9 +410,13 @@ describe("wormhole-gateway", () => { newLimit ); await expectIxSuccess([updateLimitIx], [authority]); - await wormholeGateway.checkState(authority.publicKey, newLimit); + await wormholeGateway.checkCustodian({ + authority: authority.publicKey, + mintingLimit: newLimit, + pendingAuthority: null, + }); await expectIxSuccess([failingIx], [payer]); - }); + }); it("receive tbtc", async () => { // Set up new wallet @@ -386,7 +519,11 @@ describe("wormhole-gateway", () => { newLimit ); await expectIxSuccess([updateLimitIx], [authority]); - await wormholeGateway.checkState(authority.publicKey, newLimit); + 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, @@ -461,7 +598,11 @@ describe("wormhole-gateway", () => { newLimit ); await expectIxSuccess([updateLimitIx], [authority]); - await wormholeGateway.checkState(authority.publicKey, newLimit); + 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 From 242b9097157825f93722f66b474822d30af1c733 Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 16:25:40 -0500 Subject: [PATCH 36/38] solana: fix consts --- .../programs/wormhole-gateway/src/constants.rs | 12 ++++++++---- .../src/processor/send_tbtc/gateway.rs | 12 +++++++++--- .../src/processor/send_tbtc/wrapped.rs | 11 +++++++---- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/cross-chain/solana/programs/wormhole-gateway/src/constants.rs b/cross-chain/solana/programs/wormhole-gateway/src/constants.rs index 31180515f..75ece6f5b 100644 --- a/cross-chain/solana/programs/wormhole-gateway/src/constants.rs +++ b/cross-chain/solana/programs/wormhole-gateway/src/constants.rs @@ -1,14 +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, + 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")] +/// 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, 0x18, 0x08, 0x4f, 0xbA, 0x66, 0x6a, - 0x33, 0xd3, 0x75, 0x92, 0xfa, 0x26, 0x33, 0xfD, 0x49, 0xa7, 0x4d, 0xd9, 0x3a, 0x88, + 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/processor/send_tbtc/gateway.rs b/cross-chain/solana/programs/wormhole-gateway/src/processor/send_tbtc/gateway.rs index bdac3890f..c1764e758 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::{ @@ -62,7 +65,10 @@ pub struct SendTbtcGateway<'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>, @@ -181,7 +187,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"]], ], 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 7fe40a5d2..79c691edd 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,4 +1,4 @@ -use crate::state::Custodian; +use crate::{constants::MSG_SEED_PREFIX, state::Custodian}; use anchor_lang::prelude::*; use anchor_spl::token; use wormhole_anchor_sdk::{ @@ -55,8 +55,11 @@ pub struct SendTbtcWrapped<'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>, @@ -164,7 +167,7 @@ pub fn send_tbtc_wrapped(ctx: Context, args: SendTbtcWrappedArg &[ &[Custodian::SEED_PREFIX, &[custodian.bump]], &[ - b"msg", + MSG_SEED_PREFIX, &ctx.accounts.core_emitter_sequence.value().to_le_bytes(), &[ctx.bumps["core_message"]], ], From c86b26dc755dd8ca5e980670fd6ad6c16073f9ec Mon Sep 17 00:00:00 2001 From: A5 Pickle Date: Fri, 4 Aug 2023 16:31:00 -0500 Subject: [PATCH 37/38] solana: uptick to solana 1.14.22; fix Cargo.lock --- cross-chain/solana/Cargo.lock | 29 ++++++++++--------- cross-chain/solana/programs/tbtc/Cargo.toml | 6 ++-- .../programs/wormhole-gateway/Cargo.toml | 4 ++- 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/cross-chain/solana/Cargo.lock b/cross-chain/solana/Cargo.lock index 414b454c2..b9f9f8463 100644 --- a/cross-chain/solana/Cargo.lock +++ b/cross-chain/solana/Cargo.lock @@ -1591,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", @@ -1625,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", @@ -1637,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", @@ -1648,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", @@ -1697,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", @@ -1748,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", @@ -1761,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", @@ -2218,6 +2218,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "solana-program", "tbtc", "wormhole-anchor-sdk", ] diff --git a/cross-chain/solana/programs/tbtc/Cargo.toml b/cross-chain/solana/programs/tbtc/Cargo.toml index 7d82bc736..b80c4641c 100644 --- a/cross-chain/solana/programs/tbtc/Cargo.toml +++ b/cross-chain/solana/programs/tbtc/Cargo.toml @@ -18,9 +18,9 @@ no-log-ix-name = [] cpi = ["no-entrypoint"] [dependencies] -anchor-lang = { version = "=0.28.0", features = ["derive", "init-if-needed"] } -anchor-spl = { version = "=0.28.0", features = ["metadata"] } +anchor-lang = { version = "0.28.0", features = ["derive", "init-if-needed"] } +anchor-spl = { version = "0.28.0", features = ["metadata"] } -solana-program = "1.14.20" +solana-program = "=1.14" mpl-token-metadata = "1.13.1" diff --git a/cross-chain/solana/programs/wormhole-gateway/Cargo.toml b/cross-chain/solana/programs/wormhole-gateway/Cargo.toml index b26b89a13..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 = { version = "0.1.0-alpha.1", features = ["token-bridge"], default-features = false } +solana-program = "=1.14" tbtc = { path = "../tbtc", features = ["cpi"] } \ No newline at end of file From 29918c08021e2a3157ceb233edaaac25362cea55 Mon Sep 17 00:00:00 2001 From: gator-boi Date: Fri, 4 Aug 2023 16:36:21 -0500 Subject: [PATCH 38/38] solana: refactor gateway tests --- .../solana/tests/02__wormholeGateway.ts | 130 +++++++++++++++++- 1 file changed, 126 insertions(+), 4 deletions(-) diff --git a/cross-chain/solana/tests/02__wormholeGateway.ts b/cross-chain/solana/tests/02__wormholeGateway.ts index f9a0f7610..334a54e75 100644 --- a/cross-chain/solana/tests/02__wormholeGateway.ts +++ b/cross-chain/solana/tests/02__wormholeGateway.ts @@ -309,10 +309,22 @@ describe("wormhole-gateway", () => { 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("other", () => { - it("deposit wrapped tokens", async () => { + describe("deposit wrapped tbtc", () => { + it("cannot deposit wrapped tbtc (custodian not a minter)", async () => { // Set up new wallet const payer = await generatePayer(authority); @@ -343,6 +355,36 @@ describe("wormhole-gateway", () => { ); 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 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, @@ -388,6 +430,25 @@ describe("wormhole-gateway", () => { 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. @@ -400,6 +461,35 @@ describe("wormhole-gateway", () => { 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 + ); + + // Cannot deposit past minting limit. + const failingIx = await wormholeGateway.depositWormholeTbtcIx( + { + recipientWrappedToken, + recipientToken, + recipient: payer.publicKey, + }, + BigInt(50000) + ); // Will succeed if minting limit is increased. const newLimit = BigInt(70000); @@ -417,7 +507,11 @@ describe("wormhole-gateway", () => { }); await expectIxSuccess([failingIx], [payer]); }); + }); + describe("receive tbtc", () => { + let replayVaa; + it("receive tbtc", async () => { // Set up new wallet const payer = await generatePayer(authority); @@ -466,7 +560,31 @@ describe("wormhole-gateway", () => { // Check balance change. expect(tbtcAfter.amount).to.equal(tbtcBefore.amount + sentAmount); - expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + sentAmount); + expect(gatewayAfter.amount).to.equal(gatewayBefore.amount + 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"); @@ -751,7 +869,9 @@ describe("wormhole-gateway", () => { ); await expectIxFail([failingIx], [payer], "RecipientZeroAddress"); }); + }); + describe("send tbtc", () => { it("send tbtc to gateway", async () => { // Use common token account. const sender = commonTokenOwner.publicKey; @@ -922,8 +1042,10 @@ describe("wormhole-gateway", () => { } ); await expectIxFail([ix], [commonTokenOwner], "AccountNotInitialized"); - }); + }); + }); + describe("send wrapped tbtc", () => { it("send wrapped tbtc", async () => { // Use common token account. const sender = commonTokenOwner.publicKey;