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
44 changes: 44 additions & 0 deletions apps/dapp/lib/__tests__/solana-signed-checks-guarded.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { join, relative } from 'node:path'

import { describe, expect, it } from 'vitest'

const ROOT = join(__dirname, '..', '..')

function sources(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
const path = join(dir, name)
if (statSync(path).isDirectory()) {
return name === 'node_modules' || name === '__tests__' || name.startsWith('.')
? []
: sources(path)
}
return /\.tsx?$/.test(name) ? [path] : []
})
}

/** Signed Solana bytes handed to a check as they came from the wallet, guards and all. */
const RAW_SIGNED = /transaction:\s*[\w.]*\bsignedTransaction\b/

describe('every check of a signed Solana transaction', () => {
it('first leaves out the wallet’s own Lighthouse guards', () => {
// Phantom adds them before signing. A check that reads the raw bytes refuses every Phantom user,
// as the cash-out deposit did on 6 November after the chain submit route was fixed alone.
const raw = [join(ROOT, 'app'), join(ROOT, 'lib')]
.flatMap(sources)
.filter((file) => RAW_SIGNED.test(readFileSync(file, 'utf8')))
.map((file) => relative(ROOT, file))
expect(raw).toEqual([])
})

it('would have caught the two places that read them raw', () => {
expect(RAW_SIGNED.test("{ family: 'solana', transaction: signed.signedTransaction }")).toBe(
true
)
expect(
RAW_SIGNED.test(
"{ family: 'solana', transaction: withoutWalletGuards(signed.signedTransaction) }"
)
).toBe(false)
})
})
34 changes: 34 additions & 0 deletions apps/dapp/lib/__tests__/weave-deposit-solana.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,40 @@ describe('submit', () => {
expect(fetchMock).not.toHaveBeenCalled()
})

/** What Phantom adds before signing: a Lighthouse assertion on the signer's own account. */
const lighthouse: Instruction = {
programAddress: 'L2TExMFKdjpN9kozasaurPirfHy9P8sbXoAN1qA3S95',
accounts: [{ address: ACCOUNT, role: AccountRole.READONLY }],
data: Uint8Array.from([5, 0, 1]),
}

it('sends a deposit Phantom guarded, exactly as the wallet signed it', async () => {
const fetchMock = stubRpc()
const guarded = signed(compile([createAta(), transfer(), lighthouse]))
const result = await solanaDeposit.submit(
{ family: 'solana', signedTransaction: guarded },
EXPECTATION
)
expect(result.ok).toBe(true)
const sent = fetchMock.mock.calls
.map(
([, init]) =>
JSON.parse(String((init as RequestInit).body)) as { method: string; params: unknown[] }
)
.find((call) => call.method === 'sendTransaction')
expect(sent?.params[0]).toBe(guarded)
})

it('still refuses any other program a wallet adds beside the guard', async () => {
const fetchMock = stubRpc()
const other: Instruction = { programAddress: OTHER, accounts: [], data: Uint8Array.from([1]) }
const tampered = signed(compile([createAta(), transfer(), lighthouse, other]))
await expect(
solanaDeposit.submit({ family: 'solana', signedTransaction: tampered }, EXPECTATION)
).rejects.toThrow(/does not need/)
expect(fetchMock).not.toHaveBeenCalled()
})

it('refuses another family without sending', async () => {
const fetchMock = stubRpc()
const result = await solanaDeposit.submit({ family: 'near', hashes: ['x'] }, EXPECTATION)
Expand Down
7 changes: 6 additions & 1 deletion apps/dapp/lib/weave/deposit/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import { venueFetch } from '../../chains/solana/http'
import { createSolanaSubmit, explorerUrl, feePayerSignature } from '../../chains/solana/submit'
import { SOLANA_TOKENS } from '../../chains/solana/tokens'
import { withoutWalletGuards } from '../../chains/solana/wallet-guards'
import {
ChainActionRefused,
tokenBySymbol,
Expand Down Expand Up @@ -416,7 +417,11 @@ async function submit(
if (signed.family !== 'solana') {
return { ok: false, error: 'That is not a Solana transaction, so nothing was sent.' }
}
assertSafe({ family: 'solana', transaction: signed.signedTransaction }, expectation)
// Phantom's Lighthouse guards are its own and only make the transfer fail; the rest is checked.
assertSafe(
{ family: 'solana', transaction: withoutWalletGuards(signed.signedTransaction) },
expectation
)
const hash = feePayerSignature(signed.signedTransaction)
if (hash === undefined) {
return {
Expand Down
Loading