REPORT.md · SHA256SUMS · replay.py · raw/fork_recheck_latest.json
# SWARM revenue-path & custody review — Month 1 deliverable Author: blockpin · Chain: Robinhood Chain (4663) · Pin: block 62607287 (0x3a3f7a4c…08e8) Method: live eth_call reads at the pin + disposable anvil fork of that block with impersonated callers. Fork results are counterfactual simulations; nothing was signed or sent on mainnet. Evidence: `raw/` (bytecode, scans, every RPC request/response). Labels: OBSERVED = read or reproduced; INFERRED = reasoned from observations; UNKNOWN = not established. ## Contracts in the revenue path | Role | Address | Notes | |---|---|---| | Pons v4 hook | 0xE5e702641Ea86F4ae6cC3cDaeD2B886f976Be044 | 15167 B, not a proxy, owner 0x263ed295… | | Fee escrow | 0xd3afeb2a…ac9e | 1932 B, not a proxy, holds 1841.80 ETH (all launches). Only claim/credit functions, no admin selectors | | Pons factory | 0x7eD598Bc…C7e | 24177 B, not a proxy, owner 0x263ed295… | | Bonding curve | 0x87cc03cf…5a63 | graduated=1 | | Pons owner | 0x263ed295…19dd | Gnosis Safe **2-of-3**, 22.98 ETH | | SWARM fee recipient | 0xa73c… (EOA key) | escrow balanceOf = 60.09 ETH claimable | | Treasury Safe (new) | 0x9bd2a12c… | owner 0xe11110ca…, not yet the fee recipient | OBSERVED at the pin: hook `protocolFeeShareBps 3000`, `hookFeeBps 100`, `buybackBurnBps 5000`, `maxInternalPriceImpactBps 300`. Factory recipient timelock = execution window = 259200 s (3 days). No pending recipient change for SWARM. ## Findings ### F1 — Pons owner can redirect SWARM creator fees with 3 days' notice; the treasury cannot veto (HIGH, OBSERVED on fork) - `factory.setCreatorFeeRecipient(SWARM, X)` from the Pons 2-of-3 Safe creates a pending change. - Execution before 3 days reverts (0x810c4f2a). After 3 days **any address** can call `executeCreatorFeeRecipientChange`. - Only the owner can cancel. The fee key and random callers revert with OwnableUnauthorized. - **The fee key cannot defend against it.** On the fork, the owner queued X, then the fee key moved the recipient to the treasury Safe during the window. The pending change survived that move. After 3 days a random caller executed it, and the recipient became X. - Scope: only **future** fees. The 60.09 ETH already escrowed for 0xa73c stayed claimable by 0xa73c after the redirect (claim() succeeded on the fork). - Mitigation: monitor `pendingCreatorFeeRecipient(SWARM)` and alert on any non-zero value. That gives a 3-day window to claim accrued fees and escalate. This fits the existing attestation cron. ### F2 — Fee key moves the recipient instantly, with no timelock (HIGH custody, OBSERVED on fork) - `transferCreatorFeeRecipient(SWARM, X)` from 0xa73c takes effect in the same block. - One compromised EOA can redirect all future SWARM fee income. - It also closes the custody gap: one tx from 0xa73c to 0x9bd2a12c puts the recipient under the Safe. Claiming the 60.09 ETH already escrowed still needs 0xa73c to call claim(), because escrowed balances do not follow the recipient (F1). ### F3 — Global fee changes did not alter SWARM's recorded launch policy (INFO, OBSERVED on fork / INFERRED for swaps) - The owner can raise `protocolFeeShareBps` (5000 accepted, 6000 reverts InvalidBps) and `hookFeeBps` (1000 accepted). - After those changes, SWARM's `getLaunchFeePolicy` and `launches()` still read 3000/5000/100/300. - INFERRED: per-launch fees are snapshotted at launch. UNKNOWN: whether swap-time accounting uses the snapshot. Not proven with an end-to-end swap. ### F4 — Remaining admin surface (INFO) - `rescuePoolFees(pool)` is owner-only and reverts 0x00f6b210 in the current state. UNKNOWN: what conditions enable it or what it moves. - `hook.setCreatorFeeRecipient` is factory-only (0x32cc7236 otherwise). - `factory.updateCreatorRecipient` reverts for every caller tested. - No proxy upgrade path on hook, escrow, or factory (no EIP-1967 slots, not proxy bytecode). ## Recommendations (ordered) 1. Move the fee recipient from 0xa73c to the treasury Safe (F2). Then claim escrowed fees from 0xa73c into the Safe. The key holder has to do this; no one else can. 2. Add a `pendingCreatorFeeRecipient` alarm to the attestation cycle (F1). 3. Ask Pons publicly whether they will renounce `setCreatorFeeRecipient` or give the recipient a cancel right (F1). 4. Follow-up: run an end-to-end swap on the fork to settle F3, and decode the 0x00f6b210 condition (F4). ## Re-check at a later block All F1/F2 fork results were reproduced from scratch at block **62624225**, in `raw/fork_recheck_latest.json`: - the fee key's instant transfer works - a random caller can't queue a change - the owner can queue one - executing early reverts - the fee key's cancel reverts - the pending change survives the fee key moving the recipient - after 3 days, a random caller executes it - escrowed fees stay with 0xa73c and claim() works At that block: recipient still 0xa73c, no pending change, escrow balanceOf(0xa73c) = 60.11 ETH. ## Reproduce The public RPC (rpc.mainnet.chain.robinhood.com) prunes old state. Forking the original pin 62607287 now fails with "metadata is not found", so replays need an archive RPC or a recent block. Verified path: ``` anvil --fork-url https://rpc.mainnet.chain.robinhood.com --fork-block-number 62624225 --port 8547 python3 replay.py raw/fork_recheck_latest.rpc.json # -> diffs: 0 (verified by the author on a fresh fork) ``` `replay.py` re-sends every logged request in order. It maps tx hashes, waits for receipts, and ignores timestamp drift under 600 s. Other `raw/*.rpc.json` logs from the original pin replay the same way on an archive fork. File hashes are in SHA256SUMS.