Repository for the Gecko programs — txs and all the needed on-chain operations.
DEVNET-ONLY. Never deployed to mainnet. No committed keypairs.
| Program | Framework | Purpose | Status |
|---|---|---|---|
custody-probe |
Anchor 1.0 | Privy custody-policy CPI-introspection probe | deployed (devnet) |
gecko-firewall |
Pinocchio | Token-2022 transfer-hook + per-mint denylist (the ENFORCE tier) | built, not deployed |
gecko-receipt |
Anchor 1.0 | verdict-hash PDA + oracle config (the LEDGER tier) | built, not deployed |
Toolchain note.
custody-probewas bumpedanchor-lang 0.32.1 → 1.0.2so the whole Cargo workspace resolves on a single Solana 3.x crate tree (mixing an anchor 0.32 / solana-2.x program with an anchor 1.0 / solana-sdk-3 program in one workspace fails to compile —five8_core 0.1.2'sstd-onlyErrorimpl collides with the 3.x tree'score::error::Errorbound). The bump does not change anydeclare_id!/ program ID.Cargo.lockpinsfive8/five8_const1.0.0ontofive8_core 1.0.0(the>=0.1.1,<2requirement otherwise lets Cargo pick thestd-only0.1.2).
Devnet / localnet only. Never deployed to mainnet. Moves only test lamports.
Our Privy custody policy ALLOWs a small allowlist of program IDs (Jupiter / Kamino / Drift) and pins direct (top-level) system transfers to the user's own address — deny-by-default for everything else.
L2 proved Privy denies a top-level foreign transfer, but left one case open:
a foreign transfer nested as a CPI inside an allowed program call. That open
case is what gates our custody execute path.
custody-probe is the allowed-program stand-in for that test. The cross-repo
test (in gecko-mcpay-api) allowlists this program's ID in a scoped Privy
policy, then has the policy-scoped Privy wallet call probe_cpi_transfer, which
CPIs a system_program::transfer to an arbitrary recipient.
The test then observes Privy's verdict:
| Privy verdict | Interpretation | Consequence |
|---|---|---|
| DENY | Privy inspected the CPI-nested transfer | Safe — we can un-gate the custody execute |
| ALLOW | Privy only saw the allowed top-level program ID | Unsafe — keep execute gated |
This is a verification harness, not production.
| Program ID | |
|---|---|
Declared (declare_id! / IDL address) |
vDSFZB3vgEndA4qmtWfKq8bvBMQAeHauT9bd3uKDdHy |
| Devnet (deployed) | vDSFZB3vgEndA4qmtWfKq8bvBMQAeHauT9bd3uKDdHy |
Deployed to devnet (cluster https://api.devnet.solana.com). Confirmed
on-chain; the IDL is also published on-chain (fetchable via
anchor idl fetch vDSFZB3vgEndA4qmtWfKq8bvBMQAeHauT9bd3uKDdHy --provider.cluster devnet).
The program keypair lives at
target/deploy/custody_probe-keypair.jsonand is gitignored (devnet/localnet only). To rebuild against the same ID, restore that keypair beforeanchor build; otherwise regenerate and updatedeclare_id!+Anchor.toml.
CPIs a System Program transfer of amount lamports from -> to. The transfer is
nested one CPI level deep inside this (allowlisted) program — that nesting is
the entire point of the probe.
Account layout (the order the cross-repo gecko-mcpay-api L2 smoke must build):
| # | Name | Type | Signer | Writable | Notes |
|---|---|---|---|---|---|
| 0 | from |
Signer |
yes | yes | The policy-scoped Privy custody wallet (pays the lamports) |
| 1 | to |
SystemAccount |
no | yes | Arbitrary recipient — intentionally NOT the policy-pinned address |
| 2 | system_program |
Program<System> |
no | no | 11111111111111111111111111111111 |
Arg: amount: u64 — lamports to move (use a few thousand; this is test value).
- IDL:
target/idl/custody_probe.json - TS types:
target/types/custody_probe.ts - On-chain IDL: account
7dTQ8JW4nmsQQATNVYFm5GR5bpsfLSVzaWhnLaskWTuton devnet
The gecko-mcpay-api L2 smoke can build the instruction from the IDL or directly:
take the discriminator from the IDL for probe_cpi_transfer, then the 3 accounts
above in order plus a little-endian u64 amount.
anchor build # compiles custody_probe, regenerates IDL + types
anchor test # spins up a local validator, runs tests/custody-probe.tstests/custody-probe.ts moves a few lamports from the funded payer to a fresh
recipient and asserts the recipient balance increased — proving the nested CPI
path works on a real validator.
anchor deploy --provider.cluster devnetProgram deploy needs roughly 1.5–2.5 SOL on devnet for a ~177 KB program. If the deployer wallet is underfunded:
solana airdrop 2 --url devnet # may rate-limitNever deploy to mainnet. This program moves only test lamports.
Pinocchio, zero-copy, #![no_std]. DEVNET-ONLY. Not deployed.
VERIFY, not execute. This is the issuer's program enforcing the issuer's
own denylist at transfer time. Gecko (the oracle) only writes the row
(UpdateDenylist populates the denylist PDA with the verdict hash h + the
denied wallets). The Token-2022 transfer hook is what reverts a disallowed
transfer. Gecko never moves the funds and never blocks the transfer itself.
HqpkdrPNBMEfGAqniF64728PD9JwYbMqZdHcCqypksGP
Hardcoded in declare_id!. No keypair committed; a real deploy manages keys out
of tree.
| Disc | Instruction | Signer |
|---|---|---|
0x00 |
InitConfig{oracle} |
issuer admin |
0x01 |
SetOracle{new_oracle} |
issuer admin |
0x02 |
UpdateDenylist{mint, wallets[], h, freeze} |
Gecko oracle |
0x03 |
InitExtraMetas{} |
mint authority (issuer) |
0x04 |
CloseDenylist{mint} |
issuer admin |
| SPL 8-byte | Execute (transfer hook) |
(Token-2022 CPI) |
Execute 8-byte discriminator = sha256("spl-transfer-hook-interface:execute")[0..8]
= [105,37,101,197,75,251,102,26]. The entrypoint branches on the 8-byte SPL
disc FIRST (unambiguous), then falls through to the single-byte admin match.
| Item | Value |
|---|---|
FirewallConfig PDA seeds |
[b"fw_config"] |
Denylist PDA seeds |
[b"denylist", mint] |
ExtraAccountMetaList PDA seeds |
[b"extra-account-metas", mint] |
| Schema version | 1 (== gecko:v1:) |
Hash h |
32 raw bytes; off-chain receipt_hash (never recomputed on-chain) |
verdict_code |
0=ok 1=caution 2=block 3=unknown |
Account byte layouts in src/state.rs (FirewallConfig 73 bytes; DenylistHeader
73 bytes + 32×N packed entries, bounded ~300). The SPL TLV /
transfer-hook-interface crates are NOT used (they depend on solana-program;
this is a no_std crate) — the ExtraAccountMetaList bytes are hand-built to
match the SPL on-disk format, verified byte-for-byte by a host unit test.
| Case | CU |
|---|---|
| allow, denylist empty | 2,506 |
| allow, 50 entries (full no-hit scan) | 5,906 |
| deny, 50 entries (worst-case scan) | 4,038 |
| deny, 300 entries (scan ceiling) | 12,538 |
.so ≈ 39 KB (per-package opt-level = "z" + strip in the workspace profile).
Anchor 1.0. DEVNET-ONLY. Not deployed.
VERIFY, not execute. This is Gecko's own Anchor program: Gecko's oracle
anchors the verdict hashes Gecko computed. It STORES, it does not enforce — no
token movement. A durable content-addressed PDA successor to the v0 devnet
SPL-memo anchor: same h, same gecko:v1: schema.
HFDEukquuWS79DRAUqBVMPEekHq2uYB8Rp5sfkP3dCYt
Hardcoded in declare_id!. No keypair committed.
| Instruction | Signer | Notes |
|---|---|---|
init_config(oracle) |
admin | one-time; create the singleton ReceiptConfig PDA |
set_oracle(new_oracle) |
admin | rotate the oracle key |
set_paused(paused) |
admin | kill-switch over anchor_receipt |
anchor_receipt(h, verdict_code) |
oracle | write the content-addressed Receipt PDA |
close_receipt() |
admin | revival-safe teardown (Anchor close) |
| Item | Value |
|---|---|
ReceiptConfig PDA seeds |
[b"receipt_config"] |
Receipt PDA seeds |
[b"receipt", h] (h = 32 raw bytes, one seed slot) |
| Schema version | 1 (== gecko:v1:) |
Hash h |
32 raw bytes; off-chain receipt_hash (never recomputed on-chain) |
verdict_code |
0=ok 1=caution 2=block 3=unknown (bucket, never a raw score) |
Account byte layouts in gecko-receipt/src/state.rs (ReceiptConfig 94 data
bytes; Receipt 104 data bytes; space = T::DISCRIMINATOR.len() + T::INIT_SPACE).
Stored canonical bumps, checked ops, no unwrap/expect/init_if_needed,
has_one/constraint validation, one #[error_code] enum. anchor_receipt
write ≈ 9.7K CU.
Caveat — remaining integration proof. Both programs are devnet-only and not deployed. The Mollusk suites drive every instruction in-process (including
Executedirectly), but the full Token-2022-CPIs-drive-the-hook live round-trip on a fork is the one path Mollusk does not exercise — that live e2e is the remaining integration verification before any deploy.
The two new programs build via cargo build-sbf per-crate (the Pinocchio
firewall has no IDL, so it is not an anchor build target). custody-probe +
gecko-receipt also build through anchor build for IDL/TS-type generation.
# --- whole workspace ---
cargo check --workspace # custody-probe + gecko-firewall + gecko-receipt + test crates
cargo build-sbf --manifest-path programs/gecko-firewall/Cargo.toml
cargo build-sbf --manifest-path programs/gecko-receipt/Cargo.toml
cargo build-sbf --manifest-path programs/custody-probe/Cargo.toml
# --- IDL / TS types for the Anchor programs (frozen IDs => --ignore-keys) ---
# `--ignore-keys` honors declare_id! when no matching committed keypair exists.
anchor build --ignore-keys
# --- firewall tests (Mollusk loads the .so from SBF_OUT_DIR) ---
SBF_OUT_DIR="$(pwd)/target/deploy" cargo test -p gecko-firewall-tests # 14 Mollusk
cargo test -p gecko-firewall # 2 host (TLV byte-exactness)
# --- receipt tests ---
SBF_OUT_DIR="$(pwd)/target/deploy" cargo test -p gecko-receipt-tests # 11 Mollusk
cargo test -p gecko-receipt --lib # 5 host (frozen layouts)
# --- custody-probe localnet TS test (needs a validator) ---
anchor testgecko-programs/
├── Anchor.toml # toolchain 1.0.2; localnet + devnet entries for all 3 programs
├── Cargo.toml # workspace (explicit members; [workspace.package]; firewall size profile)
├── package.json / tsconfig.json
├── programs/
│ ├── custody-probe/ # Anchor 1.0 — probe_cpi_transfer (deployed devnet)
│ ├── gecko-firewall/ # Pinocchio — Token-2022 hook + denylist
│ ├── gecko-receipt/ # Anchor 1.0 — verdict-hash PDA
│ └── tests/
│ ├── mollusk/ # gecko-firewall-tests (Mollusk 0.13 + solana-sdk 3)
│ └── receipt/ # gecko-receipt-tests (Mollusk 0.13 + solana-sdk 3 + sha2)
├── tests/
│ └── custody-probe.ts # localnet nested-CPI assertion
├── migrations/deploy.ts
└── target/ # gitignored (idl/, types/, deploy/)
rustc/cargo1.94.x,solana-cli/cargo-build-sbf3.1.x (Agave), platform-tools v1.52,anchor-cli1.0.x.- Pinocchio
=0.8.4(+pinocchio-system/-pubkey/-log); Anchor1.0.2; Mollusk0.13+solana-sdk3for the test harness.