Skip to content

fix: mark fee payer as signer in accounts list before hash computation#114

Merged
tracy-codes merged 1 commit intomainfrom
fix/issue-107-fee-payer-signer-mismatch
Mar 2, 2026
Merged

fix: mark fee payer as signer in accounts list before hash computation#114
tracy-codes merged 1 commit intomainfrom
fix/issue-107-fee-payer-signer-mismatch

Conversation

@tracy-codes
Copy link
Contributor

@tracy-codes tracy-codes commented Mar 2, 2026

Summary

Fixes #107 — Transaction simulation fails with custom program error: 0xbd2 (PermissionDeniedSecp256r1InvalidMessageHash) when a secp256r1/secp256k1-signed Swig wallet transaction includes transfer instructions where one sends SOL back to the fee payer.

Root Cause

The Solana runtime always marks the transaction fee payer as is_signer=true on all AccountInfos in every instruction. When the fee payer appears as a destination in inner instructions (e.g., system_instruction::transfer back to the payer), compactInstructions() adds it to the accounts list with is_signer=false (since it's just a recipient). The on-chain program then computes the message hash with is_signer=true for the payer, but the client computed it with is_signer=false, causing the keccak256 hash to differ.

Fix

The payer public key already flows from SwigOptions through the call chain to the secp256r1/secp256k1 signing functions via InstructionDataOptions. The fix:

  1. Declares payer on InstructionDataOptions so it can be accessed type-safely in the signing functions
  2. In secp256r1.ts and secp256k1.ts signV2Instruction: after compactInstructions() returns and before hash computation, finds the payer in the account metas and marks it as isSigner=true

This is the minimal-surface fix — only 3 source files changed, no modifications to compactInstructions, swig/index.ts, token authority classes, or any other shared code.

Usage

No code changes are required for users of getSignInstructionContext or getSignInstructions. The payer option was already part of SwigOptions and already being passed through the call chain — this fix simply makes the signing functions aware of it.

The following pattern that previously failed with 0xbd2 now works correctly:

import {
  getSignInstructionContext,
  SolInstruction,
} from '@swig-wallet/lib';

// Transfer 1: swig wallet -> some recipient
const transfer1 = getTransferSolInstruction({
  source: swigWalletAddress,
  destination: recipientAddress,
  amount: 100_000_000n,
});

// Transfer 2: swig wallet -> fee payer (previously caused 0xbd2)
const transfer2 = getTransferSolInstruction({
  source: swigWalletAddress,
  destination: payerAddress,  // <-- sending back to the fee payer
  amount: 50_000_000n,
});

const signIx = await getSignInstructionContext(
  swig,
  roleId,
  [transfer1, transfer2].map(SolInstruction.from),
  false,
  {
    payer: payer.publicKey,     // already required by existing API
    currentSlot: slot,
    signingFn: authority.signingFn,
  },
);

For users of the lower-level Secp256r1Instruction.signV2Instruction or Secp256k1Instruction.signV2Instruction directly, pass payer in the options parameter:

const ctx = await Secp256r1Instruction.signV2Instruction(
  { swig: swigAddress, swigSystemAddress },
  { authorityData, innerInstructions, roleId },
  {
    signingFn,
    currentSlot,
    odometer,
    payer: payerPublicKey,   // <-- new: ensures correct hash when payer is in inner instructions
  },
);

Files Changed

File Change
packages/lib/src/authority/instructions/interface.ts Added optional payer to InstructionDataOptions type
packages/lib/src/authority/instructions/secp256r1.ts Mark payer as signer in metas after compactInstructions(), before hash
packages/lib/src/authority/instructions/secp256k1.ts Same fix
packages/lib/tests/instructions/sign.test.ts 2 regression tests (secp256r1 + secp256k1)

Testing

@tracy-codes tracy-codes force-pushed the fix/issue-107-fee-payer-signer-mismatch branch 2 times, most recently from 1fdcf96 to 9dc6587 Compare March 2, 2026 07:10
…p256r1/k1

When the fee payer appears as a destination in inner instructions (e.g.,
transferring SOL back to the payer), it gets added to the accounts list
with is_signer=false. However, the Solana runtime always marks the fee
payer as is_signer=true on all AccountInfos. This caused a message hash
mismatch (error 0xbd2 / PermissionDeniedSecp256r1InvalidMessageHash).

The fix adds a payer field to InstructionDataOptions (which already flows
from SwigOptions through the call chain) and uses it in the signV2Instruction
methods of secp256r1.ts and secp256k1.ts to mark the payer as signer in the
account metas after compactInstructions() returns, before the hash is computed.

Fixes: #107
@tracy-codes tracy-codes force-pushed the fix/issue-107-fee-payer-signer-mismatch branch from 9dc6587 to 11b4abf Compare March 2, 2026 07:14
@tracy-codes tracy-codes merged commit 77cb698 into main Mar 2, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Transaction simulation fails with custom program error: 0xbd2

1 participant