Skip to content

fix(devnet): validate faucet and auto-fund transaction execution results - #2436

Open
Bayyan16 wants to merge 1 commit into
dcccrypto:playgroundfrom
Bayyan16:security/confirmation-result-validation
Open

fix(devnet): validate faucet and auto-fund transaction execution results#2436
Bayyan16 wants to merge 1 commit into
dcccrypto:playgroundfrom
Bayyan16:security/confirmation-result-validation

Conversation

@Bayyan16

Copy link
Copy Markdown
Contributor

Fixes #2435

Summary

This PR fixes a devnet funding-integrity issue where the Faucet SOL, Faucet USDC, and Auto-fund USDC flows could report funding success even when Solana RPC confirmation explicitly reported an on-chain execution failure.

The affected call sites awaited confirmTransaction(), but discarded its returned SignatureResult:

await connection.confirmTransaction(...);

A resolved RPC confirmation request does not necessarily mean that the transaction executed successfully. Solana may return:

{
  value: {
    err: {
      InstructionError: [
        0,
        {
          Custom: 1,
        },
      ],
    },
  },
}

In that condition, the transaction has failed on-chain even though confirmTransaction() resolved normally.

Before this patch, the affected routes could continue into their success paths and return or record:

funded = true
sol_airdropped = true
usdc_minted = true

despite the wallet receiving no assets.

This PR validates the confirmation execution result before any success state is recorded.


Root Cause

Three production call sites treated a successfully resolved RPC call as proof of successful transaction execution.

Faucet SOL

The previous flow effectively performed:

sig = await pubConn.requestAirdrop(
  walletPk,
  SOL_AIRDROP_AMOUNT,
);

await pubConn.confirmTransaction(
  sig,
  "confirmed",
);

break;

The confirmation result was discarded, and the RPC fallback loop exited as though the airdrop had succeeded.

The route subsequently recorded the faucet claim and returned:

funded = true
sol_airdropped = true

Faucet USDC

The previous USDC path performed:

const sig =
  await connection.sendRawTransaction(
    signedTransaction.serialize(),
  );

await connection.confirmTransaction(
  {
    signature: sig,
    blockhash,
    lastValidBlockHeight,
  },
  "confirmed",
);

The route then recorded the claim and successful mint analytics without checking confirmation.value.err.

Auto-fund USDC

The previous Auto-fund USDC path performed:

const sig =
  await connection.sendRawTransaction(
    signedTransaction.serialize(),
  );

await connection.confirmTransaction(
  {
    signature: sig,
    blockhash,
    lastValidBlockHeight,
  },
  "confirmed",
);

results.usdc_minted = true;

This allowed a failed MintTo execution to set the success flag, retain the claim, write successful analytics, and return funded: true.


Required Invariant

A funding operation may only be recorded as successful when:

confirmation.value.err === null

The following conditions must be treated as failure:

confirmation.value.err contains an execution error
confirmation.value is missing
confirmation.value.err is missing

The code must distinguish between:

RPC request completed

and:

transaction completed successfully on-chain

These conditions are not equivalent.


Changes

1. Added a shared fail-closed confirmation validator

New file:

app/lib/transaction-confirmation.ts

The helper accepts only an explicit value.err === null as success:

export interface TransactionConfirmationLike {
  value?: {
    err?: unknown | null;
  };
}

export function assertSuccessfulConfirmation(
  confirmation: TransactionConfirmationLike,
  operation: string,
): void {
  const executionError =
    confirmation?.value?.err;

  if (executionError === null) {
    return;
  }

  let serializedError: string;

  try {
    serializedError =
      JSON.stringify(executionError);
  } catch {
    serializedError =
      String(executionError);
  }

  throw new Error(
    `${operation} confirmed but failed on-chain: ${serializedError}`,
  );
}

The validator fails closed when the RPC result is malformed or incomplete.


2. Fixed Faucet SOL confirmation handling

The SOL faucet now stores and validates the returned confirmation:

const confirmation =
  await pubConn.confirmTransaction(
    sig,
    "confirmed",
  );

assertSuccessfulConfirmation(
  confirmation,
  "SOL airdrop",
);

The RPC fallback loop exits through its success branch only after execution validation passes.

When value.err is populated:

  • the operation is treated as failed;
  • success state is not recorded;
  • the route does not return funded: true;
  • the existing failure and retry handling remains active.

3. Fixed Faucet USDC confirmation handling

The USDC faucet now validates the MintTo transaction before recording its claim or analytics:

const confirmation =
  await connection.confirmTransaction(
    {
      signature: sig,
      blockhash,
      lastValidBlockHeight,
    },
    "confirmed",
  );

assertSuccessfulConfirmation(
  confirmation,
  "USDC faucet mint",
);

When execution fails:

  • funded: true is not returned;
  • usdc_minted: true is not returned;
  • successful funding analytics are not recorded;
  • the reserved claim is released through the existing failure path.

4. Fixed Auto-fund USDC confirmation handling

Auto-fund now validates execution before setting its success flags:

const confirmation =
  await connection.confirmTransaction(
    {
      signature: sig,
      blockhash,
      lastValidBlockHeight,
    },
    "confirmed",
  );

assertSuccessfulConfirmation(
  confirmation,
  "Auto-fund USDC mint",
);

results.usdc_minted = true;

If execution fails:

results.usdc_minted remains false
results.usdc_amount is not assigned
successful analytics are not inserted
the claim is released when nothing else was funded
the user remains eligible to retry

The route preserves its existing non-fatal Auto-fund behavior and may still return HTTP 200, but it no longer reports funding success.


Existing Safe Control

The Auto-fund SOL path already stores the confirmation result and checks:

if (airdropResult.value.err) {
  throw new Error(...);
}

This PR applies the same execution-result invariant consistently to the three remaining affected call sites.

The existing Auto-fund SOL behavior is not otherwise changed.


Behavior Before and After

Faucet SOL

Before:

confirmation.value.err != null
→ HTTP 200
→ funded=true
→ sol_airdropped=true

After:

confirmation.value.err != null
→ operation rejected
→ HTTP 500
→ no funding success recorded

Faucet USDC

Before:

confirmation.value.err != null
→ HTTP 200
→ funded=true
→ usdc_minted=true
→ successful analytics recorded

After:

confirmation.value.err != null
→ operation rejected
→ HTTP 500
→ claim released
→ no successful analytics

Auto-fund USDC

Before:

confirmation.value.err != null
→ funded=true
→ usdc_minted=true
→ claim retained
→ successful analytics recorded

After:

confirmation.value.err != null
→ HTTP 200 remains non-fatal
→ funded=false
→ usdc_minted=false
→ claim released
→ no successful analytics

Regression Coverage

This PR adds focused tests for the shared invariant and every affected route.

Shared validator

app/__tests__/lib/transaction-confirmation.test.ts

Coverage:

value.err === null
    => accepted

value.err contains an execution error
    => rejected

value is missing
    => rejected

err is missing
    => rejected

Faucet SOL

app/__tests__/api/faucet-confirmation-result.test.ts

Coverage:

execution error
    => HTTP 500
    => no false funding flags

err === null
    => existing success behavior preserved

Faucet USDC

app/__tests__/api/faucet-usdc-confirmation-result.test.ts

Coverage:

execution error
    => HTTP 500
    => no false funding flags

err === null
    => existing success behavior preserved

Auto-fund USDC

app/__tests__/api/auto-fund-usdc-confirmation-result.test.ts

Coverage:

execution error
    => funded=false
    => usdc_minted=false
    => claim released
    => no successful analytics

err === null
    => existing success behavior preserved

RED Evidence Before Fix

Each affected route was tested with a confirmation response that resolved normally but contained:

value: {
  err: {
    InstructionError: [
      0,
      {
        Custom: 1,
      },
    ],
  },
}

Faucet SOL

Observed:

status: 200
funded: true
sol_airdropped: true

Regression failure:

Expected: 500
Received: 200

Result:

1 failed
1 passed

Faucet USDC

Observed:

status: 200
funded: true
usdc_minted: true

Regression failure:

Expected: 500
Received: 200

Result:

1 failed
1 passed

Auto-fund USDC

Observed:

funded: true
usdc_minted: true
releaseFaucetClaim calls: 0
analyticsInsert calls: 1

Regression failure:

Expected: false
Received: true

Result:

1 failed
1 passed

Each RED suite included a successful-confirmation positive control.


GREEN Validation

Command:

pnpm --filter @percolator/app exec vitest run \
  __tests__/lib/transaction-confirmation.test.ts \
  __tests__/api/faucet-confirmation-result.test.ts \
  __tests__/api/faucet-usdc-confirmation-result.test.ts \
  __tests__/api/auto-fund-usdc-confirmation-result.test.ts \
  --reporter=verbose

Result:

Test Files  4 passed
Tests       10 passed
Exit status: 0

Verified cases:

PASS successful confirmation is accepted
PASS execution error is rejected
PASS missing value fails closed
PASS missing err fails closed
PASS Faucet SOL rejects failed execution
PASS Faucet SOL success control remains valid
PASS Faucet USDC rejects failed execution
PASS Faucet USDC success control remains valid
PASS Auto-fund USDC does not report failed execution as funded
PASS Auto-fund USDC success control remains valid

Existing Related Test Validation

All existing Faucet and Auto-fund related tests were run after applying the patch.

Result:

Test Files  8 passed
Tests       69 passed
Exit status: 0

This includes existing coverage for:

request parsing
wallet validation
SOL and USDC dispatch
rate limiting
RPC fallback
retryable error classification
mint authority validation
network guards
Auto-fund amount behavior
Faucet UI hook behavior

TypeScript Validation

Command:

pnpm --filter @percolator/app exec tsc \
  --noEmit \
  -p tsconfig.json

Result:

PASS
No TypeScript errors.

Production Build

Command:

pnpm --filter @percolator/app build

Result:

PASS

Production build completed successfully.
Static pages generated successfully.
Page optimization completed successfully.
Exit status: 0.

Existing non-fatal warnings related to Next.js configuration, middleware migration, module.register(), and BigInt native bindings were not introduced by this PR.


Patch Integrity

Commands:

git diff upstream/playground...HEAD --check
git diff upstream/playground...HEAD --name-status

Result:

PASS
No whitespace or malformed-patch errors.

Final scope:

A  app/__tests__/api/auto-fund-usdc-confirmation-result.test.ts
A  app/__tests__/api/faucet-confirmation-result.test.ts
A  app/__tests__/api/faucet-usdc-confirmation-result.test.ts
A  app/__tests__/lib/transaction-confirmation.test.ts
M  app/app/api/auto-fund/route.ts
M  app/app/api/faucet/route.ts
A  app/lib/transaction-confirmation.ts

Scope

This PR is intentionally limited to the transaction-confirmation result issue reported in #2435.

It does not modify:

  • core trading behavior;
  • matcher or risk-engine logic;
  • margin or liquidation behavior;
  • settlement logic;
  • wallet authorization;
  • signer ownership;
  • on-chain program instructions;
  • mainnet funding;
  • production assets;
  • RPC provider selection;
  • faucet amount constants;
  • rate-limit duration.

The five newly added files are intentional remediation and regression-coverage files, not missing upstream dependencies.


Files Changed

app/app/api/faucet/route.ts
app/app/api/auto-fund/route.ts
app/lib/transaction-confirmation.ts
app/__tests__/lib/transaction-confirmation.test.ts
app/__tests__/api/faucet-confirmation-result.test.ts
app/__tests__/api/faucet-usdc-confirmation-result.test.ts
app/__tests__/api/auto-fund-usdc-confirmation-result.test.ts

Revisions

Target branch:
playground

Tested base commit:
2ae3f35827affb719a90b645cbb13dda3ffcf782

Fix commit:
d37471befc0746d411ec4ac0b8500140beffd1ec

Ahead / behind:
0 / 1

Review Notes

The most important review points are:

  1. Only an explicit confirmation.value.err === null is accepted as success.
  2. Faucet SOL validates execution before exiting its RPC fallback loop.
  3. Faucet USDC validates execution before recording the claim or analytics.
  4. Auto-fund USDC validates execution before setting results.usdc_minted.
  5. Auto-fund failure remains non-fatal but no longer reports false success.
  6. Reserved claims are released when no funding operation succeeds.
  7. Existing successful-confirmation behavior remains unchanged.
  8. Auto-fund SOL's existing validation remains intact.
  9. The patch contains no unrelated production changes.
  10. Route-level and helper-level regression coverage prevents recurrence.

Commit

d37471be fix(devnet): validate faucet transaction execution results

@Bayyan16
Bayyan16 requested a review from dcccrypto as a code owner July 17, 2026 13:01
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

@Bayyan16 is attempting to deploy a commit to the Khubair Nasir's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b7adef8d-2b65-4188-82fd-0e04174c1270

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@dcccrypto

Copy link
Copy Markdown
Owner

Independent verification — not an approval (QA/Security own that), just evidence for whoever reviews.

I've been mutation-checking security/correctness PRs in this repo because there's a recurring failure mode here: tests that pass without exercising the thing they're named for (11 confirmed instances so far, including one of my own and one in a sibling PR). This one holds up well.

Method: neutralised only the new assertion on the PR branch, restoring the #2435 behaviour (report success even when the confirmation carries an execution error):

export function assertSuccessfulConfirmation(...) {
  return;   // <- injected
  ...
}

Result:

assertion present    → 10 passed
assertion neutralised → 6 FAILED / 4 files red

The 6 that fail are the right 6:

  • rejects a confirmation containing an on-chain execution error
  • fails closed when value is missing
  • fails closed when err is missing
  • must not report USDC funding when confirmation contains an on-chain execution error
  • must reject a USDC mint whose confirmation contains an on-chain execution error
  • must reject a SOL airdrop whose confirmation contains an on-chain execution error

Two things worth calling out as good practice:

1. It's tested at the route layer, not just the helper. All three route tests (faucet SOL, faucet USDC, auto-fund USDC) fail under the mutation. That's the part that actually protects the user-visible behaviour — a helper-only test would still pass if a route later stopped calling the helper. Worth contrasting with #2437, where the unit tests catch the bug but the route-level file named after the issue doesn't.

2. It fails closed on malformed input. value missing and err missing both throw rather than being treated as success. Given the bug is "reported success when it shouldn't have", defaulting the ambiguous cases to failure is the right direction.

No changes requested from me.

@Bayyan16

Copy link
Copy Markdown
Contributor Author

Thank you for independently mutation-checking the confirmation invariant and route-level coverage.

The result confirms that the six failure-path assertions are non-vacuous, while the four legitimate-success controls remain intentionally unaffected.

I’ll keep the current head unchanged and leave formal QA/Security approval and merge decisions to the appropriate reviewers.

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.

2 participants