Skip to content

feat: Gnosis Safe admin integration#7

Merged
Wieedze merged 32 commits into
mainfrom
feat/safe-admin-integration
Apr 28, 2026
Merged

feat: Gnosis Safe admin integration#7
Wieedze merged 32 commits into
mainfrom
feat/safe-admin-integration

Conversation

@Wieedze
Copy link
Copy Markdown
Owner

@Wieedze Wieedze commented Apr 28, 2026

Read .claude/SAFE_INTEGRATION_PLAN.md for full context

Wieedze and others added 30 commits April 27, 2026 13:27
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>
Wieedze and others added 2 commits April 27, 2026 20:57
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>
@Wieedze Wieedze merged commit 14a7ea5 into main Apr 28, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant