(idl, provider);
+const program = getSpokePoolProgram(provider);
const programId = program.programId;
// Parse arguments
diff --git a/src/DeploymentUtils.ts b/src/DeploymentUtils.ts
index 58844ac45..b822aa933 100644
--- a/src/DeploymentUtils.ts
+++ b/src/DeploymentUtils.ts
@@ -6,7 +6,11 @@ interface DeploymentExport {
const deployments: DeploymentExport = deployments_ as any;
// Returns the deployed address of any contract on any network.
-export function getDeployedAddress(contractName: string, networkId: number, throwOnError = true): string | undefined {
+export function getDeployedAddress(
+ contractName: string,
+ networkId: number | string,
+ throwOnError = true
+): string | undefined {
const address = deployments[networkId.toString()]?.[contractName]?.address;
if (!address && throwOnError) {
throw new Error(`Contract ${contractName} not found on ${networkId} in deployments.json`);
diff --git a/src/svm/helpers.ts b/src/svm/helpers.ts
index 54a9d518e..7b737280d 100644
--- a/src/svm/helpers.ts
+++ b/src/svm/helpers.ts
@@ -7,7 +7,7 @@ import { ethers } from "ethers";
*/
export const getSolanaChainId = (cluster: "devnet" | "mainnet"): BigNumber => {
return BigNumber.from(
- BigInt(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`solana-${cluster}`))) & BigInt("0xFFFFFFFFFFFFFFFF")
+ BigInt(ethers.utils.keccak256(ethers.utils.toUtf8Bytes(`solana-${cluster}`))) & BigInt("0xFFFFFFFFFFFF")
);
};
diff --git a/src/svm/index.ts b/src/svm/index.ts
index 614baeef3..8276197e4 100644
--- a/src/svm/index.ts
+++ b/src/svm/index.ts
@@ -4,6 +4,8 @@ export * from "./conversionUtils";
export * from "./transactionUtils";
export * from "./solanaProgramUtils";
export * from "./coders";
+export * from "./programConnectors";
+export * from "./assets";
export * from "./constants";
export * from "./helpers";
export * from "./cctpHelpers";
diff --git a/src/svm/programConnectors.ts b/src/svm/programConnectors.ts
new file mode 100644
index 000000000..ddcfeefae
--- /dev/null
+++ b/src/svm/programConnectors.ts
@@ -0,0 +1,61 @@
+import { AnchorProvider, Idl, Program } from "@coral-xyz/anchor";
+import { getDeployedAddress } from "../DeploymentUtils";
+import { SupportedNetworks } from "../types/svm";
+import {
+ MessageTransmitterAnchor,
+ MessageTransmitterIdl,
+ MulticallHandlerAnchor,
+ MulticallHandlerIdl,
+ SvmSpokeAnchor,
+ SvmSpokeIdl,
+ TokenMessengerMinterAnchor,
+ TokenMessengerMinterIdl,
+} from "./assets";
+import { getSolanaChainId, isSolanaDevnet } from "./helpers";
+
+type ProgramOptions = { network?: SupportedNetworks; programId?: string };
+
+export function getConnectedProgram(idl: P, provider: AnchorProvider, programId: string) {
+ idl.address = programId;
+ return new Program
(idl, provider);
+}
+
+// Resolves the program ID from options or defaults to the deployed address. Prioritizes programId, falls back to
+// network, and if network is not defined, determines the network from the provider's RPC URL. Throws an error if
+// the program ID cannot be resolved.
+function resolveProgramId(programName: string, provider: AnchorProvider, options?: ProgramOptions): string {
+ const { network, programId } = options ?? {};
+
+ if (programId) {
+ return programId; // Prioritize explicitly provided programId
+ }
+
+ const resolvedNetwork = network ?? (isSolanaDevnet(provider) ? "devnet" : "mainnet");
+ const deployedAddress = getDeployedAddress(programName, getSolanaChainId(resolvedNetwork).toString());
+
+ if (!deployedAddress) {
+ throw new Error(`${programName} Program ID not found for ${resolvedNetwork}`);
+ }
+
+ return deployedAddress;
+}
+
+export function getSpokePoolProgram(provider: AnchorProvider, options?: ProgramOptions) {
+ const id = resolveProgramId("SvmSpoke", provider, options);
+ return getConnectedProgram(SvmSpokeIdl, provider, id);
+}
+
+export function getMessageTransmitterProgram(provider: AnchorProvider, options?: ProgramOptions) {
+ const id = resolveProgramId("MessageTransmitter", provider, options);
+ return getConnectedProgram(MessageTransmitterIdl, provider, id);
+}
+
+export function getTokenMessengerMinterProgram(provider: AnchorProvider, options?: ProgramOptions) {
+ const id = resolveProgramId("TokenMessengerMinter", provider, options);
+ return getConnectedProgram(TokenMessengerMinterIdl, provider, id);
+}
+
+export function getMulticallHandlerProgram(provider: AnchorProvider, options?: ProgramOptions) {
+ const id = resolveProgramId("MulticallHandler", provider, options);
+ return getConnectedProgram(MulticallHandlerIdl, provider, id);
+}
diff --git a/src/types/svm.ts b/src/types/svm.ts
index 63b26d27a..e181eec29 100644
--- a/src/types/svm.ts
+++ b/src/types/svm.ts
@@ -131,3 +131,8 @@ export interface EventType {
blockTime: number;
signature: string;
}
+
+/**
+ * Supported Networks
+ */
+export type SupportedNetworks = "mainnet" | "devnet";
diff --git a/test/svm/SvmSpoke.Bundle.ts b/test/svm/SvmSpoke.Bundle.ts
index e84f9f36f..38c8182af 100644
--- a/test/svm/SvmSpoke.Bundle.ts
+++ b/test/svm/SvmSpoke.Bundle.ts
@@ -7,7 +7,7 @@ import {
mintTo,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
-import { ComputeBudgetProgram, Keypair, PublicKey } from "@solana/web3.js";
+import { ComputeBudgetProgram, Keypair, PublicKey, TransactionInstruction } from "@solana/web3.js";
import { assert } from "chai";
import * as crypto from "crypto";
import { ethers } from "ethers";
@@ -241,13 +241,23 @@ describe("svm_spoke.bundle", () => {
program: program.programId,
};
const proofAsNumbers = proof.map((p) => Array.from(p));
- await loadExecuteRelayerRefundLeafParams(program, owner, stateAccountData.rootBundleId, leaf, proofAsNumbers);
+ const instructionParams = await loadExecuteRelayerRefundLeafParams(
+ program,
+ owner,
+ stateAccountData.rootBundleId,
+ leaf,
+ proofAsNumbers
+ );
const tx = await program.methods
.executeRelayerRefundLeaf()
.accounts(executeRelayerRefundLeafAccounts)
.remainingAccounts(remainingAccounts)
.rpc();
+ // Verify the instruction params account has been automatically closed.
+ const instructionParamsInfo = await program.provider.connection.getAccountInfo(instructionParams);
+ assert.isNull(instructionParamsInfo, "Instruction params account should be closed");
+
// Verify the ExecutedRelayerRefundRoot event
let events = await readEventsUntilFound(connection, tx, [program]);
let event = events.find((event) => event.name === "executedRelayerRefundRoot")?.data;
@@ -1015,7 +1025,7 @@ describe("svm_spoke.bundle", () => {
} else if (!testConfig.deferredRefunds && testConfig.atomicAccountCreation) {
refundAccounts.push(tokenAccount);
} else {
- await program.methods.initializeClaimAccount(mint, tokenOwner).rpc();
+ await program.methods.initializeClaimAccount().accounts({ mint, refundAddress: tokenOwner }).rpc();
refundAccounts.push(claimAccount);
}
@@ -1397,8 +1407,8 @@ describe("svm_spoke.bundle", () => {
[Buffer.from("claim_account"), mint.toBuffer(), relayerB.publicKey.toBuffer()],
program.programId
);
- await program.methods.initializeClaimAccount(mint, relayerA.publicKey).rpc();
- await program.methods.initializeClaimAccount(mint, relayerB.publicKey).rpc();
+ await program.methods.initializeClaimAccount().accounts({ mint, refundAddress: relayerA.publicKey }).rpc();
+ await program.methods.initializeClaimAccount().accounts({ mint, refundAddress: relayerB.publicKey }).rpc();
}
// Prepare leaf using token accounts.
@@ -1732,4 +1742,220 @@ describe("svm_spoke.bundle", () => {
assert.include(err.toString(), "Invalid Merkle proof", "Expected merkle verification to fail");
}
});
+
+ describe("Execute Max multiple refunds with claims", async () => {
+ const executeMaxRefundClaims = async (testConfig: {
+ solanaDistributions: number;
+ useAddressLookup: boolean;
+ separatePhases: boolean;
+ }) => {
+ // Add leaves for other EVM chains to have non-empty proofs array to ensure we don't run out of memory when processing.
+ const evmDistributions = 100; // This would fit in 7 proof array elements.
+
+ const refundAddresses: web3.PublicKey[] = []; // These are relayer authority addresses used in leaf building.
+ const claimAccounts: web3.PublicKey[] = [];
+ const tokenAccounts: web3.PublicKey[] = [];
+ const refundAmounts: BN[] = [];
+ const initializeInstructions: TransactionInstruction[] = [];
+ const claimInstructions: TransactionInstruction[] = [];
+
+ for (let i = 0; i < testConfig.solanaDistributions; i++) {
+ // Create the token account.
+ const tokenOwner = Keypair.generate().publicKey;
+ const tokenAccount = (await getOrCreateAssociatedTokenAccount(connection, payer, mint, tokenOwner)).address;
+ refundAddresses.push(tokenOwner);
+ tokenAccounts.push(tokenAccount);
+
+ const [claimAccount] = PublicKey.findProgramAddressSync(
+ [Buffer.from("claim_account"), mint.toBuffer(), tokenOwner.toBuffer()],
+ program.programId
+ );
+
+ // Create instruction to initialize claim account.
+ initializeInstructions.push(
+ await program.methods.initializeClaimAccount().accounts({ mint, refundAddress: tokenOwner }).instruction()
+ );
+ claimAccounts.push(claimAccount);
+
+ refundAmounts.push(new BN(randomBigInt(2).toString()));
+
+ // Create instruction to claim refund to the token account.
+ const claimRelayerRefundAccounts = {
+ signer: owner,
+ initializer: owner,
+ state,
+ vault,
+ mint,
+ tokenAccount,
+ refundAddress: tokenOwner,
+ claimAccount,
+ tokenProgram: TOKEN_PROGRAM_ID,
+ program: program.programId,
+ };
+ claimInstructions.push(
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).instruction()
+ );
+ }
+
+ const { relayerRefundLeaves, merkleTree } = buildRelayerRefundMerkleTree({
+ totalEvmDistributions: evmDistributions,
+ totalSolanaDistributions: testConfig.solanaDistributions,
+ mixLeaves: false,
+ chainId: chainId.toNumber(),
+ mint,
+ svmRelayers: refundAddresses,
+ svmRefundAmounts: refundAmounts,
+ });
+
+ const root = merkleTree.getRoot();
+ const proof = merkleTree.getProof(relayerRefundLeaves[0]);
+ const leaf = relayerRefundLeaves[0] as RelayerRefundLeafSolana;
+
+ const stateAccountData = await program.account.state.fetch(state);
+ const rootBundleId = stateAccountData.rootBundleId;
+
+ const rootBundleIdBuffer = Buffer.alloc(4);
+ rootBundleIdBuffer.writeUInt32LE(rootBundleId);
+ const seeds = [Buffer.from("root_bundle"), seed.toArrayLike(Buffer, "le", 8), rootBundleIdBuffer];
+ const [rootBundle] = PublicKey.findProgramAddressSync(seeds, program.programId);
+
+ // Relay root bundle
+ 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
+ const proofAsNumbers = proof.map((p) => Array.from(p));
+
+ const [instructionParams] = PublicKey.findProgramAddressSync(
+ [Buffer.from("instruction_params"), owner.toBuffer()],
+ program.programId
+ );
+
+ const executeAccounts = {
+ instructionParams,
+ state,
+ rootBundle: rootBundle,
+ signer: owner,
+ vault,
+ tokenProgram: TOKEN_PROGRAM_ID,
+ mint,
+ transferLiability,
+ program: program.programId,
+ };
+
+ const executeRemainingAccounts = claimAccounts.map((account) => ({
+ pubkey: account,
+ isWritable: true,
+ isSigner: false,
+ }));
+
+ // Build the instruction to execute relayer refund leaf and write its instruction args to the data account.
+ await loadExecuteRelayerRefundLeafParams(program, owner, stateAccountData.rootBundleId, leaf, proofAsNumbers);
+
+ const executeInstruction = await program.methods
+ .executeRelayerRefundLeafDeferred()
+ .accounts(executeAccounts)
+ .remainingAccounts(executeRemainingAccounts)
+ .instruction();
+
+ // Initialize, execute and claim depending on the chosen method.
+ const instructions = [...initializeInstructions, executeInstruction, ...claimInstructions];
+ if (!testConfig.separatePhases) {
+ // Pack all instructions in one transaction.
+ if (testConfig.useAddressLookup)
+ await sendTransactionWithLookupTable(
+ connection,
+ instructions,
+ (anchor.AnchorProvider.env().wallet as anchor.Wallet).payer
+ );
+ else
+ await web3.sendAndConfirmTransaction(
+ connection,
+ new web3.Transaction().add(...instructions),
+ [(anchor.AnchorProvider.env().wallet as anchor.Wallet).payer],
+ {
+ commitment: "confirmed",
+ }
+ );
+ } else {
+ // Send claim account initialization, execution and claim in separate transactions.
+ if (testConfig.useAddressLookup) {
+ await sendTransactionWithLookupTable(
+ connection,
+ initializeInstructions,
+ (anchor.AnchorProvider.env().wallet as anchor.Wallet).payer
+ );
+ await sendTransactionWithLookupTable(
+ connection,
+ [executeInstruction],
+ (anchor.AnchorProvider.env().wallet as anchor.Wallet).payer
+ );
+ await sendTransactionWithLookupTable(
+ connection,
+ claimInstructions,
+ (anchor.AnchorProvider.env().wallet as anchor.Wallet).payer
+ );
+ } else {
+ await web3.sendAndConfirmTransaction(
+ connection,
+ new web3.Transaction().add(...initializeInstructions),
+ [(anchor.AnchorProvider.env().wallet as anchor.Wallet).payer],
+ {
+ commitment: "confirmed",
+ }
+ );
+ await web3.sendAndConfirmTransaction(
+ connection,
+ new web3.Transaction().add(executeInstruction),
+ [(anchor.AnchorProvider.env().wallet as anchor.Wallet).payer],
+ {
+ commitment: "confirmed",
+ }
+ );
+ await web3.sendAndConfirmTransaction(
+ connection,
+ new web3.Transaction().add(...claimInstructions),
+ [(anchor.AnchorProvider.env().wallet as anchor.Wallet).payer],
+ {
+ commitment: "confirmed",
+ }
+ );
+ }
+ }
+
+ // Verify all refund token account balances.
+ const refundBalances = await Promise.all(
+ tokenAccounts.map(async (account) => {
+ return (await connection.getTokenAccountBalance(account)).value.amount;
+ })
+ );
+ refundBalances.forEach((balance, i) => {
+ assertSE(balance, refundAmounts[i].toString(), `Refund account ${i} balance should match refund amount`);
+ });
+ };
+
+ it("Execute Max multiple refunds with claims in one legacy transaction", async () => {
+ // Larger amount would hit transaction message size limit.
+ const solanaDistributions = 5;
+ await executeMaxRefundClaims({ solanaDistributions, useAddressLookup: false, separatePhases: false });
+ });
+
+ it("Execute Max multiple refunds with claims in one versioned transaction", async () => {
+ // Larger amount would hit maximum instruction trace length limit.
+ const solanaDistributions = 12;
+ await executeMaxRefundClaims({ solanaDistributions, useAddressLookup: true, separatePhases: false });
+ });
+
+ it("Execute Max multiple refunds with claims in separate phase legacy transactions", async () => {
+ // Larger amount would hit transaction message size limit.
+ const solanaDistributions = 7;
+ await executeMaxRefundClaims({ solanaDistributions, useAddressLookup: false, separatePhases: true });
+ });
+
+ it("Execute Max multiple refunds with claims in separate phase versioned transactions", async () => {
+ // Larger amount would hit maximum instruction trace length limit.
+ const solanaDistributions = 21;
+ await executeMaxRefundClaims({ solanaDistributions, useAddressLookup: true, separatePhases: true });
+ });
+ });
});
diff --git a/test/svm/SvmSpoke.Ownership.ts b/test/svm/SvmSpoke.Ownership.ts
index c9ff1c46c..644225f0a 100644
--- a/test/svm/SvmSpoke.Ownership.ts
+++ b/test/svm/SvmSpoke.Ownership.ts
@@ -124,8 +124,17 @@ describe("svm_spoke.ownership", () => {
it("Transfers ownership", async () => {
// Transfer ownership to newOwner
- const transferOwnershipAccounts = { state, signer: owner };
- await program.methods.transferOwnership(newOwner.publicKey).accounts(transferOwnershipAccounts).rpc();
+ const transferOwnershipAccounts = { state, signer: owner, program: program.programId };
+ const tx = await program.methods.transferOwnership(newOwner.publicKey).accounts(transferOwnershipAccounts).rpc();
+
+ // Verify the TransferredOwnership event
+ let events = await readEventsUntilFound(provider.connection, tx, [program]);
+ let transferredOwnershipEvents = events.filter((event) => event.name === "transferredOwnership");
+ assert.equal(
+ transferredOwnershipEvents[0].data.newOwner.toString(),
+ newOwner.publicKey.toString(),
+ "TransferredOwnership event should indicate the new owner"
+ );
// Verify the new owner
let stateAccountData = await program.account.state.fetch(state);
@@ -133,7 +142,7 @@ describe("svm_spoke.ownership", () => {
// Try to transfer ownership as non-owner
try {
- const transferOwnershipAccounts = { state, signer: nonOwner.publicKey };
+ const transferOwnershipAccounts = { state, signer: nonOwner.publicKey, program: program.programId };
await program.methods
.transferOwnership(nonOwner.publicKey)
.accounts(transferOwnershipAccounts)
diff --git a/test/svm/SvmSpoke.RefundClaims.ts b/test/svm/SvmSpoke.RefundClaims.ts
index 01c35ff09..0b6bc9215 100644
--- a/test/svm/SvmSpoke.RefundClaims.ts
+++ b/test/svm/SvmSpoke.RefundClaims.ts
@@ -4,7 +4,14 @@ import { Keypair, PublicKey } from "@solana/web3.js";
import { assert } from "chai";
import { common } from "./SvmSpoke.common";
import { MerkleTree } from "@uma/common/dist/MerkleTree";
-import { createMint, getOrCreateAssociatedTokenAccount, mintTo, TOKEN_PROGRAM_ID } from "@solana/spl-token";
+import {
+ AuthorityType,
+ createMint,
+ getOrCreateAssociatedTokenAccount,
+ mintTo,
+ setAuthority,
+ TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
import { RelayerRefundLeafSolana, RelayerRefundLeafType } from "../../src/types/svm";
import { loadExecuteRelayerRefundLeafParams, readEventsUntilFound, relayerRefundHashFn } from "../../src/svm";
@@ -31,6 +38,7 @@ describe("svm_spoke.refund_claims", () => {
state: PublicKey;
vault: PublicKey;
mint: PublicKey;
+ refundAddress: PublicKey;
tokenAccount: PublicKey;
claimAccount: PublicKey;
tokenProgram: PublicKey;
@@ -42,8 +50,8 @@ describe("svm_spoke.refund_claims", () => {
const initializeClaimAccount = async (initializer = claimInitializer) => {
const initializeClaimAccountIx = await program.methods
- .initializeClaimAccount(mint, relayer.publicKey)
- .accounts({ signer: initializer.publicKey })
+ .initializeClaimAccount()
+ .accounts({ signer: initializer.publicKey, mint, refundAddress: relayer.publicKey })
.instruction();
await web3.sendAndConfirmTransaction(connection, new web3.Transaction().add(initializeClaimAccountIx), [
initializer,
@@ -142,6 +150,7 @@ describe("svm_spoke.refund_claims", () => {
state,
vault,
mint,
+ refundAddress: relayer.publicKey,
tokenAccount,
claimAccount,
tokenProgram: TOKEN_PROGRAM_ID,
@@ -169,10 +178,7 @@ describe("svm_spoke.refund_claims", () => {
const iRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
// Claim refund for the relayer.
- const tx = await program.methods
- .claimRelayerRefundFor(relayer.publicKey)
- .accounts(claimRelayerRefundAccounts)
- .rpc();
+ const tx = await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
// The relayer should have received funds from the vault.
const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
@@ -194,11 +200,11 @@ describe("svm_spoke.refund_claims", () => {
await executeRelayerRefundToClaim(relayerRefund);
// Claim refund for the relayer.
- await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
// The claim account should have been automatically closed, so repeated claim should fail.
try {
- await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
assert.fail("Claiming refund from closed account should fail");
} catch (error: any) {
assert.instanceOf(error, AnchorError);
@@ -212,7 +218,7 @@ describe("svm_spoke.refund_claims", () => {
// After reinitalizing the claim account, the repeated claim should still fail.
await initializeClaimAccount();
try {
- await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
assert.fail("Claiming refund from reinitalized account should fail");
} catch (error: any) {
assert.instanceOf(error, AnchorError);
@@ -231,7 +237,7 @@ describe("svm_spoke.refund_claims", () => {
const iRelayerBal = (await connection.getTokenAccountBalance(tokenAccount)).value.amount;
// Claim refund for the relayer.
- await await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
+ await await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
// The relayer should have received both refunds.
const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
@@ -256,7 +262,7 @@ describe("svm_spoke.refund_claims", () => {
// Claiming with default initializer should fail.
try {
- await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
} catch (error: any) {
assert.instanceOf(error, AnchorError);
assert.strictEqual(
@@ -268,7 +274,7 @@ describe("svm_spoke.refund_claims", () => {
// Claim refund for the relayer passing the correct initializer account.
claimRelayerRefundAccounts.initializer = anotherInitializer.publicKey;
- await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
// The relayer should have received funds from the vault.
const fVaultBal = (await connection.getTokenAccountBalance(vault)).value.amount;
@@ -283,7 +289,10 @@ describe("svm_spoke.refund_claims", () => {
// Should not be able to close the claim account from default wallet as the initializer was different.
try {
- await program.methods.closeClaimAccount(mint, relayer.publicKey).accounts({ signer: payer.publicKey }).rpc();
+ await program.methods
+ .closeClaimAccount()
+ .accounts({ signer: payer.publicKey, mint, refundAddress: relayer.publicKey })
+ .rpc();
assert.fail("Closing claim account from different initializer should fail");
} catch (error: any) {
assert.instanceOf(error, AnchorError);
@@ -296,8 +305,8 @@ describe("svm_spoke.refund_claims", () => {
// Close the claim account from initializer before executing relayer refunds.
await program.methods
- .closeClaimAccount(mint, relayer.publicKey)
- .accounts({ signer: claimInitializer.publicKey })
+ .closeClaimAccount()
+ .accounts({ signer: claimInitializer.publicKey, mint, refundAddress: relayer.publicKey })
.signers([claimInitializer])
.rpc();
@@ -318,8 +327,8 @@ describe("svm_spoke.refund_claims", () => {
// It should be not possible to close the claim account with non-zero refund liability.
try {
await program.methods
- .closeClaimAccount(mint, relayer.publicKey)
- .accounts({ signer: claimInitializer.publicKey })
+ .closeClaimAccount()
+ .accounts({ signer: claimInitializer.publicKey, mint, refundAddress: relayer.publicKey })
.signers([claimInitializer])
.rpc();
assert.fail("Closing claim account with non-zero refund liability should fail");
@@ -329,25 +338,50 @@ describe("svm_spoke.refund_claims", () => {
}
});
- it("Cannot claim refund on behalf of relayer to wrong token account", async () => {
+ it("Cannot claim refund on behalf of relayer to wrongly owned token account", async () => {
// Execute relayer refund using claim account.
const relayerRefund = new BN(500000);
await executeRelayerRefundToClaim(relayerRefund);
- // Claim refund for the relayer to a custom token account.
+ // Claim refund for the relayer to a custom token account owned by another authority.
const wrongOwner = Keypair.generate().publicKey;
const wrongTokenAccount = (await getOrCreateAssociatedTokenAccount(connection, payer, mint, wrongOwner)).address;
claimRelayerRefundAccounts.tokenAccount = wrongTokenAccount;
try {
- await program.methods.claimRelayerRefundFor(relayer.publicKey).accounts(claimRelayerRefundAccounts).rpc();
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
+ assert.fail("Claiming refund to custom token account should fail");
+ } catch (error: any) {
+ assert.instanceOf(error, AnchorError);
+ assert.strictEqual(
+ error.error.errorCode.code,
+ "InvalidRefundTokenAccount",
+ "Expected error code InvalidRefundTokenAccount"
+ );
+ }
+ });
+
+ it("Cannot claim refund on behalf of relayer to wrong associated token account", async () => {
+ // Execute relayer refund using claim account.
+ const relayerRefund = new BN(500000);
+ await executeRelayerRefundToClaim(relayerRefund);
+
+ // Claim refund for the relayer to a custom token account owned by the relayer, but not being its associated token account.
+ const wrongOwner = Keypair.generate();
+ const wrongTokenAccount = (await getOrCreateAssociatedTokenAccount(connection, payer, mint, wrongOwner.publicKey))
+ .address;
+ claimRelayerRefundAccounts.tokenAccount = wrongTokenAccount;
+ await setAuthority(connection, payer, wrongTokenAccount, wrongOwner, AuthorityType.AccountOwner, relayer.publicKey);
+
+ try {
+ await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
assert.fail("Claiming refund to custom token account should fail");
} catch (error: any) {
assert.instanceOf(error, AnchorError);
assert.strictEqual(
error.error.errorCode.code,
- "ConstraintTokenOwner",
- "Expected error code ConstraintTokenOwner"
+ "InvalidRefundTokenAccount",
+ "Expected error code InvalidRefundTokenAccount"
);
}
});
@@ -389,6 +423,7 @@ describe("svm_spoke.refund_claims", () => {
await executeRelayerRefundToClaim(relayerRefund);
// Claim refund for the relayer with the default signer should fail as relayer address is part of claim account derivation.
+ claimRelayerRefundAccounts.refundAddress = owner;
try {
await program.methods.claimRelayerRefund().accounts(claimRelayerRefundAccounts).rpc();
assert.fail("Claiming refund with wrong signer should fail");
diff --git a/yarn.lock b/yarn.lock
index 3ae6d88a9..188a32aa4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,10 +2,10 @@
# yarn lockfile v1
-"@across-protocol/constants@^3.1.24":
- version "3.1.24"
- resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.24.tgz#01fe49330bb467dd01813387ddbac741bc74a035"
- integrity sha512-guKtvIbif//vsmSZbwGubTWVtfkWiyWenr2sVyo63U/68GOW89ceJRLu4efLjeLVGiSrNAJtFUCv9dTwrrosWA==
+"@across-protocol/constants@^3.1.30":
+ version "3.1.30"
+ resolved "https://registry.yarnpkg.com/@across-protocol/constants/-/constants-3.1.30.tgz#b5bb82b5efcf3f63658332eece240ecdb645c0bc"
+ integrity sha512-1lEhQmYiqcMKg05fnPfSeCk9QTRaHdVykD+Wcr5tcsyPYgOMtXOXvxxvtSOe9FK+ckpRypp4ab2WUN2iitnzpw==
"@across-protocol/contracts@^0.1.4":
version "0.1.4"