diff --git a/Anchor.toml b/Anchor.toml index 32869301..21ad41fe 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -6,7 +6,6 @@ types = "sdk/solana/tbrv3/idl" [features] resolution = true -skip-lint = true [programs.localnet] #token_bridge_relayer = "7TLiBkpDGshV4o3jmacTCx93CLkmo3VjZ111AsijN9f8" diff --git a/sdk/solana/tbrv3/idl/token_bridge_relayer.ts b/sdk/solana/tbrv3/idl/token_bridge_relayer.ts index 360f6270..bad574fe 100644 --- a/sdk/solana/tbrv3/idl/token_bridge_relayer.ts +++ b/sdk/solana/tbrv3/idl/token_bridge_relayer.ts @@ -116,6 +116,116 @@ export type TokenBridgeRelayer = { "because we will update roles depending on the operation." ], "writable": true + }, + { + "name": "upgradeLock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, + { + "name": "programData", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 13, + 74, + 247, + 118, + 36, + 164, + 201, + 97, + 25, + 221, + 241, + 144, + 142, + 148, + 63, + 218, + 160, + 137, + 78, + 28, + 18, + 140, + 195, + 112, + 127, + 26, + 150, + 227, + 211, + 125, + 216, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 2, + 168, + 246, + 145, + 78, + 136, + 161, + 176, + 226, + 16, + 21, + 62, + 247, + 99, + 174, + 43, + 0, + 194, + 185, + 61, + 22, + 193, + 36, + 210, + 192, + 83, + 122, + 16, + 4, + 128, + 0, + 0 + ] + } + } + }, + { + "name": "bpfLoaderUpgradeable", + "address": "BPFLoaderUpgradeab1e11111111111111111111111" } ], "args": [] @@ -287,7 +397,10 @@ export type TokenBridgeRelayer = { "writable": true }, { - "name": "peer" + "name": "peer", + "docs": [ + "The TBR peer (_i.e._ `data().from_address()`). We do not care about the Token Bridge peer `vaa.meta.emitter_address`." + ] }, { "name": "tokenBridgeConfig" @@ -437,10 +550,6 @@ export type TokenBridgeRelayer = { ] } }, - { - "name": "previousOwner", - "signer": true - }, { "name": "authBadgePreviousOwner", "writable": true, @@ -477,6 +586,30 @@ export type TokenBridgeRelayer = { ], "writable": true }, + { + "name": "upgradeLock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, { "name": "programData", "writable": true, @@ -593,10 +726,7 @@ export type TokenBridgeRelayer = { "signer": true }, { - "name": "owner", - "docs": [ - "The designated owner of the program." - ] + "name": "owner" }, { "name": "authBadge", @@ -628,7 +758,7 @@ export type TokenBridgeRelayer = { "name": "tbrConfig", "docs": [ "Owner Config account. This program requires that the `owner` specified", - "in the context equals the pubkey specified in this account. Mutable." + "in the context equals the pubkey specified in this account." ], "writable": true, "pda": { @@ -824,13 +954,6 @@ export type TokenBridgeRelayer = { "Proof that the signer is authorized." ] }, - { - "name": "tbrConfig", - "docs": [ - "Owner Config account. This program requires that the `signer` specified", - "in the context equals an authorized pubkey specified in this account." - ] - }, { "name": "peer", "writable": true, @@ -1053,14 +1176,6 @@ export type TokenBridgeRelayer = { { "name": "chainConfig", "writable": true - }, - { - "name": "tbrConfig", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1073,7 +1188,12 @@ export type TokenBridgeRelayer = { { "name": "submitOwnerTransferRequest", "docs": [ - "Updates the owner account. This needs to be either cancelled or approved." + "Updates the owner account. This needs to be either cancelled or approved.", + "", + "For safety reasons, transferring ownership is a 2-step process. This first step is to set the", + "new owner, and the second step is for the new owner to claim the ownership.", + "This is to prevent a situation where the ownership is transferred to an", + "address that is not able to claim the ownership (by mistake)." ], "discriminator": [ 99, @@ -1116,6 +1236,116 @@ export type TokenBridgeRelayer = { } ] } + }, + { + "name": "upgradeLock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, + { + "name": "programData", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 13, + 74, + 247, + 118, + 36, + 164, + 201, + 97, + 25, + 221, + 241, + 144, + 142, + 148, + 63, + 218, + 160, + 137, + 78, + 28, + 18, + 140, + 195, + 112, + 127, + 26, + 150, + 227, + 211, + 125, + 216, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 2, + 168, + 246, + 145, + 78, + 136, + 161, + 176, + 226, + 16, + 21, + 62, + 247, + 99, + 174, + 43, + 0, + 194, + 185, + 61, + 22, + 193, + 36, + 210, + 192, + 83, + 122, + 16, + 4, + 128, + 0, + 0 + ] + } + } + }, + { + "name": "bpfLoaderUpgradeable", + "address": "BPFLoaderUpgradeab1e11111111111111111111111" } ], "args": [ @@ -1199,9 +1429,6 @@ export type TokenBridgeRelayer = { }, { "name": "feeRecipient", - "docs": [ - "Fee recipient's account. The fee will be transferred to this account." - ], "writable": true, "relations": [ "tbrConfig" @@ -1387,18 +1614,7 @@ export type TokenBridgeRelayer = { "docs": [ "Owner of the program as set in the [`TbrConfig`] account." ], - "writable": true, - "signer": true, - "relations": [ - "tbrConfig" - ] - }, - { - "name": "tbrConfig", - "docs": [ - "Owner Config account. This program requires that the `owner` specified", - "in the context equals the `owner` pubkey specified in this account." - ] + "signer": true }, { "name": "peer" @@ -1555,14 +1771,6 @@ export type TokenBridgeRelayer = { { "name": "chainConfig", "writable": true - }, - { - "name": "tbrConfig", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1610,14 +1818,6 @@ export type TokenBridgeRelayer = { { "name": "chainConfig", "writable": true - }, - { - "name": "tbrConfig", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1759,63 +1959,73 @@ export type TokenBridgeRelayer = { }, { "code": 6007, + "name": "dropoffExceedingMaximum", + "msg": "dropoffExceedingMaximum" + }, + { + "code": 6008, "name": "feeExceedingMaximum", "msg": "feeExceedingMaximum" }, { - "code": 6008, + "code": 6009, "name": "wrongFeeRecipient", "msg": "wrongFeeRecipient" }, { - "code": 6009, + "code": 6010, "name": "wronglySetOptionalAccounts", "msg": "wronglySetOptionalAccounts" }, { - "code": 6010, + "code": 6011, "name": "wrongMintAuthority", "msg": "wrongMintAuthority" }, { - "code": 6011, + "code": 6012, "name": "invalidRecipient", "msg": "invalidRecipient" }, { - "code": 6012, + "code": 6013, "name": "evmChainPriceNotSet", "msg": "evmChainPriceNotSet" }, { - "code": 6013, + "code": 6014, "name": "chainPriceMismatch", "msg": "chainPriceMismatch" }, { - "code": 6014, + "code": 6015, "name": "pausedTransfers", "msg": "pausedTransfers" }, { - "code": 6015, + "code": 6016, "name": "invalidSendingPeer", "msg": "invalidSendingPeer" }, { - "code": 6016, + "code": 6017, "name": "cannotRegisterSolana", "msg": "cannotRegisterSolana" }, { - "code": 6017, + "code": 6018, "name": "invalidPeerAddress", "msg": "invalidPeerAddress" }, { - "code": 6018, + "code": 6019, "name": "missingAssociatedTokenAccount", "msg": "missingAssociatedTokenAccount" + }, + { + "code": 6020, + "name": "overflow", + "msg": "overflow" } ], "types": [ @@ -2059,6 +2269,11 @@ export type TokenBridgeRelayer = { "name": "seedPrefixTemporary", "type": "bytes", "value": "[116, 109, 112]" + }, + { + "name": "seedPrefixUpgradeLock", + "type": "bytes", + "value": "[117, 112, 103, 114, 97, 100, 101, 32, 108, 111, 99, 107]" } ] }; diff --git a/sdk/solana/tbrv3/token-bridge-relayer.ts b/sdk/solana/tbrv3/token-bridge-relayer.ts index 7e9bb65c..612679eb 100644 --- a/sdk/solana/tbrv3/token-bridge-relayer.ts +++ b/sdk/solana/tbrv3/token-bridge-relayer.ts @@ -260,7 +260,7 @@ export class SolanaTokenBridgeRelayer { chain === undefined ? undefined : Buffer.from( - serializeLayout({ ...layoutItems.chainItem(), endianness: 'big' }, chain), + serializeLayout({ ...layoutItems.chainItem(), endianness: 'little' }, chain), ); const states = await this.program.account.peerState .all(filter) @@ -344,7 +344,7 @@ export class SolanaTokenBridgeRelayer { ]).address; const wallet = uaToPubkey(deserializeTbrV3Message(vaa.payload.payload).recipient); - const associatedTokenAccount = getAssociatedTokenAccount(wallet, mint); + const associatedTokenAccount = spl.getAssociatedTokenAddressSync(mint, wallet); return { wallet, associatedTokenAccount }; } @@ -410,7 +410,6 @@ export class SolanaTokenBridgeRelayer { .confirmOwnerTransferRequest() .accounts({ newOwner: config.pendingOwner ?? throwError('No pending owner in the program'), - previousOwner: config.owner, tbrConfig: this.account.config().address, }) .instruction(); @@ -424,7 +423,7 @@ export class SolanaTokenBridgeRelayer { return this.program.methods .cancelOwnerTransferRequest() - .accountsStrict({ + .accountsPartial({ owner: config.owner, tbrConfig: this.account.config().address, }) @@ -468,7 +467,50 @@ export class SolanaTokenBridgeRelayer { /** * Signer: the Owner or an Admin. */ - async registerPeer( + async registerFirstPeer( + signer: PublicKey, + chain: Chain, + peerAddress: UniversalAddress, + config: { + maxGasDropoffMicroToken: number; + relayerFeeMicroUsd: number; + pausedOutboundTransfers: boolean; + }, + ): Promise { + // Check if there are no existing peers: + const existingPeers = await this.read.allPeers(chain); + if (existingPeers.length > 0) { + throw new Error('Peers already exist. Use registerAdditionalPeer to add more peers.'); + } + + const updateMaxGasDropoffIx = + config.maxGasDropoffMicroToken !== 0 + ? [this.updateMaxGasDropoff(signer, chain, config.maxGasDropoffMicroToken)] + : []; + + return Promise.all([ + this.registerPeer(signer, chain, peerAddress), + this.updateBaseFee(signer, chain, config.relayerFeeMicroUsd), + this.setPauseForOutboundTransfers(signer, chain, config.pausedOutboundTransfers), + ...updateMaxGasDropoffIx, + ]); + } + + async registerAdditionalPeer( + signer: PublicKey, + chain: Chain, + peerAddress: UniversalAddress, + ): Promise { + // Check if there are no existing peers: + const existingPeers = await this.read.allPeers(chain); + if (existingPeers.length === 0) { + throw new Error('No peer for this chain. Use registerFirstPeer instead.'); + } + + return this.registerPeer(signer, chain, peerAddress); + } + + private async registerPeer( signer: PublicKey, chain: Chain, peerAddress: UniversalAddress, @@ -478,7 +520,6 @@ export class SolanaTokenBridgeRelayer { .accountsStrict({ signer, authBadge: this.account.authBadge(signer).address, - tbrConfig: this.account.config().address, peer: this.account.peer(chain, peerAddress).address, chainConfig: this.account.chainConfig(chain).address, systemProgram: SystemProgram.programId, @@ -499,7 +540,6 @@ export class SolanaTokenBridgeRelayer { .updateCanonicalPeer() .accountsStrict({ owner: config.owner, - tbrConfig: this.account.config().address, peer: this.account.peer(chain, peerAddress).address, chainConfig: this.account.chainConfig(chain).address, systemProgram: SystemProgram.programId, @@ -523,7 +563,6 @@ export class SolanaTokenBridgeRelayer { signer, authBadge: this.account.authBadge(signer).address, chainConfig: this.account.chainConfig(chain).address, - tbrConfig: this.account.config().address, }) .instruction(); } @@ -542,15 +581,16 @@ export class SolanaTokenBridgeRelayer { signer, authBadge: this.account.authBadge(signer).address, chainConfig: this.account.chainConfig(chain).address, - tbrConfig: this.account.config().address, }) .instruction(); } /** + * Change the fee asked for relaying a transfer. + * * Signer: the Owner or an Admin. */ - async updateRelayerFee( + async updateBaseFee( signer: PublicKey, chain: Chain, relayerFee: number, @@ -561,7 +601,6 @@ export class SolanaTokenBridgeRelayer { signer, authBadge: this.account.authBadge(signer).address, chainConfig: this.account.chainConfig(chain).address, - tbrConfig: this.account.config().address, }) .instruction(); } @@ -899,13 +938,6 @@ function findPda(programId: PublicKey, seeds: Array) { }; } -function getAssociatedTokenAccount(wallet: PublicKey, mint: PublicKey): PublicKey { - return PublicKey.findProgramAddressSync( - [wallet.toBuffer(), spl.TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], - spl.ASSOCIATED_TOKEN_PROGRAM_ID, - )[0]; -} - /** Return both the address and an idempotent instruction to create it. */ async function createAssociatedTokenAccountIdempotent({ signer, @@ -916,7 +948,7 @@ async function createAssociatedTokenAccountIdempotent({ mint: PublicKey; wallet: PublicKey; }): Promise { - const recipientTokenAccount = getAssociatedTokenAccount(wallet, mint); + const recipientTokenAccount = spl.getAssociatedTokenAddressSync(mint, wallet); const createAtaIdempotentIx = spl.createAssociatedTokenAccountIdempotentInstruction( signer, @@ -1018,6 +1050,13 @@ function range(from: bigint, to: bigint): bigint[] { return Array.from(generator(from, to)); } +/** + * Simulates the transaction and returns the result. Throws if it failed. + * @param connection The connection used to run the simulation. + * @param payer The payer. No signature is needed, so no fee will be payed. + * @param instructions The instructions to simulate. + * @returns + */ async function simulateTransaction( connection: Connection, payer: PublicKey, diff --git a/solana/programs/token-bridge-relayer/src/error.rs b/solana/programs/token-bridge-relayer/src/error.rs index cc1676e5..823dffff 100644 --- a/solana/programs/token-bridge-relayer/src/error.rs +++ b/solana/programs/token-bridge-relayer/src/error.rs @@ -33,6 +33,10 @@ pub(crate) enum TokenBridgeRelayerError { #[msg("AlreadyTheCanonicalPeer")] AlreadyTheCanonicalPeer, + /// The dropoff amount it higher than the one authorized for the target chain. + #[msg("DropoffExceedingMaximum")] + DropoffExceedingMaximum, + /// Fee exceed what the user has set as a maximum. #[msg("FeeExceedingMaximum")] FeeExceedingMaximum, @@ -86,4 +90,8 @@ pub(crate) enum TokenBridgeRelayerError { /// unwrap intent is set to `true`. #[msg("MissingAssociatedTokenAccount")] MissingAssociatedTokenAccount, + + /// Numerical overflow. + #[msg("Overflow")] + Overflow, } diff --git a/solana/programs/token-bridge-relayer/src/lib.rs b/solana/programs/token-bridge-relayer/src/lib.rs index 873a61f0..1b7ce340 100644 --- a/solana/programs/token-bridge-relayer/src/lib.rs +++ b/solana/programs/token-bridge-relayer/src/lib.rs @@ -18,6 +18,9 @@ pub mod constant { #[constant] pub const SEED_PREFIX_TEMPORARY: &[u8] = b"tmp"; + + #[constant] + pub const SEED_PREFIX_UPGRADE_LOCK: &[u8] = b"upgrade_lock"; } #[program] @@ -35,6 +38,11 @@ pub mod token_bridge_relayer { /* Roles */ /// Updates the owner account. This needs to be either cancelled or approved. + /// + /// For safety reasons, transferring ownership is a 2-step process. This first step is to set the + /// new owner, and the second step is for the new owner to claim the ownership. + /// This is to prevent a situation where the ownership is transferred to an + /// address that is not able to claim the ownership (by mistake). pub fn submit_owner_transfer_request( ctx: Context, new_owner: Pubkey, diff --git a/solana/programs/token-bridge-relayer/src/processor/chain_config.rs b/solana/programs/token-bridge-relayer/src/processor/chain_config.rs index 20467bf0..7eb917a7 100644 --- a/solana/programs/token-bridge-relayer/src/processor/chain_config.rs +++ b/solana/programs/token-bridge-relayer/src/processor/chain_config.rs @@ -1,6 +1,6 @@ use crate::{ error::TokenBridgeRelayerError, - state::{AuthBadgeState, ChainConfigState, TbrConfigState}, + state::{AuthBadgeState, ChainConfigState}, }; use anchor_lang::prelude::*; @@ -16,11 +16,6 @@ pub struct UpdateChainConfig<'info> { #[account(mut)] pub chain_config: Account<'info, ChainConfigState>, - - /// Program Config account. This program requires that the [`signer`] specified - /// in the context equals a pubkey specified in this account. Mutable, - /// because we will update roles depending on the operation. - pub tbr_config: Account<'info, TbrConfigState>, } pub fn set_pause_for_outbound_transfers( diff --git a/solana/programs/token-bridge-relayer/src/processor/inbound.rs b/solana/programs/token-bridge-relayer/src/processor/inbound.rs index 498799a2..881a4dcd 100644 --- a/solana/programs/token-bridge-relayer/src/processor/inbound.rs +++ b/solana/programs/token-bridge-relayer/src/processor/inbound.rs @@ -23,7 +23,7 @@ pub struct CompleteTransfer<'info> { pub tbr_config: Box>, /// Mint info. This is the SPL token that will be bridged over to the - /// foreign contract. Mutable. + /// foreign contract. /// /// In the case of a native transfer, it's the mint for the token wrapped by Wormhole; /// in the case of a wrapped transfer, it's the native SPL token mint. @@ -32,7 +32,7 @@ pub struct CompleteTransfer<'info> { /// Recipient associated token account. The recipient authority check /// is necessary to ensure that the recipient is the intended recipient - /// of the bridged tokens. Mutable. + /// of the bridged tokens. #[account( mut, associated_token::mint = mint, @@ -44,7 +44,7 @@ pub struct CompleteTransfer<'info> { /// transaction. This instruction verifies that the recipient key /// passed in this context matches the intended recipient in the vaa. #[account(mut)] - pub recipient: AccountInfo<'info>, + pub recipient: UncheckedAccount<'info>, /// Verified Wormhole message account. Read-only. pub vaa: Account<'info, PostedRelayerMessage>, @@ -61,10 +61,10 @@ pub struct CompleteTransfer<'info> { #[account(mut)] pub temporary_account: UncheckedAccount<'info>, + /// The TBR peer (_i.e._ `data().from_address()`). We do not care about the Token Bridge peer `vaa.meta.emitter_address`. #[account( - constraint = { - peer.address == *vaa.data().from_address() - } @ TokenBridgeRelayerError::InvalidSendingPeer + constraint = peer.address == *vaa.data().from_address() && peer.chain_id == vaa.meta.emitter_chain + @ TokenBridgeRelayerError::InvalidSendingPeer )] pub peer: Account<'info, PeerState>, @@ -89,7 +89,7 @@ pub struct CompleteTransfer<'info> { /// CHECK: Token Bridge custody. This is the Token Bridge program's token /// account that holds this mint's balance. This account needs to be /// unchecked because a token account may not have been created for this - /// mint yet. Mutable. + /// mint yet. /// /// # Exclusive /// diff --git a/solana/programs/token-bridge-relayer/src/processor/initialize.rs b/solana/programs/token-bridge-relayer/src/processor/initialize.rs index d222543a..e4160491 100644 --- a/solana/programs/token-bridge-relayer/src/processor/initialize.rs +++ b/solana/programs/token-bridge-relayer/src/processor/initialize.rs @@ -1,6 +1,7 @@ use crate::{ error::TokenBridgeRelayerError, state::{AuthBadgeState, TbrConfigState}, + utils::DrainAccount, }; use anchor_lang::prelude::*; use anchor_lang::solana_program::{bpf_loader_upgradeable, program::invoke}; @@ -15,7 +16,7 @@ pub struct Initialize<'info> { #[account(mut)] pub deployer: Signer<'info>, - /// The designated owner of the program. + /// CHECK: The account to be used as the owner of the program. pub owner: UncheckedAccount<'info>, #[account( @@ -28,7 +29,7 @@ pub struct Initialize<'info> { pub auth_badge: Account<'info, AuthBadgeState>, /// Owner Config account. This program requires that the `owner` specified - /// in the context equals the pubkey specified in this account. Mutable. + /// in the context equals the pubkey specified in this account. #[account( init, payer = deployer, @@ -46,18 +47,21 @@ pub struct Initialize<'info> { )] program_data: Account<'info, ProgramData>, + /// CHECK: An account used by the Token Bridge. #[account( seeds = [token_bridge::SEED_PREFIX_SENDER], bump )] pub wormhole_sender: UncheckedAccount<'info>, + /// CHECK: An account used by the Token Bridge. #[account( seeds = [token_bridge::SEED_PREFIX_REDEEMER], bump )] pub wormhole_redeemer: UncheckedAccount<'info>, + /// CHECK: The BPF loader program. #[account(address = bpf_loader_upgradeable::ID)] pub bpf_loader_upgradeable: UncheckedAccount<'info>, @@ -114,11 +118,20 @@ pub fn initialize<'a, 'b, 'c, 'info>( ); for (admin, badge_acc_info) in zip(admins, ctx.remaining_accounts) { - let bump = Pubkey::find_program_address( + let (_pubkey, bump) = Pubkey::find_program_address( &[AuthBadgeState::SEED_PREFIX, admin.to_bytes().as_ref()], ctx.program_id, - ) - .1; + ); + let badge_seeds = [AuthBadgeState::SEED_PREFIX, &admin.to_bytes(), &[bump]]; + + // Before calling `create_account`, we need to verify that the account + // has an empty balance, otherwise the instruction would fail: + DrainAccount { + system_program: ctx.accounts.system_program.to_account_info(), + account: badge_acc_info.to_account_info(), + recipient: ctx.accounts.deployer.to_account_info(), + } + .run_with_seeds(&badge_seeds)?; anchor_lang::system_program::create_account( CpiContext::new_with_signer( @@ -127,11 +140,7 @@ pub fn initialize<'a, 'b, 'c, 'info>( from: ctx.accounts.deployer.to_account_info(), to: badge_acc_info.clone(), }, - &[&[ - AuthBadgeState::SEED_PREFIX, - admin.to_bytes().as_ref(), - &[bump], - ]], + &[&badge_seeds], ), Rent::get()?.minimum_balance(8 + AuthBadgeState::INIT_SPACE), (8 + AuthBadgeState::INIT_SPACE) as u64, diff --git a/solana/programs/token-bridge-relayer/src/processor/outbound.rs b/solana/programs/token-bridge-relayer/src/processor/outbound.rs index aeef23a6..0fc64532 100644 --- a/solana/programs/token-bridge-relayer/src/processor/outbound.rs +++ b/solana/programs/token-bridge-relayer/src/processor/outbound.rs @@ -36,7 +36,7 @@ pub struct OutboundTransfer<'info> { pub chain_config: Box>, /// Mint info. This is the SPL token that will be bridged over to the - /// canonical peer. Mutable. + /// canonical peer. /// /// In the case of a native transfer, it's the native mint; in the case of a /// wrapped transfer, it's the token wrapped by Wormhole. @@ -62,7 +62,7 @@ pub struct OutboundTransfer<'info> { #[account(mut)] pub temporary_account: UncheckedAccount<'info>, - /// Fee recipient's account. The fee will be transferred to this account. + /// CHECK: Fee recipient's account. The fee will be transferred to this account. #[account(mut)] pub fee_recipient: UncheckedAccount<'info>, @@ -80,7 +80,7 @@ pub struct OutboundTransfer<'info> { /// CHECK: Token Bridge custody. This is the Token Bridge program's token /// account that holds this mint's balance. This account needs to be /// unchecked because a token account may not have been created for this - /// mint yet. Mutable. + /// mint yet. /// /// # Exclusive /// @@ -116,7 +116,7 @@ pub struct OutboundTransfer<'info> { /// CHECK: Token Bridge emitter. pub token_bridge_emitter: UncheckedAccount<'info>, - /// CHECK: Token Bridge sequence. + /// CHECK: Token Bridge sequence. Mutable. #[account(mut)] pub token_bridge_sequence: UncheckedAccount<'info>, @@ -131,8 +131,9 @@ pub struct OutboundTransfer<'info> { ], bump, )] - pub wormhole_message: AccountInfo<'info>, + pub wormhole_message: UncheckedAccount<'info>, + /// CHECK: Wormhole sender. pub wormhole_sender: UncheckedAccount<'info>, /// CHECK: Wormhole fee collector. Mutable. @@ -184,6 +185,11 @@ pub fn transfer_tokens( &[ctx.accounts.tbr_config.sender_bump], ]; + require!( + dropoff_amount_micro <= ctx.accounts.chain_config.max_gas_dropoff_micro_token, + TokenBridgeRelayerError::DropoffExceedingMaximum + ); + let transferred_amount = normalize_token_amount(transferred_amount, &ctx.accounts.mint); let total_fees_lamports = calculate_total_fee( &ctx.accounts.tbr_config, diff --git a/solana/programs/token-bridge-relayer/src/processor/owner.rs b/solana/programs/token-bridge-relayer/src/processor/owner.rs index a41fbb39..be134705 100644 --- a/solana/programs/token-bridge-relayer/src/processor/owner.rs +++ b/solana/programs/token-bridge-relayer/src/processor/owner.rs @@ -1,12 +1,13 @@ //! Everything about the owner or admin role transfer. use crate::{ + constant::SEED_PREFIX_UPGRADE_LOCK, error::TokenBridgeRelayerError, state::{AuthBadgeState, TbrConfigState}, }; use anchor_lang::{ prelude::*, - solana_program::{bpf_loader_upgradeable, program::invoke}, + solana_program::{bpf_loader_upgradeable, program::invoke_signed}, }; #[derive(Accounts)] @@ -23,6 +24,25 @@ pub struct SubmitOwnerTransfer<'info> { bump = tbr_config.bump )] pub tbr_config: Account<'info, TbrConfigState>, + + /// CHECK: The seeds constraint enforces that this is the correct address + #[account( + seeds = [SEED_PREFIX_UPGRADE_LOCK], + bump, + )] + upgrade_lock: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [crate::ID.as_ref()], + bump, + seeds::program = bpf_loader_upgradeable::ID, + )] + program_data: Account<'info, ProgramData>, + + /// CHECK: The BPF loader program. + #[account(address = bpf_loader_upgradeable::ID)] + pub bpf_loader_upgradeable: UncheckedAccount<'info>, } pub fn submit_owner_transfer_request( @@ -38,6 +58,22 @@ pub fn submit_owner_transfer_request( ctx.accounts.tbr_config.pending_owner = Some(new_owner); + // Change the program authority to the upgrade lock PDA, so that the owner does not need + // to sign the transaction when confirming the ownership transfer: + invoke_signed( + &bpf_loader_upgradeable::set_upgrade_authority_checked( + &ctx.program_id, + &ctx.accounts.owner.key(), + &ctx.accounts.upgrade_lock.key(), + ), + &[ + ctx.accounts.program_data.to_account_info(), + ctx.accounts.owner.to_account_info(), + ctx.accounts.upgrade_lock.to_account_info(), + ], + &[&[SEED_PREFIX_UPGRADE_LOCK, &[ctx.bumps.upgrade_lock]]], + )?; + Ok(()) } @@ -46,8 +82,9 @@ pub struct ConfirmOwnerTransfer<'info> { #[account(mut)] pub new_owner: Signer<'info>, + /// CHECK: init_if_needed: If the new owner has already a badge, _i.e._ is an admin, we can reuse it. #[account( - init, + init_if_needed, payer = new_owner, space = 8 + AuthBadgeState::INIT_SPACE, seeds = [AuthBadgeState::SEED_PREFIX, new_owner.key.to_bytes().as_ref()], @@ -55,8 +92,6 @@ pub struct ConfirmOwnerTransfer<'info> { )] pub auth_badge_new_owner: Account<'info, AuthBadgeState>, - pub previous_owner: Signer<'info>, - #[account( mut, seeds = [AuthBadgeState::SEED_PREFIX, tbr_config.owner.to_bytes().as_ref()], @@ -75,6 +110,13 @@ pub struct ConfirmOwnerTransfer<'info> { )] pub tbr_config: Account<'info, TbrConfigState>, + /// CHECK: The seeds constraint enforces that this is the correct address + #[account( + seeds = [SEED_PREFIX_UPGRADE_LOCK], + bump, + )] + upgrade_lock: UncheckedAccount<'info>, + #[account( mut, seeds = [crate::ID.as_ref()], @@ -83,6 +125,7 @@ pub struct ConfirmOwnerTransfer<'info> { )] program_data: Account<'info, ProgramData>, + /// CHECK: The BPF loader program. #[account(address = bpf_loader_upgradeable::ID)] pub bpf_loader_upgradeable: UncheckedAccount<'info>, @@ -93,17 +136,18 @@ pub fn confirm_owner_transfer_request(ctx: Context) -> Res let tbr_config = &mut ctx.accounts.tbr_config; // Change the program authority to the new owner: - invoke( + invoke_signed( &bpf_loader_upgradeable::set_upgrade_authority( &ctx.program_id, - &ctx.accounts.previous_owner.key(), + &ctx.accounts.upgrade_lock.key(), Some(&ctx.accounts.new_owner.key()), ), &[ ctx.accounts.program_data.to_account_info(), - ctx.accounts.previous_owner.to_account_info(), + ctx.accounts.upgrade_lock.to_account_info(), ctx.accounts.new_owner.to_account_info(), ], + &[&[SEED_PREFIX_UPGRADE_LOCK, &[ctx.bumps.upgrade_lock]]], )?; tbr_config.owner = ctx.accounts.new_owner.key(); @@ -128,10 +172,44 @@ pub struct CancelOwnerTransfer<'info> { has_one = owner @ TokenBridgeRelayerError::OwnerOnly, )] pub tbr_config: Account<'info, TbrConfigState>, + + /// CHECK: The seeds constraint enforces that this is the correct address + #[account( + seeds = [SEED_PREFIX_UPGRADE_LOCK], + bump, + )] + upgrade_lock: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [crate::ID.as_ref()], + bump, + seeds::program = bpf_loader_upgradeable::ID, + )] + program_data: Account<'info, ProgramData>, + + /// CHECK: The BPF loader program. + #[account(address = bpf_loader_upgradeable::ID)] + pub bpf_loader_upgradeable: UncheckedAccount<'info>, } pub fn cancel_owner_transfer_request(ctx: Context) -> Result<()> { ctx.accounts.tbr_config.pending_owner = None; + // Transfer the program authority back to the owner: + invoke_signed( + &bpf_loader_upgradeable::set_upgrade_authority( + &ctx.program_id, + &ctx.accounts.upgrade_lock.key(), + Some(&ctx.accounts.owner.key()), + ), + &[ + ctx.accounts.program_data.to_account_info(), + ctx.accounts.upgrade_lock.to_account_info(), + ctx.accounts.owner.to_account_info(), + ], + &[&[SEED_PREFIX_UPGRADE_LOCK, &[ctx.bumps.upgrade_lock]]], + )?; + Ok(()) } diff --git a/solana/programs/token-bridge-relayer/src/processor/peers.rs b/solana/programs/token-bridge-relayer/src/processor/peers.rs index 07d17bd4..cdc7253e 100644 --- a/solana/programs/token-bridge-relayer/src/processor/peers.rs +++ b/solana/programs/token-bridge-relayer/src/processor/peers.rs @@ -1,6 +1,6 @@ use crate::{ error::TokenBridgeRelayerError, - state::{AuthBadgeState, ChainConfigState, PeerState, TbrConfigState}, + state::{AuthBadgeState, ChainConfigState, PeerState}, }; use anchor_lang::prelude::*; @@ -18,10 +18,6 @@ pub struct RegisterPeer<'info> { #[account(constraint = &auth_badge.address == signer.key @ TokenBridgeRelayerError::OwnerOrAdminOnly)] pub auth_badge: Account<'info, AuthBadgeState>, - /// Owner Config account. This program requires that the `signer` specified - /// in the context equals an authorized pubkey specified in this account. - pub tbr_config: Account<'info, TbrConfigState>, - #[account( init, payer = signer, @@ -82,14 +78,8 @@ pub fn register_peer( #[derive(Accounts)] pub struct UpdateCanonicalPeer<'info> { /// Owner of the program as set in the [`TbrConfig`] account. - #[account(mut)] pub owner: Signer<'info>, - /// Owner Config account. This program requires that the `owner` specified - /// in the context equals the `owner` pubkey specified in this account. - #[account(has_one = owner @ TokenBridgeRelayerError::OwnerOnly)] - pub tbr_config: Account<'info, TbrConfigState>, - #[account( constraint = { peer.chain_id == chain_config.chain_id diff --git a/solana/programs/token-bridge-relayer/src/utils.rs b/solana/programs/token-bridge-relayer/src/utils.rs index 1a78da8b..5e10d929 100644 --- a/solana/programs/token-bridge-relayer/src/utils.rs +++ b/solana/programs/token-bridge-relayer/src/utils.rs @@ -57,19 +57,36 @@ pub fn calculate_total_fee( */ // Mwei = gas * Mwei/gas + bytes * Mwei/byte + µToken * Mwei/µToken - let total_fees_mwei = config.evm_transaction_gas * u64::from(oracle_evm_prices.gas_price) - + config.evm_transaction_size * u64::from(oracle_evm_prices.price_per_byte) - + u64::from(dropoff_amount_micro) * MWEI_PER_MICRO_ETH; + let total_fees_mwei = (|| { + let evm_transaction_fee_mwei = config + .evm_transaction_gas + .checked_mul(u64::from(oracle_evm_prices.gas_price))?; + let evm_tx_size_fee_mwei = config + .evm_transaction_size + .checked_mul(u64::from(oracle_evm_prices.price_per_byte))?; + let dropoff_mwei = u64::from(dropoff_amount_micro).checked_mul(MWEI_PER_MICRO_ETH)?; + + evm_transaction_fee_mwei + .checked_add(evm_tx_size_fee_mwei)? + .checked_add(dropoff_mwei) + })() + .ok_or(TokenBridgeRelayerError::Overflow)?; // μusd = Mwei * μusd/Token / Mwei/Token + μusd) let total_fees_micro_usd = u64::try_from( u128::from(total_fees_mwei) * u128::from(oracle_evm_prices.gas_token_price) / MWEI_PER_ETH, ) - .expect("Overflow") - + u64::from(chain_config.relayer_fee_micro_usd); + .map_err(|_| TokenBridgeRelayerError::Overflow)? + .checked_add(u64::from(chain_config.relayer_fee_micro_usd)) + .ok_or(TokenBridgeRelayerError::Overflow)?; // lamports/SOL * μusd / μusd/SOL - Ok((LAMPORTS_PER_SOL * total_fees_micro_usd) / oracle_config.sol_price) + let fee = total_fees_micro_usd + .checked_mul(LAMPORTS_PER_SOL) + .map(|sol| sol / oracle_config.sol_price) + .ok_or(TokenBridgeRelayerError::Overflow)?; + + Ok(fee) } /// This is a basic security against a wrong manip, to be sure that the prices @@ -156,6 +173,15 @@ impl<'info> CreateAndInitTokenAccount<'info> { pub fn run_with_seeds(self, seeds: &[&[u8]]) -> Result<()> { const LEN: usize = anchor_spl::token::TokenAccount::LEN; + // Before calling `create_account`, we need to verify that the account + // has an empty balance, otherwise the instruction would fail: + DrainAccount { + system_program: self.system_program.clone(), + account: self.account.clone(), + recipient: self.payer.clone(), + } + .run_with_seeds(seeds)?; + system_program::create_account( CpiContext::new_with_signer( self.system_program, @@ -184,3 +210,30 @@ impl<'info> CreateAndInitTokenAccount<'info> { Ok(()) } } + +/// Empties the account balance to the provided recipient. +pub struct DrainAccount<'info> { + pub system_program: AccountInfo<'info>, + pub account: AccountInfo<'info>, + pub recipient: AccountInfo<'info>, +} + +impl<'info> DrainAccount<'info> { + pub fn run_with_seeds(self, seeds: &[&[u8]]) -> Result<()> { + if self.account.lamports() != 0 { + anchor_lang::system_program::transfer( + CpiContext::new_with_signer( + self.system_program.to_account_info(), + anchor_lang::system_program::Transfer { + from: self.account.clone(), + to: self.recipient, + }, + &[seeds], + ), + self.account.lamports(), + )?; + } + + Ok(()) + } +} diff --git a/solana/tests/token-bridge-relayer-tests.ts b/solana/tests/token-bridge-relayer-tests.ts index c97d9de7..d1383766 100644 --- a/solana/tests/token-bridge-relayer-tests.ts +++ b/solana/tests/token-bridge-relayer-tests.ts @@ -1,6 +1,12 @@ import { chainToChainId } from '@wormhole-foundation/sdk-base'; import { toNative, UniversalAddress } from '@wormhole-foundation/sdk-definitions'; -import { Keypair, PublicKey, SendTransactionError, Transaction } from '@solana/web3.js'; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SendTransactionError, + Transaction, +} from '@solana/web3.js'; import * as spl from '@solana/spl-token'; import { assert, @@ -34,6 +40,9 @@ const authorityKeypair = './target/deploy/token_bridge_relayer-keypair.json'; const $ = new TestsHelper(); +/** SOL amount in lamports */ const sol = (n: number) => BigInt(LAMPORTS_PER_SOL * n); +/** ETH amount in micro-ETH */ const eth = (n: number) => 1_000_000 * n; +/** USD amount in micro-USD */ const usd = (n: number) => 1_000_000 * n; const uaToPubkey = (address: UniversalAddress) => toNative('Solana', address).unwrap(); describe('Token Bridge Relayer Program', () => { @@ -78,7 +87,11 @@ describe('Token Bridge Relayer Program', () => { const ethereumTbrPeer1 = $.universalAddress.generate('ethereum'); const ethereumTbrPeer2 = $.universalAddress.generate('ethereum'); const oasisTbrPeer = $.universalAddress.generate('ethereum'); - const bpfProgram = new BpfLoaderUpgradeableProgram(ownerClient.client.program.programId, $.connection); + + const bpfProgram = new BpfLoaderUpgradeableProgram( + ownerClient.client.program.programId, + $.connection, + ); before(async () => { await $.airdrop([ @@ -144,6 +157,9 @@ describe('Token Bridge Relayer Program', () => { DEBUG, ); + // Let's credit a badge, to verify that we cannot trigger a denial of service: + await $.airdrop(upgradeAuthorityClient.account.authBadge(adminClient1.publicKey).address); + await upgradeAuthorityClient.initialize({ feeRecipient, owner: ownerClient.publicKey, @@ -184,12 +200,12 @@ describe('Token Bridge Relayer Program', () => { it('Rejects a transfer validation by an unauthorized account', async () => { await assert - .promise(unauthorizedClient.confirmOwnerTransferRequest(ownerClient.signer)) + .promise(unauthorizedClient.confirmOwnerTransferRequest()) .failsWith('Signature verification failed'); }); it('Accepts a transfer validation by the rightful new owner', async () => { - await newOwnerClient.confirmOwnerTransferRequest(ownerClient.signer); + await newOwnerClient.confirmOwnerTransferRequest(); // Verify that the authority has been updated to the new owner. const { upgradeAuthority } = await bpfProgram.getdata(); @@ -215,7 +231,7 @@ describe('Token Bridge Relayer Program', () => { // Now the original owner cannot accept the ownership: await assert - .promise(ownerClient.confirmOwnerTransferRequest(ownerClient.signer)) + .promise(ownerClient.confirmOwnerTransferRequest()) .failsWith('No pending owner in the program'); }); @@ -274,13 +290,22 @@ describe('Token Bridge Relayer Program', () => { describe('Peers', () => { it('Registers peers', async () => { - await newOwnerClient.registerPeer(ETHEREUM, ethereumTbrPeer1); + // First ETH peer: + + await assert + .promise(newOwnerClient.registerAdditionalPeer(ETHEREUM, ethereumTbrPeer1)) + .failsWith('Use registerFirstPeer instead'); + + const ethConfig = { + maxGasDropoffMicroToken: 1000, + pausedOutboundTransfers: false, + relayerFeeMicroUsd: 200, + }; + await newOwnerClient.registerFirstPeer(ETHEREUM, ethereumTbrPeer1, ethConfig); assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, canonicalPeer: uaToArray(ethereumTbrPeer1), - maxGasDropoffMicroToken: 0, - pausedOutboundTransfers: true, - relayerFeeMicroUsd: 0, + ...ethConfig, }); expect( await unauthorizedClient.account.peer(ETHEREUM, ethereumTbrPeer1).fetch(), @@ -289,13 +314,17 @@ describe('Token Bridge Relayer Program', () => { address: uaToArray(ethereumTbrPeer1), }); - await adminClient1.registerPeer(ETHEREUM, ethereumTbrPeer2); + // Second ETH peer: + + await assert + .promise(newOwnerClient.registerFirstPeer(ETHEREUM, ethereumTbrPeer2, ethConfig)) + .failsWith('Peers already exist'); + + await adminClient1.registerAdditionalPeer(ETHEREUM, ethereumTbrPeer2); assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, canonicalPeer: uaToArray(ethereumTbrPeer1), - maxGasDropoffMicroToken: 0, - pausedOutboundTransfers: true, - relayerFeeMicroUsd: 0, + ...ethConfig, }); expect( await unauthorizedClient.account.peer(ETHEREUM, ethereumTbrPeer2).fetch(), @@ -304,20 +333,23 @@ describe('Token Bridge Relayer Program', () => { address: uaToArray(ethereumTbrPeer2), }); - await adminClient1.registerPeer(OASIS, oasisTbrPeer); + // First OASIS peer: + + const oasisConfig = { + maxGasDropoffMicroToken: 650, + pausedOutboundTransfers: true, + relayerFeeMicroUsd: 430, + }; + await adminClient1.registerFirstPeer(OASIS, oasisTbrPeer, oasisConfig); assert.chainConfig(await unauthorizedClient.account.chainConfig(OASIS).fetch()).equal({ chainId: OASIS_ID, canonicalPeer: uaToArray(oasisTbrPeer), - maxGasDropoffMicroToken: 0, - pausedOutboundTransfers: true, - relayerFeeMicroUsd: 0, + ...oasisConfig, }); assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, canonicalPeer: uaToArray(ethereumTbrPeer1), - maxGasDropoffMicroToken: 0, - pausedOutboundTransfers: true, - relayerFeeMicroUsd: 0, + ...ethConfig, }); expect(await unauthorizedClient.account.peer(OASIS, oasisTbrPeer).fetch()).deep.include({ chainId: OASIS_ID, @@ -331,9 +363,9 @@ describe('Token Bridge Relayer Program', () => { assert.chainConfig(await unauthorizedClient.account.chainConfig(ETHEREUM).fetch()).equal({ chainId: ETHEREUM_ID, canonicalPeer: uaToArray(ethereumTbrPeer2), - maxGasDropoffMicroToken: 0, - pausedOutboundTransfers: true, - relayerFeeMicroUsd: 0, + maxGasDropoffMicroToken: 1000, + pausedOutboundTransfers: false, + relayerFeeMicroUsd: 200, }); }); @@ -352,7 +384,7 @@ describe('Token Bridge Relayer Program', () => { it('Does not let unauthorized signers register or update a peer', async () => { // Unauthorized cannot register a peer: await assert - .promise(unauthorizedClient.registerPeer(ETHEREUM, $.universalAddress.generate())) + .promise(unauthorizedClient.registerAdditionalPeer(ETHEREUM, $.universalAddress.generate())) .failsWith('AnchorError caused by account: auth_badge. Error Code: AccountNotInitialized.'); // Admin cannot make another peer canonical: @@ -364,8 +396,8 @@ describe('Token Bridge Relayer Program', () => { describe('Chain Config', () => { it('Values are updated', async () => { - const maxGasDropoffMicroToken = 10_000_000; // ETH10 maximum - const relayerFeeMicroUsd = 900_000; // $0.9 + const maxGasDropoffMicroToken = eth(10); + const relayerFeeMicroUsd = usd(0.9); await Promise.all([ adminClient1.setPauseForOutboundTransfers(ETHEREUM, false), adminClient1.updateMaxGasDropoff(ETHEREUM, maxGasDropoffMicroToken), @@ -420,7 +452,7 @@ describe('Token Bridge Relayer Program', () => { describe('Querying the relaying fee', () => { it('No discrepancy between SDK and program calculation', async () => { - const dropoff = 50000; // ETH0.05 + const dropoff = eth(0.05); const simulatedResult = await unauthorizedClient.relayingFeeSimulated(ETHEREUM, dropoff); const offChainResult = await unauthorizedClient.relayingFee(ETHEREUM, dropoff); @@ -458,12 +490,15 @@ describe('Token Bridge Relayer Program', () => { const foreignAddress = $.universalAddress.generate(); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); + // Let's credit the temporary token account, to verify that we cannot trigger a denial of service: + await $.airdrop(unauthorizedClient.account.temporary(spl.NATIVE_MINT).address); + await unauthorizedClient.transferTokens({ recipient: { address: foreignAddress, chain: ETHEREUM }, userTokenAccount: tokenAccount.publicKey, transferredAmount, gasDropoffAmount, - maxFeeLamports: 100_000_000n, // 0.1SOL max + maxFeeLamports: sol(0.1), unwrapIntent, mintAddress: spl.NATIVE_MINT, }); @@ -485,7 +520,6 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.payload.token.amount).equal(transferredAmount / 10n); expect(vaa.payload.payload.recipient).deep.equal(foreignAddress); - // We need to divide by 1 million because it's deserialized as the token, not µToken: expect(vaa.payload.payload.gasDropoff).equal(gasDropoffAmount); expect(vaa.payload.payload.unwrapIntent).equal(unwrapIntent); }); @@ -495,7 +529,7 @@ describe('Token Bridge Relayer Program', () => { const unwrapIntent = false; // Does not matter anyway const transferredAmount = 321654n; - const tokenAccount = await barMint.mint(1_000_000_000n, unauthorizedClient.signer); // + const tokenAccount = await barMint.mint(1_000_000_000n, unauthorizedClient.signer); const foreignAddress = $.universalAddress.generate(); const canonicalEthereum = await unauthorizedClient.read.canonicalPeer(ETHEREUM); @@ -505,7 +539,7 @@ describe('Token Bridge Relayer Program', () => { userTokenAccount: tokenAccount.publicKey, transferredAmount, gasDropoffAmount, - maxFeeLamports: 100_000_000n, // 0.1SOL max + maxFeeLamports: sol(0.1), unwrapIntent, mintAddress: barMint.address, }); @@ -526,13 +560,12 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.payload.token.amount).equal(transferredAmount / 100n); expect(vaa.payload.payload.recipient).deep.equal(foreignAddress); - // We need to divide by 1 million because it's deserialize as the token, not µToken: - expect(vaa.payload.payload.gasDropoff).equal(gasDropoffAmount / 1_000_000); + expect(vaa.payload.payload.gasDropoff).equal(gasDropoffAmount); expect(vaa.payload.payload.unwrapIntent).equal(unwrapIntent); }); it('Gets wrapped SOL back from another chain', async () => { - const [payer, recipient] = await $.airdrop([Keypair.generate(), $.keypair.generate()]); + const [payer, recipient] = await $.airdrop($.keypair.several(2)); // Associated token account already existing (to test if it breaks the transfer completion): const recipientTokenAccount = await spl.createAssociatedTokenAccount( $.connection, @@ -609,7 +642,7 @@ describe('Token Bridge Relayer Program', () => { { chain: ETHEREUM, tokenBridge: ethereumTokenBridge, tbrPeer: ethereumTbrPeer1 }, { recipient: new UniversalAddress(recipient.publicKey.toBuffer()), - gasDropoff: 0.1, // We want 0.1 SOL + gasDropoff: 0.1, // SOL unwrapIntent: false, }, ); @@ -673,7 +706,7 @@ describe('Token Bridge Relayer Program', () => { userTokenAccount: recipientTokenAccountForeignToken, transferredAmount, gasDropoffAmount, - maxFeeLamports: 100_000_000n, // 0.1SOL max + maxFeeLamports: sol(0.1), unwrapIntent, mintAddress: (await tokenBridgeClient.client.getWrappedAsset({ address: ethereumTokenAddressFoo, @@ -705,6 +738,27 @@ describe('Token Bridge Relayer Program', () => { expect(vaa.payload.payload.recipient).deep.equal(foreignAddress); }); + it('Fails to transfer a token due to dropoff exceeding maximum', async () => { + const gasDropoffAmount = 11; + const unwrapIntent = false; + const transferredAmount = 321654n; + + const tokenAccount = await barMint.mint(1_000_000_000n, unauthorizedClient.signer); + + const foreignAddress = $.universalAddress.generate(); + + const transferPromise = unauthorizedClient.transferTokens({ + recipient: { address: foreignAddress, chain: ETHEREUM }, + userTokenAccount: tokenAccount.publicKey, + transferredAmount, + gasDropoffAmount, + maxFeeLamports: sol(0.1), + unwrapIntent, + mintAddress: barMint.address, + }); + await assert.promise(transferPromise).failsWith('DropoffExceedingMaximum'); + }); + after(async () => { await clientForeignToken.close(); }); diff --git a/solana/tests/utils/helpers.ts b/solana/tests/utils/helpers.ts index 4d9564ac..356d7e3f 100644 --- a/solana/tests/utils/helpers.ts +++ b/solana/tests/utils/helpers.ts @@ -27,8 +27,6 @@ import { isTypedArray } from 'util/types'; const execAsync = promisify(exec); -const LOCALHOST = 'http://localhost:8899'; - export interface ErrorConstructor { new (...args: any[]): Error; } @@ -105,6 +103,10 @@ function extractPubkey(from: HasPublicKey): PublicKey { } } +type Tuple = R['length'] extends N + ? R + : Tuple; + export class TestsHelper { static readonly LOCALHOST = 'http://localhost:8899'; readonly connection: Connection; @@ -115,7 +117,7 @@ export class TestsHelper { constructor(finality: Finality = 'confirmed') { if (TestsHelper.connections[finality] === undefined) { - TestsHelper.connections[finality] = new Connection(LOCALHOST, finality); + TestsHelper.connections[finality] = new Connection(TestsHelper.LOCALHOST, finality); } this.connection = TestsHelper.connections[finality]; this.finality = finality; @@ -126,7 +128,8 @@ export class TestsHelper { read: async (path: string): Promise => this.keypair.read(path).then((kp) => kp.publicKey), from: (hasPublicKey: HasPublicKey): PublicKey => extractPubkey(hasPublicKey), - several: (amount: number): PublicKey[] => Array.from({ length: amount }).map(PublicKey.unique), + several: (amount: number): Tuple => + Array.from({ length: amount }).map(PublicKey.unique) as Tuple, }; keypair = { @@ -134,7 +137,8 @@ export class TestsHelper { read: async (path: string): Promise => this.keypair.from(JSON.parse(await fs.readFile(path, { encoding: 'utf8' }))), from: (bytes: number[]): Keypair => Keypair.fromSecretKey(Uint8Array.from(bytes)), - several: (amount: number): Keypair[] => Array.from({ length: amount }).map(Keypair.generate), + several: (amount: N): Tuple => + Array.from({ length: amount }).map(Keypair.generate) as Tuple, }; universalAddress = { @@ -144,8 +148,14 @@ export class TestsHelper { Buffer.concat([Buffer.alloc(12), PublicKey.unique().toBuffer().subarray(12)]), ) : new UniversalAddress(PublicKey.unique().toBuffer()), - several: (amount: number, ethereum?: 'ethereum'): UniversalAddress[] => - Array.from({ length: amount }).map(() => this.universalAddress.generate(ethereum)), + several: ( + amount: number, + ethereum?: 'ethereum', + ): Tuple => + Array.from({ length: amount }).map(() => this.universalAddress.generate(ethereum)) as Tuple< + UniversalAddress, + N + >, }; /** Waits that a transaction is confirmed. */ @@ -231,7 +241,7 @@ export class TestsHelper { // Deploy: await execAsync( - `solana --url ${LOCALHOST} -k ${authorityKeypair} program deploy ${binary} --program-id ${programKeypair}`, + `solana --url ${TestsHelper.LOCALHOST} -k ${authorityKeypair} program deploy ${binary} --program-id ${programKeypair}`, ); // Wait for deploy to be finalized. Don't remove. diff --git a/solana/tests/utils/tbr-wrapper.ts b/solana/tests/utils/tbr-wrapper.ts index fa9f7b5c..2757d61d 100644 --- a/solana/tests/utils/tbr-wrapper.ts +++ b/solana/tests/utils/tbr-wrapper.ts @@ -92,9 +92,9 @@ export class TbrWrapper { ); } - async confirmOwnerTransferRequest(owner: Signer): Promise { + async confirmOwnerTransferRequest(): Promise { return $.getTransaction( - $.sendAndConfirm(await this.client.confirmOwnerTransferRequest(), this.signer, owner), + $.sendAndConfirm(await this.client.confirmOwnerTransferRequest(), this.signer), ); } @@ -114,13 +114,30 @@ export class TbrWrapper { ); } - async registerPeer( + async registerFirstPeer( chain: Chain, peerAddress: UniversalAddress, + config: { + maxGasDropoffMicroToken: number; + relayerFeeMicroUsd: number; + pausedOutboundTransfers: boolean; + }, ): Promise { return $.getTransaction( $.sendAndConfirm( - await this.client.registerPeer(this.publicKey, chain, peerAddress), + await this.client.registerFirstPeer(this.publicKey, chain, peerAddress, config), + this.signer, + ), + ); + } + + async registerAdditionalPeer( + chain: Chain, + peerAddress: UniversalAddress, + ): Promise { + return $.getTransaction( + $.sendAndConfirm( + await this.client.registerAdditionalPeer(this.publicKey, chain, peerAddress), this.signer, ), ); @@ -165,7 +182,7 @@ export class TbrWrapper { ): Promise { return $.getTransaction( $.sendAndConfirm( - await this.client.updateRelayerFee(this.publicKey, chain, relayerFee), + await this.client.updateBaseFee(this.publicKey, chain, relayerFee), this.signer, ), ); diff --git a/target/idl/token_bridge_relayer.json b/target/idl/token_bridge_relayer.json index 353f806e..648ac6e3 100644 --- a/target/idl/token_bridge_relayer.json +++ b/target/idl/token_bridge_relayer.json @@ -1,5 +1,5 @@ { - "address": "HrZSAEW9QVbC4kQu51C7oFMHaam18AvAhpbBYeY9mKht", + "address": "ttbrcA1ckR3D3Ff4VR1MJNCvA7t4d4XV9TcvrVp4AoM", "metadata": { "name": "token_bridge_relayer", "version": "3.0.0", @@ -110,6 +110,116 @@ "because we will update roles depending on the operation." ], "writable": true + }, + { + "name": "upgrade_lock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, + { + "name": "program_data", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 13, + 74, + 247, + 118, + 36, + 164, + 201, + 97, + 25, + 221, + 241, + 144, + 142, + 148, + 63, + 218, + 160, + 137, + 78, + 28, + 18, + 140, + 195, + 112, + 127, + 26, + 150, + 227, + 211, + 125, + 216, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 2, + 168, + 246, + 145, + 78, + 136, + 161, + 176, + 226, + 16, + 21, + 62, + 247, + 99, + 174, + 43, + 0, + 194, + 185, + 61, + 22, + 193, + 36, + 210, + 192, + 83, + 122, + 16, + 4, + 128, + 0, + 0 + ] + } + } + }, + { + "name": "bpf_loader_upgradeable", + "address": "BPFLoaderUpgradeab1e11111111111111111111111" } ], "args": [] @@ -281,7 +391,10 @@ "writable": true }, { - "name": "peer" + "name": "peer", + "docs": [ + "The TBR peer (_i.e._ `data().from_address()`). We do not care about the Token Bridge peer `vaa.meta.emitter_address`." + ] }, { "name": "token_bridge_config" @@ -431,10 +544,6 @@ ] } }, - { - "name": "previous_owner", - "signer": true - }, { "name": "auth_badge_previous_owner", "writable": true, @@ -471,6 +580,30 @@ ], "writable": true }, + { + "name": "upgrade_lock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, { "name": "program_data", "writable": true, @@ -587,10 +720,7 @@ "signer": true }, { - "name": "owner", - "docs": [ - "The designated owner of the program." - ] + "name": "owner" }, { "name": "auth_badge", @@ -622,7 +752,7 @@ "name": "tbr_config", "docs": [ "Owner Config account. This program requires that the `owner` specified", - "in the context equals the pubkey specified in this account. Mutable." + "in the context equals the pubkey specified in this account." ], "writable": true, "pda": { @@ -818,13 +948,6 @@ "Proof that the signer is authorized." ] }, - { - "name": "tbr_config", - "docs": [ - "Owner Config account. This program requires that the `signer` specified", - "in the context equals an authorized pubkey specified in this account." - ] - }, { "name": "peer", "writable": true, @@ -1047,14 +1170,6 @@ { "name": "chain_config", "writable": true - }, - { - "name": "tbr_config", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1067,7 +1182,12 @@ { "name": "submit_owner_transfer_request", "docs": [ - "Updates the owner account. This needs to be either cancelled or approved." + "Updates the owner account. This needs to be either cancelled or approved.", + "", + "For safety reasons, transferring ownership is a 2-step process. This first step is to set the", + "new owner, and the second step is for the new owner to claim the ownership.", + "This is to prevent a situation where the ownership is transferred to an", + "address that is not able to claim the ownership (by mistake)." ], "discriminator": [ 99, @@ -1110,6 +1230,116 @@ } ] } + }, + { + "name": "upgrade_lock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, + { + "name": "program_data", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 13, + 74, + 247, + 118, + 36, + 164, + 201, + 97, + 25, + 221, + 241, + 144, + 142, + 148, + 63, + 218, + 160, + 137, + 78, + 28, + 18, + 140, + 195, + 112, + 127, + 26, + 150, + 227, + 211, + 125, + 216, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 2, + 168, + 246, + 145, + 78, + 136, + 161, + 176, + 226, + 16, + 21, + 62, + 247, + 99, + 174, + 43, + 0, + 194, + 185, + 61, + 22, + 193, + 36, + 210, + 192, + 83, + 122, + 16, + 4, + 128, + 0, + 0 + ] + } + } + }, + { + "name": "bpf_loader_upgradeable", + "address": "BPFLoaderUpgradeab1e11111111111111111111111" } ], "args": [ @@ -1193,9 +1423,6 @@ }, { "name": "fee_recipient", - "docs": [ - "Fee recipient's account. The fee will be transferred to this account." - ], "writable": true, "relations": [ "tbr_config" @@ -1381,18 +1608,7 @@ "docs": [ "Owner of the program as set in the [`TbrConfig`] account." ], - "writable": true, - "signer": true, - "relations": [ - "tbr_config" - ] - }, - { - "name": "tbr_config", - "docs": [ - "Owner Config account. This program requires that the `owner` specified", - "in the context equals the `owner` pubkey specified in this account." - ] + "signer": true }, { "name": "peer" @@ -1549,14 +1765,6 @@ { "name": "chain_config", "writable": true - }, - { - "name": "tbr_config", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1604,14 +1812,6 @@ { "name": "chain_config", "writable": true - }, - { - "name": "tbr_config", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1753,63 +1953,73 @@ }, { "code": 6007, + "name": "DropoffExceedingMaximum", + "msg": "DropoffExceedingMaximum" + }, + { + "code": 6008, "name": "FeeExceedingMaximum", "msg": "FeeExceedingMaximum" }, { - "code": 6008, + "code": 6009, "name": "WrongFeeRecipient", "msg": "WrongFeeRecipient" }, { - "code": 6009, + "code": 6010, "name": "WronglySetOptionalAccounts", "msg": "WronglySetOptionalAccounts" }, { - "code": 6010, + "code": 6011, "name": "WrongMintAuthority", "msg": "WrongMintAuthority" }, { - "code": 6011, + "code": 6012, "name": "InvalidRecipient", "msg": "InvalidRecipient" }, { - "code": 6012, + "code": 6013, "name": "EvmChainPriceNotSet", "msg": "EvmChainPriceNotSet" }, { - "code": 6013, + "code": 6014, "name": "ChainPriceMismatch", "msg": "ChainPriceMismatch" }, { - "code": 6014, + "code": 6015, "name": "PausedTransfers", "msg": "PausedTransfers" }, { - "code": 6015, + "code": 6016, "name": "InvalidSendingPeer", "msg": "InvalidSendingPeer" }, { - "code": 6016, + "code": 6017, "name": "CannotRegisterSolana", "msg": "CannotRegisterSolana" }, { - "code": 6017, + "code": 6018, "name": "InvalidPeerAddress", "msg": "InvalidPeerAddress" }, { - "code": 6018, + "code": 6019, "name": "MissingAssociatedTokenAccount", "msg": "MissingAssociatedTokenAccount" + }, + { + "code": 6020, + "name": "Overflow", + "msg": "Overflow" } ], "types": [ @@ -2053,6 +2263,11 @@ "name": "SEED_PREFIX_TEMPORARY", "type": "bytes", "value": "[116, 109, 112]" + }, + { + "name": "SEED_PREFIX_UPGRADE_LOCK", + "type": "bytes", + "value": "[117, 112, 103, 114, 97, 100, 101, 32, 108, 111, 99, 107]" } ] } \ No newline at end of file diff --git a/target/types/token_bridge_relayer.ts b/target/types/token_bridge_relayer.ts index 360f6270..bad574fe 100644 --- a/target/types/token_bridge_relayer.ts +++ b/target/types/token_bridge_relayer.ts @@ -116,6 +116,116 @@ export type TokenBridgeRelayer = { "because we will update roles depending on the operation." ], "writable": true + }, + { + "name": "upgradeLock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, + { + "name": "programData", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 13, + 74, + 247, + 118, + 36, + 164, + 201, + 97, + 25, + 221, + 241, + 144, + 142, + 148, + 63, + 218, + 160, + 137, + 78, + 28, + 18, + 140, + 195, + 112, + 127, + 26, + 150, + 227, + 211, + 125, + 216, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 2, + 168, + 246, + 145, + 78, + 136, + 161, + 176, + 226, + 16, + 21, + 62, + 247, + 99, + 174, + 43, + 0, + 194, + 185, + 61, + 22, + 193, + 36, + 210, + 192, + 83, + 122, + 16, + 4, + 128, + 0, + 0 + ] + } + } + }, + { + "name": "bpfLoaderUpgradeable", + "address": "BPFLoaderUpgradeab1e11111111111111111111111" } ], "args": [] @@ -287,7 +397,10 @@ export type TokenBridgeRelayer = { "writable": true }, { - "name": "peer" + "name": "peer", + "docs": [ + "The TBR peer (_i.e._ `data().from_address()`). We do not care about the Token Bridge peer `vaa.meta.emitter_address`." + ] }, { "name": "tokenBridgeConfig" @@ -437,10 +550,6 @@ export type TokenBridgeRelayer = { ] } }, - { - "name": "previousOwner", - "signer": true - }, { "name": "authBadgePreviousOwner", "writable": true, @@ -477,6 +586,30 @@ export type TokenBridgeRelayer = { ], "writable": true }, + { + "name": "upgradeLock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, { "name": "programData", "writable": true, @@ -593,10 +726,7 @@ export type TokenBridgeRelayer = { "signer": true }, { - "name": "owner", - "docs": [ - "The designated owner of the program." - ] + "name": "owner" }, { "name": "authBadge", @@ -628,7 +758,7 @@ export type TokenBridgeRelayer = { "name": "tbrConfig", "docs": [ "Owner Config account. This program requires that the `owner` specified", - "in the context equals the pubkey specified in this account. Mutable." + "in the context equals the pubkey specified in this account." ], "writable": true, "pda": { @@ -824,13 +954,6 @@ export type TokenBridgeRelayer = { "Proof that the signer is authorized." ] }, - { - "name": "tbrConfig", - "docs": [ - "Owner Config account. This program requires that the `signer` specified", - "in the context equals an authorized pubkey specified in this account." - ] - }, { "name": "peer", "writable": true, @@ -1053,14 +1176,6 @@ export type TokenBridgeRelayer = { { "name": "chainConfig", "writable": true - }, - { - "name": "tbrConfig", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1073,7 +1188,12 @@ export type TokenBridgeRelayer = { { "name": "submitOwnerTransferRequest", "docs": [ - "Updates the owner account. This needs to be either cancelled or approved." + "Updates the owner account. This needs to be either cancelled or approved.", + "", + "For safety reasons, transferring ownership is a 2-step process. This first step is to set the", + "new owner, and the second step is for the new owner to claim the ownership.", + "This is to prevent a situation where the ownership is transferred to an", + "address that is not able to claim the ownership (by mistake)." ], "discriminator": [ 99, @@ -1116,6 +1236,116 @@ export type TokenBridgeRelayer = { } ] } + }, + { + "name": "upgradeLock", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 117, + 112, + 103, + 114, + 97, + 100, + 101, + 32, + 108, + 111, + 99, + 107 + ] + } + ] + } + }, + { + "name": "programData", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 13, + 74, + 247, + 118, + 36, + 164, + 201, + 97, + 25, + 221, + 241, + 144, + 142, + 148, + 63, + 218, + 160, + 137, + 78, + 28, + 18, + 140, + 195, + 112, + 127, + 26, + 150, + 227, + 211, + 125, + 216, + 108 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 2, + 168, + 246, + 145, + 78, + 136, + 161, + 176, + 226, + 16, + 21, + 62, + 247, + 99, + 174, + 43, + 0, + 194, + 185, + 61, + 22, + 193, + 36, + 210, + 192, + 83, + 122, + 16, + 4, + 128, + 0, + 0 + ] + } + } + }, + { + "name": "bpfLoaderUpgradeable", + "address": "BPFLoaderUpgradeab1e11111111111111111111111" } ], "args": [ @@ -1199,9 +1429,6 @@ export type TokenBridgeRelayer = { }, { "name": "feeRecipient", - "docs": [ - "Fee recipient's account. The fee will be transferred to this account." - ], "writable": true, "relations": [ "tbrConfig" @@ -1387,18 +1614,7 @@ export type TokenBridgeRelayer = { "docs": [ "Owner of the program as set in the [`TbrConfig`] account." ], - "writable": true, - "signer": true, - "relations": [ - "tbrConfig" - ] - }, - { - "name": "tbrConfig", - "docs": [ - "Owner Config account. This program requires that the `owner` specified", - "in the context equals the `owner` pubkey specified in this account." - ] + "signer": true }, { "name": "peer" @@ -1555,14 +1771,6 @@ export type TokenBridgeRelayer = { { "name": "chainConfig", "writable": true - }, - { - "name": "tbrConfig", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1610,14 +1818,6 @@ export type TokenBridgeRelayer = { { "name": "chainConfig", "writable": true - }, - { - "name": "tbrConfig", - "docs": [ - "Program Config account. This program requires that the [`signer`] specified", - "in the context equals a pubkey specified in this account. Mutable,", - "because we will update roles depending on the operation." - ] } ], "args": [ @@ -1759,63 +1959,73 @@ export type TokenBridgeRelayer = { }, { "code": 6007, + "name": "dropoffExceedingMaximum", + "msg": "dropoffExceedingMaximum" + }, + { + "code": 6008, "name": "feeExceedingMaximum", "msg": "feeExceedingMaximum" }, { - "code": 6008, + "code": 6009, "name": "wrongFeeRecipient", "msg": "wrongFeeRecipient" }, { - "code": 6009, + "code": 6010, "name": "wronglySetOptionalAccounts", "msg": "wronglySetOptionalAccounts" }, { - "code": 6010, + "code": 6011, "name": "wrongMintAuthority", "msg": "wrongMintAuthority" }, { - "code": 6011, + "code": 6012, "name": "invalidRecipient", "msg": "invalidRecipient" }, { - "code": 6012, + "code": 6013, "name": "evmChainPriceNotSet", "msg": "evmChainPriceNotSet" }, { - "code": 6013, + "code": 6014, "name": "chainPriceMismatch", "msg": "chainPriceMismatch" }, { - "code": 6014, + "code": 6015, "name": "pausedTransfers", "msg": "pausedTransfers" }, { - "code": 6015, + "code": 6016, "name": "invalidSendingPeer", "msg": "invalidSendingPeer" }, { - "code": 6016, + "code": 6017, "name": "cannotRegisterSolana", "msg": "cannotRegisterSolana" }, { - "code": 6017, + "code": 6018, "name": "invalidPeerAddress", "msg": "invalidPeerAddress" }, { - "code": 6018, + "code": 6019, "name": "missingAssociatedTokenAccount", "msg": "missingAssociatedTokenAccount" + }, + { + "code": 6020, + "name": "overflow", + "msg": "overflow" } ], "types": [ @@ -2059,6 +2269,11 @@ export type TokenBridgeRelayer = { "name": "seedPrefixTemporary", "type": "bytes", "value": "[116, 109, 112]" + }, + { + "name": "seedPrefixUpgradeLock", + "type": "bytes", + "value": "[117, 112, 103, 114, 97, 100, 101, 32, 108, 111, 99, 107]" } ] };