diff --git a/programs/svm-spoke/src/instructions/admin.rs b/programs/svm-spoke/src/instructions/admin.rs index 71f3fd92..cf35de45 100644 --- a/programs/svm-spoke/src/instructions/admin.rs +++ b/programs/svm-spoke/src/instructions/admin.rs @@ -176,7 +176,11 @@ pub struct SetEnableRoute<'info> { )] pub vault: InterfaceAccount<'info, TokenAccount>, - #[account(mint::token_program = token_program)] + #[account( + mint::token_program = token_program, + // IDL build fails when requiring `address = input_token` for mint, thus using a custom constraint. + constraint = origin_token_mint.key() == origin_token.into() @ CustomError::InvalidMint + )] pub origin_token_mint: InterfaceAccount<'info, Mint>, pub token_program: Interface<'info, TokenInterface>, diff --git a/test/svm/SvmSpoke.HandleReceiveMessage.ts b/test/svm/SvmSpoke.HandleReceiveMessage.ts index 07566999..e18a2f53 100644 --- a/test/svm/SvmSpoke.HandleReceiveMessage.ts +++ b/test/svm/SvmSpoke.HandleReceiveMessage.ts @@ -310,7 +310,7 @@ describe("svm_spoke.handle_receive_message", () => { it("Enables and disables route remotely", async () => { // Enable the route. - const originToken = Keypair.generate().publicKey; + const originToken = await createMint(provider.connection, provider.wallet.payer, owner, owner, 6); const routeChainId = 1; let calldata = ethereumIface.encodeFunctionData("setEnableRoute", [originToken.toBuffer(), routeChainId, true]); let messageBody = Buffer.from(calldata.slice(2), "hex"); @@ -327,8 +327,7 @@ describe("svm_spoke.handle_receive_message", () => { // Remaining accounts specific to SetEnableRoute. const routePda = createRoutePda(originToken, new anchor.BN(routeChainId)); - const tokenMint = await createMint(provider.connection, provider.wallet.payer, owner, owner, 6); - const vault = getVaultAta(tokenMint, state); + const vault = getVaultAta(originToken, state); // Same 3 remaining accounts passed for HandleReceiveMessage context. const enableRouteRemainingAccounts = remainingAccounts.slice(0, 3); // payer in self-invoked SetEnableRoute. @@ -359,7 +358,7 @@ describe("svm_spoke.handle_receive_message", () => { enableRouteRemainingAccounts.push({ isSigner: false, isWritable: false, - pubkey: tokenMint, + pubkey: originToken, }); // token_program in self-invoked SetEnableRoute. enableRouteRemainingAccounts.push({ diff --git a/test/svm/SvmSpoke.Routes.ts b/test/svm/SvmSpoke.Routes.ts index c0b821e4..07143ada 100644 --- a/test/svm/SvmSpoke.Routes.ts +++ b/test/svm/SvmSpoke.Routes.ts @@ -12,28 +12,25 @@ describe("svm_spoke.routes", () => { anchor.setProvider(provider); const nonOwner = Keypair.generate(); - let state: PublicKey, tokenMint: PublicKey; + let state: PublicKey, tokenMint: PublicKey, routePda: PublicKey, vault: PublicKey; + let routeChainId: BN; + let setEnableRouteAccounts: any; - before("Creates token mint and associated token accounts", async () => { - tokenMint = await createMint(provider.connection, provider.wallet.payer, owner, owner, 6); - }); + before("Creates token mint and associated token accounts", async () => {}); beforeEach(async () => { state = await initializeState(); - }); - - it("Sets, retrieves, and controls access to route enablement", async () => { - const originToken = Keypair.generate().publicKey; - const routeChainId = new BN(1); + tokenMint = await createMint(provider.connection, provider.wallet.payer, owner, owner, 6); // Create a PDA for the route - const routePda = createRoutePda(originToken, routeChainId); + routeChainId = new BN(1); + routePda = createRoutePda(tokenMint, routeChainId); // Create ATA for the origin token to be stored by state (vault). - const vault = getVaultAta(tokenMint, state); + vault = getVaultAta(tokenMint, state); // Common accounts object - const accounts = { + setEnableRouteAccounts = { signer: owner, payer: owner, state, @@ -44,9 +41,14 @@ describe("svm_spoke.routes", () => { associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, systemProgram: anchor.web3.SystemProgram.programId, }; + }); + it("Sets, retrieves, and controls access to route enablement", async () => { // Enable the route as owner - await program.methods.setEnableRoute(originToken.toBytes(), routeChainId, true).accounts(accounts).rpc(); + await program.methods + .setEnableRoute(tokenMint.toBytes(), routeChainId, true) + .accounts(setEnableRouteAccounts) + .rpc(); await new Promise((resolve) => setTimeout(resolve, 500)); // Retrieve and verify the route is enabled @@ -58,12 +60,15 @@ describe("svm_spoke.routes", () => { (event) => event.name === "enabledDepositRoute" ); let event = events[0].data; - assert.strictEqual(event.originToken.toString(), originToken.toString(), "originToken event match"); + assert.strictEqual(event.originToken.toString(), tokenMint.toString(), "originToken event match"); assert.strictEqual(event.destinationChainId.toString(), routeChainId.toString(), "destinationChainId should match"); assert.isTrue(event.enabled, "enabledDepositRoute enabled"); // Disable the route as owner - await program.methods.setEnableRoute(originToken.toBytes(), routeChainId, false).accounts(accounts).rpc(); + await program.methods + .setEnableRoute(tokenMint.toBytes(), routeChainId, false) + .accounts(setEnableRouteAccounts) + .rpc(); await new Promise((resolve) => setTimeout(resolve, 500)); // Retrieve and verify the route is disabled @@ -75,15 +80,15 @@ describe("svm_spoke.routes", () => { (event) => event.name === "enabledDepositRoute" ); event = events[0].data; // take most recent event, index 0. - assert.strictEqual(event.originToken.toString(), originToken.toString(), "originToken event match"); + assert.strictEqual(event.originToken.toString(), tokenMint.toString(), "originToken event match"); assert.strictEqual(event.destinationChainId.toString(), routeChainId.toString(), "destinationChainId should match"); assert.isFalse(event.enabled, "enabledDepositRoute disabled"); // Try to enable the route as non-owner try { await program.methods - .setEnableRoute(originToken.toBytes(), routeChainId, true) - .accounts({ ...accounts, signer: nonOwner.publicKey }) + .setEnableRoute(tokenMint.toBytes(), routeChainId, true) + .accounts({ ...setEnableRouteAccounts, signer: nonOwner.publicKey }) .signers([nonOwner]) .rpc(); assert.fail("Non-owner should not be able to set route enablement"); @@ -103,4 +108,20 @@ describe("svm_spoke.routes", () => { const stateAccount = await program.account.state.fetch(state); assert.strictEqual(stateAccount.owner.toBase58(), owner.toBase58(), "State owner should be the expected owner"); }); + + it("Cannot misconfigure route with wrong origin token", async () => { + const wrongOriginToken = Keypair.generate().publicKey; + const wrongRoutePda = createRoutePda(wrongOriginToken, routeChainId); + + try { + await program.methods + .setEnableRoute(wrongOriginToken.toBytes(), routeChainId, true) + .accounts({ ...setEnableRouteAccounts, route: wrongRoutePda }) + .rpc(); + assert.fail("Setting route with wrong origin token should fail"); + } catch (err) { + assert.instanceOf(err, anchor.AnchorError); + assert.strictEqual(err.error.errorCode.code, "InvalidMint", "Expected error code InvalidMint"); + } + }); });