Minimal RWA tokenisation flow built for the 86 assignment. Users deposit ETH into a Treasury contract and receive ERC20 tokens representing fractional ownership. Owner can withdraw ETH from the treasury.
- Solidity + Hardhat + OpenZeppelin
- Node.js / TypeScript backend (Express + ethers.js v6)
# root deps (hardhat, contracts)
npm install
# backend deps
cd backend && npm install && cd ..
# compile contracts
npm run compileTerminal 1 — local chain:
npm run nodeTerminal 2 — deploy contracts:
npm run deploy:localCopy the printed addresses into backend/.env:
RPC_URL=http://127.0.0.1:8545
RWA_TOKEN_ADDRESS=<from deploy output>
TREASURY_ADDRESS=<from deploy output>
PORT=3000
Start the backend:
cd backend && npm run devnpm test18 tests covering deposit flow, withdrawal flow, and edge cases (unauthorized access, zero deposit, etc).
curl http://localhost:3000/healthcurl http://localhost:3000/api/balance/0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266{
"address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"rwaTokenBalance": "1000.0",
"rwaTokenBalanceRaw": "1000000000000000000000",
"ethBalance": "9998.9994",
"symbol": "RWAT",
"name": "RWA Asset Token"
}curl "http://localhost:3000/api/transactions?limit=10"
# filter by address
curl "http://localhost:3000/api/transactions?address=0xf39F...&limit=10"curl -X POST http://localhost:3000/api/deposit-preview \
-H "Content-Type: application/json" \
-d '{"amount": "1.5"}'{
"inputEth": "1.5",
"tokensToMint": "1500.0",
"exchangeRate": "1000",
"note": "Rate: 1000 RWAT per 1 ETH"
}AccessControl on RWAToken instead of Ownable — MINTER_ROLE lets multiple contracts mint without changing ownership. Only Treasury has this role for now.
Fixed exchange rate — 1000 RWAT per ETH keeps the math simple. In production this would come from a price oracle.
previewDeposit is a pure function on-chain — backend delegates the calculation to the contract so the preview is always consistent with what deposit() will actually mint. No duplicated logic.
ReentrancyGuard on Treasury — both withdraw functions send ETH so nonReentrant is the safe default.
Transactions endpoint queries last 1000 blocks — fine for local/testnet. For production you'd use an indexer like The Graph.