From d30e5c2391f2facfadc23f4d8bc014f29f40d6bb Mon Sep 17 00:00:00 2001 From: Aung Nanda Oo Date: Thu, 20 Aug 2026 19:43:12 -0700 Subject: [PATCH 1/2] security: fix non-functional and replayable ethereum signature check in external-delegate-token-master MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent bugs in `transfer_tokens`, the one instruction this example exists to demonstrate — which has zero test coverage, exactly why both survived. 1. `verify_ethereum_signature` sliced `&pubkey_bytes[1..]` before hashing, assuming a 0x04-prefixed 65-byte key. `solana_secp256k1_recover::Secp256k1Pubkey::to_bytes()` returns the bare 64-byte X||Y with no prefix (verified against the pinned crate source), so this hashed 63 bytes starting one byte into the X-coordinate. No genuine Ethereum signature could ever pass — the instruction was dead code. 2. `message: [u8; 32]` was a free-form caller-supplied value with no cryptographic tie to `amount`, `recipient_token_account`, or a nonce — a captured signature could be replayed for any amount, to any recipient, indefinitely. The two bugs masked each other: bug 1 made the instruction unreachable, so bug 2 was latent. Fixing only the obvious byte-slice typo would have produced a working but fully replayable signature check, so both are fixed together. The digest is now derived on-chain from a domain separator, program id, both token account keys, amount, and a new per-account nonce; `message` is no longer caller-supplied. Added the transfer_tokens test coverage that never existed: a valid-signature happy path, replay-after-nonce-advance, parameter-tampering, and wrong-signer rejection. Verified bug 1 reproduces against the unpatched code with a standalone script using a genuinely-derived and correctly-signed key, and confirmed all new tests fail against the pre-fix program and pass after. Co-Authored-By: Claude Sonnet 5 --- .../anchor/package.json | 2 + .../anchor/pnpm-lock.yaml | 20 +- .../external-delegate-token-master/src/lib.rs | 77 ++++-- .../anchor/tests/litesvm.test.ts | 223 +++++++++++++++++- 4 files changed, 290 insertions(+), 32 deletions(-) diff --git a/tokens/external-delegate-token-master/anchor/package.json b/tokens/external-delegate-token-master/anchor/package.json index b364ec843..0662e922a 100644 --- a/tokens/external-delegate-token-master/anchor/package.json +++ b/tokens/external-delegate-token-master/anchor/package.json @@ -16,6 +16,8 @@ "@solana/web3.js": "^1.98.4" }, "devDependencies": { + "@noble/curves": "^1.9.7", + "@noble/hashes": "^2.2.0", "@types/bn.js": "^5.1.0", "@types/chai": "^5.2.3", "@types/mocha": "^10.0.10", diff --git a/tokens/external-delegate-token-master/anchor/pnpm-lock.yaml b/tokens/external-delegate-token-master/anchor/pnpm-lock.yaml index 50bb02794..70378550d 100644 --- a/tokens/external-delegate-token-master/anchor/pnpm-lock.yaml +++ b/tokens/external-delegate-token-master/anchor/pnpm-lock.yaml @@ -24,6 +24,12 @@ importers: specifier: ^1.98.4 version: 1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10) devDependencies: + '@noble/curves': + specifier: ^1.9.7 + version: 1.9.7 + '@noble/hashes': + specifier: ^2.2.0 + version: 2.3.0 '@types/bn.js': specifier: ^5.1.0 version: 5.2.0 @@ -255,14 +261,18 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@noble/curves@1.9.0': - resolution: {integrity: sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==} + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} engines: {node: ^14.21.3 || >=16} '@noble/hashes@1.8.0': resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} + '@noble/hashes@2.3.0': + resolution: {integrity: sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==} + engines: {node: '>= 20.19.0'} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1517,12 +1527,14 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@noble/curves@1.9.0': + '@noble/curves@1.9.7': dependencies: '@noble/hashes': 1.8.0 '@noble/hashes@1.8.0': {} + '@noble/hashes@2.3.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -2139,7 +2151,7 @@ snapshots: '@solana/web3.js@1.98.4(bufferutil@4.0.9)(typescript@5.9.3)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.27.1 - '@noble/curves': 1.9.0 + '@noble/curves': 1.9.7 '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 '@solana/codecs-numbers': 2.1.1(typescript@5.9.3) diff --git a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs index 425bf8aee..19d7917b3 100644 --- a/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs +++ b/tokens/external-delegate-token-master/anchor/programs/external-delegate-token-master/src/lib.rs @@ -14,30 +14,36 @@ pub mod external_delegate_token_master { let user_account = &mut ctx.accounts.user_account; user_account.authority = ctx.accounts.authority.key(); user_account.ethereum_address = [0; 20]; + user_account.nonce = 0; Ok(()) } - pub fn set_ethereum_address( - ctx: Context, - ethereum_address: [u8; 20], - ) -> Result<()> { + pub fn set_ethereum_address(ctx: Context, ethereum_address: [u8; 20]) -> Result<()> { let user_account = &mut ctx.accounts.user_account; user_account.ethereum_address = ethereum_address; Ok(()) } - pub fn transfer_tokens( - ctx: Context, - amount: u64, - signature: [u8; 65], - message: [u8; 32], - ) -> Result<()> { - let user_account = &ctx.accounts.user_account; + pub fn transfer_tokens(ctx: Context, amount: u64, signature: [u8; 65]) -> Result<()> { + // The digest is derived on-chain from the exact parameters that determine where funds + // move, plus a nonce, so a signature can't be replayed for a different transfer or reused + // after it's been consumed. It is signed raw (no EIP-191 prefix), so a wallet's default + // personal_sign output is intentionally not compatible. + let digest = transfer_digest( + &ctx.accounts.user_account.key(), + &ctx.accounts.user_token_account.key(), + &ctx.accounts.recipient_token_account.key(), + amount, + ctx.accounts.user_account.nonce, + ); - if !verify_ethereum_signature(&user_account.ethereum_address, &message, &signature) { + if !verify_ethereum_signature(&ctx.accounts.user_account.ethereum_address, &digest, &signature) { return Err(ErrorCode::InvalidSignature.into()); } + let user_account = &mut ctx.accounts.user_account; + user_account.nonce = user_account.nonce.checked_add(1).ok_or(ErrorCode::NonceOverflow)?; + // Transfer tokens let transfer_instruction = Transfer { from: ctx.accounts.user_token_account.to_account_info(), @@ -49,7 +55,7 @@ pub mod external_delegate_token_master { CpiContext::new_with_signer( ctx.accounts.token_program.key(), transfer_instruction, - &[&[user_account.key().as_ref(), &[ctx.bumps.user_pda]]], + &[&[ctx.accounts.user_account.key().as_ref(), &[ctx.bumps.user_pda]]], ), amount, )?; @@ -69,10 +75,7 @@ pub mod external_delegate_token_master { CpiContext::new_with_signer( ctx.accounts.token_program.key(), transfer_instruction, - &[&[ - ctx.accounts.user_account.key().as_ref(), - &[ctx.bumps.user_pda], - ]], + &[&[ctx.accounts.user_account.key().as_ref(), &[ctx.bumps.user_pda]]], ), amount, )?; @@ -83,7 +86,7 @@ pub mod external_delegate_token_master { #[derive(Accounts)] pub struct Initialize<'info> { - #[account(init, payer = authority, space = 8 + 32 + 20)] + #[account(init, payer = authority, space = 8 + 32 + 20 + 8)] // Ensure this is only for user_account pub user_account: Account<'info, UserAccount>, #[account(mut)] @@ -100,7 +103,7 @@ pub struct SetEthereumAddress<'info> { #[derive(Accounts)] pub struct TransferTokens<'info> { - #[account(has_one = authority)] + #[account(mut, has_one = authority)] pub user_account: Account<'info, UserAccount>, pub authority: Signer<'info>, #[account(mut)] @@ -136,27 +139,49 @@ pub struct AuthorityTransfer<'info> { pub struct UserAccount { pub authority: Pubkey, pub ethereum_address: [u8; 20], + pub nonce: u64, } #[error_code] pub enum ErrorCode { #[msg("Invalid Ethereum signature")] InvalidSignature, + #[msg("Nonce overflow")] + NonceOverflow, +} + +const TRANSFER_DOMAIN: &[u8] = b"external-delegate-token-master:transfer_tokens:v1"; + +/// Binds every value that determines where funds move (plus a nonce) into the digest that +/// must be Ethereum-signed, so a signature can't be replayed for a different transfer. +fn transfer_digest( + user_account: &Pubkey, + user_token_account: &Pubkey, + recipient_token_account: &Pubkey, + amount: u64, + nonce: u64, +) -> [u8; 32] { + let mut hasher = Keccak256::new(); + hasher.update(TRANSFER_DOMAIN); + hasher.update(crate::ID.as_ref()); + hasher.update(user_account.as_ref()); + hasher.update(user_token_account.as_ref()); + hasher.update(recipient_token_account.as_ref()); + hasher.update(&amount.to_le_bytes()); + hasher.update(&nonce.to_le_bytes()); + hasher.finalize().into() } -fn verify_ethereum_signature( - ethereum_address: &[u8; 20], - message: &[u8; 32], - signature: &[u8; 65], -) -> bool { +fn verify_ethereum_signature(ethereum_address: &[u8; 20], digest: &[u8; 32], signature: &[u8; 65]) -> bool { let recovery_id = signature[64]; let mut sig = [0u8; 64]; sig.copy_from_slice(&signature[..64]); - if let Ok(pubkey) = secp256k1_recover(message, recovery_id, &sig) { + if let Ok(pubkey) = secp256k1_recover(digest, recovery_id, &sig) { + // `to_bytes()` already returns the bare 64-byte X||Y with no 0x04 prefix — do not slice. let pubkey_bytes = pubkey.to_bytes(); let mut recovered_address = [0u8; 20]; - recovered_address.copy_from_slice(&keccak256(&pubkey_bytes[1..])[12..]); + recovered_address.copy_from_slice(&keccak256(&pubkey_bytes[..])[12..]); recovered_address == *ethereum_address } else { false diff --git a/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts b/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts index 3a85f7b13..ed4a96efe 100644 --- a/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts +++ b/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts @@ -1,4 +1,6 @@ import * as anchor from '@anchor-lang/core'; +import { secp256k1 } from '@noble/curves/secp256k1'; +import { keccak_256 } from '@noble/hashes/sha3.js'; import { createAssociatedTokenAccountInstruction, createInitializeMint2Instruction, @@ -10,13 +12,64 @@ import { import { Keypair, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction } from '@solana/web3.js'; import { getTokenDecoder } from '@solana-program/token'; import { LiteSVMProvider } from 'anchor-litesvm'; -import { expect } from 'chai'; +import { assert, expect } from 'chai'; import { LiteSVM } from 'litesvm'; import IDL from '../target/idl/external_delegate_token_master.json'; import type { ExternalDelegateTokenMaster } from '../target/types/external_delegate_token_master'; const PROGRAM_ID = new PublicKey(IDL.address); +// Must match `TRANSFER_DOMAIN` in lib.rs exactly. +const TRANSFER_DOMAIN = Buffer.from('external-delegate-token-master:transfer_tokens:v1', 'ascii'); + +const expectAnchorError = async (promise: Promise, code: string) => { + let caught: any; + try { + await promise; + } catch (error) { + caught = error; + } + assert.isDefined(caught, `expected the transaction to fail with ${code}`); + assert.strictEqual(caught?.error?.errorCode?.code, code, `expected ${code}, got: ${caught}`); +}; + +// Derives an Ethereum address from a raw secp256k1 private key. Note this DOES slice off the +// leading byte: @noble/curves returns an uncompressed key prefixed with 0x04, unlike +// `solana_secp256k1_recover::Secp256k1Pubkey::to_bytes()` on the Rust side, which is already +// the bare 64-byte X||Y with no prefix. Conflating the two is exactly the bug this fix corrects. +const deriveEthAddress = (privateKey: Uint8Array): Uint8Array => { + const uncompressed = secp256k1.getPublicKey(privateKey, false); + return keccak_256(uncompressed.slice(1)).slice(-20); +}; + +// Byte-for-byte identical to `transfer_digest` in lib.rs. +const buildTransferDigest = ( + userAccountKey: PublicKey, + userTokenAccountKey: PublicKey, + recipientTokenAccountKey: PublicKey, + amount: anchor.BN, + nonce: anchor.BN +): Uint8Array => + keccak_256( + Buffer.concat([ + TRANSFER_DOMAIN, + PROGRAM_ID.toBuffer(), + userAccountKey.toBuffer(), + userTokenAccountKey.toBuffer(), + recipientTokenAccountKey.toBuffer(), + amount.toArrayLike(Buffer, 'le', 8), + nonce.toArrayLike(Buffer, 'le', 8), + ]) + ); + +const signDigest = (digest: Uint8Array, privateKey: Uint8Array): Buffer => { + const sig = secp256k1.sign(digest, privateKey); + const out = Buffer.alloc(65); + Buffer.from(sig.toCompactRawBytes()).copy(out, 0); + out[64] = sig.recovery; + return out; +}; + describe('External Delegate Token Master Tests', () => { const client = new LiteSVM(); client.addProgramFromFile(PROGRAM_ID, 'target/deploy/external_delegate_token_master.so'); @@ -45,6 +98,7 @@ describe('External Delegate Token Master Tests', () => { const account = await program.account.userAccount.fetch(userAccount.publicKey); expect(account.authority.toString()).to.equal(authority.publicKey.toString()); expect(account.ethereumAddress).to.deep.equal(new Array(20).fill(0)); + expect(account.nonce.toString()).to.equal('0'); }); it('should set ethereum address', async () => { @@ -82,7 +136,7 @@ describe('External Delegate Token Master Tests', () => { createInitializeMint2Instruction(mint, 6, authority.publicKey, null), createAssociatedTokenAccountInstruction(wallet.publicKey, userTokenAccount, userPda, mint), createAssociatedTokenAccountInstruction(wallet.publicKey, recipientTokenAccount, recipient.publicKey, mint), - createMintToInstruction(mint, userTokenAccount, authority.publicKey, 1_000_000_000), + createMintToInstruction(mint, userTokenAccount, authority.publicKey, 1_000_000_000) ); await provider.sendAndConfirm!(setupTx, [mintKeypair, authority]); @@ -105,4 +159,169 @@ describe('External Delegate Token Master Tests', () => { const userBalance = getTokenDecoder().decode(client.getAccount(userTokenAccount)!.data).amount; expect(userBalance).to.equal(BigInt(1_000_000_000 - 250)); }); + + describe('transferTokens (Ethereum-signature-gated)', () => { + const ethPrivateKey = secp256k1.utils.randomSecretKey(); + const otherEthPrivateKey = secp256k1.utils.randomSecretKey(); + let mint: PublicKey; + let userTokenAccount: PublicKey; + let recipientTokenAccount: PublicKey; + let happyPathSignature: Buffer; + let happyPathAmount: anchor.BN; + + before(async () => { + await program.methods + .setEthereumAddress(Array.from(deriveEthAddress(ethPrivateKey))) + .accountsPartial({ userAccount: userAccount.publicKey, authority: authority.publicKey }) + .signers([authority]) + .rpc(); + + const mintKeypair = Keypair.generate(); + mint = mintKeypair.publicKey; + userTokenAccount = getAssociatedTokenAddressSync(mint, userPda, true); + const recipient = Keypair.generate(); + recipientTokenAccount = getAssociatedTokenAddressSync(mint, recipient.publicKey); + + const lamports = await provider.connection.getMinimumBalanceForRentExemption(MINT_SIZE); + const setupTx = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint, + space: MINT_SIZE, + lamports, + programId: TOKEN_PROGRAM_ID, + }), + createInitializeMint2Instruction(mint, 6, authority.publicKey, null), + createAssociatedTokenAccountInstruction(wallet.publicKey, userTokenAccount, userPda, mint), + createAssociatedTokenAccountInstruction( + wallet.publicKey, + recipientTokenAccount, + recipient.publicKey, + mint + ), + createMintToInstruction(mint, userTokenAccount, authority.publicKey, 1_000_000_000) + ); + await provider.sendAndConfirm!(setupTx, [mintKeypair, authority]); + }); + + it('transfers tokens with a valid, correctly-bound ethereum signature', async () => { + const { nonce } = await program.account.userAccount.fetch(userAccount.publicKey); + const amount = new anchor.BN(300); + const digest = buildTransferDigest( + userAccount.publicKey, + userTokenAccount, + recipientTokenAccount, + amount, + nonce + ); + const signature = signDigest(digest, ethPrivateKey); + + await program.methods + .transferTokens(amount, Array.from(signature)) + .accountsPartial({ + userAccount: userAccount.publicKey, + authority: authority.publicKey, + userTokenAccount, + recipientTokenAccount, + userPda, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([authority]) + .rpc(); + + const recipientBalance = getTokenDecoder().decode(client.getAccount(recipientTokenAccount)!.data).amount; + expect(recipientBalance).to.equal(BigInt(300)); + const senderBalance = getTokenDecoder().decode(client.getAccount(userTokenAccount)!.data).amount; + expect(senderBalance).to.equal(BigInt(1_000_000_000 - 300)); + + const account = await program.account.userAccount.fetch(userAccount.publicKey); + expect(account.nonce.toString()).to.equal('1'); + + // Kept for the replay test below — must be the exact bytes that were consumed here. + happyPathSignature = signature; + happyPathAmount = amount; + }); + + it('rejects the same signature replayed after the nonce has advanced', async () => { + // Without this, the resubmitted transaction is byte-identical to the one already + // processed in the happy-path test and gets rejected as a duplicate at the runtime + // level (before program logic runs) rather than exercising the on-chain nonce check. + client.expireBlockhash(); + + await expectAnchorError( + program.methods + .transferTokens(happyPathAmount, Array.from(happyPathSignature)) + .accountsPartial({ + userAccount: userAccount.publicKey, + authority: authority.publicKey, + userTokenAccount, + recipientTokenAccount, + userPda, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([authority]) + .rpc(), + 'InvalidSignature' + ); + }); + + it('rejects a valid signature submitted with a different amount', async () => { + const { nonce } = await program.account.userAccount.fetch(userAccount.publicKey); + const signedAmount = new anchor.BN(100); + const digest = buildTransferDigest( + userAccount.publicKey, + userTokenAccount, + recipientTokenAccount, + signedAmount, + nonce + ); + const signature = signDigest(digest, ethPrivateKey); + const submittedAmount = new anchor.BN(999); + + await expectAnchorError( + program.methods + .transferTokens(submittedAmount, Array.from(signature)) + .accountsPartial({ + userAccount: userAccount.publicKey, + authority: authority.publicKey, + userTokenAccount, + recipientTokenAccount, + userPda, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([authority]) + .rpc(), + 'InvalidSignature' + ); + }); + + it('rejects a correctly-bound digest signed by an unregistered ethereum key', async () => { + const { nonce } = await program.account.userAccount.fetch(userAccount.publicKey); + const amount = new anchor.BN(100); + const digest = buildTransferDigest( + userAccount.publicKey, + userTokenAccount, + recipientTokenAccount, + amount, + nonce + ); + const signature = signDigest(digest, otherEthPrivateKey); + + await expectAnchorError( + program.methods + .transferTokens(amount, Array.from(signature)) + .accountsPartial({ + userAccount: userAccount.publicKey, + authority: authority.publicKey, + userTokenAccount, + recipientTokenAccount, + userPda, + tokenProgram: TOKEN_PROGRAM_ID, + }) + .signers([authority]) + .rpc(), + 'InvalidSignature' + ); + }); + }); }); From e5300bae7a4cf4c53560abe527999c566b66b2db Mon Sep 17 00:00:00 2001 From: Aung Nanda Oo Date: Thu, 20 Aug 2026 19:49:10 -0700 Subject: [PATCH 2/2] style: match root prettier config in external-delegate-token-master tests Root CI's prettier check uses @solana/prettier-config-solana via prettier 3.x (trailingComma: all), which differs from this project's own pinned prettier 2.x default. Whitespace only, no logic change. Co-Authored-By: Claude Sonnet 5 --- .../anchor/tests/litesvm.test.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts b/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts index ed4a96efe..57d445cfd 100644 --- a/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts +++ b/tokens/external-delegate-token-master/anchor/tests/litesvm.test.ts @@ -48,7 +48,7 @@ const buildTransferDigest = ( userTokenAccountKey: PublicKey, recipientTokenAccountKey: PublicKey, amount: anchor.BN, - nonce: anchor.BN + nonce: anchor.BN, ): Uint8Array => keccak_256( Buffer.concat([ @@ -59,7 +59,7 @@ const buildTransferDigest = ( recipientTokenAccountKey.toBuffer(), amount.toArrayLike(Buffer, 'le', 8), nonce.toArrayLike(Buffer, 'le', 8), - ]) + ]), ); const signDigest = (digest: Uint8Array, privateKey: Uint8Array): Buffer => { @@ -136,7 +136,7 @@ describe('External Delegate Token Master Tests', () => { createInitializeMint2Instruction(mint, 6, authority.publicKey, null), createAssociatedTokenAccountInstruction(wallet.publicKey, userTokenAccount, userPda, mint), createAssociatedTokenAccountInstruction(wallet.publicKey, recipientTokenAccount, recipient.publicKey, mint), - createMintToInstruction(mint, userTokenAccount, authority.publicKey, 1_000_000_000) + createMintToInstruction(mint, userTokenAccount, authority.publicKey, 1_000_000_000), ); await provider.sendAndConfirm!(setupTx, [mintKeypair, authority]); @@ -197,9 +197,9 @@ describe('External Delegate Token Master Tests', () => { wallet.publicKey, recipientTokenAccount, recipient.publicKey, - mint + mint, ), - createMintToInstruction(mint, userTokenAccount, authority.publicKey, 1_000_000_000) + createMintToInstruction(mint, userTokenAccount, authority.publicKey, 1_000_000_000), ); await provider.sendAndConfirm!(setupTx, [mintKeypair, authority]); }); @@ -212,7 +212,7 @@ describe('External Delegate Token Master Tests', () => { userTokenAccount, recipientTokenAccount, amount, - nonce + nonce, ); const signature = signDigest(digest, ethPrivateKey); @@ -261,7 +261,7 @@ describe('External Delegate Token Master Tests', () => { }) .signers([authority]) .rpc(), - 'InvalidSignature' + 'InvalidSignature', ); }); @@ -273,7 +273,7 @@ describe('External Delegate Token Master Tests', () => { userTokenAccount, recipientTokenAccount, signedAmount, - nonce + nonce, ); const signature = signDigest(digest, ethPrivateKey); const submittedAmount = new anchor.BN(999); @@ -291,7 +291,7 @@ describe('External Delegate Token Master Tests', () => { }) .signers([authority]) .rpc(), - 'InvalidSignature' + 'InvalidSignature', ); }); @@ -303,7 +303,7 @@ describe('External Delegate Token Master Tests', () => { userTokenAccount, recipientTokenAccount, amount, - nonce + nonce, ); const signature = signDigest(digest, otherEthPrivateKey); @@ -320,7 +320,7 @@ describe('External Delegate Token Master Tests', () => { }) .signers([authority]) .rpc(), - 'InvalidSignature' + 'InvalidSignature', ); }); });