Monorepo for a versioned, upgradeable fee proxy on top of the Intuition MultiVault, with a permissionless Factory for one-click deployment and a web UI to manage individual proxies (fees, admins, versions, metrics).
- A versioned fee-proxy contract (ERC-7936 pattern) that routes every call through a pinned logic implementation, collects fees in-contract, and lets a proxy admin ship new logic versions without displacing the ones users already trust.
- A permissionless Factory so anyone can deploy their own proxy in a single transaction.
- A webapp with wallet connect, deploy form, per-proxy detail page, full light/dark docs — designed for web3 infra operators, not a landing template.
- On-chain metrics baked into the implementation: total atoms, triples, deposits, volume, unique users, last-activity block — aggregated every call and exposed via
getMetrics()for dashboards.
intuition-fee-proxy-template/
├── packages/
│ ├── contracts/ # Solidity — V2 + V2Sponsored + Factory + ERC-7936 versioned proxy
│ ├── sdk/ # Shared ABIs, addresses, chains, canonical-version registry, readers
│ └── webapp/ # Vite + React UI — deploy / my-proxies / explore / proxy-detail / docs
├── scripts/
│ └── sync-abis.ts # Copy compiled ABIs to SDK after contracts change
└── .claude/ # Project context, rules, and skills (see .claude/README.md)
- Bun (package manager + runtime)
- Node.js 20+ (for Hardhat compatibility — Node 18 works with warnings)
bun install# Contracts
bun contracts:compile # hardhat compile
bun contracts:test # hardhat test (V1 + V2 + V2Sponsored + Factory + Versioned)
bun contracts:node # local hardhat node on :8545
bun contracts:deploy:local # deploy full stack on local node (writes webapp/.env.local)
bun contracts:deploy:testnet # Intuition testnet (chainId 13579)
bun contracts:deploy:mainnet # Intuition mainnet (chainId 1155)
bun contracts:e2e:local # end-to-end standard lifecycle
bun contracts:e2e:sponsored:local # end-to-end sponsored-pool lifecycle
bun contracts:deploy:v3mock:local # deploy a mock new-version impl for manual UX testing
# SDK
bun sdk:sync # copy compiled ABIs from contracts/ into sdk/
# Webapp
bun webapp:dev # http://localhost:3000
bun webapp:build # production build
bun webapp:preview # preview production build# Terminal 1
bun contracts:node
# Terminal 2
bun contracts:deploy:local # also writes packages/webapp/.env.local
# Terminal 3
bun webapp:devMetaMask → add http://127.0.0.1:8545, chainId 31337, import one of the hardhat test keys printed by contracts:node. Account #0 is the factory deployer/owner; the MockMultiVault address on a fresh node is always 0x5FbDB2315678afecb367f032d93F642f64180aa3.
End-to-end validation (optional but recommended):
bun contracts:e2e:localWalks the full lifecycle — Factory createProxy → user deposits → registerVersion + setDefaultVersion → executeAtVersion pinning → withdrawAll — and prints a metrics snapshot at each step.
A Factory deploys a versioned proxy that delegatecalls a pinned implementation, which reads/writes the proxy's storage and forwards ETH to the Intuition MultiVault. Admins register new implementations; users either follow the default or pin their own via executeAtVersion.
Full explanation at /docs in the running webapp.
The .claude/ directory holds the planning, architecture, rules, and skill playbooks:
- .claude/README.md — index
- .claude/08-rules.md — project rules (design, copy, code, storage)
- .claude/09-skills.md — step-by-step playbooks (ship a new version, local test, etc.)
The codebase has not been audited.
What has been done is two internal security-review passes (self-review guided by Trail of Bits' Building Secure Contracts checklist, plus static analysis) it's documented here for transparency.
| Role | Holder (recommended) | Powers | Limits |
|---|---|---|---|
proxyAdmin (per-proxy) |
Safe multisig (M-of-N, M ≥ 3) | Register new impl versions, switch default, rename, transfer admin (2-step) | Cannot raise fees above MAX_FEE_PERCENTAGE = 10% / MAX_FIXED_FEE = 10 TRUST without registering a new reviewed impl (bytecode constants). Can change what fallback callers receive by activating a new default version — see "Default-version trust" below. |
Factory owner |
Project Safe multisig | Update the default impl used for FUTURE deployments, UUPS-upgrade the Factory, rotate ownership (2-step via Ownable2Step) |
Existing proxies untouched — each carries its own proxyAdmin. |
whitelistedAdmin |
Per-proxy operator | Adjust fees (bounded 0–10%), add/remove admins, withdraw accumulated fees, fund/reclaim sponsor pool | Cannot mint shares on behalf of users in the current impls (every write path forces receiver = msg.sender). Cannot drain the sponsor pool via the fee-withdraw path: withdraw / withdrawAll only touch accumulatedFees, reclaimFromPool only touches sponsorPool — the two counters are accounted separately. |
The MultiVault does not enforce receiver == msg.sender. It enforces receiver == msg.sender || approvals[receiver][msg.sender] & DEPOSIT != 0. From the MultiVault's point of view, msg.sender is always the proxy contract — never the EOA.
The current IntuitionFeeProxyV2 impl always passes the EOA caller as receiver. This is enforced by the impl's bytecode, not by the MultiVault. A future logic version registered by proxyAdmin and activated via setDefaultVersion could legally pass any address that has approved the proxy on the MultiVault — including an admin-controlled treasury.
The defense is layered:
proxyAdminis a Safe multisig. Switching the default requires M-of-N signatures. We recommend M ≥ 3 with diverse signers.- Pin a specific version to be immune. Users who never want to be exposed to default-version changes can call
executeAtVersion(versionTheyTrust, calldata)instead of using the fallback. The pinned version's bytecode never changes. - Revoke
MultiVault.approve(proxy, DEPOSIT)when idle. A user with no standing approval cannot have funds redirected — every deposit re-grants approval scoped to that single tx (or via a multicall pattern: approve → deposit → revoke). We recommend this for any user not actively transacting.
If all three defenses are bypassed simultaneously (Safe compromised + user not pinned + standing MV approval), the worst case is that the user's in-flight msg.value for new fallback calls gets routed to an admin-controlled receiver. Existing minted shares are not at risk — those require a separate REDEMPTION approval that the proxy does not request.
ReentrancyGuardon every payable entry + all withdraw paths (including the 4 Sponsored overrides)- Inverse-formula
deposit()splitsmsg.valueexactly (no refund leak) _refundExcessreturns overpayment oncreateAtoms/createTriples/depositBatchwithdraw/withdrawAllare capped ataccumulatedFees;reclaimFromPoolis capped atsponsorPool— separate counters keep fee withdraws away from the sponsor pool- ERC-7201 namespaced storage on VersionedFeeProxy + V2Sponsored (no slot collision)
_disableInitializers()on all upgradeable impls- Last-admin self-revoke guard (V1 + V2)
- 2-step ownership transfer on Factory (
Ownable2Step) and VersionedFeeProxy (pendingProxyAdmin/acceptProxyAdmin) uint128-boundedsetClaimLimitsto prevent silent truncation- No
receive()/fallback()that blindly accepts ETH — direct transfers revert
MIT