Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions go/mechanisms/svm/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions go/mechanisms/svm/exact/facilitator/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 27 additions & 3 deletions go/mechanisms/svm/exact/facilitator/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go/mechanisms/svm/exact/v1/facilitator/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
30 changes: 27 additions & 3 deletions go/mechanisms/svm/exact/v1/facilitator/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions specs/schemes/exact/scheme_exact_svm.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions typescript/packages/mechanisms/svm/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
39 changes: 35 additions & 4 deletions typescript/packages/mechanisms/svm/src/exact/facilitator/scheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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;
Expand Down
Loading