Add Solana tool plugins: token-risk-check (T0) and spl-transfer-build (T1) - #136
Open
OxToF wants to merge 1 commit into
Open
Add Solana tool plugins: token-risk-check (T0) and spl-transfer-build (T1)#136OxToF wants to merge 1 commit into
OxToF wants to merge 1 commit into
Conversation
…uild (T1) token-risk-check answers one question before an agent accepts or sends a token: who else has power over it? Freeze and mint authorities, and the full Token-2022 extension set — permanent delegates, transfer hooks (armed vs. merely armable), transfer fees and whether they can be raised, default-frozen accounts, pausable transfers, non-transferable, interest-bearing, scaled UI amounts, confidential transfers — plus holder concentration and metadata mutability. RED/AMBER/GREEN with reasons, two RPC round trips, ~190 tokens of output. Its threat model runs backwards from most tools: its whole job is to put strings written by a stranger in front of a language model. A token's name and symbol are chosen by whoever deployed the mint. The verdict is computed from account structure alone, flagged metadata is withheld entirely rather than merely sanitized, everything else is neutralized and fenced, and metadata that reads like an instruction is itself a red finding. Four injection tests cover the payload arriving through the mint's own TLV, through Metaplex, hidden in bidi-override characters, and in a form no marker matches. spl-transfer-build returns an unsigned, already-simulated v0 transaction plus a summary a human can read on a phone. Per-mint spend caps live in config.toml and double as the allowlist, so default-deny falls out of the data structure; no tool argument raises a cap, adds a mint, changes the sender, or disables the simulation. Optional durable-nonce support so an approval queue cannot outlive the transaction, idempotent recipient account creation, and a message digest the human compares against their wallet. It holds no key and cannot sign. Both are built on solana-wasi, a new MIT crate of Solana primitives that compile to wasm32-wasip2: no solana-sdk, no bincode, no borsh, no C toolchain, no async runtime, and no keypair type. The transaction encoding and PDA derivations are verified against mainnet-beta rather than only against their own tests — a built transfer simulates with err: null, and the derived ATA and metadata addresses are the accounts the cluster holds. 169 host tests across the three crates, no network and no wasm toolchain required. Clean clippy on both the host and wasm32-wasip2 targets. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two
toolplugins, plus the crate that makes them short. Built for theSuperteam Brasil Solana plugin bounty, but written to be merged rather than to
be demoed: I would rather win you over than route around you.
token-risk-check— T0, read-onlyAnswers one question before an agent accepts or sends a token: who else has
power over it?
Freeze and mint authorities, and the full Token-2022 extension set — permanent
delegates, transfer hooks (armed vs. merely armable), transfer fees and whether
they can be raised later, default-frozen accounts, pausable transfers,
non-transferable, interest-bearing, scaled UI amounts, confidential transfers
with an auditor, mint close authority. Plus holder concentration and whether the
token's own name is still mutable. An extension type the parser does not know is
reported, never silently ignored.
Two RPC round trips (one batched
getMultipleAccounts, onegetTokenLargestAccounts), ~190 tokens of output, hard-capped by a budget wherethe verdict is the one line that can never be dropped. A node that declines the
optional holder query degrades to a note and cannot silence the report.
Calibration was the hard part.
RED/AMBERmean somebody other than theholder has power over the funds; everything else is a
NOTEthat can neverraise a verdict. Wrapped SOL comes back green, USDC amber with its two reasons,
PYUSD red. A checker that returns amber for everything is one operators learn to
click past.
Threat model: this plugin's job is to feed a model attacker-controlled text
A mint's name and symbol are written by whoever deployed it. Four defences:
move a finding.
fenced it, and a test caught that the first 48 characters of a payload still
reached the model. Now nothing of it is rendered.
characters stripped, whitespace collapsed so a name cannot forge a line break
or a speaker turn, backticks and angle brackets mapped, length bounded,
wrapped in an explicit untrusted fence.
addressed to an LLM.
Four injection tests cover the payload arriving through the mint's own TLV,
through a Metaplex account, hidden in bidi-override characters, and in a form no
marker matches — the last one asserts inertness rather than detection.
spl-transfer-build— T1, holds no keyReturns an unsigned, already-simulated v0 transaction plus a summary a human
reads on a phone. It cannot sign; the
senderin config is a public key.Per-mint spend caps live in
config.tomland double as the allowlist, sodefault-deny falls out of the data structure rather than a flag someone can
forget. No tool argument raises a cap, adds a mint, changes the sender, or
disables the simulation.
Also: optional durable-nonce support so an approval queue cannot outlive the
transaction, idempotent recipient-account creation, a SHA-256 message digest the
human compares against their wallet, refusal when the recipient is a token
account or a mint rather than a wallet, and disclosure of transfer fees and
permanent delegates in the summary. A refusal returns
success: falsewith areason, so a model can never read "the node timed out" as "policy allowed it".
Dependency
Both import
solana-wasi0.1.0 (MIT,published for this), a pure-Rust crate of Solana primitives that compile to
wasm32-wasip2: nosolana-sdk, nobincode, noborsh, no C toolchain, noasync runtime, and no keypair type. Its JSON-RPC client sits behind a transport
trait, which is what lets both plugins run their whole logic under a plain host
cargo testwith no wasm toolchain and no network.Source and the verification write-up: https://github.com/OxToF/solana-wasi
Verification
cargo test --locked,cargo clippy --locked --all-targets -- -D warnings,cargo clippy --locked --target wasm32-wasip2 -- -D warningsandcargo build --locked --target wasm32-wasip2 --releaseall pass for bothplugins against the published crate.
tools/build-registry.py --check-metadatareports both as pending unpublished source, and
tools/testspasses.75 host tests here, 94 more in the crate. Beyond that, the parts that would be
expensive to get quietly wrong are checked against the cluster rather than
against my own opinion:
simulateTransactiononmainnet-beta, executes with
err: nulland moves balances by exactly theencoded amount;
spl-transfer-buildpath ran end-to-end against mainnet, includingidempotent ATA creation, simulating at 35,755 compute units;
holds;
byte;
wasm-tools component witon both artifacts reports exactly thetool-pluginworld.
Notes for review
manifest.tomldeclares onlyhttp_clientandconfig_readfor each. Nosockets, no websockets, no filesystem, no memory.
plugins/redact-text: pure core in its own module with no wasmdependency, thin
#[cfg(target_family = "wasm")]shim,["cdylib", "rlib"],standalone
[workspace], structured logging throughlog-recordonly.wit/v0is untouched.🤖 Generated with Claude Code