diff --git a/programs/svm-spoke/src/error.rs b/programs/svm-spoke/src/error.rs index cd31886b2..d32365b9c 100644 --- a/programs/svm-spoke/src/error.rs +++ b/programs/svm-spoke/src/error.rs @@ -10,6 +10,8 @@ pub enum CalldataError { InvalidBool, #[msg("Invalid solidity address argument")] InvalidAddress, + #[msg("Invalid solidity uint32 argument")] + InvalidUint32, #[msg("Invalid solidity uint64 argument")] InvalidUint64, #[msg("Invalid solidity uint128 argument")] diff --git a/programs/svm-spoke/src/instructions/admin.rs b/programs/svm-spoke/src/instructions/admin.rs index 6389b0d50..c9ad0e036 100644 --- a/programs/svm-spoke/src/instructions/admin.rs +++ b/programs/svm-spoke/src/instructions/admin.rs @@ -210,18 +210,20 @@ pub fn set_enable_route( #[derive(Accounts)] pub struct RelayRootBundle<'info> { #[account( - mut, // TODO: remove this mut and have separate payer when adding support to invoke this via CCTP. constraint = is_local_or_remote_owner(&signer, &state) @ CustomError::NotOwner )] pub signer: Signer<'info>, + #[account(mut)] + pub payer: Signer<'info>, + // TODO: standardize usage of state.seed vs state.key() #[account(mut, seeds = [b"state", state.seed.to_le_bytes().as_ref()], bump)] pub state: Account<'info, State>, // TODO: consider deriving seed from state.seed instead of state.key() as this could be cheaper (need to verify). #[account(init, // TODO: add comment explaining why init - payer = signer, + payer = payer, space = DISCRIMINATOR_SIZE + RootBundle::INIT_SPACE, seeds =[b"root_bundle", state.key().as_ref(), state.root_bundle_id.to_le_bytes().as_ref()], bump)] @@ -257,17 +259,21 @@ pub fn relay_root_bundle( #[instruction(root_bundle_id: u32)] pub struct EmergencyDeleteRootBundle<'info> { #[account( - mut, constraint = is_local_or_remote_owner(&signer, &state) @ CustomError::NotOwner )] pub signer: Signer<'info>, + #[account(mut)] + // We do not restrict who can receive lamports from closing root_bundle account as that would require storing the + // original payer when root bundle was relayed and unnecessarily make it more expensive to relay in the happy path. + pub closer: SystemAccount<'info>, + #[account(seeds = [b"state", state.seed.to_le_bytes().as_ref()], bump)] pub state: Account<'info, State>, #[account(mut, seeds =[b"root_bundle", state.key().as_ref(), root_bundle_id.to_le_bytes().as_ref()], - close = signer, + close = closer, bump)] pub root_bundle: Account<'info, RootBundle>, } diff --git a/programs/svm-spoke/src/instructions/handle_receive_message.rs b/programs/svm-spoke/src/instructions/handle_receive_message.rs index 238aff66c..6f101d5f5 100644 --- a/programs/svm-spoke/src/instructions/handle_receive_message.rs +++ b/programs/svm-spoke/src/instructions/handle_receive_message.rs @@ -82,6 +82,18 @@ fn translate_message(data: &Vec) -> Result> { (origin_token, destination_chain_id, enabled) .encode_instruction_data("global:set_enable_route") } + s if s == utils::encode_solidity_selector("relayRootBundle(bytes32,bytes32)") => { + let relayer_refund_root = utils::get_solidity_arg(data, 0)?; + let slow_relay_root = utils::get_solidity_arg(data, 1)?; + + (relayer_refund_root, slow_relay_root) + .encode_instruction_data("global:relay_root_bundle") + } + s if s == utils::encode_solidity_selector("emergencyDeleteRootBundle(uint256)") => { + let root_id = utils::decode_solidity_uint32(&utils::get_solidity_arg(data, 0)?)?; + + root_id.encode_instruction_data("global:emergency_delete_root_bundle") + } _ => Err(CalldataError::UnsupportedSelector.into()), } } diff --git a/programs/svm-spoke/src/utils/cctp_utils.rs b/programs/svm-spoke/src/utils/cctp_utils.rs index 2df447393..6bd23da55 100644 --- a/programs/svm-spoke/src/utils/cctp_utils.rs +++ b/programs/svm-spoke/src/utils/cctp_utils.rs @@ -61,6 +61,15 @@ pub fn get_self_authority_pda() -> Pubkey { pda_address } +pub fn decode_solidity_uint32(data: &[u8; 32]) -> Result { + let h_value = u128::from_be_bytes(data[..16].try_into().unwrap()); + let l_value = u128::from_be_bytes(data[16..].try_into().unwrap()); + if h_value > 0 || l_value > u32::MAX as u128 { + return err!(CalldataError::InvalidUint32); + } + Ok(l_value as u32) +} + pub fn decode_solidity_uint64(data: &[u8; 32]) -> Result { let h_value = u128::from_be_bytes(data[..16].try_into().unwrap()); let l_value = u128::from_be_bytes(data[16..].try_into().unwrap()); diff --git a/scripts/svm/simpleFakeRelayerRepayment.ts b/scripts/svm/simpleFakeRelayerRepayment.ts index 6edb579c6..e663c4198 100644 --- a/scripts/svm/simpleFakeRelayerRepayment.ts +++ b/scripts/svm/simpleFakeRelayerRepayment.ts @@ -167,6 +167,7 @@ async function testBundleLogic(): Promise { state: statePda, rootBundle: rootBundle, signer: signer, + payer: signer, systemProgram: SystemProgram.programId, }) .rpc(); diff --git a/test/svm/SvmSpoke.Bundle.ts b/test/svm/SvmSpoke.Bundle.ts index 5ad50c5d9..971d97479 100644 --- a/test/svm/SvmSpoke.Bundle.ts +++ b/test/svm/SvmSpoke.Bundle.ts @@ -101,7 +101,13 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Try to relay root bundle as non-owner - let relayRootBundleAccounts = { state: state, rootBundle, signer: nonOwner.publicKey, program: program.programId }; + let relayRootBundleAccounts = { + state: state, + rootBundle, + signer: nonOwner.publicKey, + payer: nonOwner.publicKey, + program: program.programId, + }; try { await program.methods .relayRootBundle(relayerRefundRootArray, slowRelayRootArray) @@ -114,7 +120,7 @@ describe("svm_spoke.bundle", () => { } // Relay root bundle as owner - relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods .relayRootBundle(relayerRefundRootArray, slowRelayRootArray) .accounts(relayRootBundleAccounts) @@ -146,7 +152,13 @@ describe("svm_spoke.bundle", () => { const seeds2 = [Buffer.from("root_bundle"), state.toBuffer(), rootBundleIdBuffer2]; const [rootBundle2] = PublicKey.findProgramAddressSync(seeds2, program.programId); - relayRootBundleAccounts = { state, rootBundle: rootBundle2, signer: owner, program: program.programId }; + relayRootBundleAccounts = { + state, + rootBundle: rootBundle2, + signer: owner, + payer: owner, + program: program.programId, + }; await program.methods .relayRootBundle(relayerRefundRootArray2, slowRelayRootArray2) .accounts(relayRootBundleAccounts) @@ -170,7 +182,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle as owner - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; const tx = await program.methods .relayRootBundle(relayerRefundRootArray, slowRelayRootArray) .accounts(relayRootBundleAccounts) @@ -220,7 +232,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - let relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + let relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const remainingAccounts = [ @@ -351,7 +363,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - let relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + let relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const remainingAccounts = [ @@ -492,7 +504,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - let relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + let relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const remainingAccounts = [ @@ -554,7 +566,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - let relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + let relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const remainingAccounts = [ @@ -616,7 +628,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - let relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + let relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const remainingAccounts = [{ pubkey: relayerTA, isWritable: true, isSigner: false }]; @@ -706,7 +718,7 @@ describe("svm_spoke.bundle", () => { const seeds = [Buffer.from("root_bundle"), state.toBuffer(), rootBundleIdBuffer]; const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods .relayRootBundle(relayerRefundRootArray, slowRelayRootArray) .accounts(relayRootBundleAccounts) @@ -722,6 +734,7 @@ describe("svm_spoke.bundle", () => { state, rootBundle, signer: nonOwner.publicKey, + closer: nonOwner.publicKey, program: program.programId, }; await program.methods @@ -735,7 +748,13 @@ describe("svm_spoke.bundle", () => { } // Execute the emergency delete - const emergencyDeleteRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const emergencyDeleteRootBundleAccounts = { + state, + rootBundle, + signer: owner, + closer: owner, + program: program.programId, + }; await program.methods.emergencyDeleteRootBundle(rootBundleId).accounts(emergencyDeleteRootBundleAccounts).rpc(); // Verify that the root bundle has been deleted @@ -763,7 +782,13 @@ describe("svm_spoke.bundle", () => { `Root bundle index should be ${initialRootBundleId + 1}` ); - const newRelayRootBundleAccounts = { state, rootBundle: newRootBundle, signer: owner, program: program.programId }; + const newRelayRootBundleAccounts = { + state, + rootBundle: newRootBundle, + signer: owner, + payer: owner, + program: program.programId, + }; await program.methods .relayRootBundle(newRelayerRefundRootArray, newSlowRelayRootArray) .accounts(newRelayRootBundleAccounts) @@ -869,7 +894,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); // Verify valid leaf @@ -1021,7 +1046,7 @@ describe("svm_spoke.bundle", () => { rootBundleIdBuffer.writeUInt32LE(rootBundleId); const seeds = [Buffer.from("root_bundle"), state.toBuffer(), rootBundleIdBuffer]; const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); - let relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + let relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const proofAsNumbers = proof.map((p) => Array.from(p)); const executeRelayerRefundLeafAccounts = { @@ -1097,7 +1122,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const remainingAccounts = [{ pubkey: relayerTA, isWritable: true, isSigner: false }]; @@ -1171,7 +1196,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); const remainingAccounts = [ @@ -1274,7 +1299,7 @@ describe("svm_spoke.bundle", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); // Pass refund addresses in remaining accounts. diff --git a/test/svm/SvmSpoke.HandleReceiveMessage.ts b/test/svm/SvmSpoke.HandleReceiveMessage.ts index 89dd82fae..dcaaf5092 100644 --- a/test/svm/SvmSpoke.HandleReceiveMessage.ts +++ b/test/svm/SvmSpoke.HandleReceiveMessage.ts @@ -1,4 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; +import * as crypto from "crypto"; import { BN, web3, workspace, Program, AnchorProvider, AnchorError } from "@coral-xyz/anchor"; import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, createMint } from "@solana/spl-token"; import { Keypair } from "@solana/web3.js"; @@ -38,6 +39,8 @@ describe("svm_spoke.handle_receive_message", () => { "function pauseFills(bool pause)", "function setCrossDomainAdmin(address newCrossDomainAdmin)", "function setEnableRoute(bytes32 originToken, uint64 destinationChainId, bool enabled)", + "function relayRootBundle(bytes32 relayerRefundRoot, bytes32 slowRelayRoot)", + "function emergencyDeleteRootBundle(uint256 rootBundleId)", ]); beforeEach(async () => { @@ -406,4 +409,168 @@ describe("svm_spoke.handle_receive_message", () => { routeAccount = await program.account.route.fetch(routePda); assert.isFalse(routeAccount.enabled, "Route should be disabled"); }); + + it("Relays root bundle remotely", async () => { + // Encode relayRootBundle message. + const relayerRefundRoot = crypto.randomBytes(32); + const slowRelayRoot = crypto.randomBytes(32); + const calldata = ethereumIface.encodeFunctionData("relayRootBundle", [relayerRefundRoot, slowRelayRoot]); + const messageBody = Buffer.from(calldata.slice(2), "hex"); + const message = encodeMessageHeader({ + version: cctpMessageversion, + sourceDomain: remoteDomain.toNumber(), + destinationDomain: localDomain, + nonce: BigInt(nonce), + sender: crossDomainAdmin, + recipient: program.programId, + destinationCaller, + messageBody, + }); + + // Remaining accounts specific to RelayRootBundle. + const rootBundleId = (await program.account.state.fetch(state)).rootBundleId; + const rootBundleIdBuffer = Buffer.alloc(4); + rootBundleIdBuffer.writeUInt32LE(rootBundleId); + const seeds = [Buffer.from("root_bundle"), state.toBuffer(), rootBundleIdBuffer]; + const [rootBundle] = web3.PublicKey.findProgramAddressSync(seeds, program.programId); + // Same 3 remaining accounts passed for HandleReceiveMessage context. + const relayRootBundleRemainingAccounts = remainingAccounts.slice(0, 3); + // payer in self-invoked SetEnableRoute. + relayRootBundleRemainingAccounts.push({ + isSigner: true, + isWritable: true, + pubkey: provider.wallet.publicKey, + }); + // state in self-invoked RelayRootBundle. + relayRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: true, + pubkey: state, + }); + // root_bundle in self-invoked RelayRootBundle. + relayRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: true, + pubkey: rootBundle, + }); + // system_program in self-invoked RelayRootBundle. + relayRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: web3.SystemProgram.programId, + }); + // event_authority in self-invoked RelayRootBundle (appended by Anchor with event_cpi macro). + relayRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: eventAuthority, + }); + // program in self-invoked RelayRootBundle (appended by Anchor with event_cpi macro). + relayRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: program.programId, + }); + + // Invoke remote CCTP message to relay root bundle. + await messageTransmitterProgram.methods + .receiveMessage({ message, attestation }) + .accounts(receiveMessageAccounts) + .remainingAccounts(relayRootBundleRemainingAccounts) + .rpc(); + + // Check the updated relayer refund and slow relay root in the root bundle account. + const rootBundleAccountData = await program.account.rootBundle.fetch(rootBundle); + const updatedRelayerRefundRoot = Buffer.from(rootBundleAccountData.relayerRefundRoot); + const updatedSlowRelayRoot = Buffer.from(rootBundleAccountData.slowRelayRoot); + assert.isTrue(updatedRelayerRefundRoot.equals(relayerRefundRoot), "Relayer refund root should be set"); + assert.isTrue(updatedSlowRelayRoot.equals(slowRelayRoot), "Slow relay root should be set"); + }); + + it("Emergency deletes root bundle remotely", async () => { + // Relay root bundle. + const relayerRefundRoot = crypto.randomBytes(32); + const slowRelayRoot = crypto.randomBytes(32); + const rootBundleId = (await program.account.state.fetch(state)).rootBundleId; + const rootBundleIdBuffer = Buffer.alloc(4); + rootBundleIdBuffer.writeUInt32LE(rootBundleId); + const seeds = [Buffer.from("root_bundle"), state.toBuffer(), rootBundleIdBuffer]; + const [rootBundle] = web3.PublicKey.findProgramAddressSync(seeds, program.programId); + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; + await program.methods + .relayRootBundle(Array.from(relayerRefundRoot), Array.from(slowRelayRoot)) + .accounts(relayRootBundleAccounts) + .rpc(); + + // Ensure the root bundle exists before deletion + let rootBundleData = await program.account.rootBundle.fetch(rootBundle); + assert.isNotNull(rootBundleData, "Root bundle should exist before deletion"); + + // Encode emergencyDeleteRootBundle message. + const calldata = ethereumIface.encodeFunctionData("emergencyDeleteRootBundle", [rootBundleId]); + const messageBody = Buffer.from(calldata.slice(2), "hex"); + const message = encodeMessageHeader({ + version: cctpMessageversion, + sourceDomain: remoteDomain.toNumber(), + destinationDomain: localDomain, + nonce: BigInt(nonce), + sender: crossDomainAdmin, + recipient: program.programId, + destinationCaller, + messageBody, + }); + + // Remaining accounts specific to EmergencyDeleteRootBundle. + // Same 3 remaining accounts passed for HandleReceiveMessage context. + const emergencyDeleteRootBundleRemainingAccounts = remainingAccounts.slice(0, 3); + // closer in self-invoked EmergencyDeleteRootBundle. + emergencyDeleteRootBundleRemainingAccounts.push({ + isSigner: true, + isWritable: true, + pubkey: provider.wallet.publicKey, + }); + // state in self-invoked EmergencyDeleteRootBundle. + emergencyDeleteRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: state, + }); + // root_bundle in self-invoked EmergencyDeleteRootBundle. + emergencyDeleteRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: true, + pubkey: rootBundle, + }); + // event_authority in self-invoked EmergencyDeleteRootBundle (appended by Anchor with event_cpi macro). + emergencyDeleteRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: eventAuthority, + }); + // program in self-invoked EmergencyDeleteRootBundle (appended by Anchor with event_cpi macro). + emergencyDeleteRootBundleRemainingAccounts.push({ + isSigner: false, + isWritable: false, + pubkey: program.programId, + }); + + // Invoke remote CCTP message to delete the root bundle. + await messageTransmitterProgram.methods + .receiveMessage({ message, attestation }) + .accounts(receiveMessageAccounts) + .remainingAccounts(emergencyDeleteRootBundleRemainingAccounts) + .rpc(); + + // Verify that the root bundle has been deleted + try { + rootBundleData = await program.account.rootBundle.fetch(rootBundle); + assert.fail("Root bundle should have been deleted"); + } catch (err: any) { + assert.include( + err.toString(), + "Account does not exist or has no data", + "Expected error when fetching deleted root bundle" + ); + } + }); }); diff --git a/test/svm/SvmSpoke.RefundClaims.ts b/test/svm/SvmSpoke.RefundClaims.ts index c913fc45c..16348be5c 100644 --- a/test/svm/SvmSpoke.RefundClaims.ts +++ b/test/svm/SvmSpoke.RefundClaims.ts @@ -78,7 +78,7 @@ describe("svm_spoke.refund_claims", () => { const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId); // Relay root bundle - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods.relayRootBundle(Array.from(root), Array.from(root)).accounts(relayRootBundleAccounts).rpc(); // Pass claim account as relayer refund address. diff --git a/test/svm/SvmSpoke.SlowFill.ts b/test/svm/SvmSpoke.SlowFill.ts index 618694580..ee97325b8 100644 --- a/test/svm/SvmSpoke.SlowFill.ts +++ b/test/svm/SvmSpoke.SlowFill.ts @@ -120,7 +120,7 @@ describe("svm_spoke.slow_fill", () => { const relayerRefundRoot = crypto.randomBytes(32); // Relay root bundle - const relayRootBundleAccounts = { state, rootBundle, signer: owner, program: program.programId }; + const relayRootBundleAccounts = { state, rootBundle, signer: owner, payer: owner, program: program.programId }; await program.methods .relayRootBundle(Array.from(relayerRefundRoot), Array.from(slowRelayRoot)) .accounts(relayRootBundleAccounts) diff --git a/test/svm/SvmSpoke.TokenBridge.ts b/test/svm/SvmSpoke.TokenBridge.ts index 49beb1812..d924548f7 100644 --- a/test/svm/SvmSpoke.TokenBridge.ts +++ b/test/svm/SvmSpoke.TokenBridge.ts @@ -161,6 +161,7 @@ describe("svm_spoke.token_bridge", () => { state, rootBundle, signer: owner, + payer: owner, program: program.programId, }; await program.methods