@oracle-agent/oracle 0.24.1 → 0.24.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/skills/chain/SKILL.md +34 -0
- package/dist/assets/skills/chain-defi-ecosystem-analysis/SKILL.md +181 -0
- package/dist/assets/skills/chain-ecosystem-gap-analysis/SKILL.md +162 -0
- package/dist/assets/skills/cross-chain-twap-execution/SKILL.md +125 -0
- package/dist/assets/skills/defi-protocol-pmf-assessment/SKILL.md +332 -0
- package/dist/assets/skills/evm-contract-research.md +8 -3
- package/dist/assets/skills/multi-venue-prepare-only-ranking/SKILL.md +93 -0
- package/dist/assets/skills/oracle-access-control/SKILL.md +71 -0
- package/dist/assets/skills/oracle-action-arming/SKILL.md +99 -0
- package/dist/assets/skills/oracle-airdrop-calculator/SKILL.md +111 -0
- package/dist/assets/skills/oracle-desk-product/SKILL.md +341 -0
- package/dist/assets/skills/oracle-evm/SKILL.md +55 -0
- package/dist/assets/skills/oracle-harness/SKILL.md +44 -0
- package/dist/assets/skills/oracle-mcp-install/SKILL.md +140 -0
- package/dist/assets/skills/oracle-multichain-convert/SKILL.md +87 -0
- package/dist/assets/skills/oracle-native-harness/SKILL.md +32 -0
- package/dist/assets/skills/oracle-ownership-gate/SKILL.md +42 -0
- package/dist/assets/skills/oracle-public-product-ux/SKILL.md +114 -0
- package/dist/assets/skills/oracle-tailscale/SKILL.md +32 -0
- package/dist/assets/skills/oracle-thin-client/SKILL.md +47 -0
- package/dist/assets/skills/perp-venue-funding-research/SKILL.md +161 -0
- package/dist/assets/skills/polymarket/SKILL.md +160 -0
- package/dist/assets/skills/protocol-api-key-integration/SKILL.md +141 -0
- package/dist/assets/skills/self-custodial-onchain-execution/SKILL.md +1284 -0
- package/dist/assets/skills/setup/SKILL.md +40 -0
- package/dist/assets/skills/stable-launch-ops/SKILL.md +89 -0
- package/dist/assets/skills/trade-loop-circuit-breaker/SKILL.md +441 -0
- package/dist/assets/skills/venue-capability-boundaries/SKILL.md +32 -0
- package/dist/bin/desk-server.mjs +16 -16
- package/dist/bin/oracle-data-mcp.mjs +1 -1
- package/dist/bin/oracle-equities.mjs +1 -1
- package/dist/bin/oracle-gateway.mjs +18 -15
- package/dist/bin/oracle-init.mjs +9 -9
- package/dist/cli/commands/bootstrap.mjs +1 -1
- package/dist/cli/commands/chat.mjs +86 -77
- package/dist/cli/commands/doctor.mjs +8 -6
- package/dist/cli/commands/eval.mjs +1 -1
- package/dist/cli/commands/harness.mjs +6 -6
- package/dist/cli/commands/model.mjs +87 -78
- package/dist/cli/commands/receipt.mjs +5 -0
- package/dist/cli/commands/setup.mjs +1 -1
- package/dist/cli/commands/venues.mjs +3 -0
- package/dist/cli/commands/watch.mjs +16 -0
- package/dist/equities/index.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
- package/public/install.ps1 +102 -0
- package/public/install.sh +1 -1
- package/public/oracle-splash/downloads/index.html +2 -0
- package/public/oracle-splash/index.html +1 -0
- package/public/oracle-splash/install.ps1 +102 -0
- package/public/oracle-splash/install.sh +1 -1
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: self-custodial-onchain-execution
|
|
3
|
+
description: Build public token and NFT scanners that prepare wallet-signed swaps or mints without backend custody, with verified routes, simulations, honest risk coverage, and fail-closed transaction gates.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
> Oracle native tools: `oracle_cli` (read/prepare), `vault_status`, `signer_status`, `signer_execute` (needs human confirmationNonce), `skill_load`. No generic shell. No fleet SSH.
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Self-custodial on-chain execution
|
|
10
|
+
|
|
11
|
+
Use when a scanner, launchpad, or trading dashboard moves from read-only/paper mode to transactions signed by the user's Rabby or EVM wallet.
|
|
12
|
+
|
|
13
|
+
## Authority model
|
|
14
|
+
|
|
15
|
+
A wallet signature proves address control, not operator/admin authority.
|
|
16
|
+
|
|
17
|
+
- Public wallet session: personalized data, paper records, and transactions that the same wallet must sign.
|
|
18
|
+
- Token-gated session: public scope plus an on-chain ownership check.
|
|
19
|
+
- Operator scope: explicit server allowlist or enrollment.
|
|
20
|
+
- Admin scope: separate credentials for backend signers, portfolios, policy changes, and kill switches.
|
|
21
|
+
|
|
22
|
+
An open preview may accept any signed wallet only when every money-moving action remains self-custodial and backend live submission is absent from the build.
|
|
23
|
+
|
|
24
|
+
## Arming posture: key present = armed, user asks = it runs
|
|
25
|
+
|
|
26
|
+
**DEMI correction — do not gate each capability behind its own env flag.**
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
Key present -> capability is ARMED
|
|
30
|
+
User asks for it -> it RUNS
|
|
31
|
+
Autonomous mode -> the ONLY separate opt-in
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Supplying a signing key **is** the authorization decision. Making someone who
|
|
35
|
+
already handed over a key hunt down a second and third `ORACLE_X_ENABLED=1` to
|
|
36
|
+
buy an NFT is friction, not security — and it teaches people to set every flag,
|
|
37
|
+
which is strictly worse than one coherent default. What actually protects the
|
|
38
|
+
user is that every action is **user-initiated** and **bounded by caps at the
|
|
39
|
+
moment it runs**.
|
|
40
|
+
|
|
41
|
+
Autonomous/unattended trading is the sole exception, because it is the only mode
|
|
42
|
+
where the agent initiates value movement without being asked. An agent that
|
|
43
|
+
trades while you sleep is a different risk class from one that buys the NFT you
|
|
44
|
+
just named.
|
|
45
|
+
|
|
46
|
+
Keep an `execute: true` marker at the call site (it stops a send happening as a
|
|
47
|
+
side effect of a read path) but do not add an env arm beside it. `armed` and
|
|
48
|
+
`allowed` are distinct states and must be reported separately: key present with
|
|
49
|
+
no user request is `armed: true, allowed: false`. Only the literal string `"1"`
|
|
50
|
+
enables autonomous — `"true"`, `"yes"`, `"01"` must all stay off.
|
|
51
|
+
|
|
52
|
+
Full rule, the `capabilityStatus` shape, and the retro-fit note (delete arms you
|
|
53
|
+
already added): `references/key-material-and-arming-posture.md`.
|
|
54
|
+
|
|
55
|
+
## Wallet UX
|
|
56
|
+
|
|
57
|
+
Wallet connect is a primary control, not a small utility button. Keep it persistent in the top bar and repeat a dominant connect/sign action inside execution. Discover ALL wallets with EIP-6963 (RH AGENT must support Rabby AND MetaMask), rank by rdns for the default (rabby=0, metamask=1, other=2), and render a picker `<select>` in the top bar when more than one wallet announces. Switching wallets must fully reset the session (token, gate status, balances, quotes). Switch/add the target chain, request accounts, sign short-lived single-use challenges only for authenticated actions, and invalidate sessions on account or chain changes. Copy rule: never hardcode "Rabby" in notices/buttons — use the active `walletName` ("WAITING FOR WALLET", "confirm in your wallet") so MetaMask users aren't told to check Rabby.
|
|
58
|
+
|
|
59
|
+
### Keep wallet identities separate
|
|
60
|
+
|
|
61
|
+
Never collapse these three concepts in API responses or UI:
|
|
62
|
+
|
|
63
|
+
- **Connected wallet:** browser signer and holder-gate identity. Its Robinhood Chain balance comes from its address through RPC.
|
|
64
|
+
- **Agent API key owner:** address bound to the `rhag_` credential. The key authenticates reads/prepares; it is not a private key or spending authority.
|
|
65
|
+
- **Executor wallet:** separate capped signer for unattended actions. Put its address, RH balance, caps, spend today, funding state, and kill-switch state in a dedicated card beside Agent Connect.
|
|
66
|
+
|
|
67
|
+
`GET /api/agent/policy` is policy, not portfolio data. Expose the API-key owner's balance through `GET /api/agent/positions`, and the executor balance through a dedicated status endpoint. Label both with address and chain.
|
|
68
|
+
|
|
69
|
+
A locally generated executor JSON is **not installed** merely because an address exists. Do not tell the user to fund it until the signer has a secure backup, the key has moved directly into production secret storage without chat/log exposure, executor code is deployed, and the website resolves the same address. Never offer a website private-key download. Deploy code/status first, verify the unfunded capped state, then fund, then separately GO-gate broadcasts.
|
|
70
|
+
|
|
71
|
+
### Executor discovery, ownership, and activation
|
|
72
|
+
|
|
73
|
+
An executor address is public, not a secret. Make it easy for the authorized operator to find and verify:
|
|
74
|
+
- Show the full address, copy control, chain, balance, signer-installed state, caps, daily spend, and enabled/halted state in the dedicated Agent Connect card.
|
|
75
|
+
- Expose the same public fields through an authenticated status endpoint. If policy is public, exposing the address there is acceptable, but never expose key material or imply that address visibility grants control.
|
|
76
|
+
- Before funding, derive the address independently from the production signer file and require an exact match with both UI and API.
|
|
77
|
+
|
|
78
|
+
Do not let public users fund one shared operator executor. A server-wide hot wallet is appropriate only for the operator's own capped automation. Multi-user execution needs one isolated executor or session key per user, tied to that user's API-key owner, with a separate balance, ledger, caps, allowlists, and kill switch. Otherwise the product creates pooled custody, cross-user accounting, and shared-compromise risk.
|
|
79
|
+
|
|
80
|
+
Treat activation as a ladder, not one switch:
|
|
81
|
+
1. Deploy signer and status with execution disabled.
|
|
82
|
+
2. Verify production address, file ownership/mode, zero balance, caps, and blocked execute routes.
|
|
83
|
+
3. On explicit GO, enable `agent:execute` only for a controller allowlist.
|
|
84
|
+
4. Never promote a key pasted into chat or screenshots. Revoke it, generate a replacement directly into owner-only secret storage, and verify its scope without printing the raw key.
|
|
85
|
+
5. Keep token/NFT and scanner auto-allowlists empty while enabling scope. Execute scope alone must not create a tradeable asset universe.
|
|
86
|
+
6. Add one verified resource allowlist, fund narrowly, execute one tiny idempotent request, wait for a successful receipt, reconcile, then expand.
|
|
87
|
+
|
|
88
|
+
Report each gate separately. `agent:execute=true` does not mean "trading": an unfunded wallet or empty asset allowlists still blocks every action.
|
|
89
|
+
|
|
90
|
+
### Chat-leaked burner keys are still burn-only
|
|
91
|
+
|
|
92
|
+
If a user pastes a Solana secret key, BTC WIF, seed phrase, or Xverse seed into chat, do **not** import or use it, even when they say it is only a burner. Treat the key as compromised, tell them to generate/import a fresh burner directly on the machine that will sign, and accept only the public address in chat. Long base58 Solana strings are often secret keys; validate by reading the public address only. For Bitcoin inscriptions, prefer a fresh local WIF file that pays commit/reveal fees while inscribing **to** a separate ordinal receive address. Never store an Xverse seed for agentic testing.
|
|
93
|
+
|
|
94
|
+
Creator/operator urgency is not an exception. If DEMI says "use it for now", "save it locally", or "we already did it for EVM", keep the key-ingress invariant and make the safe local path fast: create the owner-only key directory, create empty `0600` files, and provide/run a prompt-only local installer (`read -s`, no echo, no logs) for the user to paste on the signing machine. After install, the agent may report file mode/size, derive public addresses, and check balances/readiness; never print or copy the raw secret.
|
|
95
|
+
|
|
96
|
+
Detailed response pattern: `references/chat-leaked-key-ingestion.md`.
|
|
97
|
+
|
|
98
|
+
### Operator installer, owner-gate, and launch hardening
|
|
99
|
+
|
|
100
|
+
The execution policy is only as strong as the files and launchers that select it. Treat
|
|
101
|
+
operator installation as part of the signer boundary:
|
|
102
|
+
|
|
103
|
+
- Do not let a model-writable adjacent file reassign the human owner of a pre-tool gate.
|
|
104
|
+
Pin a dedicated operator identity in reviewed private code, or load it from an
|
|
105
|
+
integrity-protected authority outside the model's ordinary write surface. Add a regression
|
|
106
|
+
proving `owner.id`-style files cannot change the caller accepted by the hook. Keep
|
|
107
|
+
machine-specific identities out of canonical/public defaults.
|
|
108
|
+
- A text-arm must be scoped to the current conversation and current owner. Resolve the actual
|
|
109
|
+
caller from session state, reject non-owner/unknown sessions, and avoid accepting a stale
|
|
110
|
+
`GO` from an unrelated message as standing authorization.
|
|
111
|
+
- Never `source` credential env files from shell or generated service launchers. That executes
|
|
112
|
+
their contents as code. Require regular non-symlink owner-only files, parse them as data, or
|
|
113
|
+
use the runtime's data-only env-file option. Pass bearer headers through stdin or an
|
|
114
|
+
equivalent non-argv channel, and make authenticated status helpers reject non-loopback
|
|
115
|
+
destinations and reflected secrets.
|
|
116
|
+
- Secret migration/recovery writes must be same-directory temp-file + atomic rename, with
|
|
117
|
+
`lstat` checks that reject symlinks and non-regular destinations. Secure the parent directory,
|
|
118
|
+
use `0600`, refuse orphaned passphrase/keystore mismatches, verify the derived public address
|
|
119
|
+
before replacement, and retain a rollback copy without logging key material.
|
|
120
|
+
- Test installers and launchers as security code: injection-shaped env values remain inert,
|
|
121
|
+
broad file modes fail closed, symlinked destinations are refused, and final status proves the
|
|
122
|
+
execution plane is still disarmed.
|
|
123
|
+
|
|
124
|
+
### One key controls MANY addresses — enumerate before concluding
|
|
125
|
+
|
|
126
|
+
A single secp256k1 key controls P2PKH (`1…`), P2SH-P2WPKH (`3…`), P2WPKH
|
|
127
|
+
(`bc1q…`) and P2TR (`bc1p…`) simultaneously. Old wallets and exchanges default to
|
|
128
|
+
`3…`; modern signing code defaults to `bc1q…`. Funds sitting at one script type
|
|
129
|
+
while the signer builds inputs for another looks *exactly* like "the agent can't
|
|
130
|
+
read the key" or "insufficient funds" — and it is neither.
|
|
131
|
+
|
|
132
|
+
**Before reporting a balance, before "insufficient funds", and before signing,
|
|
133
|
+
derive and check every address type the key controls.** Use the signing library's
|
|
134
|
+
own derivation (`@scure/btc-signer` `p2wpkh`/`p2tr`/`p2pkh`/`p2sh`), never a
|
|
135
|
+
hand-rolled bech32/base58 path — hand-rolling needs the BIP341 TapTweak by hand
|
|
136
|
+
and produces a plausible-looking wrong taproot address.
|
|
137
|
+
|
|
138
|
+
When funds are at the wrong script type, consolidate first: a P2SH-wrapped
|
|
139
|
+
segwit input needs **both** `witnessUtxo` and `redeemScript`. Assert derived
|
|
140
|
+
addresses against expected constants before broadcast, and check the UTXO set
|
|
141
|
+
for ordinal/dust outputs (`<= 1000 sats`) before sweeping — an ordinals address
|
|
142
|
+
with hundreds of funded outputs must never be auto-selected.
|
|
143
|
+
|
|
144
|
+
### Encrypt local signer keys at rest
|
|
145
|
+
|
|
146
|
+
`0600` is one backup tarball, one `~/.config` cloud-sync, or one stolen disk from
|
|
147
|
+
spendable. Wrap key files: scrypt(N=2^17) → KEK, AES-256-GCM wraps a random DEK,
|
|
148
|
+
DEK encrypts the material, JSON header bound as AAD so a KDF-cost downgrade fails
|
|
149
|
+
authentication instead of silently weakening decryption. Two layers mean a
|
|
150
|
+
passphrase rotation rewraps 32 bytes and never exposes the secret.
|
|
151
|
+
|
|
152
|
+
State the threat model honestly — it defends backups, sync, and stolen disks; it
|
|
153
|
+
does **not** defend a live process already holding the key or a keylogger. Never
|
|
154
|
+
oversell a file-level scheme.
|
|
155
|
+
|
|
156
|
+
Operational invariants: verify decrypt-to-byte-identical **before** touching the
|
|
157
|
+
original, never delete the plaintext yourself (move to `.bak`, tell the user to
|
|
158
|
+
`shred -u`), passphrase never in argv (`ps` leaks it), passphrase file outside
|
|
159
|
+
the keys directory, raw key files keep loading unchanged (encryption is opt-in),
|
|
160
|
+
and no code path writes a decrypted key to disk.
|
|
161
|
+
|
|
162
|
+
Both, with the derivation snippet, vault design, and the tests that carry weight:
|
|
163
|
+
`references/key-material-and-arming-posture.md`.
|
|
164
|
+
|
|
165
|
+
## Swap gate
|
|
166
|
+
|
|
167
|
+
Before returning calldata:
|
|
168
|
+
|
|
169
|
+
1. Resolve factory, router, quoter, wrapped native token, and any Universal Router from authoritative docs plus verified explorer source and live bytecode. Explorer names alone are unsafe because clones share names.
|
|
170
|
+
2. Bind token, pool, fee, side, amount, wallet, and quote to the current server-side market record.
|
|
171
|
+
3. Confirm the advertised pool equals `factory.getPool(token, wrappedNative, fee)`.
|
|
172
|
+
4. Quote on-chain, timestamp it, and reject stale or mismatched quotes.
|
|
173
|
+
5. Apply a bounded slippage cap and nonzero `amountOutMinimum`.
|
|
174
|
+
6. Use a short deadline.
|
|
175
|
+
7. For sells, approve only the exact amount. Never default to unlimited approval.
|
|
176
|
+
8. Simulate the exact transaction with `eth_call` from the connected wallet.
|
|
177
|
+
9. Return a wallet-signable transaction. The server never signs.
|
|
178
|
+
10. Re-quote and re-simulate immediately before preparation, not only when displaying the quote.
|
|
179
|
+
|
|
180
|
+
## Atomic pre-buy sellability simulation
|
|
181
|
+
|
|
182
|
+
When honeypot APIs do not support a chain, use a matching verified Universal Router to simulate a full buy and sell in one state-free `eth_call`:
|
|
183
|
+
|
|
184
|
+
1. `WRAP_ETH` into the router.
|
|
185
|
+
2. `V3_SWAP_EXACT_IN` from WETH to token, recipient router, internal payer.
|
|
186
|
+
3. `V3_SWAP_EXACT_IN` from token back to WETH using `CONTRACT_BALANCE`.
|
|
187
|
+
4. Require a minimum WETH balance.
|
|
188
|
+
5. `UNWRAP_WETH` to the simulated sender.
|
|
189
|
+
|
|
190
|
+
Quote in both directions first and set minimum round-trip return below the no-tax reverse quote by a bounded tolerance. Treat quote failure, transfer failure, insufficient sender balance, router revert, or minimum-return miss as `FAIL`.
|
|
191
|
+
|
|
192
|
+
Chain-specific verified addresses, ABI differences, command bytes, and live probe results belong under `references/`. For Robinhood Chain, load `references/robinhood-chain.md` before implementation.
|
|
193
|
+
|
|
194
|
+
### Venue-agnostic round-trip sell-sim (any chain, any DEX)
|
|
195
|
+
|
|
196
|
+
When there is no Universal Router to simulate through, get the same signal from
|
|
197
|
+
quotes: **quote the buy, then quote selling exactly what that buy would yield,
|
|
198
|
+
and compare against what went in.** Pass in a quote function so one module covers
|
|
199
|
+
every chain and venue.
|
|
200
|
+
|
|
201
|
+
DEMI asks for this by name ("a sell sim so people don't ape an unsafe contract
|
|
202
|
+
address or honeypot"). It is the single highest-value guard in the product: the
|
|
203
|
+
most expensive failure in memecoin trading is not a bad entry, it is an entry you
|
|
204
|
+
cannot exit, and a honeypot looks identical to a healthy token on a chart *and*
|
|
205
|
+
on a buy-side quote.
|
|
206
|
+
|
|
207
|
+
Four verdicts, never a bare boolean — the caller must be able to show the user
|
|
208
|
+
why:
|
|
209
|
+
|
|
210
|
+
| Verdict | Trigger |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `HONEYPOT` | sell leg fails to quote, returns zero, or round-trip loss ≥ 5000 bps |
|
|
213
|
+
| `CAUTION` | round-trip loss ≥ 1500 bps (high tax or thin liquidity) |
|
|
214
|
+
| `SAFE` | round trip within normal fees |
|
|
215
|
+
| `UNKNOWN` | buy leg unroutable, or unparseable quote output |
|
|
216
|
+
|
|
217
|
+
Rules that matter:
|
|
218
|
+
|
|
219
|
+
- **Sell the tokens the buy actually produced**, not a guessed amount. Assert
|
|
220
|
+
this in a test by capturing the amount passed to the sell leg.
|
|
221
|
+
- **Buy routes + sell won't quote = HONEYPOT**, not `UNKNOWN`. That asymmetry is
|
|
222
|
+
the classic shape.
|
|
223
|
+
- **Fail CLOSED on `UNKNOWN`.** "Could not verify you can exit" must never read
|
|
224
|
+
as "safe to ape". `allowUnknown` exists only for research surfaces where no
|
|
225
|
+
money moves.
|
|
226
|
+
- Garbage quote output (`outAmount: "not-a-number"`) is `UNKNOWN`, never a false
|
|
227
|
+
`SAFE`.
|
|
228
|
+
- Attach the full result to the thrown error (`err.sellSim`) so the refusal
|
|
229
|
+
carries its evidence.
|
|
230
|
+
|
|
231
|
+
### Structural risk is a SECOND question — run it beside the sell-sim
|
|
232
|
+
|
|
233
|
+
A sell-sim is **behavioural**: it proves the token is sellable *right now*. It
|
|
234
|
+
cannot see a killswitch that has not fired. A token passes a sell-sim perfectly
|
|
235
|
+
while its owner holds `blacklist()`, a mint function, or a fee setter — the
|
|
236
|
+
honeypot is armed, just not triggered yet. So also ask the **structural**
|
|
237
|
+
question: what powers does the deployer still hold?
|
|
238
|
+
|
|
239
|
+
Read bytecode for retained-power selectors (mint / blacklist / pause / fee /
|
|
240
|
+
upgrade) plus proxy implementation slots. Rules that carry the weight:
|
|
241
|
+
|
|
242
|
+
- **Derive selectors with `keccak_256` in code, never transcribe hex.** A wrong
|
|
243
|
+
security constant fails *silently* — it never matches and the guard reports
|
|
244
|
+
clean forever.
|
|
245
|
+
- **Check every proxy slot.** EIP-1967 alone reported **USDC on mainnet as "not
|
|
246
|
+
a proxy"**, because its implementation sits in the older unstructured
|
|
247
|
+
OpenZeppelin slot. Check EIP-1967 + zeppelinos + EIP-1822 and report which
|
|
248
|
+
matched plus the implementation address.
|
|
249
|
+
- **Never return "safe".** Three levels: `unknown-risk` / `elevated` / `high`.
|
|
250
|
+
The best result is "no owner powers found, which is weak evidence, not a
|
|
251
|
+
guarantee" — proxies hide the real logic and absence proves nothing.
|
|
252
|
+
- **An RPC failure is `unavailable`, never clean.** No code at the address is
|
|
253
|
+
`high` (an approval there is unrecoverable). `owner()` alone is `elevated`,
|
|
254
|
+
not `high` — crying wolf on every ordinary token trains people to ignore it.
|
|
255
|
+
- **Take the transport as an option** (`opts.rpcCall`): ESM exports cannot be
|
|
256
|
+
mocked (`Cannot redefine property`), and injection keeps the suite offline.
|
|
257
|
+
|
|
258
|
+
Selector table, proxy slot constants, the NFT drainer-flagging heuristics (the
|
|
259
|
+
marketplace `spam` flag cannot see an unlisted airdrop, and NSFW is not spam),
|
|
260
|
+
and the full registry-wiring checklist for adding a provider:
|
|
261
|
+
`references/structural-contract-and-nft-risk.md`.
|
|
262
|
+
|
|
263
|
+
## NFT access gate
|
|
264
|
+
|
|
265
|
+
Use direct contract RPC as the source of truth for holder-only dashboard access. Do not use OpenSea for access control, it can lag or fail.
|
|
266
|
+
|
|
267
|
+
### But DO use OpenSea as the cross-chain NFT *index*
|
|
268
|
+
|
|
269
|
+
Access control and discovery are different jobs. OpenSea v2 indexes NFTs across
|
|
270
|
+
~23 chains keyed by a chain **slug**, so it is the one place to resolve a bare
|
|
271
|
+
contract address on **any** chain (DEMI: "nfts across all chains should be
|
|
272
|
+
scanned through opensea").
|
|
273
|
+
|
|
274
|
+
Keep a slug↔chainId map (`ethereum:1`, `matic:137`, `base:8453`,
|
|
275
|
+
`arbitrum:42161`, `ape_chain:33139`, `berachain:80094`, `abstract:2741`,
|
|
276
|
+
`ronin:2020`, `zora`, `blast`, `unichain`, `soneium`, `bsc`, … plus `solana`
|
|
277
|
+
with a `null` chainId since it is non-EVM). Expose both directions: the slug is
|
|
278
|
+
what the API path wants, the chainId is what the rest of the agent speaks.
|
|
279
|
+
|
|
280
|
+
Users paste an address without knowing the chain, and guessing wrong is how
|
|
281
|
+
people buy the wrong asset. So fan out across the supported set with bounded
|
|
282
|
+
concurrency (~6) and report **every** hit rather than the first:
|
|
283
|
+
|
|
284
|
+
- `400`/`404` from one chain means "not on this chain" during a fan-out — return
|
|
285
|
+
`null` and keep going, do not abort the scan.
|
|
286
|
+
- one chain erroring must not fail the whole sweep
|
|
287
|
+
- return `{ chainsScanned, found, matches[] }` so "not found anywhere" is
|
|
288
|
+
distinguishable from "we only looked at one chain"
|
|
289
|
+
|
|
290
|
+
Pattern:
|
|
291
|
+
1. User connects wallet and signs a short-lived nonce challenge.
|
|
292
|
+
2. Backend verifies the recovered signer.
|
|
293
|
+
3. Operator allowlist bypasses holder checks for DEMI/admin wallets.
|
|
294
|
+
4. Everyone else must pass `balanceOf(wallet) > 0` against the configured ERC-721 contract, or `balanceOf(wallet, tokenId) > 0` for ERC-1155.
|
|
295
|
+
5. Session token stores the access class, e.g. `operator` or `nft-holder`; API routes check the session server-side.
|
|
296
|
+
6. The locked frontend must not fetch protected scanner/market/risk routes before auth, otherwise expected 401s become noisy console errors and hide real issues.
|
|
297
|
+
|
|
298
|
+
When a specific production wallet needs access without owning the NFT, use a separate access-only allowlist that maps to `nft-holder`, never the development operator bypass. Deployment and verification pattern: `references/production-access-whitelist.md`.
|
|
299
|
+
|
|
300
|
+
Locals Only holder gate reference, pulled from the Polymarket bot UI: HyperEVM chain `999`, contract `0x62FCFAf7573AD8B41a0FBF347AfEb85e06599A75`, name `Locals Only`, symbol `LOCALS`.
|
|
301
|
+
|
|
302
|
+
## NFT mint gate
|
|
303
|
+
|
|
304
|
+
- Discover ERC-721 and ERC-1155 contracts from canonical RPC logs with a bounded startup window and incremental block cursor; use explorer registries only as optional enrichment.
|
|
305
|
+
- Require verified source and ABI.
|
|
306
|
+
- Allow only recognized quantity functions such as `mint(uint256)`, `publicMint(uint256)`, or `mintPublic(uint256)`.
|
|
307
|
+
- Reject ambiguous `mint(address,uint256)` because the integer may be a token ID.
|
|
308
|
+
- Cap quantity.
|
|
309
|
+
- For payable mints, derive price only from a verified no-argument view function.
|
|
310
|
+
- Enforce gas-war limits before returning any wallet-signable mint tx: total gas spend (`gasLimit * maxFeePerGas` or legacy `gasLimit * gasPrice`) must fit the grant/user cap, and optional per-unit `maxFeePerGas` plus `maxPriorityFeePerGas` tip caps must fail closed when exceeded.
|
|
311
|
+
- Treat mint price/value caps and gas caps as separate money bounds. A mint bot with a max price but uncapped gas is not public-safe.
|
|
312
|
+
- Simulate from the connected wallet before returning the transaction.
|
|
313
|
+
- Never accept raw user calldata.
|
|
314
|
+
|
|
315
|
+
### A guard nobody calls is not a guard
|
|
316
|
+
|
|
317
|
+
`validateNftMintGasWar()` shipped with a full test suite and **zero call sites** —
|
|
318
|
+
its only appearance outside its own file was a barrel re-export. The caps were
|
|
319
|
+
documented, tested, and enforced on nothing. Tests prove a guard *works*, not
|
|
320
|
+
that it *runs*:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
rg -n 'guardName\(' src/ bin/ apps/ | grep -v 'export function' | grep -v 'export {'
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Zero means the feature is unprotected regardless of its test file. The
|
|
327
|
+
re-export line is what makes a naive grep look healthy.
|
|
328
|
+
|
|
329
|
+
### Discover the entrypoint; never assume it
|
|
330
|
+
|
|
331
|
+
ERC-721 standardizes ownership and transfer, **not creation**, so every
|
|
332
|
+
collection invents its own entrypoint. Discover it in deployed bytecode; report
|
|
333
|
+
`unsupported` rather than encoding a guess (guessing wrong in a contested mint
|
|
334
|
+
burns gas *and* the allocation), and report **ambiguous** when several match.
|
|
335
|
+
Verified live: Doodles → `mint(uint256)`, BAYC → **none** (it uses `mintApe`),
|
|
336
|
+
which is the proof discovery is reading bytecode rather than hoping.
|
|
337
|
+
|
|
338
|
+
Derive every 4-byte selector with `keccak_256` in code. Two silent bugs in one
|
|
339
|
+
session: `publicMint(uint256)` is `2db11544` (not `26092b83`), and `mintActive()`
|
|
340
|
+
is `25fd90f3` — the value first written, `1249c58b`, is actually **`mint()`**, so
|
|
341
|
+
the mint-open check would have probed a mint *function* as a state flag.
|
|
342
|
+
|
|
343
|
+
### Bid competitively, but bounded
|
|
344
|
+
|
|
345
|
+
Tips come from `eth_feeHistory` **reward percentiles** (p50/p75/p90 by urgency),
|
|
346
|
+
not `eth_maxPriorityFeePerGas` — one node's opinion hides the distribution, and
|
|
347
|
+
in a gas war the distribution is the signal. Median across blocks so one whale
|
|
348
|
+
cannot skew it.
|
|
349
|
+
|
|
350
|
+
`maxFeePerGas` is a **refundable ceiling** (headroom against a climbing basefee
|
|
351
|
+
costs nothing); the tip is real spend that orders you against competitors. So:
|
|
352
|
+
generous multiplier on the ceiling, disciplined percentile on the tip.
|
|
353
|
+
|
|
354
|
+
When a cap would push the bid below observed market, return `viable: false` with
|
|
355
|
+
the arithmetic — **never silently clamp to a losing number**. Paying gas to lose
|
|
356
|
+
is worse than not bidding.
|
|
357
|
+
|
|
358
|
+
### Arm only on an exact request
|
|
359
|
+
|
|
360
|
+
Execute requires **all** of: `actionMode` equal to the exact string `"execute"`,
|
|
361
|
+
`ORACLE_AUTONOMOUS_TRADING=1`, and a grant naming this exact contract and chain
|
|
362
|
+
with `maxTotalGasWei`, `maxValueWei`, and a bounded expiry. `"Execute"`,
|
|
363
|
+
`"exec"`, `true`, `1`, `"arm"`, `"fire"` all resolve to `alert_only`. Downgrade
|
|
364
|
+
with a stated reason rather than erroring — an error teaches the caller to retry
|
|
365
|
+
with looser input. Decide arming at creation so a trigger cannot self-upgrade,
|
|
366
|
+
re-check the spend ceiling at fire time, and treat an unreadable sale flag as
|
|
367
|
+
**not open**.
|
|
368
|
+
|
|
369
|
+
Selector tables, the percentile/multiplier table with live values, the
|
|
370
|
+
`mint(address,uint256)` ambiguity conflict, and the registry-wiring checklist:
|
|
371
|
+
`references/nft-mint-discovery-and-gas-bidding.md`.
|
|
372
|
+
|
|
373
|
+
## OpenSea drop mint + floor sweep gate
|
|
374
|
+
|
|
375
|
+
Marketplace mint/sweep is a second path beside verified-ABI local mints. Same custody model: backend prepares, wallet signs.
|
|
376
|
+
|
|
377
|
+
1. Keep marketplace API keys server-only (`OPENSEA_API_KEY`). Never `VITE_*` or client bundles.
|
|
378
|
+
2. Accept only opensea.io collection URLs; parse slug server-side.
|
|
379
|
+
3. Hard-fail non-target chains (RH bot: Robinhood Chain only). Scanner NFT counts are not proof of mint/sweep readiness.
|
|
380
|
+
4. Drop mint: build mint tx via marketplace API, validate `to`/hex calldata, simulate with `eth_call` from the connected wallet, return wallet-signable payload only after PASS.
|
|
381
|
+
5. Floor listings: best listings sorted ascending; filter to target chain + ACTIVE; expose tokenId, orderHash, price, currency for UI caps.
|
|
382
|
+
6. Floor sweep: cap max items and max price per item (and total spend) before fulfillment. Preview listings against the cap, then request sweep/fulfillment steps.
|
|
383
|
+
7. Normalize marketplace step blobs into concrete `{from,to,data,value}` txs. OpenSea sweep responses may wrap `from` and `to` as `{ value, chainArch }`, return `value` as decimal wei, and expose the chain under `chainIdentifier.chainId`; unwrap and validate all three before simulation. Reject wrong buyer, wrong chain, non-hex data, invalid targets, or missing steps.
|
|
384
|
+
8. Simulate every returned sweep step before returning to the client. On multi-step sweeps, client confirms each step in order and waits for receipt between steps.
|
|
385
|
+
9. Never auto-broadcast. No backend signer. No raw user-supplied marketplace calldata.
|
|
386
|
+
|
|
387
|
+
RH OpenSea endpoint shapes, payment-token native zero-address, and test fixtures: `references/opensea-rh-mint-sweep.md`.
|
|
388
|
+
|
|
389
|
+
For Rootio/Photon-style RH bot scanner + mint UI patterns, including honest `UNKNOWN` treatment for unavailable tax/initial-liquidity data, Quick Buy constraints, gas display, and browser verification, use `references/rhbot-rootio-ui-gates.md`.
|
|
390
|
+
|
|
391
|
+
## Agent Connect API + MCP (RH AGENT)
|
|
392
|
+
|
|
393
|
+
API keys first, MCP second. Full surface, scopes, caps, and verification: `references/agent-connect-api-mcp.md`. For deploy/regression proof before claiming RH AGENT works, use `references/rh-agent-api-mcp-smoke.md`.
|
|
394
|
+
|
|
395
|
+
For limited automatic execution through a separate capped hot wallet, use `references/rh-agent-capped-hot-wallet-executor.md`. That reference also carries the fail-closed security-review defect taxonomy for auditing an executor diff (kill-switch TOCTOU, uncapped gas, provenance-as-trust bypass, non-atomic caps, replay/idempotency, create-time-only authz).
|
|
396
|
+
|
|
397
|
+
Short rules:
|
|
398
|
+
- Locals/operator session creates hashed `rhag_` keys; raw key shown once.
|
|
399
|
+
- Prepare-only scopes are markets/nfts/positions/watch/prepare/shadow.
|
|
400
|
+
- `agent:execute` must be invalid unless `RH_AGENT_EXECUTE_ENABLED=1`; prod deploys should keep it blocked by default.
|
|
401
|
+
- MCP (`server/mcp.js`) is a thin wrapper over the same REST endpoints; exposing an MCP execute tool does not enable execution unless server policy allows it.
|
|
402
|
+
- Signing stays with the connected wallet, a separate capped session key, or a tiny hot wallet explicitly GO-gated by DEMI. Never use DEMI's main seed/private key.
|
|
403
|
+
- x402 is payments later, not trade authority.
|
|
404
|
+
|
|
405
|
+
### Multi-user distribution end-state (DEMI product rule)
|
|
406
|
+
Everyone gets agent auto-prepare; **hot wallet is theirs, on their machine** — not the house executor.
|
|
407
|
+
**Users never set up two Hermes profiles** (dual profiles = operator cost plumbing only).
|
|
408
|
+
|
|
409
|
+
**Two website setup paths (AGENTIC TRADING tabs on rhagent.demi.la):**
|
|
410
|
+
1. **Hosted** — connect wallet → create `rhag_` key → TEST → COPY Hermes/Claude MCP → user signs.
|
|
411
|
+
2. **Your VPS** — self-host rhbot → local MCP (`RH_AGENT_BASE_URL=http://127.0.0.1:8792`) → same prepare model; optional thin hot wallet on *their* box only.
|
|
412
|
+
|
|
413
|
+
Docs: `/agent/setup.md` (server file `public/agent-setup.md`).
|
|
414
|
+
|
|
415
|
+
**Split buys/sells (same plan house + users + MCP):**
|
|
416
|
+
- `server/split-exec.js`: `planSplitAmounts`, `executeSplitSwap`, `prepareSplitSwaps`.
|
|
417
|
+
- `POST /api/agent/execute-trade` → multi-tx house broadcast; fail mid-split **stops**.
|
|
418
|
+
- `POST /api/agent/prepare-trade` + `POST /api/swaps/prepare` → `mode: prepare-split`, `slices[]`, back-compat `prepared` = slice 1; user signs in order with gap.
|
|
419
|
+
- Exit large sells re-slice when above threshold.
|
|
420
|
+
- MCP `rh_prepare_trade` / `rh_prepare_trade_split` → prepare only (still needs API key to some backend).
|
|
421
|
+
- Env: `RH_SPLIT_ENABLED`, `RH_SPLIT_CHUNKS`, `RH_SPLIT_MAX_CHUNK_ETH`, `RH_SPLIT_GAP_MS`. Multi-tx over time is intentional; single-block multicall is weak/optional.
|
|
422
|
+
- Detail: `references/prepare-trade-response-shape.md` + `references/split-execution-and-setup-paths.md`.
|
|
423
|
+
- Local: `rhag_` + born-encrypted keystore → prepare → sign → broadcast.
|
|
424
|
+
|
|
425
|
+
## NFT floor exit plans (minimum price + take profit)
|
|
426
|
+
|
|
427
|
+
NFT positions get server-side exit plans evaluated against the live OpenSea floor, same custody model as token TP/SL: the server tracks and flags, the wallet sells.
|
|
428
|
+
|
|
429
|
+
- Plan record: `{ slug, contract?, entryPriceEth, minPriceEth (defaults to entry), takeProfitPct }` → derived `targetFloorEth = entry * (1 + tp/100)`. Reject targets below the minimum.
|
|
430
|
+
- Evaluation against live floor (OpenSea `/collections/{slug}/stats` → `total.floor_price`): `BELOW_MIN` (floor under minimum, refuse to sell), `WAITING`, `TRIGGERED` (floor ≥ target, tell user to list/sell in wallet), `UNKNOWN` when floor is missing/zero — never treat a missing floor as a trigger.
|
|
431
|
+
- Routes owner-bound via `requireUser`; check endpoint filters by actor + `type === "nft_take_profit"` and audits every check. No auto-sale, ever.
|
|
432
|
+
- OpenSea stats + recent mints (zero-address transfers filtered to target chain) are read surfaces exposed to both UI and Agent Connect (`rh_get_nft_stats`, `rh_get_nft_mints`) so an agent can watch floors without a browser tab.
|
|
433
|
+
|
|
434
|
+
Implementation shape: `server/nft-exit-plan.js` (pure build/evaluate, unit-testable), reuses the existing `exitPolicies` store.
|
|
435
|
+
|
|
436
|
+
## Autonomous SELL execution (exit engine `executeSell` wiring)
|
|
437
|
+
|
|
438
|
+
The token exit engine (`server/exit-engine.js`) ships PREPARE-ONLY — on a stop-loss/TP trigger it prepares the swap and emits `requiresWalletSignature:true`, status `TRIGGERED`; it does NOT sign/broadcast (`executeSell` is passed `null`). To make it truly hands-off (auto sell-back to native ETH on trigger), wire an `executeSell` closure to a NEW sell-only executor (`executePreparedSell` + `assertSellExecutionAllowed`) behind its own flag `RH_AGENT_EXECUTOR_ALLOW_SELL`. This is the ONE legit backend-hot-key unattended path: it's downside protection on a manually-funded, manually-armed, capped, GO-gated position — NOT entry aping (keep buy/sell conceptually split). Full wiring, the sell gate (value==0, skip ETH-cap, keep halt/router/gas/allowlist), the three blocking traps (wallet-alignment, fresh-wallet `kind:"approval"` blocking the first fire, scanner-quoteToken dependency), the rhbot `loadEnvFile`-not-`dotenv` verify gotcha, and the tiny-size live test sequence: `references/autonomous-exit-executor-sell-wiring.md`.
|
|
439
|
+
|
|
440
|
+
## Merge / branch inventory before "shipped" claims
|
|
441
|
+
|
|
442
|
+
When DEMI asks if NFT/OpenSea work is in and ready to merge:
|
|
443
|
+
|
|
444
|
+
1. Diff `main` against feature branches (`origin/feat/*`), not only the current worktree.
|
|
445
|
+
2. Grep for mint + floor/sweep/fulfillment paths. Mint-only hardening is not floor sweep.
|
|
446
|
+
3. State explicitly what would be left out if current `main` were committed/pushed alone.
|
|
447
|
+
4. Prefer integrating missing gates into the active UI branch over claiming a sibling branch is included by adjacency.
|
|
448
|
+
5. For RH AGENT main claims: confirm gate + Agent Connect + shadow agent + `rhagent.demi.la` prod markers, not only the open feature branch.
|
|
449
|
+
|
|
450
|
+
## Real-time discovery and conditional execution
|
|
451
|
+
|
|
452
|
+
A live scanner and a live executor are separate authority domains.
|
|
453
|
+
|
|
454
|
+
- Detect launches from factory events at block cadence, dedupe by transaction hash plus log index, then enrich through the normal scanner before presenting executable data.
|
|
455
|
+
- Push launch and refresh signals to clients with SSE or WebSocket, but retain bounded periodic snapshot polling as fallback.
|
|
456
|
+
- Persist conditional orders with token contract, side, target price, amount, actor, and explicit execution mode.
|
|
457
|
+
- Browser-evaluated limits and TP/SL are **tab-armed** only. Say clearly that closing the tab disables trigger handling.
|
|
458
|
+
- A trigger opens a fresh trade ticket. Re-quote, re-run sellability/simulation, enforce slippage and size limits, then require Rabby confirmation.
|
|
459
|
+
- Never describe tab-armed triggers as unattended execution. Unattended execution requires an audited conditional-order contract or a restricted smart-wallet/session-key permission path. A backend hot key is not an acceptable shortcut.
|
|
460
|
+
- Persist TP/SL entry price and token contract with each rule; percentages without an entry reference cannot be evaluated correctly.
|
|
461
|
+
- Deduplicate triggered rule IDs client-side so each price update does not reopen the same ticket.
|
|
462
|
+
- A `BLIND APE` UX may skip manual evidence review, but it must never bypass route membership, reverse quote/sell simulation, position caps, slippage caps, or wallet confirmation.
|
|
463
|
+
|
|
464
|
+
Implementation pattern and DexScreener embed caveats: `references/realtime-conditional-orders.md`.
|
|
465
|
+
|
|
466
|
+
RPC-native NFT and V3 factory discovery, live proof layers, marketplace runtime verification, temporary smoke-access cleanup, and per-source freshness: `references/rpc-native-discovery-and-live-proof.md`.
|
|
467
|
+
|
|
468
|
+
## Extensible multi-chain scanners (chain = config, not code)
|
|
469
|
+
|
|
470
|
+
When the ask is "do we have a scanner for every chain", the answer is a **framework**,
|
|
471
|
+
not N hand-wired integrations. Define a capability contract, implement it once
|
|
472
|
+
generically over standard JSON-RPC, and let a new chain register from a config object.
|
|
473
|
+
|
|
474
|
+
Two rules keep it honest:
|
|
475
|
+
|
|
476
|
+
- **Unimplemented ≠ faked.** An unsupported capability throws naming what *is*
|
|
477
|
+
supported; it never returns `undefined` a caller reads as a negative result.
|
|
478
|
+
- **Capabilities that need a verified per-chain router (`quote`, `sellSimulation`,
|
|
479
|
+
`prepareUnsignedTx`) stay ABSENT until provenance is recorded.** A chain with no
|
|
480
|
+
verified venue is fail-closed for routing value — read/research still works. That is
|
|
481
|
+
a safe state to advertise, not a gap to apologize for.
|
|
482
|
+
|
|
483
|
+
Ship ONE chain wired end-to-end as the reference so the pattern is copyable, and leave
|
|
484
|
+
the rest at the generic tier rather than bulk-adding unverified routers.
|
|
485
|
+
|
|
486
|
+
Full contract, the `this`-binding trap that silently blinds `scoreRisk`, evidence/risk
|
|
487
|
+
label semantics, round-trip retention thresholds, the `getAmountsOut` array-decode
|
|
488
|
+
trap, and the non-yielding slippage ceiling: `references/extensible-chain-scanner.md`.
|
|
489
|
+
|
|
490
|
+
### Venue adapters are per-AMM-shape, never one bent to fit
|
|
491
|
+
|
|
492
|
+
V3 is not V2 with different addresses: no `getAmountsOut`, prices through a separate
|
|
493
|
+
Quoter, needs an explicit fee tier per hop. Encoding a V3 swap with V2 assumptions
|
|
494
|
+
yields a tx that reverts — or worse, routes through the wrong pool at a price nobody
|
|
495
|
+
quoted. Write a **sibling** adapter and assert separation on the *selector*
|
|
496
|
+
(`d06ca61f` vs `c6a5026a`), which cannot appear in prose by accident.
|
|
497
|
+
|
|
498
|
+
Select by declared shape and require the whole set: a V3 chain needs **both** quoter
|
|
499
|
+
and router. A router alone stays fail-closed — it could encode a swap with no priced
|
|
500
|
+
expectation to guard against, which is exactly the state that produces an unbounded
|
|
501
|
+
fill.
|
|
502
|
+
|
|
503
|
+
**Search every fee tier (100/500/3000/10000) and keep the best.** Load-bearing, not
|
|
504
|
+
thoroughness theatre: liquidity concentrates in one tier and which tier varies by pair
|
|
505
|
+
and chain. Live on Arbitrum WETH→USDC the 500 tier returned 1908.72 vs 1862.15 on the
|
|
506
|
+
100 tier — 2.4% a hardcoded 0.3% default would have eaten.
|
|
507
|
+
|
|
508
|
+
### Verify a venue functionally — codesize proves deployment, not identity
|
|
509
|
+
|
|
510
|
+
Ask the candidate to **price a pair whose answer you can sanity-check**. A contract
|
|
511
|
+
that correctly quotes WETH→USDC *is* a working quoter, whatever any doc claims.
|
|
512
|
+
|
|
513
|
+
The canonical mainnet QuoterV2 address returns ~2109 bytes of bytecode on Base — some
|
|
514
|
+
contract, just not one that can price that chain's pairs. A codesize check allowlists
|
|
515
|
+
it; the functional probe caught it and pointed at Base's real quoter (8273 bytes).
|
|
516
|
+
|
|
517
|
+
Commit the prober as a re-runnable script (`npm run verify:venues`), not a one-off,
|
|
518
|
+
and record provenance as `{ method, source, date, chainId }` — with a test asserting
|
|
519
|
+
`verified.chainId` matches the chain it is listed under. An address verified on
|
|
520
|
+
Arbitrum is not verified on Ethereum, even when the deployment shares an address.
|
|
521
|
+
|
|
522
|
+
## Best-execution routing (cheapest swap / bridge)
|
|
523
|
+
|
|
524
|
+
Cheapest ≠ highest quote. Rank on **net received after gas and fees**, querying every
|
|
525
|
+
source in parallel. Gas is part of the price, and the crossover between "better quote"
|
|
526
|
+
and "cheaper gas" moves with trade size — so any fixed preference is wrong on one side
|
|
527
|
+
of it. Intent venues (CoW) where a solver eats the gas routinely win on net while
|
|
528
|
+
losing on gross.
|
|
529
|
+
|
|
530
|
+
Unknown cost is **never** scored as zero — that is how the worst route wins a
|
|
531
|
+
comparison. Mark it, rank on gross, warn. Quote a spread only when the top two routes
|
|
532
|
+
measure cost the same way, and name the basis.
|
|
533
|
+
|
|
534
|
+
Per-source cost-semantics table, the spread-basis bug caught live, source-outage
|
|
535
|
+
handling (Odos 410 mid-build), bridge duration reporting, and the deterministic test
|
|
536
|
+
targets: `references/best-execution-routing.md`.
|
|
537
|
+
|
|
538
|
+
### Hand the winner to prepare — a comparison is not a signature
|
|
539
|
+
|
|
540
|
+
A ranked winner the user must rebuild by hand is a leak, not a feature. Close the loop:
|
|
541
|
+
**compare → pick → re-quote → build the signable artifact**, so "best route" and "the tx
|
|
542
|
+
you sign" are provably the same route.
|
|
543
|
+
|
|
544
|
+
Two rules dominate:
|
|
545
|
+
|
|
546
|
+
- **Artifact kinds are not interchangeable.** `unsigned-transaction` (LI.FI, ParaSwap,
|
|
547
|
+
0x) gets broadcast; `typed-data-order` (CoW) is signed and submitted to an order API
|
|
548
|
+
with **nothing to broadcast**. Return `artifactKind` and branch on it — conflating
|
|
549
|
+
them fails at *signing time, after tokens were already approved*.
|
|
550
|
+
- **`requiresApproval.spender` is not `destination`.** CoW pulls funds through the
|
|
551
|
+
**vault relayer**, not the settlement contract; approving the wrong one yields an
|
|
552
|
+
order that silently never fills. Read the provider's field, never derive it.
|
|
553
|
+
|
|
554
|
+
Prepare must **re-quote** (the comparison is already stale) and report `driftBps` against
|
|
555
|
+
a bounded tolerance — zero tolerance fails constantly between blocks, unbounded silently
|
|
556
|
+
prepares a worse trade than the one agreed to.
|
|
557
|
+
|
|
558
|
+
Validate the taker locally: reject missing, malformed, and placeholder/burn addresses
|
|
559
|
+
before any network call. **Quoting is anonymous; preparing is not.** ParaSwap rejects
|
|
560
|
+
`0x…dEaD`; other venues happily build a transaction for an address nobody controls, so
|
|
561
|
+
relying on the venue means relying on the least careful one.
|
|
562
|
+
|
|
563
|
+
Some venues refuse to build calldata until the approval **already exists on-chain**
|
|
564
|
+
(ParaSwap: `Not enough WETH allowance given to TokenTransferProxy`). That is a genuine
|
|
565
|
+
ordering constraint — classify it (`approval-required-first`) rather than surfacing a
|
|
566
|
+
bare HTTP 400 that reads like the router is broken. Read `err.body`, not just
|
|
567
|
+
`err.message`: the status line alone flattens every venue rejection into a generic
|
|
568
|
+
provider error.
|
|
569
|
+
|
|
570
|
+
Never dead-end and never substitute silently: an unpreparable winner names an
|
|
571
|
+
`alternative`, and a forced non-winner is flagged `wasWinner: false`.
|
|
572
|
+
|
|
573
|
+
Full artifact table, the CoW relayer assertion, error-classification pattern, drift math,
|
|
574
|
+
and the key-gate test trap that exposed an unreachable preparer:
|
|
575
|
+
`references/route-prepare-handoff.md`.
|
|
576
|
+
|
|
577
|
+
### Bridge prepare: the artifact is a sequence, and confirmation is not arrival
|
|
578
|
+
|
|
579
|
+
Same compare→prepare shape, but every bridge failure is **unrecoverable** — funds leave
|
|
580
|
+
the origin chain before anything confirms on the destination. Four rules:
|
|
581
|
+
|
|
582
|
+
- **Always return a LIST of transactions**, even when there is one. Some routes need an
|
|
583
|
+
approval *and* a deposit; a bare object for one source and a list for another
|
|
584
|
+
guarantees someone signs the first item and believes they are done — **approved, not
|
|
585
|
+
bridged**. State the count, and that multi-tx routes must be signed in order.
|
|
586
|
+
- **Echo both chains.** Transactions execute on the origin and credit the destination;
|
|
587
|
+
showing only the signing chain hides half the trust decision. **Refuse** a tx whose
|
|
588
|
+
`chainId` ≠ origin (`chain-mismatch`) rather than letting a wallet prompt on the wrong
|
|
589
|
+
network.
|
|
590
|
+
- **Say "not atomic" unprompted.** Origin confirmation does not mean funds arrived. This
|
|
591
|
+
is the most common bridging panic and silence causes **double-sends** — ship the ETA
|
|
592
|
+
and an explicit "do not re-send while pending".
|
|
593
|
+
- **Duration is reported, never scored.** Saving $2 over 30 minutes vs paying $2 more to
|
|
594
|
+
land in 20 seconds is the caller's trade-off. Assert duration never enters score math.
|
|
595
|
+
|
|
596
|
+
**Every fact stated in prose that a caller may act on needs a structured field carrying
|
|
597
|
+
it.** A `no-prepare-path` branch named the fallback in its message but returned
|
|
598
|
+
`alternative: undefined` — and that branch fires often, because Across wins on gross
|
|
599
|
+
(unreported gas) yet cannot be prepared. An agent must not have to regex an error string.
|
|
600
|
+
|
|
601
|
+
### Never brand a public artifact with a private name
|
|
602
|
+
|
|
603
|
+
A prepared transaction carried `madSlippage` — the *private* codebase's name — in the
|
|
604
|
+
payload handed to users (16 occurrences, 14 files). This leak class is invisible to a
|
|
605
|
+
secret scan (no key material), a portability test (no path), and a docs test (not prose):
|
|
606
|
+
it lives in the **output payload shape**. Audit field names in anything a stranger
|
|
607
|
+
receives, not just source comments and env vars.
|
|
608
|
+
|
|
609
|
+
Migrate with a **dual-read window**, never a hard cut: producers emit only the neutral
|
|
610
|
+
name, consumers accept `new ?? legacy` (an artifact prepared *before* the rename must not
|
|
611
|
+
fail its guard *after* it), and keep one legacy-named fixture so back-compat stays
|
|
612
|
+
exercised rather than assumed.
|
|
613
|
+
|
|
614
|
+
## Non-EVM prepare lanes (Solana, HyperCore, Bitcoin)
|
|
615
|
+
|
|
616
|
+
Same custody model, different artifact. A Jupiter or Magic Eden prepare is a base64
|
|
617
|
+
**serialized Solana transaction**; a HyperCore stake/delegate/unstake is **EIP-712
|
|
618
|
+
typed data** signed with an EVM key and POSTed to `/exchange` (there is no chain to
|
|
619
|
+
broadcast to — never call it "broadcast"); Bitcoin is a PSBT. Returning one generic
|
|
620
|
+
"prepared" blob across these is the same category error as conflating an unsigned tx
|
|
621
|
+
with a CoW order.
|
|
622
|
+
|
|
623
|
+
Two rules that carry most of the weight:
|
|
624
|
+
|
|
625
|
+
- **Prepare is not proof; simulate is.** Jupiter builds a valid-looking swap for a
|
|
626
|
+
placeholder pubkey and it fails one step later at
|
|
627
|
+
`simulateTransaction: ... failed to sanitize accounts offsets correctly`. An E2E that
|
|
628
|
+
stops at "returned base64" proves nothing — assert `err === null` from a real
|
|
629
|
+
simulate against a real funded pubkey, parameterized by env.
|
|
630
|
+
- **Enforce prepare-only structurally.** Scan the module's own export names for
|
|
631
|
+
`submit|broadcast|send|sign` and fail the test if one exists, rather than trusting
|
|
632
|
+
convention. (Watch the `/sign/i` vs `HL_SIGNATURE_CHAIN_ID` false positive — use an
|
|
633
|
+
explicit allowlist, not a lookahead.)
|
|
634
|
+
|
|
635
|
+
Provider auth tiers probed live, the Magic Eden cap-before-network-call test, HYPE's
|
|
636
|
+
8-decimal wei and the two-bucket stake/delegate/undelegate/unstake sequence with its
|
|
637
|
+
7-day queue, and the public-constant test-fixture rule:
|
|
638
|
+
`references/non-evm-prepare-lanes.md`.
|
|
639
|
+
|
|
640
|
+
### Ordinals inscriptions: a confirmed transaction is not a working inscription
|
|
641
|
+
|
|
642
|
+
The hardest Bitcoin failure class is one where **everything reports success**. The
|
|
643
|
+
broadcast returns 200, the tx confirms in a block, your local content hash matches
|
|
644
|
+
byte-for-byte — and the inscription is dead. It is valid Bitcoin that simply is not
|
|
645
|
+
an inscription, and nothing in the send path can tell you.
|
|
646
|
+
|
|
647
|
+
Three encoding bugs produced exactly that on one release, each cheap to detect
|
|
648
|
+
locally and expensive to detect on-chain:
|
|
649
|
+
|
|
650
|
+
- **A spendable taproot internal key means the envelope never enters the witness.**
|
|
651
|
+
`p2tr(xOnly, {script: leaf})` lets the signer take the cheaper key path, so the
|
|
652
|
+
body is never revealed and the node rejects it as an *invalid Schnorr signature* —
|
|
653
|
+
which reads as a signing bug and is not one. Ordinals need a **NUMS (unspendable)**
|
|
654
|
+
internal key: `p2tr(undefined, {script: leaf})`. **The tell is `vsize`**: a 28KB
|
|
655
|
+
payload cannot fit in 111 vbytes, so check the size before debugging the signature.
|
|
656
|
+
- **`allowUnknownInputs: true` is required to finalize.** An ordinals envelope is not
|
|
657
|
+
a standard script template, so `@scure/btc-signer` classifies the leaf as `unknown`
|
|
658
|
+
and throws `Finalize: Unknown tapLeafScript`. `allowUnknownOutputs` alone is not
|
|
659
|
+
enough — different flags, different sides of the transaction.
|
|
660
|
+
- **The body tag is a bare `OP_0` (`0x00`), not a 1-byte push of zero.**
|
|
661
|
+
`encodePush([0])` emits `01 00`; ord reads that as an unknown tag, skips it, and
|
|
662
|
+
records an inscription **with an empty body**. This one confirms *and indexes*, so
|
|
663
|
+
the only symptom is a blank render and `content size: 0 bytes`.
|
|
664
|
+
|
|
665
|
+
**Diff against a live indexed inscription rather than reading the spec.** Pulling a
|
|
666
|
+
recently-indexed reveal off mainnet settles in one command what spec-reading argues
|
|
667
|
+
about for an hour.
|
|
668
|
+
|
|
669
|
+
**Gate the reveal on envelope structure before it spends anything** — assert the
|
|
670
|
+
content-type tag is `0101`, the body tag is `0x00`, `vsize > 1000`, and the body head
|
|
671
|
+
appears in the raw hex. Then verify *after* broadcast by pulling the tx back and
|
|
672
|
+
reassembling the body from the witness pushes, never by re-hashing your local file:
|
|
673
|
+
that matched perfectly through both dead attempts.
|
|
674
|
+
|
|
675
|
+
Funds at a superseded **key-spendable** commit are recoverable via the key path by
|
|
676
|
+
rebuilding the *original* leaf encoding — assert the derived address equals the funded
|
|
677
|
+
one before signing. A NUMS commit is not recoverable (that is the point), so size its
|
|
678
|
+
output at exactly `revealFee + 546`.
|
|
679
|
+
|
|
680
|
+
Byte-level envelope layout, the `finalizeIdx` source that mandates
|
|
681
|
+
`allowUnknownInputs`, the reassembly parser, the recovery script, and why the
|
|
682
|
+
operator's "0 bytes" report *was* the diagnosis:
|
|
683
|
+
`references/ordinals-inscription-envelope.md`.
|
|
684
|
+
|
|
685
|
+
## Pre-publish security audit (before a repo goes public)
|
|
686
|
+
|
|
687
|
+
Run an adversarial review with **several independent model lineages** before
|
|
688
|
+
open-sourcing anything that moves money. One model is a spot check; three is an
|
|
689
|
+
audit. Agreement across lineages is the strongest evidence a finding is real
|
|
690
|
+
(Grok and Opus independently reported the same three CRITICALs), and each lineage
|
|
691
|
+
catches defects the others miss (Fable alone found a capless transfer guard that
|
|
692
|
+
authorized `MaxUint256 - 1`).
|
|
693
|
+
|
|
694
|
+
Mechanics that matter: dispatch a **model** through a neutral profile
|
|
695
|
+
(`hermes -p <neutral> -m <model-id>`), because a dispatch-only profile's SOUL will
|
|
696
|
+
refuse and try to hand the audit to a bus instead; the brief must **forbid
|
|
697
|
+
delegation** or subagents time out and return an empty log; run in background
|
|
698
|
+
with `notify_on_complete`, since a real audit takes 20-45 minutes.
|
|
699
|
+
|
|
700
|
+
**Provider safety filters block adversarial framing.** OpenAI-family models
|
|
701
|
+
refuse with a cyber-risk flag — that is provider policy, not a harness bug, so do
|
|
702
|
+
not debug the gateway. Reframe honestly as *pre-release engineering review of my
|
|
703
|
+
own first-party code*, ask about **defect classes** ("input shapes where a limit
|
|
704
|
+
is silently not applied") rather than exploits, and drop the words *attacker*,
|
|
705
|
+
*exploit*, *bypass*, *drain*. If it still refuses, that lane is unavailable —
|
|
706
|
+
say so and proceed with the lanes that answered.
|
|
707
|
+
|
|
708
|
+
**Verify every fix by execution**, one script printing `HOLDS | BROKEN` with
|
|
709
|
+
observed evidence per finding. This caught a wrong function name in the probe
|
|
710
|
+
itself; an unverified fix and an unverified probe look identical on paper. Assert
|
|
711
|
+
the optimization still survives alongside the fix (dedupe still collapses
|
|
712
|
+
same-credential calls after the cache key was hardened).
|
|
713
|
+
|
|
714
|
+
**Report findings without loss verbs.** Writing that a hostile object "drained
|
|
715
|
+
Satflow, Magic Eden, 1inch, 0x, OpenSea" made DEMI ask *"wait they drained us?"* —
|
|
716
|
+
these were code defects found by reading source, not incidents. Say "would have
|
|
717
|
+
allowed" / "was reachable in code", state up front that nothing was exploited,
|
|
718
|
+
name why each finding could not have fired (a cross-tenant cache leak needs two
|
|
719
|
+
concurrent users with different keys; a private repo has one), and **verify
|
|
720
|
+
balances live and show the numbers** the moment the owner sounds alarmed — before
|
|
721
|
+
any explanation. Finding these while the repo is still private is the system
|
|
722
|
+
working, not failing.
|
|
723
|
+
|
|
724
|
+
Full brief template, reframing language, verification pattern, and the HMAC
|
|
725
|
+
retro-fit note (re-sign mutations so bound-tests still test bounds, and add one
|
|
726
|
+
unsigned-mutation test for forgery): `references/prepublish-multimodel-audit.md`.
|
|
727
|
+
|
|
728
|
+
Round-7 concrete repros (allowance decode parity, fresh-window on approvals,
|
|
729
|
+
approvalLike dest allowlist, mandatory slippage HMAC, HL action allowlist,
|
|
730
|
+
keyFile non-leak) and the test-secret pinning pattern:
|
|
731
|
+
`references/oracle-round7-guard-parity.md`.
|
|
732
|
+
|
|
733
|
+
### When your own scanner flags your own public output, change the OUTPUT
|
|
734
|
+
|
|
735
|
+
A public-plane response scanner returning 500 `secret-leak-blocked` on a route
|
|
736
|
+
that ships no secret is a signal about the payload, not a reason to soften the
|
|
737
|
+
rule. The audit chain's genesis hash (64 zero hex chars) tripped
|
|
738
|
+
`raw-32-byte-hex-bare`, and it was right to: that shape is indistinguishable from
|
|
739
|
+
a private key.
|
|
740
|
+
|
|
741
|
+
Wrong fixes, in ascending order of damage: add the field to the hex64 allowlist
|
|
742
|
+
(converts a control into a formality, and the next value under that name is
|
|
743
|
+
unchecked forever); special-case "all zeros is fine" (paddable — invites a
|
|
744
|
+
"mostly zeros" rule a real key can satisfy); `0x`-prefix it (fails
|
|
745
|
+
`raw-32-byte-hex-key`, correctly, since that is the canonical key shape).
|
|
746
|
+
|
|
747
|
+
Right fix: **stop shipping a key-shaped value.** Genesis carries zero entropy, so
|
|
748
|
+
describe it (`genesisBytes: 32`, `genesisDescription: "32 zero bytes"`) and let
|
|
749
|
+
the integrator reconstruct it. Leave a comment saying why, or a later session
|
|
750
|
+
"simplifies" it back. Ask in order: does the consumer need this exact value or a
|
|
751
|
+
description; can it be a length/boolean/label/prefix instead; and only then, is
|
|
752
|
+
the field name allowlist-worthy on its own merits independent of today's
|
|
753
|
+
inconvenience.
|
|
754
|
+
|
|
755
|
+
**Keep both scan layers** — they disagreed here. The graph walk gives the
|
|
756
|
+
actionable path (`$.audit.genesis`); the serialized scan is authoritative because
|
|
757
|
+
`toJSON()`, getters, symbol keys and BigInt coercion put bytes on the wire the
|
|
758
|
+
graph never visits. One intermediate attempt passed serialized while still
|
|
759
|
+
failing graph.
|
|
760
|
+
|
|
761
|
+
**Dogfooding a secret scanner over first-party source returns false positives, and
|
|
762
|
+
you must say so.** 67 files produced 7 findings, all benign on inspection: a
|
|
763
|
+
zero-address constant, an ERC1967 bytecode fragment, and — four times — the
|
|
764
|
+
scanner's own rule definitions (`{ rule: "keystore-path", re: /keystore/i }`).
|
|
765
|
+
Same class as a forbidden-token test that scans its own test file: exclude the
|
|
766
|
+
rule-defining files or strip obvious regex sources first. Report the real
|
|
767
|
+
false-positive rate rather than "7 blockers found", and do not tune the scanner
|
|
768
|
+
into silence either — the public-boundary scan (0 findings) was the check that
|
|
769
|
+
actually mattered and it was clean.
|
|
770
|
+
|
|
771
|
+
Full worked case, the two-layer disagreement, and the false-positive table:
|
|
772
|
+
`references/secret-scanner-flags-own-output.md`.
|
|
773
|
+
|
|
774
|
+
## Venue datapoint joins (derive once, not per surface)
|
|
775
|
+
|
|
776
|
+
When a venue's read API returns **parallel arrays** and stringly-typed numbers
|
|
777
|
+
(Hyperliquid `metaAndAssetCtxs` → `[{universe}, [ctx…]]`, joined by index), every
|
|
778
|
+
chart, card and alert ends up re-deriving the same math and they drift. Do the
|
|
779
|
+
join **once** in a provider module and expose flat rows.
|
|
780
|
+
|
|
781
|
+
The derivations worth centralizing, because they are the ones people get wrong:
|
|
782
|
+
|
|
783
|
+
- **Index-joined metadata + context.** `universe[i]` pairs with `assetCtxs[i]`;
|
|
784
|
+
leverage/decimals come from one, price/OI/funding from the other.
|
|
785
|
+
- **Open interest is denominated in the COIN.** Multiply by mark for USD.
|
|
786
|
+
- **Funding is a per-interval decimal** (8h on HL). Annualize explicitly
|
|
787
|
+
(`rate * (8760/8) * 100`) and return **both** forms — consumers reach for APR
|
|
788
|
+
and silently mislabel the raw rate as one.
|
|
789
|
+
- **Filter delisted markets** out of boards and totals.
|
|
790
|
+
- **Missing numeric → `null`, never `NaN`.** A sparse context with no
|
|
791
|
+
`prevDayPx` must yield `change24hPct: null`, not a value that renders as
|
|
792
|
+
`NaN%`.
|
|
793
|
+
|
|
794
|
+
Ship the board cuts (`gainers`/`losers`/`byVolume`/`byOpenInterest`/funding
|
|
795
|
+
extremes) from the same joined rows so a leaderboard can never disagree with the
|
|
796
|
+
market table. Test against **fixed fixtures**, asserting the arithmetic
|
|
797
|
+
(10 coins @ $100 → `1000` OI USD; `0.0001` per 8h → `10.95%` APR) rather than
|
|
798
|
+
live values.
|
|
799
|
+
|
|
800
|
+
**Read lanes are not trade lanes — say so unprompted.** Hyperliquid `/info`
|
|
801
|
+
gives markets, positions, fills and book; **placing a perp order is a separate
|
|
802
|
+
action set** (`order`, `cancel`, `modify`, `updateLeverage`) EIP-712-signed to
|
|
803
|
+
`/exchange`. An `/exchange` URL constant existing in a staking module does not
|
|
804
|
+
mean orders are wired. When the owner asks "can we trade perps?", grep for the
|
|
805
|
+
order-placement function and answer from that, not from the presence of a venue
|
|
806
|
+
integration.
|
|
807
|
+
|
|
808
|
+
### Perp order lanes (limit / market / leverage / brackets)
|
|
809
|
+
|
|
810
|
+
When that answer is "no" and the owner asks for it, the trade lane is its own
|
|
811
|
+
prepare-only module. Two things carry most of the risk:
|
|
812
|
+
|
|
813
|
+
- **Venue precision is a correctness requirement, not tidiness.** Hyperliquid
|
|
814
|
+
rejects over-precise orders, and a rejected order is indistinguishable from a
|
|
815
|
+
missed fill at the moment it matters. Price obeys **both** a 5-significant-figure
|
|
816
|
+
cap and a `6 − szDecimals` decimal cap (tighter wins; integers are exempt);
|
|
817
|
+
size rounds to `szDecimals`. Round in exact decimal with `BigInt`, never through
|
|
818
|
+
a float, and **throw** when a size rounds to zero rather than sending it.
|
|
819
|
+
- **There is no market order type.** A "market" order is an IOC limit priced
|
|
820
|
+
aggressively off mark (`mark * (1 ± bps/10_000)`), so it *requires* a live mark
|
|
821
|
+
price and reuses the house **100 bps** slippage ceiling.
|
|
822
|
+
|
|
823
|
+
Leverage is bounded by the asset's `maxLeverage`, cross vs isolated is explicit
|
|
824
|
+
(never a silent default that hides which balance is at risk), and above ~20x
|
|
825
|
+
attach the liquidation distance — `100 / leverage` ≈ the adverse move that
|
|
826
|
+
liquidates, before fees. Bracket protection legs must be **reduce-only and on the
|
|
827
|
+
opposite side**; a non-reduce-only leg opens a *new* position on trigger.
|
|
828
|
+
|
|
829
|
+
Wire shapes, the rounding table with observed values, asset-index resolution, the
|
|
830
|
+
full guardrail list, and the fixture trap (asserting sig-figs with `szDecimals=5`
|
|
831
|
+
actually exercises the decimal limit): `references/hyperliquid-perp-orders.md`.
|
|
832
|
+
|
|
833
|
+
### Crossing into live execution (sign + submit)
|
|
834
|
+
|
|
835
|
+
When the owner asks to **trade, not prepare**, the signing lane is its own
|
|
836
|
+
provider — not a flag on the prepare module. Keep the prepare modules free of
|
|
837
|
+
write exports and add one execution provider beside them.
|
|
838
|
+
|
|
839
|
+
Three things carry the risk:
|
|
840
|
+
|
|
841
|
+
- **Verify the signer against the venue's own known-answer vector, and PULL that
|
|
842
|
+
vector from the SDK source rather than recalling it.** An unverified signer
|
|
843
|
+
produces orders the venue silently rejects, which is indistinguishable from
|
|
844
|
+
"not trading". A first attempt here failed against a fixture reconstructed from
|
|
845
|
+
memory — the implementation was correct and the *expected value* was invented;
|
|
846
|
+
fetching the real `signing_test.py` fixture matched immediately.
|
|
847
|
+
- **A transport `200` is not a fill.** Hyperliquid returns `status: "ok"` while
|
|
848
|
+
individual entries in `response.data.statuses[]` carry rejections. Surface the
|
|
849
|
+
venue's error and return **no order id** rather than inventing one.
|
|
850
|
+
- **The key never appears in a returned object** — assert it with a
|
|
851
|
+
`JSON.stringify(result).includes(...)` test. The derived address is fine to
|
|
852
|
+
surface; the material is not.
|
|
853
|
+
|
|
854
|
+
Give execution **its own provider id** and exempt exactly that id from the
|
|
855
|
+
write-op denylist, so the exemption is one greppable line rather than a weakened
|
|
856
|
+
predicate — and assert the boundary on a **structured** catalog field
|
|
857
|
+
(`execution: "live"`), never a regex over free-text notes the catalog projection
|
|
858
|
+
may not even expose.
|
|
859
|
+
|
|
860
|
+
When a prepare-only project grows an exec lane, grep the docs for "never signs"
|
|
861
|
+
and **scope** the claim (routing still never signs) instead of leaving a blanket
|
|
862
|
+
promise that is now false in one module.
|
|
863
|
+
|
|
864
|
+
Action-hash construction, the phantom-agent EIP-712 envelope, the msgpack
|
|
865
|
+
key-order dependency, key resolution, and venue-verdict parsing:
|
|
866
|
+
`references/hyperliquid-live-execution.md`.
|
|
867
|
+
|
|
868
|
+
## Testing security guards that live in the transport
|
|
869
|
+
|
|
870
|
+
Two traps make a security test **pass while the hole is open**:
|
|
871
|
+
|
|
872
|
+
- **`fetch()` rewrites the `Host` header from the URL.** A DNS-rebinding test
|
|
873
|
+
written with `fetch(url, { headers: { Host: 'attacker.example' } })` silently
|
|
874
|
+
sends the loopback Host, gets `200`, and reports the guard broken — or worse,
|
|
875
|
+
reports it working when it is not. Use a raw `node:net` socket and write the
|
|
876
|
+
request line and headers verbatim.
|
|
877
|
+
- **A spawned server hangs the runner.** Give the test an explicit
|
|
878
|
+
`{ timeout: N }`, spawn `detached: true` and kill the **process group**
|
|
879
|
+
(`process.kill(-proc.pid)`) — an orphan keeps the port and the next run blocks
|
|
880
|
+
on startup. And pass the server's **actual** env var names: a generic `PORT`
|
|
881
|
+
that the server reads as `ORACLE_DATA_PORT` starts it on its default port and
|
|
882
|
+
the readiness poll never succeeds, which reads as "server is broken."
|
|
883
|
+
|
|
884
|
+
### `assert.throws` on an ASYNC function asserts nothing
|
|
885
|
+
|
|
886
|
+
`assert.throws(() => asyncFn(...), /pattern/)` **always fails** — the call returns
|
|
887
|
+
a promise rather than throwing, so the assertion is testing the wrong thing while
|
|
888
|
+
appearing to cover the guard. Found live: a 0/0 unprotected-LP-exit guard whose
|
|
889
|
+
implementation was correct the whole time, but whose test had never actually
|
|
890
|
+
exercised it.
|
|
891
|
+
|
|
892
|
+
```js
|
|
893
|
+
// WRONG: the guard is never asserted; the rejection is unhandled
|
|
894
|
+
assert.throws(() => lpPrepareDecrease({ amount0Min: "0", amount1Min: "0" }), /unprotected/);
|
|
895
|
+
|
|
896
|
+
// RIGHT
|
|
897
|
+
await assert.rejects(() => lpPrepareDecrease({ ... }), /unprotected/);
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
Grep the suite for `assert.throws` whose callee is `async` after adding any
|
|
901
|
+
`await` to a guard path. A test that has never seen its guard fire is not
|
|
902
|
+
coverage, and this shape hides that fact.
|
|
903
|
+
|
|
904
|
+
### Two test files binding the same port collide under a concurrent runner
|
|
905
|
+
|
|
906
|
+
`node --test` runs FILES in parallel. Two suites that each `spawn` a server on a
|
|
907
|
+
hardcoded `PORT = 18801` pass individually and fail together — one process loses
|
|
908
|
+
the bind, and its tests surface as `404`/`ECONNREFUSED` assertion failures that
|
|
909
|
+
read exactly like a broken product route.
|
|
910
|
+
|
|
911
|
+
**The tell: green in isolation, red in the full run.** That combination is almost
|
|
912
|
+
never a product bug. Give every server-spawning test file a distinct port and
|
|
913
|
+
comment why. Verify the same way you found it — run the file alone, then run the
|
|
914
|
+
full suite, and require both green.
|
|
915
|
+
|
|
916
|
+
## Risk coverage
|
|
917
|
+
|
|
918
|
+
Do not call holder concentration bundle detection. Bundles require launch-block transaction clustering and common-funder evidence. Label each check `LIVE`, `STALE`, `UNKNOWN`, or `UNAVAILABLE`, include timestamps and limitations, and distinguish near-real-time polling from block-by-block surveillance.
|
|
919
|
+
|
|
920
|
+
## Public data-plane HTTP hardening
|
|
921
|
+
|
|
922
|
+
A self-hosted scanner runs on free public venue APIs, where the two dominant failures
|
|
923
|
+
are rate limiting and one slow venue poisoning a whole health sweep. Fix both in the
|
|
924
|
+
shared HTTP helper, not in thirty providers: bounded retry that honours `Retry-After`
|
|
925
|
+
and **never retries a 4xx**, single-flight dedupe so N concurrent identical GETs cost
|
|
926
|
+
one upstream call, and `mapLimit` fan-out (~8) instead of `Promise.all` across every
|
|
927
|
+
provider. Dedupe is a request collapser, not a cache — no TTL, so quotes and balances
|
|
928
|
+
never go stale. Prove dedupe by counting upstream calls, never by timing.
|
|
929
|
+
|
|
930
|
+
Full rules, the key-shape that keeps keyed and keyless callers separate, and the
|
|
931
|
+
pitfalls: `references/public-data-plane-hardening.md`.
|
|
932
|
+
|
|
933
|
+
## Verification
|
|
934
|
+
|
|
935
|
+
- Unit-test route mismatch, stale quote, slippage overflow, exact approval, deadline, simulation revert, ambiguous mint ABI, unsupported fee paths, OpenSea non-RH chain rejection, max-price sweep block, and mint/sweep simulation failure.
|
|
936
|
+
- Run a live read-only quote against a real pool.
|
|
937
|
+
- Run one live atomic round-trip `eth_call` against a known executable pool and one failing token fixture.
|
|
938
|
+
- Verify the merged server boots, not only frontend build/tests. Confirm cwd/port is the worktree under test, not a stale sibling checkout.
|
|
939
|
+
- For public unsigned BFFs beside an executor, run them as a separate loopback-only service with execution envs explicitly off; verify the public/unsigned headers and prove executor routes 404/405 on that public port before claiming the self-custody boundary is enforced.
|
|
940
|
+
- When another coequal agent intentionally lands a private execution plane in the same repo, do not delete those files as "pollution" by default. Reconcile on a fresh branch from the integrated base, preserve both lanes, and enforce the custody boundary instead: public Oracle/BFF/UI code must not import private signer/executor/Solana execution modules.
|
|
941
|
+
- Browser-test wallet discovery, chain switching, quote, approval, swap, local mint, OpenSea load, and sweep UI states without broadcasting funds during QA.
|
|
942
|
+
- Before merge-ready claims for NFT scope: inventory sibling branches and confirm mint + floor-sweep paths exist in the tree being pushed.
|
|
943
|
+
|
|
944
|
+
## Pitfalls
|
|
945
|
+
|
|
946
|
+
- Paper endpoints presented as execution.
|
|
947
|
+
- Any-wallet sessions accidentally inheriting operator/admin powers.
|
|
948
|
+
- Trusting a named explorer contract without checking constructor-bound factory/WETH.
|
|
949
|
+
- Testing only the buy route and calling the token safe.
|
|
950
|
+
- Client-supplied market objects or calldata reaching the signer unchanged.
|
|
951
|
+
- Treating a successful quote as proof that token transfers or sells work.
|
|
952
|
+
- Claiming OpenSea mint/sweep shipped because a scanner shows NFT collections or a sibling branch is nearby.
|
|
953
|
+
- Merging mint-only OpenSea hardening and calling floor sweep done.
|
|
954
|
+
- Putting marketplace API keys in `VITE_*` or frontend bundles.
|
|
955
|
+
- Accepting marketplace fulfillment steps without per-step simulation and spend caps.
|
|
956
|
+
- Calling an SSE `ready` handshake proof that a real factory launch was observed.
|
|
957
|
+
- Calling browser-only limit or TP/SL monitoring unattended execution.
|
|
958
|
+
- Claiming a DexScreener graph is working because the iframe shell loads when no candles render.
|
|
959
|
+
- Broad wallet-trigger selectors such as `[aria-expanded="true"] span` rotating provider/balance text when connected-state markup wraps content in a span. Give the chevron a semantic class, rotate only that class, and regression-test the selector plus the open state.
|
|
960
|
+
- Testing only logged-out wallet UI. Connected/open states may render different wrappers, balances, provider names, and transforms; inspect computed styles for every child and verify the deployed asset hash when PWA caching or concurrent branches are possible.
|
|
961
|
+
- Capping only `tx.value` and calling the executor "capped." Gas is separate money — unbounded gasLimit/fee drains the wallet outside the ETH caps. See the executor security-review taxonomy.
|
|
962
|
+
- **Shipping a guard with zero call sites.** A green test file proves the guard
|
|
963
|
+
works, not that anything invokes it — `validateNftMintGasWar` had a full suite
|
|
964
|
+
and was enforced on nothing. Grep for real callers, excluding the definition
|
|
965
|
+
and barrel re-exports (the re-export is what makes a naive grep look healthy).
|
|
966
|
+
- Assuming a mint signature instead of discovering it from bytecode. ERC-721 does
|
|
967
|
+
not standardize creation; BAYC uses `mintApe(uint256)`. Return `unsupported`
|
|
968
|
+
rather than encoding a guess, and report ambiguity when several selectors match
|
|
969
|
+
instead of silently taking the first.
|
|
970
|
+
- Encoding `mint(address,uint256)` as a quantity mint. The uint256 may be a
|
|
971
|
+
**tokenId** — this is why the mint gate rule rejects the signature outright.
|
|
972
|
+
Its presence in an inherited selector table is not validation of the shape.
|
|
973
|
+
- Reading gas-war tips from `eth_maxPriorityFeePerGas`. It is one node's opinion
|
|
974
|
+
with no distribution; a contested inclusion needs `eth_feeHistory` reward
|
|
975
|
+
percentiles, medianed across blocks so one whale cannot skew the read.
|
|
976
|
+
- Treating `maxFeePerGas` as spend. It is a refundable ceiling — padding it is
|
|
977
|
+
free protection against a climbing basefee, and being stingy strands you on a
|
|
978
|
+
spike. The tip is the real money.
|
|
979
|
+
- Clamping a bid below the observed market and returning it anyway. Report
|
|
980
|
+
`viable: false` with the arithmetic; paying gas to lose is worse than not
|
|
981
|
+
bidding. And never let a clamp produce `maxFeePerGas < maxPriorityFeePerGas`.
|
|
982
|
+
- Arming an unattended action from a truthy or near-miss value. Exact string only
|
|
983
|
+
— `"Execute"`, `"exec"`, `true`, `1`, `"arm"` must all downgrade, and an
|
|
984
|
+
unrecognized mode should downgrade *with a reason* rather than error, since an
|
|
985
|
+
error teaches the caller to retry with looser input.
|
|
986
|
+
- Deciding arming at fire time rather than creation (a trigger that can
|
|
987
|
+
self-upgrade), accepting a grant for a different contract, or skipping the
|
|
988
|
+
fire-time spend re-check — gas moved between arming and firing, which is the
|
|
989
|
+
entire premise.
|
|
990
|
+
- Treating an unreadable sale flag as open. A probe that timed out is `unknown`;
|
|
991
|
+
firing on it is guessing in the most expensive direction.
|
|
992
|
+
- Trusting scanner-window membership as an allowlist for money movement, or reading a kill switch once at handler entry and broadcasting on that stale value seconds later.
|
|
993
|
+
- Local smoke against a stale server: a leftover `node server.js` from an earlier session can own the smoke port and serve OLD routes — new endpoints return 404/`Cannot POST` and look unimplemented. Before diagnosing "route missing", check the background process log for `EADDRINUSE` and `lsof -t -i :<port>` for a stale PID; kill it and re-smoke.
|
|
994
|
+
- Wiring an autonomous SELL and reusing the BUY executor: the buy gate hard-rejects `side !== "buy"` and asserts `value > 0`, so a sell (value==0) needs a PARALLEL sell-only gate — do not weaken the buy gate. And the exit policy's recorded `wallet` (user session) must be realigned to the EXECUTOR address that actually holds the token, or the balance-read/prepared-sender/signer disagree and the swap fails.
|
|
995
|
+
- A fresh executor wallet with ZERO router allowance makes `prepareSwap` return `kind:"approval"` (not `"swap"`), so the first stop-loss trigger THROWS the sell gate — pre-approve token→router once before expecting an autonomous sell.
|
|
996
|
+
- Verifying rhbot `.env` flags via `dotenv`: rhbot uses its own `loadEnvFile` (dotenv isn't a dep), so `pm2 env` shows nothing and inline `node -e` `import("dotenv")` fails — write the check script INTO the repo and import `./server/env.js`'s loader instead.
|
|
997
|
+
- Reading `getAmountsOut` at `amounts[0]` (the INPUT) instead of the last element; on a single hop the wrong number is obvious, on a multi-hop path it silently passes.
|
|
998
|
+
- Advertising `quote`/`prepare` support for a chain whose router was never verified on that chain, or bulk-adding routers to raise a coverage number.
|
|
999
|
+
- Verifying a third-party venue by **codesize alone**. Bytecode proves deployment, not identity — the canonical QuoterV2 address has code on Base but cannot price its pairs. Probe behaviour.
|
|
1000
|
+
- Bending a V2 adapter to cover V3 (or vice versa). Different pricing call, different selector, explicit fee tier — a mis-encoded swap reverts or routes through the wrong pool.
|
|
1001
|
+
- Defaulting to one fee tier because 0.3% is common; liquidity concentrates in a tier that varies by pair and chain.
|
|
1002
|
+
- Ranking routes on headline `amountOut`. Gas is part of the price and the winner flips with trade size.
|
|
1003
|
+
- Treating a missing gas figure as zero — it silently promotes the worst route to first place.
|
|
1004
|
+
- Quoting a spread between two routes that measure cost differently (it compares third vs fourth while reading as first vs second).
|
|
1005
|
+
- Letting one dead aggregator fail the whole comparison, or spending a timeout on a provider already known to return 410. Check `sunset`/`deprecation` response headers before assuming rate-limit.
|
|
1006
|
+
- Guessing a USD gas cost from gas UNITS with no gas price; report `null` instead.
|
|
1007
|
+
- Folding bridge duration into the score rather than surfacing it beside the price.
|
|
1008
|
+
- Ranking a winner and leaving the user to rebuild it by hand. Hand the winner to
|
|
1009
|
+
prepare so the compared route and the signed route are provably the same.
|
|
1010
|
+
- Returning one "prepared" blob for both an unsigned transaction and an EIP-712 order.
|
|
1011
|
+
A CoW order is signed and submitted, never broadcast; conflating them fails at signing
|
|
1012
|
+
time, after the user already approved tokens.
|
|
1013
|
+
- Deriving the approval spender from the destination. CoW pulls funds through the
|
|
1014
|
+
**vault relayer**, so approving the settlement contract yields an order that silently
|
|
1015
|
+
never fills.
|
|
1016
|
+
- Preparing against the comparison's quote. Re-quote inside prepare and report drift —
|
|
1017
|
+
a minimum computed from a stale quote is not a minimum.
|
|
1018
|
+
- Passing the quote placeholder (`0x…dEaD`) into a prepare call. It is correct for
|
|
1019
|
+
anonymous quoting and a category error for preparing; some venues reject it and some
|
|
1020
|
+
build a transaction for an address nobody controls.
|
|
1021
|
+
- Reporting a venue's on-chain precondition as a bare HTTP 400. "Not enough allowance"
|
|
1022
|
+
is a fixable ordering constraint (approve first), not a router bug — and the real
|
|
1023
|
+
message is usually on `err.body`, not `err.message`.
|
|
1024
|
+
- Silently substituting a different venue when the winner cannot be prepared, or
|
|
1025
|
+
presenting a caller-forced non-winner as the best route.
|
|
1026
|
+
- A preparer wired to a **key-gated** source, so on a keyless machine it is unreachable
|
|
1027
|
+
dead code that still looks like coverage. Stub the keys in the test, and pin the
|
|
1028
|
+
quote-only set so a source becoming unpreparable fails CI instead of a user.
|
|
1029
|
+
- Returning a bare transaction object for a bridge because *this* route happened to be
|
|
1030
|
+
single-tx. Another source returns an approval plus a deposit, and the caller who
|
|
1031
|
+
learned the object shape signs one of them and believes the bridge is done.
|
|
1032
|
+
- Reporting only the signing chain for a bridge, or passing along a transaction whose
|
|
1033
|
+
`chainId` is not the origin — the wallet then prompts on a network the user cannot
|
|
1034
|
+
reason about.
|
|
1035
|
+
- Letting the user infer that an origin-chain confirmation means funds arrived. It does
|
|
1036
|
+
not, and the silence is what causes double-sends. State non-atomicity and the ETA.
|
|
1037
|
+
- Stating a fact only in an error **message** that a caller must act on (the fallback
|
|
1038
|
+
venue, the failure class) while leaving the structured field `undefined`. Agents
|
|
1039
|
+
consume fields, not prose.
|
|
1040
|
+
- Emitting a field named after the private codebase (`madSlippage`) in a payload handed
|
|
1041
|
+
to users. Secret scans, portability tests, and docs tests all miss it because it lives
|
|
1042
|
+
in the output shape — and a hard rename breaks artifacts already in flight, so keep a
|
|
1043
|
+
dual-read window on consumers.
|
|
1044
|
+
- `rg -c 'pattern' -r ''` to count occurrences: `-r` is *replace*, so it rewrites the
|
|
1045
|
+
output you were trying to read. Use `rg -n` to list, `rg -o pattern | wc -l` to count.
|
|
1046
|
+
- Calling a Solana prepare verified because it returned base64. Jupiter builds a
|
|
1047
|
+
swap for a placeholder pubkey that dies at `simulateTransaction: failed to sanitize
|
|
1048
|
+
accounts offsets correctly` — only a simulate against a real funded pubkey proves it.
|
|
1049
|
+
- Hardcoding a probe wallet in an E2E script. Parameterize it; a placeholder default
|
|
1050
|
+
turns a wallet-less machine red and reads as a code regression.
|
|
1051
|
+
- Checking a `maxPrice` cap on the venue's *response*. The cap must reject before any
|
|
1052
|
+
network call — assert it with a call counter, not just a thrown error.
|
|
1053
|
+
- Passing a listing's `tokenAddress` (the ATA) where the mint belongs, or letting
|
|
1054
|
+
lamports and decimal SOL both reach the caller. Normalize units at the provider edge.
|
|
1055
|
+
- Treating HYPE staking as 18 decimals. It is **8** — `1.5 HYPE = 150000000` — and an
|
|
1056
|
+
over-precise amount must be rejected, not silently rounded.
|
|
1057
|
+
- Preparing a HyperCore `cWithdraw` without reading `delegatorSummary` first. It only
|
|
1058
|
+
moves the **undelegated** bucket, so "unstake" is two actions (undelegate, then
|
|
1059
|
+
withdraw) and the user pays a round trip to discover that.
|
|
1060
|
+
- Describing a HyperCore action POST to `/exchange` as a broadcast. It is a signed
|
|
1061
|
+
action submission; there is no chain transaction and no hash.
|
|
1062
|
+
- A `/sign/i` export-name guard that fails on a legitimate `HL_SIGNATURE_CHAIN_ID`
|
|
1063
|
+
constant. Use an explicit allowlist rather than a negative lookahead.
|
|
1064
|
+
- Using a real personal burner address as a test fixture. It trips the pre-publish
|
|
1065
|
+
private-term scan and leaks an address into a public repo even when it holds dust —
|
|
1066
|
+
use public constants (wrapped-SOL / USDC mints, `0x0…abc`).
|
|
1067
|
+
- Collapsing "provider needs a user key that isn't configured" into `fail` in an E2E
|
|
1068
|
+
report. Report `keyed` as its own state or a correctly-configured machine looks broken.
|
|
1069
|
+
- Retrying a 4xx, or ignoring `Retry-After` in favour of your own shorter backoff.
|
|
1070
|
+
- Deduping a POST or a request with a body, or sharing a deduped response between a
|
|
1071
|
+
keyed and a keyless caller.
|
|
1072
|
+
- Unbounded `Promise.all` across every provider in a health sweep — it trips public
|
|
1073
|
+
rate limits and the resulting 429s read as an outage.
|
|
1074
|
+
- Keying a single-flight dedupe on whether an auth header is **present** rather than
|
|
1075
|
+
on its value. Two callers with different API keys collapse into one upstream request
|
|
1076
|
+
and the second receives data fetched with the first one's credential. Hash the full
|
|
1077
|
+
identity header set into the key.
|
|
1078
|
+
- Replaying a non-idempotent request after a **transport-level** failure. A status code
|
|
1079
|
+
means the server refused it (safe to retry); no status means the server may have
|
|
1080
|
+
already applied it and only the response was lost. Restrict statusless retries to
|
|
1081
|
+
`GET/HEAD/OPTIONS/PUT/DELETE`.
|
|
1082
|
+
- Reporting a Bitcoin balance from ONE derived address. One key controls `1…`, `3…`,
|
|
1083
|
+
`bc1q…`, and `bc1p…` simultaneously — funds at the wrong script type read as a
|
|
1084
|
+
missing key, a bad key, or an empty wallet, and none of those is the actual problem.
|
|
1085
|
+
- Signing an input whose script type differs from the address the funds sit at. Same
|
|
1086
|
+
key, different `scriptPubKey`, invalid signature. Consolidate first.
|
|
1087
|
+
- Sweeping an address with many small UTXOs. Ordinal-range outputs (`<= 1000 sats`)
|
|
1088
|
+
can be burned as fee; refuse auto-selection and make the user name the UTXO.
|
|
1089
|
+
- `Number(wei)` on a `uint64` EIP-712 field. Above 2^53 the wallet is handed a
|
|
1090
|
+
different amount than the one prepared and displayed — silently, and in the field
|
|
1091
|
+
the user is looking at. Keep it exact as a string above `MAX_SAFE_INTEGER`.
|
|
1092
|
+
- `Number()` on any user-supplied price or amount: it accepts `"1e2"` → 100, `"0x10"`
|
|
1093
|
+
→ 16, and `Infinity`. Require a plain decimal.
|
|
1094
|
+
- **A cap accepted as a JS `number` above 2^53 rounds UP, widening itself.**
|
|
1095
|
+
`9007199254740995` arrives as `...996`, so a guard built from it authorizes one
|
|
1096
|
+
more unit than the user asked for — the cap silently becomes larger than the
|
|
1097
|
+
intended one, which is the wrong direction to fail. Reject `!Number.isSafeInteger`
|
|
1098
|
+
on any bound and tell the caller to pass a decimal string or `BigInt`. Distinct
|
|
1099
|
+
from the `Number(wei)` pitfall above: that one corrupts the *displayed* amount,
|
|
1100
|
+
this one corrupts the *limit* protecting it.
|
|
1101
|
+
- Gating an approval guard on `approve(address,uint256)` alone. **Every other
|
|
1102
|
+
allowance-granting entrypoint then skips it entirely** — `increaseAllowance`,
|
|
1103
|
+
SafeERC20 `forceApprove`/`safeApprove`/`safeIncreaseAllowance`, Permit2
|
|
1104
|
+
`approve`/`permit`, and EIP-2612 / DAI-style `permit` all grant or raise spend
|
|
1105
|
+
rights. Guard the full set, and **derive each selector with
|
|
1106
|
+
`keccak_256(signature)` in code rather than transcribing hex**: closing this exact
|
|
1107
|
+
finding I hand-wrote the `forceApprove` and `safeApprove` selectors and *both were
|
|
1108
|
+
wrong*, so the fix would have shipped the hole it claimed to close while passing a
|
|
1109
|
+
test that asserted the same recalled constants. Real values: `0x095ea7b3` approve,
|
|
1110
|
+
`0x39509351` increaseAllowance, `0x61f49ed6` forceApprove, `0xeb5625d9` safeApprove,
|
|
1111
|
+
`0xf9255c1f` safeIncreaseAllowance, `0x87517c45` Permit2 approve, `0xd505accf`
|
|
1112
|
+
EIP-2612 permit, `0x8fcbaf0c` DAI permit. Assert the negative arm too —
|
|
1113
|
+
`transfer` (`0xa9059cbb`), `transferFrom`, and `decreaseAllowance` must NOT match.
|
|
1114
|
+
Same rule for event topics, EIP-712 type hashes, and domain separators: a wrong
|
|
1115
|
+
security constant fails **silently**, unlike a wrong test constant.
|
|
1116
|
+
**After expanding recognition, expand the decoder in the same diff** — Grok
|
|
1117
|
+
round 7 caught the half-fix (recognition without decode).
|
|
1118
|
+
- Recording a spend cap on the response instead of enforcing it before the call. A
|
|
1119
|
+
`maxPriceSol` that is echoed but never compared is worse than no cap — the caller
|
|
1120
|
+
believes a ceiling held.
|
|
1121
|
+
- Accepting a **pre-signed** transaction payload from a marketplace API in a "prepare"
|
|
1122
|
+
path. Take only the documented unsigned field; a compromised or impersonated
|
|
1123
|
+
endpoint otherwise laundres arbitrary bytes past every local check.
|
|
1124
|
+
- Forwarding an env API key to a caller-controlled `baseUrl`. That is credential
|
|
1125
|
+
exfiltration and SSRF dressed as configuration — allowlist the hosts and require an
|
|
1126
|
+
explicit opt-out env var for local mocks.
|
|
1127
|
+
- Passing a provider flag that widens a bound you already enforced (Jupiter
|
|
1128
|
+
`dynamicSlippage: true` voids the bps ceiling while the returned guard still
|
|
1129
|
+
advertises the capped number). Force it off and reject the request.
|
|
1130
|
+
- A destination allowlist that fails **open** when empty. "No allowlist configured"
|
|
1131
|
+
must refuse, not permit everything — the permissive branch fires exactly when the
|
|
1132
|
+
operator has configured the least.
|
|
1133
|
+
- Gating a real market submission on a caller-supplied boolean alone. A steered agent
|
|
1134
|
+
can set `execute: true`; what it cannot set is the credential's presence or the
|
|
1135
|
+
autonomous opt-in.
|
|
1136
|
+
- **Enforcing a bound only when its field happens to be present.** Two separate
|
|
1137
|
+
CRITICALs came from this one shape: a transfer guard checked `amount` and
|
|
1138
|
+
`maxAmount` each `!= null`, so a guard omitting **both** skipped every check and
|
|
1139
|
+
authorized `MaxUint256 - 1`; and a session grant compared `maxGasWei` only when the
|
|
1140
|
+
*request* volunteered `gasWei`, so omitting the field satisfied a grant whose
|
|
1141
|
+
ceiling would have rejected it. If a guard bounds something, **require** the field —
|
|
1142
|
+
a capless guard is not a guard, and the caller who omits it is exactly the caller you
|
|
1143
|
+
are guarding against. Also reject internally incoherent guards (`amount > maxAmount`).
|
|
1144
|
+
- A spend cap that is stripped as an "internal" field and then never compared to
|
|
1145
|
+
anything (Satflow `maxSats`). Enforce it against the venue's **settled** price on the
|
|
1146
|
+
response, not just the caller's requested price, and withhold the artifact when the
|
|
1147
|
+
price is unknown.
|
|
1148
|
+
- Authenticating a guard with nothing at the **signer** boundary. If the party
|
|
1149
|
+
supplying the calldata also supplies the object vouching for it, the bound is
|
|
1150
|
+
decorative — `quoteAmountOut: "1"` makes any floor trivially satisfiable.
|
|
1151
|
+
HMAC the guard with the same attestation secret the route/vault attestations
|
|
1152
|
+
use, and verify **before** the bounds checks when signing/broadcasting.
|
|
1153
|
+
**Round 7 + Sol merge:** quote/prepare may attach an *unsigned* bound (self-host
|
|
1154
|
+
UX without a forced secret). `enforceTxPolicy` must pass `requireSigned: true`
|
|
1155
|
+
so missing secret / missing signature fails closed at sign and broadcast.
|
|
1156
|
+
Catching a missing secret and returning `null` *at that gate* was the Opus
|
|
1157
|
+
CRITICAL (forged 1-wei floor on 100 WETH). Never document unauthenticated
|
|
1158
|
+
bounds as acceptable for sign/broadcast. Mint a secret at `oracle-init --apply`
|
|
1159
|
+
into `~/.config/oracle/exec.env`. Pin a dummy secret in the npm test script
|
|
1160
|
+
under `ORACLE_TEST_ISOLATE_SECRETS=1`. See `references/oracle-round7-guard-parity.md`.
|
|
1161
|
+
- **Recognising a selector without decoding it bricks legitimate usage.** Round 7
|
|
1162
|
+
Grok DO_NOT_SHIP: allowance selectors were added to the *recognition* list so
|
|
1163
|
+
the guard demanded a bound approval, then `decodeApproveCalldata` only knew
|
|
1164
|
+
`approve()` — so `increaseAllowance` / SafeERC20 / EIP-2612 / Permit2 became
|
|
1165
|
+
unsignable. Recognition and decoding must cover the **same** set; an
|
|
1166
|
+
unknown-but-recognised selector fails closed (`recognised but not decodable`),
|
|
1167
|
+
never silently passes. Slot map: approve/increaseAllowance = w0/w1; SafeERC20
|
|
1168
|
+
and EIP-2612/Permit2 = w1/w2; DAI permit allowed bool at w4.
|
|
1169
|
+
- **Every time-bounded guard must call the shared `fresh-window` helper.**
|
|
1170
|
+
`Number(nowMs) > Number(expiresAtMs)` is false for NaN/null/"later"/{}, so a
|
|
1171
|
+
broken clock turns a short TTL permanent. Round 7: approval was the only guard
|
|
1172
|
+
still hand-rolled and accepted timeless + NaN clocks. Grep for `fresh-window`
|
|
1173
|
+
after any new guard.
|
|
1174
|
+
- **Do not exempt approval-shaped calldata from the destination allowlist.**
|
|
1175
|
+
`!approvalLike` lets any unreviewed contract with an allowance selector skip
|
|
1176
|
+
the strongest drain control. `assertApprovalGuard` binds the *spender*, not
|
|
1177
|
+
`to`. The token (or Permit2) being called is a destination like any other.
|
|
1178
|
+
- **`key present = armed` is trading posture, not withdrawal posture.** A signing
|
|
1179
|
+
lane that only checks `prepared.action` is truthy will sign `withdraw3` /
|
|
1180
|
+
`usdSend` / `approveAgent`. Allowlist prepare-emitted types only
|
|
1181
|
+
(`order`/`cancel`/`updateLeverage`/`updateIsolatedMargin` for HL).
|
|
1182
|
+
- **Never construct `new Wallet(raw)` on unvalidated key material.** ethers embeds
|
|
1183
|
+
the full value in `invalid BytesLike value (… value="…")` — a file-read oracle
|
|
1184
|
+
via `keyFile`. Validate `^0x?[0-9a-fA-F]{64}$` first; rethrow naming the source,
|
|
1185
|
+
never the contents.
|
|
1186
|
+
- A write-op denylist built from an anchored **name** regex. It blocked `sendFoo` while
|
|
1187
|
+
letting `txBroadcast`, `doSend`, `rawSign`, `orderSubmit` and `forceExecute` straight
|
|
1188
|
+
through, and blocked nothing real because every money-moving op was named `prepare*`.
|
|
1189
|
+
The actual boundary is the catalog: a provider may only be asked for an op it
|
|
1190
|
+
declares. Keep substring matching as defence in depth, not as the control.
|
|
1191
|
+
- Treating loopback bind as a security boundary for a local data server. Any page the
|
|
1192
|
+
operator visits can reach `127.0.0.1:<port>`, and DNS rebinding reaches it under an
|
|
1193
|
+
attacker hostname — both arriving with the server's own API keys. Validate the `Host`
|
|
1194
|
+
header is a loopback name and reject a cross-origin `Origin`.
|
|
1195
|
+
- `bad-txns-inputs-missingorspent` on a Bitcoin input you already verified is
|
|
1196
|
+
confirmed and unspent. That error is almost always **txid byte order**, not
|
|
1197
|
+
availability — a helper that pre-reverses the txid before handing it to a signer
|
|
1198
|
+
that also reverses internally produces a reference to a nonexistent outpoint.
|
|
1199
|
+
Decode your own signed tx, compare the input txid to the real one, and pass the
|
|
1200
|
+
display-order hex string rather than pre-reversed bytes.
|
|
1201
|
+
- Building a reveal against an **unconfirmed** commit, or re-deriving the prepare
|
|
1202
|
+
with different parameters than were funded. Assert `contentSha256` and
|
|
1203
|
+
`commitAddress` against the funded values before signing the reveal.
|
|
1204
|
+
- Debugging an `Invalid Schnorr signature` on an ordinals reveal as a signing bug.
|
|
1205
|
+
Check `vsize` first: ~111 vbytes for a multi-KB payload proves the **key path** was
|
|
1206
|
+
taken and the envelope never entered the witness. The commit must be built on a
|
|
1207
|
+
**NUMS** internal key (`p2tr(undefined, {script: leaf})`) so script-path is the only
|
|
1208
|
+
way to spend.
|
|
1209
|
+
- Setting `allowUnknownOutputs` on an ordinals reveal and not `allowUnknownInputs`.
|
|
1210
|
+
`@scure/btc-signer` classifies the envelope leaf as `unknown` and throws
|
|
1211
|
+
`Finalize: Unknown tapLeafScript` — two different flags, and only one of them is the
|
|
1212
|
+
one you need.
|
|
1213
|
+
- Encoding the ord **body tag** with a push helper. `encodePush([0])` emits `01 00`;
|
|
1214
|
+
the separator is a bare `OP_0` (`0x00`). The wrong form still confirms *and indexes*,
|
|
1215
|
+
producing an inscription with `content size: 0 bytes` — the most expensive kind of
|
|
1216
|
+
success. Diff your envelope bytes against a live indexed inscription instead of
|
|
1217
|
+
reasoning from the spec.
|
|
1218
|
+
- Verifying an inscription against your **local** file. The sha256 matched perfectly
|
|
1219
|
+
through two dead attempts; only reassembling the body from the on-chain witness (and
|
|
1220
|
+
a third-party indexer's view) can see an envelope-structure failure.
|
|
1221
|
+
- Attributing a **precise** operator symptom to indexer lag. "Blank" is vague; "content
|
|
1222
|
+
size 0 bytes" is a measurement that proves the indexer parsed the envelope and found
|
|
1223
|
+
no body — which waiting never fixes. `GET /content/<id>` returning 404 on a confirmed
|
|
1224
|
+
tx settles it in one command.
|
|
1225
|
+
- Trusting your own envelope parser's `body bytes: 0` as evidence the content is gone.
|
|
1226
|
+
Validate the parser against a known-good inscription first — searching for a
|
|
1227
|
+
zero-length push instead of a 1-byte zero push reports an intact inscription as empty.
|
|
1228
|
+
- Verifying a `package.json` change without regenerating `package-lock.json`. A bin/
|
|
1229
|
+
export drift guard fails on the lockfile, and the message reads like a missing file
|
|
1230
|
+
rather than stale metadata.
|
|
1231
|
+
- Fixing a **class-level** finding only on the files named in the repro. "Every keyed
|
|
1232
|
+
provider follows this pattern" means enumerate the full set by grepping the *shape*,
|
|
1233
|
+
fix all of them behind a shared helper, and add a table-driven test that iterates the
|
|
1234
|
+
membership — otherwise a later provider that forgets the helper ships the hole again.
|
|
1235
|
+
- Editing the repo while an audit reads it. Test counts and failures stop being
|
|
1236
|
+
attributable to any commit, and hours go into phantoms. Freeze the tree or have each
|
|
1237
|
+
auditor work from its own `git archive <sha>` snapshot.
|
|
1238
|
+
- Rolling a "looks correct on reading" fix into an all-clear. `NOT VERIFIED` is a third
|
|
1239
|
+
verdict beside `HOLDS`/`BROKEN` and must survive into the summary.
|
|
1240
|
+
- A secret resolver with a `~/.config` fallback left active under test. The maintainer's
|
|
1241
|
+
suite silently disagrees with CI and with every contributor; add a test-only isolation
|
|
1242
|
+
switch and set it in the `test` script.
|
|
1243
|
+
- Writing a DNS-rebinding test with `fetch()` and a manual `Host` header. `fetch`
|
|
1244
|
+
rewrites `Host` from the URL, so the test never sends the attack. Use a raw socket.
|
|
1245
|
+
- Spawning a server in a test without `{ timeout }`, without `detached: true` +
|
|
1246
|
+
process-group kill, or with env var names the server does not actually read — each
|
|
1247
|
+
produces a hang or a false "server is broken".
|
|
1248
|
+
- `assert.throws` on an **async** function. It never catches the rejection, so the
|
|
1249
|
+
guard it claims to cover has never once fired in the suite while the test file
|
|
1250
|
+
reads as coverage. Use `await assert.rejects`, and grep for the shape after adding
|
|
1251
|
+
any `await` to a guard path.
|
|
1252
|
+
- Two test files hardcoding the **same port**. `node --test` runs files in parallel,
|
|
1253
|
+
so one loses the bind and its tests fail as `404`/`ECONNREFUSED` that look like
|
|
1254
|
+
broken product routes. Green-alone plus red-in-full-suite is almost never a product
|
|
1255
|
+
bug — give each server-spawning file its own port.
|
|
1256
|
+
- Triaging a **wall** of unrelated test failures as separate bugs. When they share one
|
|
1257
|
+
syscall, one errno, or one port, they are one cause: ~40 failures across a dozen
|
|
1258
|
+
files, all at `writeFileSync`/`appendFileSync`/`mkdtemp` with `UNKNOWN: unknown
|
|
1259
|
+
error, write` and `errno: -122`, were a single exhausted `/tmp` tmpfs quota
|
|
1260
|
+
(`-122` is `EDQUOT`, which Node surfaces as a useless `UNKNOWN`). Probe with a
|
|
1261
|
+
one-line `mkdtempSync`+`writeFileSync`, check `df -h /tmp` and `mount | grep /tmp`
|
|
1262
|
+
for `usrquota`, and prove any reclaimed file is dead (`lsof` empty AND `tar -tf`
|
|
1263
|
+
shows a finished artifact) before deleting.
|
|
1264
|
+
- Reading a venue's parallel-array response per surface instead of joining once.
|
|
1265
|
+
Open interest denominated in the coin, funding as a per-interval decimal, and
|
|
1266
|
+
index-paired metadata each get re-derived slightly differently and the chart stops
|
|
1267
|
+
matching the card. Missing numerics must become `null`, never `NaN`.
|
|
1268
|
+
- Claiming a venue supports trading because a read integration and an `/exchange` URL
|
|
1269
|
+
constant exist. Order placement is a separate signed action set — grep for the
|
|
1270
|
+
placement function before answering "can we trade here?".
|
|
1271
|
+
- Asserting a signing implementation against a known-answer vector **recalled from
|
|
1272
|
+
memory**. A plausible-but-invented fixture fails, and the failure looks like a bug
|
|
1273
|
+
in correct code — costing a debugging detour into msgpack and byte order. Fetch the
|
|
1274
|
+
vector from the SDK's own test file, then compare.
|
|
1275
|
+
- Naming an internal module after something that collides with the owner's **brand**
|
|
1276
|
+
(`hl-media` when "HL Media" is a separate company). It reads as that product in
|
|
1277
|
+
every log line, catalog listing and doc. Rename to what it actually is
|
|
1278
|
+
(`hl-markets`), and do the rename with `git mv` plus a longest-first ordered
|
|
1279
|
+
substring pass so `hlMediaHealth`/`hl-media.mjs`/`"hl-media"` all migrate without
|
|
1280
|
+
double-replacing.
|
|
1281
|
+
- A `new URL()`-based host guard rejecting a test's mock hostname. `https://mock.0x` is
|
|
1282
|
+
genuinely unparseable (invalid TLD) while `https://mock.1inch` parses — fix the
|
|
1283
|
+
fixture rather than loosening the guard, and treat a blank `baseUrl` override as
|
|
1284
|
+
absent rather than throwing.
|