# SwanVault v0.2.0 — Internal Security Review

**Reviewer:** Claude (internal pass — NOT a substitute for an independent audit)
**Date:** Jul 17, 2026
**Scope:** `contracts/SwanVault.sol` @ solc 0.8.24+commit.e11b9ed9, optimizer 200 runs, evmVersion `paris`
**Build fingerprints v0.2.0 (verify before any deployment):**
- SwanVaultFactory initcode keccak256: `0x98c8d7f1f9ca5e74102c072d20a2c291269f4c2657925ad4e8aeca61056de6f4` (8,824 bytes)
- SwanVault initcode keccak256: `0x7a85a02bb4dab1b16b186e3b6050ef796918b5f0c9e3c03f1789f87463949ff5`
- (v0.1.0 standalone vault, deployed at 0x2b30…e49b on Robinhood Chain, had
  initcode `0x8a53dcb8…31dc92d` — superseded, drain and abandon it.)

**v0.2.0 changes:** vault constructor now takes an explicit `_owner` and an
initial token whitelist (both set atomically at creation), enabling the new
`SwanVaultFactory` — a singleton, admin-less, fee-less, fund-less front door
that creates a fully-configured vault per caller in ONE transaction
(`createVault(policyHash, agent)`, ~1.42M gas). Factory battery added to
tests: per-user ownership, whitelist preload, router preset, cross-user
isolation, vaultsOf tracking, factory-rejects-ETH. 17/17 regression+factory
checks pass on the same in-browser EVM harness (cumulative with the v0.1
53-check suite whose logic is unchanged elsewhere).

**Test evidence:** 53/53 checks passing on @ethereumjs/evm (v3.1.1), plus an
8-check regression after the H-0 fix. Batteries: deploy/constructor, deposits
& withdrawals, access control, whitelist, size cap, cooldown, pause, revoke,
hostile-router underpayment, reentrancy (2 vectors), USDT-style tokens,
limit validation, full-exit drain. Rebalance gas: ~115k.

---

## Threat model

Assets at risk: whatever the owner deposits. Adversaries considered:
(a) a fully compromised agent key, (b) a hostile/buggy router, (c) malicious
tokens, (d) arbitrary third parties, (e) a compromised SWAN frontend.

## Findings

### Fixed during review

**H-0 (informational→fixed).** `depositToken` lacked `nonReentrant` while all
other fund-moving functions had it. No exploit identified (function is
owner-only and holds no invariants), but symmetry costs nothing. **Fixed;
regression re-run.**

### Accepted residual risks (documented by design)

**R-1. A compromised agent can trade badly within the envelope.** The agent
chooses `minOut`; with `minOut = 1 wei` it can accept maximal slippage (e.g.
into a sandwich) on up to `maxTradeBps` of a sleeve per `cooldown`. Funds
cannot be exfiltrated (recipient is the vault; guaranteed by code), but value
can bleed at a bounded rate: worst case ~25%/hour of one token's balance at
default limits. Mitigations today: tight `setLimits`, `setPaused`,
`revokeAgent` (one tx). Proper fix (v1): on-chain Chainlink-oracle floor on
`minOut` so the contract itself rejects off-market trades.

**R-2. Malicious whitelisted token.** The owner whitelisting a token trusts
that token's code (standard for any vault). A malicious token could
return-bomb or lie about balances; it cannot move OTHER tokens. Only owner
action can create this exposure.

**R-3. Router trust is pinned, not verified.** `router` is immutable and
owner-chosen at deploy. The contract defends against a hostile router
(balance-delta check caught a 50%-underpaying router in tests; approvals are
exact and reset) but a router could still consume the approved `amountIn` and
revert-grief. Deploy only with the canonical Uniswap SwapRouter02.

**R-4. No allocation-band enforcement on-chain (v0 scope).** Bands need
oracles; v0 enforces custody/whitelist/venue/rate limits. The whitepaper
"contract enforces bands" claim is v1 — do not market v0 as enforcing bands.

**R-5. Owner key = vault. As with any self-custody design**, a lost or
compromised owner key is total loss. Recommend a hardware wallet for the
owner and treating v0 as pocket-money scale.

### Checked and clean

- **Access control:** every state-changing function gated (`onlyOwner` or
  agent∪owner for `rebalance`); `owner`/`router` immutable; no ownership
  transfer surface; no delegatecall, no selfdestruct, no fallback.
- **Fund egress paths:** exhaustively three — `withdrawToken` → `owner`,
  `withdrawETH` → `owner`, swap with `recipient = address(this)`. No path
  pays any other address. Withdrawals are never pausable.
- **Reentrancy:** all fund paths `nonReentrant`; both reentry vectors
  (rebalance-in-rebalance, withdraw-in-rebalance) revert in tests. State
  write `lastTradeAt` after external calls is safe under the guard.
- **ERC20 quirks:** no-return tokens handled (`_safe*` helpers, tested
  against a USDT-quirk mock incl. the approve-race restriction);
  fee-on-transfer on `tokenOut` caught by the balance-delta `minOut` check.
- **Approvals:** only to `router`, exact amount, zeroed before and after.
- **Arithmetic:** 0.8.x checked math throughout; `uint64` timestamp cast safe
  for ~10^11 years; bps math bounded (`maxTradeBps ≤ 10_000`).
- **Compiler:** zero errors, zero warnings; `paris` target (no PUSH0
  portability concerns); optimizer 200 (standard).

## Deployment guidance

- **Robinhood Chain mainnet (chain 4663)** — primary target. Router: the
  canonical Uniswap SwapRouter02 `0xCaf681a66D020601342297493863E78C959E5cb2`
  (verified on Blockscout; WETH9() = official WETH
  `0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73`; its factory's WETH/USDG 0.05%
  pool held ~1,029 WETH at review time — the four other contracts named
  "SwapRouter02" on that chain have empty factories: treat them as copycats).
- **Arbitrum One (chain 42161)** — fallback. Router: Uniswap SwapRouter02
  `0x68b3465833fb72A70ecDF485E0e4C7bd8665Fc45`.
- Suggested first-run posture: deploy with `agent = 0` (add later), whitelist
  only USDC + WETH, keep default limits, fund with a small test amount, walk
  the full cycle (deposit → owner rebalance → withdraw → revoke) before
  anything else.
- This is an internal review by the authoring model. Before ANY third-party
  funds or public launch: independent professional audit.
