⚠️ Educational / testnet demo. This project is an unaudited demonstration built for learning and testing on Anvil (31337) and Sepolia (11155111). It is not for mainnet use with real funds. No security audit has been performed; expect bugs.
Live demo: https://tokenized-vault.vercel.app
- Overview
- Features
- Architecture
- Design notes
- Known limitations
- Quickstart
- Verification
- Screenshots
- Deployment
- Contributing
- Security
- License
An ERC-4626-inspired tokenized vault: deposit an ERC-20 token, receive non-transferable
shares priced by vault balance / totalSupply, withdraw back to tokens, and claim test tokens
from a cooldown faucet. It is not ERC-4626 compliant — shares are not an ERC-20 token and
the vault exposes no preview*/convert* view functions; the resemblance is limited to the
deposit→shares→withdraw shape. The frontend is a Vite + React + TypeScript (strict)
single-page app using wagmi v2, viem, and a custom connect modal — no RainbowKit or
ConnectKit.
| # | User story | What it delivers |
|---|---|---|
| US1 | Connect & dashboard | Custom wallet modal, wrong-network banner, six-stat dashboard |
| US2 | Deposit | Live share estimate, approve → deposit flow, first-deposit (dead shares) explanation |
| US3 | Withdraw | Max shares, live token estimate, inline validation |
| US4 | Faucet | 1000-token claim with a live 24 h cooldown countdown |
| US5 | Activity feed | Newest 20 Deposit/Withdraw events, live updates, explorer links only on Sepolia |
flowchart LR
subgraph frontend["Frontend — web/ (Vite · React · TypeScript strict)"]
UI["UI components<br/>Dashboard · Deposit · Withdraw · Faucet · Activity · Modal"]
Hooks["wagmi v2 + viem hooks<br/>useVaultStats · useDeposit · useWithdraw<br/>useFaucetClaim · useActivity"]
CFG[("web/src/config/deployments.json<br/>generated — addresses + deployBlock")]
UI --> Hooks
Hooks --> CFG
end
subgraph networks["Networks (31337 / 11155111 only)"]
A["Anvil — local chain<br/>chainId 31337"]
S["Sepolia — public testnet<br/>chainId 11155111"]
end
subgraph contracts["Contracts — contracts/src (OpenZeppelin submodule)"]
V["Vault.sol<br/>shares · deposit · withdraw"]
F["FaucetToken.sol<br/>ERC-20 · faucet()"]
end
subgraph tooling["Tooling — scripts/"]
DEPLOY["deploy-anvil.sh · deploy-sepolia.sh<br/>forge script --broadcast"]
SYNC["sync-deployments.mjs · sync-abis.mjs<br/>writes generated config"]
end
Hooks -->|"JSON-RPC: reads + transactions"| A
Hooks -->|"JSON-RPC: reads + transactions"| S
A -.hosts.-> V
A -.hosts.-> F
S -.hosts.-> V
S -.hosts.-> F
DEPLOY -->|"broadcast"| A
DEPLOY -->|"broadcast"| S
DEPLOY --> SYNC --> CFG
The frontend never hardcodes chain data: addresses, deploy blocks, and ABIs flow from
deployments via the scripts/ tooling into web/src/config/, and every read is pinned to the
wallet's active chain.
- Shares are non-transferable: the vault tracks balances internally and deliberately has no
transfer/approvesurface for shares. - Fee-on-transfer tokens are unsupported: deposits assume
transferFrommoves exactly the amount the depositor authorized. - 1000 BASE UNITS of dead shares protect against first-depositor inflation attacks: the
first deposit mints 1000 base units (not whole tokens) to
0xdead…dead, so a later donor cannot skew the share price.
- Shares are non-transferable: an internal ledger only — they cannot be sent, traded, or approved.
- Fee-on-transfer and rebasing tokens are unsupported; deposits assume transfers move exactly the requested amount.
- The contracts are unaudited — a learning demo, not production code.
- Testnet/local only: Anvil (31337) and Sepolia (11155111); never mainnet with real funds.
- With
VITE_SEPOLIA_RPC_URLunset, the site falls back to a public Sepolia RPC that may rate-limit under load. - The activity feed reads logs from the contract's deploy block (chunked
getLogs, newest 20 Deposit/Withdraw events); events outside that window are not listed.
Prerequisites: Foundry and Node 24 installed. Start an Anvil node in a second terminal
(anvil), then run the happy path below — every line is executed from the repo root:
git clone --recursive https://github.com/MohammedSoliman10/tokenized-vault.git
cd tokenized-vault
(cd web && npm install) # frontend dependencies
(cd contracts && forge test) # contracts suite (30 tests)
./scripts/deploy-anvil.sh # FaucetToken + Vault → the Anvil node
node scripts/sync-deployments.mjs # write addresses + ABIs for the frontend
cd web && npm run dev # dev server → http://localhost:5173Full scenarios (Sepolia deploys, secret hygiene, CI expectations, Definition of Done): specs/001-tokenized-vault-dapp/quickstart.md
Every task gates on these commands (all must exit 0):
cd contracts && forge fmt --check && forge test
cd ../web && npm run typecheck && npm run lint && npm test && npm run buildCI runs the same commands on every push — see .github/workflows/ci.yml.
The first shot is the live production site; the rest were captured by the automated browser walkthrough against local Anvil (chain 31337). All views are 1280×800 in the blue-and-white light theme except the mobile shot (360 px viewport).
Production: https://tokenized-vault.vercel.app — Vercel Root Directory: web,
built via web/vercel.json (Vite framework, install/build commands,
dist output). The Vercel project connects to this repository with main as the
production branch; deployment protection is off so the demo is publicly reachable.
Environment variables (both optional — the app degrades gracefully without them):
| Variable | Purpose |
|---|---|
VITE_SEPOLIA_RPC_URL |
Custom Sepolia RPC; falls back to a public RPC when unset |
VITE_REOWN_PROJECT_ID |
Enables the WalletConnect option in the connect modal (injected wallets always work) |
The current production deployment sets neither: reads use the public RPC fallback and
the connect modal offers injected wallets only. Environment values belong in
Vercel/dashboard .env files only — never committed (the repo gitignores .env* and CI
ships no secrets).
Deployed by scripts/deploy-sepolia.sh at deploy block
11865644; both sources are verified on Etherscan:
| Contract | Address | Notes |
|---|---|---|
FaucetToken |
0x8f6BaF9e021a57dddb2A70E7e186e6Ffb6f000f3 |
"Vault Test Token" (VTT), 1000-token faucet |
Vault |
0xBF27A9d9b4f636783B1b19D94b7D1BE4e51998B0 |
vault.token() → the FaucetToken above |
scripts/sync-deployments.mjs mirrors both addresses and the deploy block into
web/src/config/deployments.json (chain 11155111),
which is the single source the frontend reads.
See CONTRIBUTING.md: Conventional Commits and the Definition of Done gates. By participating you agree to the Code of Conduct.
This code is unaudited and demo-scoped. To report a vulnerability, use GitHub's private vulnerability reporting (Security tab → Report a vulnerability) as described in SECURITY.md — please do not open a public issue.
MIT © 2026 Mohammed Soliman





