Upgradeable ERC20 token (capped, burnable, permit, access-controlled) deployed via Transparent Proxy on Arbitrum One.
- Capped supply with mutable cap reduction on burn
- Burn (with or without cap reduction)
- EIP-2612 Permit (gasless approvals)
- Role-based access control (MINTER_ROLE, PLUGIN_ADMIN_ROLE)
- Upgradeable via OpenZeppelin Transparent Proxy
- Delegated plugin handler (delegatecall, can be paused)
- Hardhat + TypeScript
- OpenZeppelin Contracts & Upgrades
- Arbitrum One network
contracts/Solidity sourcesscripts/Deployment & upgrade helpers.openzeppelin/Upgrade manifest (must stay committed)artifacts/+cache/(ignored)
Copy .env.example -> .env and fill values:
ARBITRUM_ONE_RPC_URL=https://arb1.arbitrum.io/rpc
PRIVATE_KEY=your_private_key_no_0x
SAFE_ADDRESS=0xYourSafeAddress
SAFE_OWNER=0xYourSafeOwnerAddress
TOKEN_CAP=100000000
CONTRACT_NAME=ParkToken
NETWORK_NAME=Arbitrum One
ARBISCAN_API_KEY=yourArbiscanKey
# Filled after deploy:
PROXY_ADDRESS=
PROXY_ADMIN_ADDRESS=
IMPLEMENTATION_ADDRESS=
Never commit .env.
npm installnpm run compileTwo approaches:
- Simple proxy deploy (no initialize auto-call):
npm run deploy:proxyThen prepare initialization calldata (if not using deployFlow):
npm run calldata:initSubmit initialize(cap, safe) via proxy (e.g. Gnosis Safe) once.
- Full flow helper (includes showing initialize calldata):
npm run deploy:flowRecords proxy, implementation, admin addresses. Update .env with PROXY_ADDRESS, PROXY_ADMIN_ADDRESS.
Token is not initialized automatically (initializer: false). Use generated calldata with Safe as owner to set:
initialCap(expressed in 18 decimals in script)ownerAddress(Safe)
After init, grant roles from Safe:
grantMinter(address)grantPluginAdmin(address)
- Prepare upgrade & calldata for Safe:
npm run prepare:upgradeFill <REPLACE_WITH_ADMIN_ADDRESS> with actual ProxyAdmin and propose Safe TX using generated calldata.
- Alternative quick flow (direct upgrade via deployer EOA - not for production Safe):
npm run upgrade:flowAfter setting env vars:
npm run verify:allMake sure IMPLEMENTATION_ADDRESS, PROXY_ADDRESS, PROXY_ADMIN_ADDRESS are in .env.
npm run get:proxyAdminreads ProxyAdmin from storage slotnpm run calldata:initprints initialization calldata
- Do NOT remove
.openzeppelin/manifests; required for safe upgrades. - Proxy is deployed with
unsafeAllow: ["delegatecall"]due to plugin handler usage — audit any handler. - Initialization must occur exactly once.
- Keep private keys & Safe addresses secure; use a hardware wallet for deployer if possible.
Enable with:
REPORT_GAS=true npx hardhat test(Add tests first.)
- Add unit tests for role flows
- Add plugin handler contract example
- Add CI (lint + compile + verify dry-run)
(Generated README for repository bootstrap.)