Skip to content

Safe Hands: Solana transaction authorization suite (T0 guard + T1 builder + T1 Squads proposer)#112

Draft
Pratiikpy wants to merge 18 commits into
zeroclaw-labs:mainfrom
Pratiikpy:safe-hands
Draft

Safe Hands: Solana transaction authorization suite (T0 guard + T1 builder + T1 Squads proposer)#112
Pratiikpy wants to merge 18 commits into
zeroclaw-labs:mainfrom
Pratiikpy:safe-hands

Conversation

@Pratiikpy

Copy link
Copy Markdown

What this is

Three wasm32-wasip2 tool components + a shared core that give a ZeroClaw agent a complete, safe path for moving Solana money:

  • solana-tx-authorize (T0): decode → declared-intent binding → deterministic policy → simulate → ALLOW/REVIEW/DENY/UNKNOWN with reason codes. Holds nothing, builds nothing.
  • spl-transfer-build (T1): unsigned SOL/SPL transfers (ATA-aware, memo) that refuse out-of-policy requests at build time.
  • squads-proposal-build (T1): builds unsigned Squads v4 proposals — but only after independently re-running the entire policy evaluation from its own operator config. A caller-supplied ALLOW is never trusted (SH-TRUST-FORGED).

The agent proposes, a Squads multisig disposes. No component holds a key or signs anything.

Prove it in one command (offline)

just prove-safety

All unit tests + a 20-fixture YAML attack arena (amount/recipient/mint swaps, hidden transfers, authority handovers, unlimited delegates, durable nonces, fail-closed paths, and a forged caller-supplied ALLOW) + clippy -D warnings on host and wasm32-wasip2 + release builds of all three components.

Already run live

  • Full 3-tool flow in a real ZeroClaw agent (kimi-k3) on devnet, ending in an on-chain Squads proposal that was approved and executed (0.05 SOL moved) — signatures in EVIDENCE.md.
  • Byte-exact encodings vs @solana/web3.js (message, ATA) and the official @sqds/multisig SDK (PDAs, both instruction encodings, Squads' inner TransactionMessage format).

Notes for review

  • Layout: libs/safe-hands-core + three plugins/* mirroring redact-text (pure core, thin wasm shim, crate-type = ["cdylib", "rlib"], committed Cargo.lock, vendored wit untouched).
  • Permissions: only config_read + http_client.
  • Happy to adjust layout/naming — opening early for feedback per the bounty guidance.

Pratiikpy added 18 commits July 21, 2026 07:30
…nding, T22 rules, velocity, fee caps (31 tests)
…ts, intent binding, injection-proof (8 tests + wasip2 component, 494KB)
…ilder policy pre-check + roundtrip invariant (6 tests, 514KB component)
…+ forged-verdict rejection + Squads v4 proposals (6 tests incl. SH-TRUST-FORGED)
…+ live devnet multisig + full 3-tool flow proven
…harness

- decode: resolve v0 Address Lookup Tables explicitly; fail closed when
  loaded addresses are missing or count-mismatched; preserve signer/
  writable flags and program-id-by-index after resolution
- policy: aggregate per-transaction spend caps and exact intent-amount
  matching across all transfers, closing the split-transfer cap bypass
- tlv: correct Token-2022 AccountType::Mint marker (1, not 2), with an
  independent golden test
- authorize: require fresh, structurally complete simulation evidence
  (context.slot + getSlot within max_slot_age); missing evidence -> UNKNOWN
- rpc: MockTransport records (method, params) pairs for exact assertions
- conformance: supply context.slot + getSlot in the mock so the 20-fixture
  arena matches the hardened simulate() contract (20/20 passing)
- LICENSE: MIT (the repo previously shipped no license text) — satisfies
  the submission's MIT requirement; holder "Safe Hands contributors"
- .gitignore: ignore env files, private keys / Solana keypairs, local
  scratch, and node_modules; no blanket *.json so YAML fixtures and
  registry.json stay tracked
Matches upstream CI (tools/ci/validate_components.sh) locked builds. Fixes
the misleading "offline" claim: tests mock RPC (no live network), but cargo
still fetches dependencies on a fresh clone, so --offline is not forced.
…+ decoder fuzz

A property-based fuzz of the decoder (arbitrary bytes -> Ok/Err, never panic)
found a real panic on hostile input: a v0 or legacy message header whose
readonly counts exceed the key partition underflowed the writable/signer
computation, and legacy parsing indexed account keys without a bounds check.
Both now validate and fail closed.

- decode: enforce `readonly_signed <= num_signers` and
  `num_signers + readonly_unsigned <= key_count` for v0 and legacy; bounds-check
  every legacy account index instead of indexing directly
- policy/tests: 6 property-based security invariants (under-cap allows;
  split-aggregate over-cap never allows; disallowed recipient never allows;
  authority-change always denies; signed never allows; evaluation deterministic)
- roundtrip: decoder totality fuzz (1024 cases) + committed regression seed

Verified: core 66, authorize 11, spl 6, squads 6, conformance 20/20.
Clone-and-run reproduction of the safety claim without `just`: host tests
(property invariants + decoder fuzz), the 20-fixture attack arena, and all
three wasm32-wasip2 release builds. No keys, no funds, no live network.
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