PolicyVault is a bounded ERC-20 spending MVP where an owner deposits tokens into a vault, creates a beneficiary-specific policy with a cap and expiry, and the beneficiary can only charge within those on-chain limits.
Wallet UX still leans heavily on broad ERC-20 approvals. That works, but it leaves a large blast radius once a spender has allowance. PolicyVault explores a narrower model:
- fund a dedicated vault instead of exposing the wallet balance directly
- bind spend rights to one beneficiary, one cap, and one expiry
- keep the policy state on-chain and event-visible
- support both classic
approve + depositand ERC-2612permit + deposit
It is intentionally interview-sized: small enough to explain quickly, but concrete enough to show contract design, script discipline, ABI sync, and wallet UX trade-offs.
PolicyVault v1 includes:
- one asset only, using
MockUSDClocally - owner-funded vault balances
- on-chain policy creation, charge, revoke, and withdraw
- deterministic policy ids
- a classic approve path and a permit path for deposit
- local deploy, seed, demo, and ABI sync scripts
- a small localhost-only Next.js dashboard
PolicyVault v1 explicitly does not include:
- multi-token support
- policy indexing or a policy list
- account abstraction
- off-chain policy creation or typed charge authorization
- production deployment targets
- backend services or an indexer
A single workspace keeps funding, policy setup, and policy use in one view.
Receipts keep readiness and recent activity beside the flow.
The mobile view preserves the same brand-first hierarchy.
contracts/Solidity contracts and interfaces.PolicyVaultis the core state machine;MockUSDCis the local 6-decimal permit-enabled token.test/Hardhat tests for the contract lifecycle, happy paths, and revert paths.scripts/Local deploy, seed, demo, and ABI/address sync tooling. The scripts use simulation before writes where practical.app/A Next.js demo UI that connects a wallet, reads contract state, and drives the bounded-spend flow through wagmi and viem.docs/andplans/Architecture notes, local ops, demo guidance, roadmap, and the active ExecPlans that record how the repo was built.
The vault has two pieces of state:
- owner vault balances via
vaultBalanceOf(owner) - policy records keyed by deterministic
policyId
The lifecycle is:
- The owner deposits
MockUSDCinto the vault. - The owner creates a policy for one beneficiary with
capandexpiresAt. - The beneficiary calls
charge(policyId, amount)within the remaining cap and before expiry. chargedecreases the owner's vault balance and increasesspent.- The owner can revoke the policy at any time.
- The owner can withdraw unused vault funds.
Important rule: createPolicy records an authorization ceiling, not reserved escrow. Funding is enforced later at charge time against the owner's live vault balance.
SafeERC20on token transfersReentrancyGuardon external mutating paths- checks-effects-interactions ordering on token-moving state changes
- custom errors instead of string-heavy reverts
- deterministic policy ids derived from owner, beneficiary, cap, expiry, and nonce
- simulate-before-write in the scripts and UI
Bootstrap once:
cp .env.example .env
cp app/.env.local.example app/.env.local
pnpm install
pnpm compile
pnpm testThen use this order:
pnpm node
pnpm deploy:local
pnpm abi:sync
pnpm seed:local
# optional after manual browser testing
pnpm demo:local
pnpm web:devNotes:
pnpm nodestarts the localhost JSON-RPC athttp://127.0.0.1:8545.pnpm deploy:localwrites the trackeddeployments/localhost.jsonartifact.pnpm abi:syncregenerates the app ABI and localhost address files from the current contract artifacts and deploy artifact.pnpm seed:localmints readable demo balances to the first three localhost wallets.pnpm demo:localruns the scripted happy path plus an intentional over-cap revert.- For a clean manual UI walkthrough, skip
pnpm demo:localuntil after browser testing so the first deposit, policy, charge, revoke, and withdraw events are still available to drive manually.
The dashboard currently supports:
- wallet state
- approve + deposit
- permit + deposit
- create policy
- load policy by id
- charge
- revoke
- withdraw
- recent event timeline
The timeline reads Deposited, PolicyCreated, Charged, PolicyRevoked, and Withdrawn logs directly from PolicyVault without an indexer.
Before the dashboard claims it is usable, it distinguishes:
Missing local deploy: no synced contract addresses yetRPC offline: localhost RPC is unavailable or not respondingNo contract code: addresses exist, but the current node has no bytecode at one or both saved addressesReady: the RPC is live and both configured addresses have deployed bytecode
Owner flow:
- Start the local node, deploy, sync ABI, seed wallets, and run
pnpm web:dev. - Connect localhost account
#0. - Confirm the dashboard reaches
Ready. - Deposit through either approve or permit and confirm wallet balance, allowance, vault balance, and timeline all update after receipt.
- Create a policy with beneficiary account
#1, note the returned policy id, and load it by id.
Beneficiary flow:
- Switch the wallet to localhost account
#1. - Use the same policy id to charge within the cap.
- Confirm the loaded policy now shows higher
spent, lowerremaining, and a matchingChargedrow in the timeline.
Owner revoke and withdraw flow:
- Switch back to localhost account
#0. - Revoke the policy.
- Withdraw the remaining vault balance to account
#2or another receiver. - Confirm the policy remains readable as revoked and the vault balance drops after withdraw.
Timeline confirmation:
- Check that the recent event list shows deposit, policy creation, charge, revoke, and withdraw in chain order.
- Use the
Refreshbutton only if you want to emphasize that the UI is reading logs directly rather than through an indexer.
- single asset
- no indexer
- no policy list
- no account abstraction
- no multi-token support
- no production deployment target yet
Those trade-offs are intentional. The goal of v1 is a narrow, explainable vertical slice for bounded spend, permit UX, and event-visible policy state.


