feat: Gnosis Safe admin integration#7
Merged
Conversation
13-commit phased plan to wire IntuitionFeeProxy admin roles to a Gnosis Safe multisig. Documents tooling decisions (Foundry Anvil + Vitest + protocol-kit/api-kit), scope (in/out), validation strategy without testnet (unit -> Anvil fork -> dry-run mainnet), and per-phase acceptance criteria. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New workspace package @intuition-fee-proxy/safe-tx that will host the Safe admin tooling. This commit only lays the foundation: - package.json: workspace package, vitest + typescript devDeps, viem peerDep - tsconfig.json: extends monorepo base, includes src/ + test/ - vitest.config.ts: node env, 30s timeout for future fork tests - README.md: scope summary + dev commands, points to plan in .claude/ - src/index.ts: empty entrypoint (exports added incrementally) - test/sanity.test.ts: trivial passing test so vitest has something to run after install No Safe SDK deps yet (added in subsequent commits as builders and modes land). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Intuition mainnet config and the Safe contract addresses needed for the protocol-kit / api-kit modes coming in later commits. src/types.ts: - SafeContracts (4 addresses we need on Intuition) - SafeNetworkConfig (chain meta + STS URL + Safe UI URL) - NetworkName union (mainnet only — testnet has no Safe infra) src/networks.ts: - INTUITION_MAINNET constant with addresses verified on-chain: * Singleton v1.3.0 L2 (canonical) * Singleton v1.4.1 L2 (also deployed by Den, future-proof) * ProxyFactory (custom Den deployment, found via Safe creation tx) * FallbackHandler (canonical) - buildSafeUiUrl: produces Den deep-links with int: prefix - buildTxServiceApiUrl: STS base URL helper for api-kit init - getNetwork: lookup helper src/index.ts: re-exports for the public API surface. test/networks.test.ts: validates chain id, shortName, STS URL, checksummed addresses, single-network registry, URL helpers. bun.lock: synced after `bun install` ran the vitest + viem deps declared in commit d45672d. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the testing infrastructure for fork-based integration tests against Intuition mainnet. All subsequent integration / e2e tests will reuse these fixtures. test/fixtures/constants.ts: - INTUITION_RPC + ANVIL_RPC + ANVIL_PORT + ANVIL_HOST - FORK_BLOCK = 3_250_000 (pinned for determinism, ~5k blocks below mainnet head as of 2026-04-27, well after Safe creation at block 142,723) - SAFE_ADDRESS + EXPECTED_OWNERS (2-of-3) + EXPECTED_THRESHOLD - SAFE_READ_ABI: minimal subset for getOwners / getThreshold / VERSION - inline recipe to bump FORK_BLOCK test/fixtures/anvil.ts: - startAnvilFork(): spawns `anvil --fork-url ... --fork-block-number ... --chain-id 1155`, polls eth_chainId until ready (20s timeout) - ENOENT handler with installation hint - stop(): SIGTERM with 3s SIGKILL fallback test/fixtures/impersonate.ts: - impersonate / stopImpersonating wrappers around anvil_*Account RPCs - setBalance via anvil_setBalance - impersonateAndFund convenience (impersonate + 100 ETH) test/integration/anvil-sanity.test.ts: - 6 assertions: chainId, Safe bytecode present, owners match expected 2-of-3, threshold == 2, VERSION == "1.3.0", impersonateAndFund credits balance correctly Anvil (Foundry) must be in PATH to run integration tests: curl -L https://foundry.paradigm.xyz | bash && foundryup Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the 10 admin operation builders that produce typed AdminOp
records (to / value / data / description). All builders are pure
functions — no I/O, no chain access, fully unit-testable.
src/types.ts:
- AdminOp type (compatible with Safe MetaTransactionData once value
is stringified)
src/ops/v2-admin.ts (5 ops, all gated by onlyWhitelistedAdmin):
- setDepositFixedFee(proxy, newFee)
- setDepositPercentageFee(proxy, newFeeBps)
- setWhitelistedAdmin(proxy, admin, status)
- withdraw(proxy, recipient, amount)
- withdrawAll(proxy, recipient)
src/ops/factory.ts (4 ops on IntuitionFeeProxyFactory, Ownable2Step):
- setImplementation(factory, newImpl, newVersion)
- setSponsoredImplementation(factory, newImpl, newVersion)
- transferOwnership(factory, newOwner)
- acceptOwnership(factory)
Note: +1 over plan spec. acceptOwnership is the receive-side of
Ownable2Step — needed when the Safe is the new owner. The plan
parenthetically referenced this case ("acceptOwnership cote Safe").
src/ops/uups-upgrade.ts (1 op for any ERC1967 / OZ UUPS proxy):
- upgradeToAndCall(proxy, newImpl, initData = '0x')
src/ops/index.ts: namespaced re-exports (v2Admin, factory, uups).
src/index.ts: public re-export of `ops` namespace + AdminOp type.
test/unit/ops/{v2-admin,factory,uups-upgrade}.test.ts:
- ~25 assertions total: selector match, ABI round-trip via
decodeFunctionData, value == 0n, description sanity
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nput fns acceptOwnership test was comparing against [], but viem decodes no-input functions with args === undefined. Coalesce to [] in the assertion to keep the round-trip semantics clear. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….sender The current claim was misleading. The MultiVault's actual rule is `receiver == msg.sender || approvals[receiver][msg.sender] & DEPOSIT`, where msg.sender is always the proxy contract. The "receiver = caller" guarantee is enforced by the impl's bytecode, not the protocol — a new default version registered by proxyAdmin could re-route deposits to any approved address. Reword README trust-model section to spell this out and document the three layered defenses (Safe multisig, executeAtVersion pinning, MV approval revocation when idle). Add an in-app banner on the Versions panel so users see the same warning where the action lives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tData Two hardening tweaks on IntuitionVersionedFeeProxy: * Document on `setDefaultVersion` that switching the default re-routes fallback callers — including users with a long-lived MV approval. The trust model relies on `proxyAdmin` being a Safe multisig and on insulated users pinning via `executeAtVersion`. * Reject `initData.length == 0` in the constructor. The Factory always passes a populated `initialize(...)` blob, but a direct deploy that skipped it would leave the impl's storage in pre-init state — anyone could front-run an `executeAtVersion(v, initialize_calldata)` call and become the first admin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
V1 is preserved in the tree only for historical/test reference. Mark it explicitly with @Custom:deprecated NatSpec so nobody redeploys it by accident. Also remove the no-op `receive()` payable: V1 is push-based (fees forwarded immediately via `_transferFee`), so any ETH that landed via a bare transfer would be silently locked forever — bare sends now revert, which is louder and safer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Reject `maxClaimPerTx > maxClaimVolumePerWindow` in `setClaimLimits`. Otherwise a fresh-window single tx that hits `maxPerTx` would always trip `_applyRateLimit`'s `consumed > capVolume` check and the proxy would be effectively unusable for max-sized tx. * Document on `setClaimLimits` that mutating `windowSec` does NOT reset existing per-user windows — shrinking/lengthening can roll users in or out of their cap unexpectedly. Admins should announce changes off-chain. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Signer abstraction so subsequent CLI / mode code can target
a single API regardless of where the EIP-712 signature comes from.
src/signers/types.ts:
- Signer = viem LocalAccount (used uniformly across strategies)
src/signers/env.ts (full implementation):
- envSigner({ envVar?, privateKey? }) -> Signer
- Defaults to PROPOSER_PK env var, accepts override for tests
- Validates 0x-prefix + 32-byte length with clear error messages
- Wraps viem privateKeyToAccount
src/signers/ledger.ts (stub):
- ledgerSigner({ derivationPath? }) throws "not yet implemented"
- Will use @ledgerhq/hw-app-eth + @ledgerhq/hw-transport-node-hid
src/signers/walletconnect.ts (stub):
- walletconnectSigner({ projectId?, metadata? }) throws "not yet implemented"
- Will use @walletconnect/sign-client v2
src/signers/factory.ts:
- getSigner('env' | 'ledger' | 'walletconnect', opts) -> Promise<Signer>
- Single entrypoint the CLI / modes will call
src/index.ts: re-exports `signers` namespace.
test/unit/signers/env.test.ts: 7 assertions
- env var lookup, opts.privateKey override, custom envVar
- error messages for missing key + bad format
- EIP-712 signature length sanity (132 chars = 0x + 65 bytes)
test/unit/signers/factory.test.ts: 3 assertions
- env strategy resolves correctly
- ledger / walletconnect reject with the expected error
Heavy SDKs (@LedgerHQ, @WalletConnect) intentionally not added yet —
they will land alongside the actual implementation in a later commit
to keep the dependency surface lean until those signers are needed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three security-driven ABI breaks bundled into one v2.x bump: * `createAtoms(data, assets, curveId)` → `createAtoms(data, assets, minShares, curveId)` Per-atom slippage floor for the post-creation deposit loop. Closes the silent-share-loss vector when the post-creation deposit was passed `minShares = 0` and a sandwich attacker could ponctionner. * `createTriples(...assets, curveId)` → `createTriples(...assets, minShares, curveId)` Same fix for triples. Slippage matters more here because triples reference existing atoms whose curves may already have value. * `deposit(termId, curveId, minShares)` → `deposit(termId, curveId, minShares, maxFeeBps, maxFixedFee)` Front-run guard: the inverse-formula deposit reads live fees and a same-block admin bump used to silently skim more from msg.value. The caller now passes the cap they saw off-chain; tx reverts with `FeeExceedsCap` if either fee exceeds it. Pass `MAX_FEE_PERCENTAGE` / `MAX_FIXED_FEE` to opt out (worst-case acceptance). Also corrects the misleading NatSpec on `createAtoms`/`createTriples` that claimed "atoms are already created" if the post-creation deposit reverts. The inner revert bubbles up and rolls back atom creation atomically — the user pays only gas, not the creation cost. Updates all internal test/e2e/doc call-sites to the new signatures. SDK ABI JSON files will be refreshed by `bun contracts:compile && bun sdk:sync`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First execution mode. Pure viem — no @safe-global/protocol-kit needed
yet (that lands with api-kit mode in the next commit). Works whenever
the canonical Safe v1.3.0 L2 singleton is deployed and reachable via
RPC, with zero external service dependency.
src/modes/direct-sign.ts:
- buildSafeTx(request, publicClient) -> SafeTxPayload
Resolves defaults, fetches Safe nonce if not provided, computes the
EIP-712 SafeTx hash via viem.hashTypedData. Hash is what owners sign.
- signSafeTx(payload, signer) -> SignedSafeTx
Wraps viem LocalAccount.signTypedData over the SafeTx domain/types.
- aggregateSignatures(signed[]) -> Hex
Sorts by signer address ascending (Safe spec) and concatenates raw
65-byte sigs. Throws on empty input or duplicate signers.
- buildPreApprovedSignature(owner) -> Hex
Produces the Safe v1.3.0 pre-approved signature blob
(pad32(owner) || bytes32(0) || 0x01) — used after an owner has
called Safe.approveHash on-chain. Critical for Anvil fork tests
where impersonated owners can't produce real EIP-712 sigs.
- executeSafeTx({ payload, signatures, walletClient, account }) -> Hex
Sends Safe.execTransaction with all default gas params zero (no
refund / no sponsor — typical for admin ops).
src/modes/index.ts: namespaced re-exports + type forwards.
src/index.ts: re-exports `modes` namespace + SafeTx types.
test/unit/modes/direct-sign.test.ts (8 assertions):
- aggregateSignatures sort order, length, empty-input rejection,
duplicate-signer rejection
- buildPreApprovedSignature byte layout (padded address + zero pad +
0x01), round-trip through aggregateSignatures
- hashTypedData determinism sanity for the SafeTx domain
test/integration/direct-sign.test.ts (1 e2e on Anvil fork, port 8546):
- Build SafeTx for a no-op (to: 0x...01, value: 0, data: 0x)
- Impersonate 2 of 3 owners, fund them, each calls approveHash
- Aggregate pre-approved sigs, execute from Anvil's default account #0
- Assert receipt.status === success and Safe nonce incremented by 1
Port 8546 (vs 8545 for anvil-sanity) avoids vitest parallelism
conflicts when both integration files run together.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…codeFnData
* Revert the `maxClaimPerTx <= maxClaimVolumePerWindow` check from
`setClaimLimits`. That config is legit: per-tx is the single-call
ceiling, volume is a stricter cumulative throttle. Tests at
IntuitionFeeProxyV2Sponsored.test.ts:419 / 435 / 455 deliberately
exercise `maxPerTx > volumeCap` to validate the volume rate-limit
path. Keep the windowSec mutation NatSpec — that's still useful.
* Add the new `maxFeeBps` / `maxFixedFee` args to two
`encodeFunctionData("deposit", …)` call sites that the bulk grep
missed (IntuitionVersionedFeeProxy.test.ts:410 + e2e-validate.ts:386).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Last leftover from the deposit ABI break — the V2_1 versioning-routing test encodes a deposit call to pin to v2.0.0 and was still passing the 3-arg form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the primary execution mode — propose / confirm / fetch SafeTx
against any Safe Transaction Service compatible backend (Den's STS
for Intuition mainnet at safe-transaction-intuition.onchainden.com).
src/modes/api-kit.ts (raw fetch, no @safe-global/api-kit dep):
- createApiKitClient({ txServiceUrl }) -> ApiKitClient
- propose(payload, signed): POST /v1/safes/{addr}/multisig-transactions/
with the proposer's EIP-712 signature attached
- confirm(safeTxHash, signed): POST /v1/multisig-transactions/{hash}/confirmations/
- getTx(safeTxHash): GET /v1/multisig-transactions/{hash}/
- getPendingTxs(safe): GET /v1/safes/{addr}/multisig-transactions/?executed=false
Raw fetch chosen over the SDK for auditability — every endpoint and
request shape is visible in source. Compatible with any STS (Safe-
hosted, self-hosted, or third-party like Den).
src/modes/direct-sign.ts (small refactor):
- buildSafeTx now accepts an optional publicClient — useful for
offline workflows / tests that pass an explicit nonce
- SAFE_TX_TYPES exported so external callers (api-kit, tests) reuse
the canonical Safe v1.3.0 EIP-712 type
src/modes/index.ts: re-exports apiKit namespace + types.
test/fixtures/mock-sts.ts (148 lines, Node http module, no deps):
- in-memory MockSts with start/stop/reset/getStored helpers
- 4 endpoints implemented matching real STS shape:
POST propose, POST confirm, GET tx, GET list
- tests can introspect what was POSTed via mock.getStored(hash)
test/integration/api-kit.test.ts (5 assertions, port 8889):
- propose stores SafeTx with proposer's signature attached
- confirm appends second signature without overwriting the first
- getTx fetches by hash, 404 on unknown hash
- getPendingTxs returns all stored txs for a Safe
No Anvil needed for this test file — pure HTTP roundtrip. Execution
of api-kit-proposed txs uses the same executeSafeTx path tested in
commit 57eb71c.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…/list
Wires everything from commits 1-7 behind a usable CLI. api-kit mode
only for now (direct mode lib is shipped + tested but no CLI surface
yet — deferred to follow-up if needed).
src/cli/op-registry.ts (declarative table of all 10 ops):
- OpRegistration { name, category, description, flags, build }
- 5 v2-admin + 4 factory + 1 uups op entries
- parseOpFlag / parseOpFlags type-safe coercion (address, bigint,
bool, hex, version-string -> bytes32 via stringToHex)
src/cli/common.ts:
- resolveContext(opts): resolves Safe address (--safe or env
SAFE_ADDRESS_MAINNET), network config, signer instance
src/cli/commands/propose.ts:
- buildProposeCommands(): generates one Commander subcommand per op
in OP_REGISTRY, each with strict flag declarations
- Action: build AdminOp, build SafeTx (fetches Safe nonce on-chain),
sign with chosen Signer, POST to Den STS, print safeTxHash + Den UI link
src/cli/commands/confirm.ts:
- safe-propose confirm --hash <safeTxHash>
- Fetches the proposed tx from STS, reconstructs SafeTxPayload,
signs with the user's Signer, POSTs the confirmation
src/cli/commands/execute.ts:
- safe-propose execute --hash <safeTxHash>
- Fetches the tx + all confirmations, aggregates sigs in canonical
order, calls executeSafeTx via direct-sign module
- Waits for receipt, exit 1 on revert
src/cli/commands/list.ts:
- safe-propose list — lists pending txs for the Safe
src/networks.ts:
- getViemChain(network): builds a viem Chain from SafeNetworkConfig
for createPublicClient / createWalletClient
bin/safe-propose.ts:
- Commander root program, registers all op + management subcommands,
dispatches via parseAsync with error formatting
packages/safe-tx/package.json:
- adds commander dep + bin entry + propose script
package.json (root):
- safe:tx -> bun packages/safe-tx/bin/safe-propose.ts
- safe:tx:test, safe:tx:typecheck convenience aliases
test/unit/cli/op-registry.test.ts (~15 assertions):
- registry shape (10 ops, 5/4/1 split per category, unique names)
- parseOpFlag for each type, error cases
- parseOpFlags missing-required, optional-skip, every-op-builds
Usage examples (after `bun install`):
bun safe:tx --help
bun safe:tx set-deposit-fixed-fee --proxy 0xPROXY --value 100 \
--safe 0xf10D... --signer env
bun safe:tx confirm --hash 0xSAFETXHASH --signer env
bun safe:tx execute --hash 0xSAFETXHASH --signer env
bun safe:tx list --safe 0xf10D...
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SafeNetworkConfig is declared in src/types.ts; src/networks.ts only re-exports values, not types. Bug surfaced by `tsc --noEmit` after commit 2a1d483 — runtime tests passed because erased types don't break execution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Re-deployed all four V2-family impls on Intuition testnet (chainId 13579) so the canonical registry points at bytecodes that include F3 (minShares[] on create*) + F5 (maxFeeBps/maxFixedFee on deposit) + F4 NatSpec corrections. v2.0.0 → 0xc6a6c8fDB94451423615e6634b01E38f104c0af4 v2.0.0-sponsored → 0xA6c3af7C06a92F17f8550a328dB3a88042370cb8 v2.1.0 → 0x9eabb1dD1A26e0f17956B30B8665A43da6986fda v2.1.0-sponsored → 0x7b67d891917627eB08feFeef870F62b1AB85D896 Existing user proxies on testnet keep pointing at the old bytecodes until their proxyAdmin (Safe) calls registerVersion + setDefaultVersion on these new addresses. Webapp will surface them via the version dropdown once the SDK is rebuilt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-shot migration: take a V2 fee proxy from EOA admin to Safe admin
in two phases. Idempotent — safe to re-run, skips steps already done.
scripts/transferAdminToSafe.ts:
- Pre-flight: reads whitelistedAdmins(eoa) and whitelistedAdmins(safe)
via the proxy's auto-getter; aborts if neither is currently admin
- Step 1 (EOA-side, single tx): EOA calls setWhitelistedAdmin(safe, true)
Skipped if Safe is already admin. Verifies PROPOSER_PK derives
exactly --eoa before sending.
- Step 2 (Safe-side, propose only): builds setWhitelistedAdmin(eoa, false)
AdminOp, wraps in SafeTx, signs with PROPOSER_PK (which must also
be a Safe owner), POSTs to Den's STS. Owners co-sign + execute via
Den UI or `bun safe:tx execute --hash ...`.
- Flags:
--no-revoke keep EOA admin alongside Safe (run side-by-side)
--dry-run print plan without sending or signing anything
Each branch logs what it did or why it skipped. On --dry-run, no key
is required and no RPC writes happen — useful for runbook validation.
Wired in package.json:
- safe-tx/package.json: `rotate-admin` script
- root package.json: `safe:rotate-admin` workspace alias
No dedicated test for this script yet — testing the full flow needs a
mainnet-deployed FeeProxy which doesn't exist yet (per memory:
project_v2_status). The pure ABI / encoding logic exercised here is
already covered by the v2-admin and api-kit test suites. Manual
smoke: `bun safe:rotate-admin --help` and `--dry-run` against any
deployed proxy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operations manual for the safe-tx tooling. Audience: anyone holding an admin role on a deployed IntuitionFeeProxy / Factory. 9 sections: - Quick reference cheatsheet (4 commands) - Pre-flight checks (network, admin status, signer is owner, STS up) - Routine admin op flow (propose / co-sign / execute) - Rotation EOA -> Safe (dry-run, full, partial --no-revoke) - Recovering from failed ops (cancel via same-nonce no-op, revert diagnostic, undo via inverse op) - Den-down fallback recipe using direct-sign mode (lib ready, no CLI wrapper yet) - Validation checklist before high-stakes mainnet ops - Out-of-scope reminders (no testnet, ledger/WC stubs, no Pausable) - Deployed addresses snapshot Recipe-driven, copy-pastable commands. Honest about the limitations (direct-mode CLI is library-only, ledger/WC throw not-implemented, testnet unsupported). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surfaces the EOA-vs-Safe distinction in the AdminsPanel without
blocking dev workflows. Dev iteration with EOA admins stays cheap;
mainnet gets a strong nudge to migrate to a Safe before going prod.
src/lib/safeDetection.ts:
- KNOWN_SAFE_SINGLETONS: canonical Safe v1.3.0+L2 + v1.4.1+L2 on
Intuition mainnet (verified on-chain via Den STS singletons endpoint)
- detectSafeStatus(client, addr): no code -> EOA, code + slot 0
matches a known singleton -> Safe, otherwise -> generic contract
src/hooks/useSafeStatus.ts:
- useSafeStatus(addr): single-address hook returning a discriminated
union { kind: 'eoa' | 'safe' | 'contract' | 'unknown' }
- useSafeStatuses(addrs): batched variant for the panel-level banner
src/components/SafeBadge.tsx:
- Inline pill showing EOA / SAFE / CONTRACT / loading
- Safe badge optionally wraps in a link to Den UI for that address
- title attributes carry full context for hover
src/components/SafeAdminHealthBanner.tsx:
- Top-of-panel banner. Three states:
* all Safe -> emerald "Safe-managed, production-ready"
* EOA-only on dev -> amber "fine for dev, see runbook"
* EOA-only mainnet -> rose "key compromise drains all admin roles,
link to runbook section 3 (rotation)"
* mixed -> amber "ok during rotation, remove EOA after"
- Uses chainId from wagmi to escalate severity on mainnet (1155)
- Hidden until detection resolves (no flash-of-warning on mount)
src/components/AdminsPanel.tsx:
- imports SafeAdminHealthBanner + SafeBadge
- Renders the banner above the admin list when admins.length > 0
- Renders SafeBadge next to each admin address row, linked to that
address's Den UI page
Aligns with the user's intent: keep dev fast with EOA admins, push
hard for Safe on mainnet without ever blocking the action.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three test gaps the post-F1-F9 internal audit flagged: * T-03 — IntuitionVersionedFeeProxy constructor with `initData = "0x"` now reverts (F9 guard). Pre-existing constructor-revert tests all tripped earlier guards (zero admin / zero version / EOA impl) so the empty-initData branch had zero coverage. * T-02 — `deposit(…, maxFeeBps, maxFixedFee)` reverts `FeeExceedsCap` when an admin bumps either knob above the caller's snapshot. Three cases: livePct > maxFeeBps, liveFixed > maxFixedFee, and equality at the boundary (must succeed). * T-01 — `createAtoms(…, minShares[], …)` reverts atomically when the inner deposit's slippage check fails. Verifies createAtoms is rolled back too (no atom creation, no fee accrual). Plus a back-compat case where minShares[i] = 0 disables slippage like before. T-01 required teaching `MockMultiVault.deposit` to enforce minShares (it used to silently ignore it). The mock now reverts `MockMultiVault_SlippageExceeded` matching the real MV's behaviour — no production-code change. 222 tests passing (was 216). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-02) L-02 from the post-F1-F9 audit. The V2 `deposit(…, maxFeeBps, maxFixedFee)` front-run guard is only useful if integrators actually pass live values — not the bytecode MAX. Without ergonomic helpers, the path of least resistance is to splice `MAX_FEE_PERCENTAGE` / `MAX_FIXED_FEE` and silently disable the protection. This module exposes: * `fetchLiveFees(client, proxy)` — read both fee knobs in parallel. * `feeCapsExact(live)` — strict, equality-required caps (recommended for non-interactive scripts). * `feeCapsWithBuffer(live, bufferBps)` — accept a relative bump in either knob, clamped to the bytecode caps so the helper never produces values that would be silently capped on-chain. * `quoteFeeCapsExact(client, proxy)` — one-call convenience. All viem-native, framework-agnostic. The webapp's deposit form should adopt `feeCapsExact` by default and surface the buffer toggle as an advanced setting. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… higher
The previous commit put a prominent banner in the fee-admin (Role 2)
panel, but Role 1 (proxyAdmin) is the one that actually deserves the
spotlight: it can swap the proxy's implementation, so an EOA
compromise there means total loss of control over the contract logic.
Role 2 is bounded to fee/withdraw/admin ops by comparison.
src/components/ProxyAdminSafeBanner.tsx (new):
- Single-address focused banner for the proxyAdmin slot
- Safe -> emerald check ("Upgrades require multisig quorum")
- Generic contract -> sky-blue info ("verify it's a multisig you trust")
- EOA on dev/testnet -> amber ("fine for dev")
- EOA on mainnet -> rose, with explicit "high-risk" framing about
implementation swap + direct link to runbook section 3 (rotation)
src/components/UpgradeAuthorityPanel.tsx:
- Renders ProxyAdminSafeBanner above the current-admin row
- Adds SafeBadge inline next to the proxyAdmin address
src/components/AdminsPanel.tsx (Role 2):
- Removes the SafeAdminHealthBanner from the top
- Keeps the inline SafeBadge per admin row (less noise, still scannable)
src/components/SafeAdminHealthBanner.tsx: deleted (unused now).
Net effect: opening any proxy detail page on mainnet with an EOA
proxyAdmin shows a red, runbook-linked banner front-and-center on
Role 1; the fee-admin panel below stays calm with just inline
EOA/SAFE badges. Aligns visual emphasis with actual blast radius.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User feedback: no emoticons in UI. Replaces emoji glyphs with plain text + Tailwind color cues (amber=warn, rose=danger, emerald=ok, muted=info) which already carry the semantic load. Also drops the github.com link to SAFE_TX_RUNBOOK.md — that URL assumes a specific repo path on main and breaks on forks / branches / private repos. Replaced with a plain reference to the runbook filename so the in-app message stays accurate everywhere. Files: - src/components/ProxyAdminSafeBanner.tsx: removed shield/alarm/ person glyphs from the 4 status banners - src/components/SafeBadge.tsx: docstring cleaned, contract badge now uses neutral subtle styling instead of sky-blue + emoji Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cleaner copy without the trailing repo-file pointer. Users who want the runbook can find it at the repo root; the banners no longer carry that bookkeeping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous copy used dev jargon (fallback caller, receiver = msg.sender, executeAtVersion, MultiVault.approve) that's opaque to the average proxy user looking at this panel. Rewritten to: - name the threat directly (admin can switch version, future deposits could be redirected) - clarify that past deposits are untouched - give the two mitigations in user-actionable terms (pin a specific version when transacting, remove MultiVault approval when idle) - bridge to the Safe story (Safe proxyAdmin makes the malicious switch much harder) — reinforces the ProxyAdminSafeBanner above No protocol detail is lost; the technical phrasing was a wall of terms, the new version is a wall of intent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Down to two sentences: name the risk, point at the fix. The mitigation detail (pin a version, revoke approval) belongs in the runbook / docs, not in a side panel hint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
GitHub Actions job that runs the full @intuition-fee-proxy/safe-tx test suite on every PR touching the package, plus on pushes to main as a regression guard. Triggers: - pull_request: packages/safe-tx/** or package.json/bun.lock change or this workflow itself - push to main: same paths - workflow_dispatch: manual trigger from the Actions tab Steps: - checkout, setup-bun, foundry-toolchain (for anvil — required by the integration tests that fork Intuition mainnet) - bun install --frozen-lockfile - safe-tx typecheck (catches type-only bugs the runtime misses) - safe-tx full test run The integration tests fork from rpc.intuition.systems by default; CI can override via the INTUITION_RPC secret if the public endpoint gets throttled or starts rate-limiting GitHub Actions IPs. 10-minute job timeout — current run is ~6s for tests + ~30s for deps install + foundry setup, so 10 min leaves generous headroom for RPC slowness without burning Actions minutes on hangs. concurrency group keyed on ref cancels stale runs when a PR is force- pushed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New "Safe multisig admin" page under the Security group, between Admin rotation and Governance. Covers in-app what SAFE_TX_RUNBOOK.md covers in the repo, framed for someone reading the webapp. Sections: - Why a Safe (operational benefit, no contract-level distinction with EOA admins) - Den hosts the Safe stack on Intuition mainnet; no testnet support - Three deployment patterns when calling Factory.createProxy: pure-Safe (rigorous), EOA + Safe (recommended), EOA-only (migrate later) - What the badges and banners in the Admins tab actually mean (SAFE / EOA / CONTRACT + per-context banner severity) - Copy-pastable CLI cheatsheet (propose / confirm / execute / list / rotate-admin) with PROPOSER_PK env hint - Pointer to SAFE_TX_RUNBOOK.md for procedures / fallbacks / validation checklists Wired into: - SectionId union (added 'safe-admin') - GROUPS Security cluster - SectionContent dispatch Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Read .claude/SAFE_INTEGRATION_PLAN.md for full context