diff --git a/go/mechanisms/svm/constants.go b/go/mechanisms/svm/constants.go index 764b8a59cc..9640c0dbb0 100644 --- a/go/mechanisms/svm/constants.go +++ b/go/mechanisms/svm/constants.go @@ -23,6 +23,14 @@ const ( // DefaultComputeUnitLimit is the default compute unit limit for transactions DefaultComputeUnitLimit uint32 = 8000 + // LighthouseProgramAddress is the Phantom/Solflare Lighthouse program address + // Phantom and Solflare wallets inject Lighthouse instructions for user protection on mainnet transactions. + // - Phantom adds 1 Lighthouse instruction (4th instruction) + // - Solflare adds 2 Lighthouse instructions (4th and 5th instructions) + // We allow these as optional instructions to support these wallets. + // See: https://github.com/coinbase/x402/issues/828 + LighthouseProgramAddress = "L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95" + // DefaultCommitment is the default commitment level for transactions DefaultCommitment = rpc.CommitmentConfirmed diff --git a/go/mechanisms/svm/exact/facilitator/errors.go b/go/mechanisms/svm/exact/facilitator/errors.go index 3cd4119a96..22cdfbaa1f 100644 --- a/go/mechanisms/svm/exact/facilitator/errors.go +++ b/go/mechanisms/svm/exact/facilitator/errors.go @@ -10,6 +10,8 @@ const ( ErrInvalidPayloadTransaction = "invalid_exact_solana_payload_transaction" ErrTransactionCouldNotBeDecoded = "invalid_exact_solana_payload_transaction_could_not_be_decoded" ErrTransactionInstructionsLength = "invalid_exact_solana_payload_transaction_instructions_length" + ErrUnknownFourthInstruction = "invalid_exact_solana_payload_unknown_fourth_instruction" + ErrUnknownFifthInstruction = "invalid_exact_solana_payload_unknown_fifth_instruction" ErrComputeLimitInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_limit_instruction" ErrComputePriceInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction" ErrComputePriceInstructionTooHigh = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction_too_high" diff --git a/go/mechanisms/svm/exact/facilitator/scheme.go b/go/mechanisms/svm/exact/facilitator/scheme.go index 384fbc0b57..2cd5619e3c 100644 --- a/go/mechanisms/svm/exact/facilitator/scheme.go +++ b/go/mechanisms/svm/exact/facilitator/scheme.go @@ -120,8 +120,13 @@ func (f *ExactSvmScheme) Verify( return nil, x402.NewVerifyError(ErrTransactionCouldNotBeDecoded, "", network, err) } - // 3 instructions: ComputeLimit + ComputePrice + TransferChecked - if len(tx.Message.Instructions) != 3 { + // Allow 3, 4, or 5 instructions: + // - 3 instructions: ComputeLimit + ComputePrice + TransferChecked + // - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection) + // - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection) + // See: https://github.com/coinbase/x402/issues/828 + numInstructions := len(tx.Message.Instructions) + if numInstructions < 3 || numInstructions > 5 { return nil, x402.NewVerifyError(ErrTransactionInstructionsLength, "", network, nil) } @@ -158,7 +163,26 @@ func (f *ExactSvmScheme) Verify( return nil, x402.NewVerifyError(err.Error(), payer, network, err) } - // Step 5: Sign and Simulate Transaction + // Step 5: Verify Lighthouse Instructions (if present) + // - 4th instruction: Lighthouse program (Phantom wallet protection) + // - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions) + if numInstructions >= 4 { + fourthProgID := tx.Message.AccountKeys[tx.Message.Instructions[3].ProgramIDIndex] + lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress) + if !fourthProgID.Equals(lighthousePubkey) { + return nil, x402.NewVerifyError(ErrUnknownFourthInstruction, payer, network, nil) + } + } + + if numInstructions == 5 { + fifthProgID := tx.Message.AccountKeys[tx.Message.Instructions[4].ProgramIDIndex] + lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress) + if !fifthProgID.Equals(lighthousePubkey) { + return nil, x402.NewVerifyError(ErrUnknownFifthInstruction, payer, network, nil) + } + } + + // Step 6: Sign and Simulate Transaction // CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc) // feePayer already validated in Step 1 diff --git a/go/mechanisms/svm/exact/v1/facilitator/errors.go b/go/mechanisms/svm/exact/v1/facilitator/errors.go index ee491a0928..1b9370c354 100644 --- a/go/mechanisms/svm/exact/v1/facilitator/errors.go +++ b/go/mechanisms/svm/exact/v1/facilitator/errors.go @@ -11,6 +11,8 @@ const ( ErrInvalidPayloadTransaction = "invalid_exact_solana_payload_transaction" ErrTransactionCouldNotBeDecoded = "invalid_exact_solana_payload_transaction_could_not_be_decoded" ErrTransactionInstructionsLength = "invalid_exact_solana_payload_transaction_instructions_length" + ErrUnknownFourthInstruction = "invalid_exact_solana_payload_unknown_fourth_instruction" + ErrUnknownFifthInstruction = "invalid_exact_solana_payload_unknown_fifth_instruction" ErrComputeLimitInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_limit_instruction" ErrComputePriceInstruction = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction" ErrComputePriceInstructionTooHigh = "invalid_exact_solana_payload_transaction_instructions_compute_price_instruction_too_high" diff --git a/go/mechanisms/svm/exact/v1/facilitator/scheme.go b/go/mechanisms/svm/exact/v1/facilitator/scheme.go index 7550d3e724..a6d4815abd 100644 --- a/go/mechanisms/svm/exact/v1/facilitator/scheme.go +++ b/go/mechanisms/svm/exact/v1/facilitator/scheme.go @@ -130,8 +130,13 @@ func (f *ExactSvmSchemeV1) Verify( return nil, x402.NewVerifyError(ErrTransactionCouldNotBeDecoded, "", network, err) } - // 3 instructions: ComputeLimit + ComputePrice + TransferChecked - if len(tx.Message.Instructions) != 3 { + // Allow 3, 4, or 5 instructions: + // - 3 instructions: ComputeLimit + ComputePrice + TransferChecked + // - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection) + // - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection) + // See: https://github.com/coinbase/x402/issues/828 + numInstructions := len(tx.Message.Instructions) + if numInstructions < 3 || numInstructions > 5 { return nil, x402.NewVerifyError(ErrTransactionInstructionsLength, "", network, nil) } @@ -155,7 +160,26 @@ func (f *ExactSvmSchemeV1) Verify( return nil, x402.NewVerifyError(err.Error(), payer, network, err) } - // Step 5: Sign and Simulate Transaction + // Step 5: Verify Lighthouse Instructions (if present) + // - 4th instruction: Lighthouse program (Phantom wallet protection) + // - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions) + if numInstructions >= 4 { + fourthProgID := tx.Message.AccountKeys[tx.Message.Instructions[3].ProgramIDIndex] + lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress) + if !fourthProgID.Equals(lighthousePubkey) { + return nil, x402.NewVerifyError(ErrUnknownFourthInstruction, payer, network, nil) + } + } + + if numInstructions == 5 { + fifthProgID := tx.Message.AccountKeys[tx.Message.Instructions[4].ProgramIDIndex] + lighthousePubkey := solana.MustPublicKeyFromBase58(svm.LighthouseProgramAddress) + if !fifthProgID.Equals(lighthousePubkey) { + return nil, x402.NewVerifyError(ErrUnknownFifthInstruction, payer, network, nil) + } + } + + // Step 6: Sign and Simulate Transaction // CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc) // feePayer already validated in Step 1 diff --git a/specs/schemes/exact/scheme_exact_svm.md b/specs/schemes/exact/scheme_exact_svm.md index b665ec79d0..bdf1e5e235 100644 --- a/specs/schemes/exact/scheme_exact_svm.md +++ b/specs/schemes/exact/scheme_exact_svm.md @@ -106,10 +106,16 @@ A facilitator verifying an `exact`-scheme SVM payment MUST enforce all of the fo 1. Instruction layout -- The decompiled transaction MUST contain 3 instructions in this exact order: +- The decompiled transaction MUST contain 3 to 5 instructions in this order: 1. Compute Budget: Set Compute Unit Limit 2. Compute Budget: Set Compute Unit Price - 4. SPL Token or Token-2022 TransferChecked + 3. SPL Token or Token-2022 TransferChecked + 4. (Optional) Lighthouse program instruction (Phantom wallet protection) + 5. (Optional) Lighthouse program instruction (Solflare wallet protection) + +- If a 4th or 5th instruction is present, the program MUST be the Lighthouse program (`L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95`). +- Phantom wallet injects 1 Lighthouse instruction; Solflare injects 2. +- These Lighthouse instructions are wallet-injected user protection mechanisms and MUST be allowed to support these wallets. 2. Fee payer (facilitator) safety diff --git a/typescript/packages/mechanisms/svm/src/constants.ts b/typescript/packages/mechanisms/svm/src/constants.ts index c32d1f1075..fa576c55bb 100644 --- a/typescript/packages/mechanisms/svm/src/constants.ts +++ b/typescript/packages/mechanisms/svm/src/constants.ts @@ -6,6 +6,16 @@ export const TOKEN_PROGRAM_ADDRESS = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5D export const TOKEN_2022_PROGRAM_ADDRESS = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"; export const COMPUTE_BUDGET_PROGRAM_ADDRESS = "ComputeBudget111111111111111111111111111111"; +/** + * Phantom/Solflare Lighthouse program address + * Phantom and Solflare wallets inject Lighthouse instructions for user protection on mainnet transactions. + * - Phantom adds 1 Lighthouse instruction (4th instruction) + * - Solflare adds 2 Lighthouse instructions (4th and 5th instructions) + * We allow these as optional instructions to support these wallets. + * See: https://github.com/coinbase/x402/issues/828 + */ +export const LIGHTHOUSE_PROGRAM_ADDRESS = "L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95"; + /** * Default RPC URLs for Solana networks */ diff --git a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts index 17ce3da616..b0733eacab 100644 --- a/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts @@ -24,7 +24,7 @@ import type { SettleResponse, VerifyResponse, } from "@x402/core/types"; -import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS } from "../../constants"; +import { LIGHTHOUSE_PROGRAM_ADDRESS, MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS } from "../../constants"; import type { FacilitatorSvmSigner } from "../../signer"; import type { ExactSvmPayloadV2 } from "../../types"; import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../utils"; @@ -137,8 +137,12 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { const decompiled = decompileTransactionMessage(compiled); const instructions = decompiled.instructions ?? []; - // 3 instructions: ComputeLimit + ComputePrice + TransferChecked - if (instructions.length !== 3) { + // Allow 3, 4, or 5 instructions: + // - 3 instructions: ComputeLimit + ComputePrice + TransferChecked + // - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection) + // - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection) + // See: https://github.com/coinbase/x402/issues/828 + if (instructions.length < 3 || instructions.length > 5) { return { isValid: false, invalidReason: "invalid_exact_svm_payload_transaction_instructions_length", @@ -257,7 +261,34 @@ export class ExactSvmScheme implements SchemeNetworkFacilitator { }; } - // Step 5: Sign and Simulate Transaction + // Step 5: Verify Lighthouse Instructions (if present) + // - 4th instruction: Lighthouse program (Phantom wallet protection) + // - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions) + if (instructions.length >= 4) { + const fourthInstruction = instructions[3]; + const fourthProgramAddress = fourthInstruction.programAddress.toString(); + if (fourthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) { + return { + isValid: false, + invalidReason: "invalid_exact_svm_payload_unknown_fourth_instruction", + payer, + }; + } + } + + if (instructions.length === 5) { + const fifthInstruction = instructions[4]; + const fifthProgramAddress = fifthInstruction.programAddress.toString(); + if (fifthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) { + return { + isValid: false, + invalidReason: "invalid_exact_svm_payload_unknown_fifth_instruction", + payer, + }; + } + } + + // Step 6: Sign and Simulate Transaction // CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc) try { const feePayer = requirements.extra.feePayer as Address; diff --git a/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts b/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts index 40d27d79cd..1fd0289b28 100644 --- a/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts +++ b/typescript/packages/mechanisms/svm/src/exact/v1/facilitator/scheme.ts @@ -25,7 +25,10 @@ import type { VerifyResponse, } from "@x402/core/types"; import type { PaymentPayloadV1, PaymentRequirementsV1 } from "@x402/core/types/v1"; -import { MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS } from "../../../constants"; +import { + LIGHTHOUSE_PROGRAM_ADDRESS, + MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS, +} from "../../../constants"; import type { FacilitatorSvmSigner } from "../../../signer"; import type { ExactSvmPayloadV1 } from "../../../types"; import { decodeTransactionFromPayload, getTokenPayerFromTransaction } from "../../../utils"; @@ -140,8 +143,12 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { const decompiled = decompileTransactionMessage(compiled); const instructions = decompiled.instructions ?? []; - // 3 instructions: ComputeLimit + ComputePrice + TransferChecked - if (instructions.length !== 3) { + // Allow 3, 4, or 5 instructions: + // - 3 instructions: ComputeLimit + ComputePrice + TransferChecked + // - 4 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse (Phantom wallet protection) + // - 5 instructions: ComputeLimit + ComputePrice + TransferChecked + Lighthouse + Lighthouse (Solflare wallet protection) + // See: https://github.com/coinbase/x402/issues/828 + if (instructions.length < 3 || instructions.length > 5) { return { isValid: false, invalidReason: "invalid_exact_svm_payload_transaction_instructions_length", @@ -260,7 +267,34 @@ export class ExactSvmSchemeV1 implements SchemeNetworkFacilitator { }; } - // Step 5: Sign and Simulate Transaction + // Step 5: Verify Lighthouse Instructions (if present) + // - 4th instruction: Lighthouse program (Phantom wallet protection) + // - 5th instruction: Lighthouse program (Solflare wallet adds 2 Lighthouse instructions) + if (instructions.length >= 4) { + const fourthInstruction = instructions[3]; + const fourthProgramAddress = fourthInstruction.programAddress.toString(); + if (fourthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) { + return { + isValid: false, + invalidReason: "invalid_exact_svm_payload_unknown_fourth_instruction", + payer, + }; + } + } + + if (instructions.length === 5) { + const fifthInstruction = instructions[4]; + const fifthProgramAddress = fifthInstruction.programAddress.toString(); + if (fifthProgramAddress !== LIGHTHOUSE_PROGRAM_ADDRESS) { + return { + isValid: false, + invalidReason: "invalid_exact_svm_payload_unknown_fifth_instruction", + payer, + }; + } + } + + // Step 6: Sign and Simulate Transaction // CRITICAL: Simulation proves transaction will succeed (catches insufficient balance, invalid accounts, etc) try { const feePayer = requirementsV1.extra.feePayer as Address;