Skip to content

0.55.2 — maybe-charges are booked, replaced withdrawals are not reported sent - #205

Merged
VickyXAI merged 20 commits into
mainfrom
fix/audit-r5
Nov 3, 2026
Merged

VickyXAI merged 20 commits into
mainfrom
fix/audit-r5

Conversation

@VickyXAI

@VickyXAI VickyXAI commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

Fixes from audit round 5: 9 scoped finders, each finding checked by one agent that tried to refute it. 12 findings were confirmed and 1 refuted. 11 of the 12 are fixed here; the image streaming cap went into #203. Every fix comes with a test that fails on the old code, except the README wording.

P1: a charge that may have settled was booked as $0

  • Solana async submit (image, music). An edge 502/504 left the tracker armed on purpose so it would be booked, but the error carried no statusCode, so mayHaveSettled said no. The error now carries the status, and the edge set is the shared ORIGIN_DID_NOT_ANSWER, so 503 is an answer, as it is on every other rail.
  • Speech, account rail. A billed 200 with an unparseable body threw RawCallSettledError before book() ran, so the user saw "failed, try again". It now books the settled cost header, and the message says the charge stands.
  • Speech and music, Base rail. An edge 502/504 on the signed request had no status, so nothing was booked and the user saw "temporary issue, try again". It is now booked as a maybe, same as Solana and the account rail. New Base cells in rail-parity.test.ts.

P2

  • Polymarket EOA withdraw. viem resolves waitForTransactionReceipt with the replacing transaction's receipt, so a nonce taken by another same-key transaction printed ✅ "bridge delivers USDC" for a transfer that never ran. onReplaced now distinguishes the cases: "replaced" or "cancelled" means no pUSD was sent and the guard is released; "repriced" still succeeds, under the new hash.

P3

  • Polymarket fund. Math.floor(usd*1e6) signed $2.009999 for "$2.01". It now uses parseUsdAmount, moved to polymarket/amounts.ts, and refuses input with more than 6 decimals.
  • Solana held settle. A "failed" poll after the settle was held said "No payment was taken" next to "MAY have been charged". The contradiction is removed.
  • Wallet-mode remedy. It told users to unset BLOCKRUN_API_KEY even when the key came from ~/.blockrun/.api-key. It now names the actual source.
  • Setup text. Solana setup said the key was in .solana-session even under BLOCKRUN_KEYCHAIN=strict, where that file is deleted, or when SOLANA_WALLET_KEY was set. It now describes the real state.
  • blockrun_phone without a body. It sent a discovery-only GET that always 402s, after asking the user to approve up to $5, then said "check your wallet balance". It is now refused before anything is reserved.
  • README. "web/X search" is now "web + news". The note claiming report runs $0.001 high is corrected.

Refuted. The account-rail chat ledger finding did not hold up.

Full suite 1458/1458, tsc clean, build OK. Pre-existing ssrf/auth/http test stubs gained the new exports. The README line 55 edit will conflict trivially with #197 (a marker refresh on the same line).

solanaPaidAsyncPost leaves the tracker armed on an edge status so the tool
books the submit as a maybe (the origin may have settled), then threw a
status-less "API error 504: {}". in-flight reads the verdict off
statusCode, never prose, so mayHaveSettled said false and image/music on
Solana booked nothing. The image test passed only because its fake body
contained the word "timeout".

The error now carries statusCode, and the helper's edge set is the shared
ORIGIN_DID_NOT_ANSWER instead of a local 502/503/504 list: 503 is the
gateway's own answer on every other rail, and leaving it armed only
produced a "precaution" that booked nothing.
Account rail: a billed 200 whose body will not parse makes apiKeyPost throw
RawCallSettledError before speech's book() runs, so the catch read it as a
plain failure, booked nothing and invited a retry. It now books the
settled cost header first, and the catch says the charge stands, as the
Base and Solana rails already did.

Base rail: an edge 502/504 on the signed request threw without a status,
so settleGiveUp could not see it, nothing was booked and the user was told
"temporary API issue, try again". speech and music now attach statusCode,
so it is booked as a maybe like on Solana and the account rail. A real
origin 500 is still an answer.
viem's waitForTransactionReceipt does not throw when the watched tx is
replaced: if another transaction from the same key mines with our nonce,
it resolves with that transaction's receipt. In EOA mode the key is shared
with setup approvals, redeems and other apps, so a successful replacer
cleared the guard and printed "Withdrawal submitted ... the bridge delivers
USDC to Base" for a transfer that can never execute.

onReplaced now records the reason. "replaced"/"cancelled" release the guard
and return an error saying no pUSD was sent; "repriced" (same call, new
gas) still succeeds and links the hash it landed under.
fund built amountMicro with Math.floor(usd * 1e6). 2.01 * 1e6 is
2009999.9999999998, so "$2.01" signed and posted $2.009999 while every
message said $2.01 (about 15% of cent amounts are affected), and input
with more than 6 decimals was silently truncated.

parseUsdAmount, which withdraw already used for exactly this, moves to
polymarket/amounts.ts (withdraw re-exports it) and fund parses the amount
once, up front, refusing over-precise input before anything is signed.
…yment was taken

A held completion settle followed by a "failed" poll threw "... No payment
was taken."; the give-up stripped only "No charge was made", so the user
read that sentence next to "it MAY have been charged". The strip now
covers both phrasings. Money handling (billing: unknown) was already right.
…ight import

The edge-status booking tests import the real in-flight.js, which needs
isTimeoutError from http.js; this file mocks http.js without it, so both
cases failed to load.
requireWalletMode and the API-key status text said "unset BLOCKRUN_API_KEY
and restart" even when the key was read from ~/.blockrun/.api-key. A user
who configured the file unset a variable they never set, restarted, and was
refused again with no mention of the file. walletModeRemedy() now names the
actual source.
The Solana setup text said the key was stored in ~/.blockrun/.solana-session
unconditionally. Under BLOCKRUN_KEYCHAIN=strict that file is deleted and the
key is only in the OS keychain, and with SOLANA_WALLET_KEY it never existed,
so a user backing up ~/.blockrun on that advice backed up nothing.
keyStorageNote() words it from the env override, strict mode, or the file.
Only voice/call/{call_id} has a real GET. Every other phone/* and voice/call
GET is the gateway's discovery handler, which always answers 402 and ignores
the payment, so an omitted body reserved (up to $5 for numbers/buy), asked
the user to approve it, signed, was refused and reported "check your wallet
balance". It is now refused up front, naming body: {} as the fix.
…ved fee

The overview still said "live web/X search"; the X source was removed and
blockrun_search refuses it. The pricing note said action:"report" runs
$0.001 high per call, but only the confirm dialog and the cap reservation
do: the ledger books the observed fee, or the settled amount on the
account rail.
The edge-504 matrix covered only Solana and the account rail, which is how
the Base rail of the two tools that sign x402 themselves drifted. The music
cell fails on the pre-fix music.ts.
51908d6 gave every non-2xx submit error its statusCode. mayHaveSettled books
any edge status on a request that carried a payment, so a Solana video
(payment-on-completion, whose submit cannot move money) answered 504 at
submit booked a phantom maybe-charge, while Base video booked nothing.
The status is now attached only when the submit can settle. Also fixes
two comments that still named 503 as an edge status.
…mode

fa44381 said "OS keychain only" whenever BLOCKRUN_KEYCHAIN=strict, but
persistKey keeps the plaintext file whenever it cannot prove the keychain
copy (no keychain on Windows, no secret-tool, a failed store or read-back)
and never retires the legacy wallet.key, so those users were told the
opposite of where their only copy lives. The note now names every key file
that still holds a key, and says keychain-only only when none does.
…names both

The env var outranks the file, so unsetting it alone hands billing to the
file key and the user is refused again after the restart.
…reserve provenance

The two skill troubleshooting rows still said only "unset BLOCKRUN_API_KEY".
The phone reserve comment and test still called numbers/search a verified
$0.0120 charge; that figure came from the discovery GET, which never
settles, and only an unknown POST reaches the reserve now.
fetchPublicUrl awaited dns.lookup (unabortable) before fetch() saw the
signal, so the inline preview's 15s budget stopped covering name
resolution, and a hung resolver held an already-paid image result past it.
The lookup is now raced against the signal.
@VickyXAI VickyXAI changed the title fix: audit round 5 — unbooked maybe-charges, replaced withdrawals, fund rounding, and P3s 0.55.2 — maybe-charges are booked, replaced withdrawals are not reported sent Nov 3, 2026
@VickyXAI

VickyXAI commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Regression round on this PR's fixes: 8 confirmed and fixed here, each with a test that fails on the previous commit.

  • P2. A Solana video submit's edge 504 became a phantom maybe-charge. Video is payment-on-completion, so its submit cannot move money.
  • P2. The key-storage note trusted strict mode even when the plaintext file was still on disk.
  • P3. The 15s preview deadline no longer covered DNS.
  • P3. A repriced withdrawal that reverted named the hash that never mined.
  • P3. With both an env key and a file key, the remedy named only the env var.
  • P3. The skill troubleshooting rows were stale.
  • P3. The phone reserve provenance comment and test were wrong.
  • P3. Two comments still named 503 as an edge status.

Three regression claims were refuted. Now bumped to 0.55.2, with #204 folded into the CHANGELOG. Full suite 1475/1475, verify:prices exit 0, build OK, stdio smoke shows 19 tools.

@VickyXAI
VickyXAI merged commit 26118c1 into main Nov 3, 2026
3 checks passed
@VickyXAI
VickyXAI deleted the fix/audit-r5 branch November 3, 2026 02:15
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.

1 participant