04/07/2026
# IsotopeStableCoin V2 — Architecture & Delivery Notes
Compiled and verified against **real solc 0.8.28 + real OpenZeppelin v5.1.0**
(both regular and upgradeable packages) in this delivery — see `BUILD_VERIFICATION.md`
for the exact commands and output. This is not a "looks right" delivery —
it actually compiles, with a checked storage layout.
# # Architecture Diagram
```mermaid
flowchart TB
User -->|deposit collateral| ISC["IsotopeStableCoinV2\n(UUPS Proxy)"]
ISC -->|mint 1:1 minus fee| User
User -->|redeem ISC| ISC
ISC -->|return chosen collateral| User
Relayer -->|permitAndDeposit + EIP-712 sig| ISC
ISC -->|verify| SigVerifier["ISignatureVerifier\n(ECDSAVerifier today)"]
ISC -->|price + staleness check| Oracle["IPriceOracle\n(ChainlinkOracleAdapter)"]
Timelock["TimelockController\n(GOVERNANCE_ROLE, UPGRADER_ROLE)"] -->|addCollateral / setFees /\nsetMaxSupply / upgrade| ISC
PauserMultisig["Pauser Multisig\n(PAUSER_ROLE)"] -->|pause only| ISC
V1["IsotopeStableCoin V1\n(existing, live)"] -->|redeem| Migrator["V1Migrator"]
Migrator -->|migrateMint, fee-free| ISC
UserV1[V1 Holder] -->|approve + migrate| Migrator
```
# # Requirement-by-Requirement Summary
# # # 1. Upgradeability ✅ built
- UUPS via `UUPSUpgradeable` + `_authorizeUpgrade` gated on `UPGRADER_ROLE`.
- Storage isolated in `ISCStorageV2.sol` with an explicit `__gap[40]` and
documented rules for safely adding fields in V3+.
- **Important finding from the compile check**: OpenZeppelin v5's own
upgradeable base contracts (`ERC20Upgradeable`, `AccessControlUpgradeable`,
etc.) now use **ERC-7201 namespaced storage** — they no longer occupy
traditional slots 0, 1, 2... This is *why* `ISCStorageV2`'s variables can
safely start at slot 0 without colliding with OZ internals. Verified by
printing the actual `storageLayout` from solc (see `BUILD_VERIFICATION.md`).
- Proxy admin: deploy `ERC1967Proxy` pointing at the logic contract, with
`UPGRADER_ROLE` held by your `TimelockController` — never an EOA.
# # # 2. Multi-Collateral System ✅ built
- `addCollateral / removeCollateral / pauseCollateral / resumeCollateral`
all implemented, gated on `GOVERNANCE_ROLE`.
- Per-collateral `CollateralInfo`: enabled flag, depeg-pause flag, decimals,
oracle address, max allocation cap, and a `minOraclePriceBP` peg floor.
- `pauseCollateralEmergency()` — a **fast path** for `PAUSER_ROLE` to pause
one specific collateral instantly (e.g. a depeg event) without waiting on
governance; resuming still requires `GOVERNANCE_ROLE`.
- Accounting is tracked **per collateral** (`netDeposited`), not just in
aggregate — a shortfall in one asset can't hide behind a surplus in
another. `getGlobalCollateralizationRatio()` gives the aggregate view.
# # # 3. Mint / Redeem Logic ✅ built
- `depositAndMint(collateral, amount)` — checks peg floor via oracle,
normalizes decimals, respects per-collateral cap and global `maxSupply`.
- `redeem(collateralOut, iscAmount)` — user chooses which collateral to
receive, gated on that collateral's own liquidity. **Exits stay open**
even if a collateral is disabled or depeg-paused — only new deposits are
blocked in those states.
- `permitAndDeposit(...)` — EIP-712 signed, gasless deposit for relayers;
this is also the concrete hook for point 10 (see below).
# # # 4. Governance ✅ built, with one honest simplification
All sensitive actions (`addCollateral`, `removeCollateral`, fee changes,
supply cap changes, upgrades) are gated on `GOVERNANCE_ROLE`.
**Design decision — read this one carefully:** V1 had bespoke mini-timelocks
hand-rolled into individual functions (a 48h fee timelock, a 72h supply
timelock) because there was no real governance module. V2 assumes
`GOVERNANCE_ROLE` is held by an OpenZeppelin `TimelockController` — which
*already* enforces a delay on every action routed through it. Keeping V1's
bespoke per-feature timelocks on top of a real Timelock would just delay
the same action twice for no added security. So in V2, the delay guarantee
comes from the Timelock's own `minDelay`, uniformly, for every governance
action — simpler and not weaker.
**What I did NOT build, and why:** a full on-chain `Governor` (voting)
contract. That requires deciding what token/electorate holds voting power
— ISC holders? A separate veISC-style governance token? This is a
tokenomics decision with real economic consequences that only you (and
your stakeholders) should make, not something I should invent by default.
What's built now — Timelock + a defined proposer set (your multisig) — is
a safe, standard, and *forward-compatible* starting point: OZ's
`TimelockController` lets you swap the proposer role from a multisig to a
full `Governor` contract later without touching `IsotopeStableCoinV2` at
all, since the token only ever talks to "whoever holds `GOVERNANCE_ROLE`."
# # # 5. Supply Control ✅ built
- `maxSupply` starts at 10,000,000 ISC (matches your spec exactly).
- No further minting once supply hits the cap — enforced by
`MaxSupplyExceeded` in `_depositAndMint`.
- Cap increases only via `setMaxSupply()`, gated on `GOVERNANCE_ROLE` (i.e.
delayed by the Timelock — see point 4).
- New supply is only ever minted against fresh collateral deposits — there
is no direct/discretionary mint function anywhere in the contract.
# # # 6. Emergency Controls ✅ built
- `pause()` — `PAUSER_ROLE`, instant. Blocks mint, redeem, and deposit
(via `whenNotPaused` on all three) — transfers of existing balances are
also blocked, matching "sab operations band" from your spec.
- `unpause()` — **`GOVERNANCE_ROLE` only.** This is deliberate: pausing is a
fast circuit-breaker for an active incident, but recovery is a more
consequential decision and goes through the slower, delayed path. If you
want pauser and governance to be the exact same multisig, that's fine —
the roles just make the *option* to separate them available.
# # # 7. Security ✅ built
- `ReentrancyGuardUpgradeable` on every state-mutating external entry point.
- `PausableUpgradeable` via `ERC20PausableUpgradeable`.
- `AccessControlUpgradeable` with 6 distinct roles (see below).
- `SafeERC20` throughout — no raw `.transfer()`/`.transferFrom()` calls.
- Custom errors everywhere (`error X(...)`) instead of require-strings,
per your spec and for lower gas.
- Events on every state-changing admin/user action.
- Checks-effects-interactions followed in `redeem`, `executeSeize`, and
`migrateMint` (state updated / balances burned before external transfers).
**Roles**: `GOVERNANCE_ROLE`, `PAUSER_ROLE`, `BLACKLIST_ROLE`, `SEIZE_ROLE`,
`UPGRADER_ROLE`, `MIGRATOR_ROLE` — deliberately separated so no single role
both flags an address *and* can drain it (`BLACKLIST_ROLE` vs `SEIZE_ROLE`).
# # # 8. Oracle Design ✅ built
- `IPriceOracle` interface — three functions, tiny, easy for RedStone/Pyth/
custom sources to implement.
- `ChainlinkOracleAdapter.sol` — concrete implementation with **staleness
checking** (a real, common oracle vulnerability class — an old/stuck
price feed reading is rejected, not silently trusted).
- `updateCollateralOracle()` — governance can swap any collateral's oracle
independently, no redeployment of the main contract needed.
- Oracle price currently acts as a **peg-deviation safety guard**
(`minOraclePriceBP`, e.g. reject deposits if a "stablecoin" is trading
below $0.98) rather than a live conversion rate — consistent with this
being a basket of USD-pegged assets, not a crypto-collateralized design
like MakerDAO. Flag this assumption if you intend to support non-pegged
collateral (ETH, BNB, etc.) later — that needs a different mint formula.
# # # 9. Migration ✅ built
`V1Migrator.sol` — one transaction for the end user:
```
user.approve(Migrator, amount) → Migrator.migrate(amount)
→ pulls V1 ISC from user
→ calls V1.redeem() (Migrator receives the collateral)
→ forwards collateral into V2.migrateMint(user, ...) — fee-free, direct to user
```
**One thing this doesn't solve for you**: V1's own `redeemFeeBP` still
applies during the V1-side redeem step, since the Migrator can't change
V1's fee. If you want migration to be completely lossless, have V1
governance temporarily propose `redeemFeeBP = 0` for the migration window
(V1 already supports this natively) — that's an operational step, not
something baked into the Migrator contract.
# # # 10. Post-Quantum Readiness ✅ built as an extension point (correctly, not as a live PQ scheme)
- `ISignatureVerifier` interface + `ECDSAVerifier` (today's default, standard
secp256k1) + a `signatureVerifier` storage slot swappable via
`setSignatureVerifier()` (governance-gated).
- Wired into a concrete, real feature — `permitAndDeposit()`, an EIP-712
gasless-deposit flow — not left as a dangling unused interface.
- **Deliberately does not implement any actual post-quantum algorithm.**
Your own spec says this correctly: no current EVM chain natively verifies
PQ signatures, and bolting on an unaudited PQ library today would be a
new attack surface, not a readiness feature. What's built is the *seam*:
when BSC/Ethereum add native PQ signature support, governance deploys a
new contract implementing `ISignatureVerifier` and points to it — zero
changes to mint/redeem logic.
# # # 11. Documentation — partially built, and here's exactly where the line is
**Done:**
- Full NatSpec comments throughout every contract.
- This architecture writeup + Mermaid diagram.
- `BUILD_VERIFICATION.md` — real compiler output, real storage layout dump.
- `test/IsotopeStableCoinV2.t.sol` — a genuine Foundry test file: unit tests
covering mint, redeem, pause/unpause role-separation, blacklist, the
seize timelock, upgrade authorization, **and one fuzz test**
(`testFuzz_DepositNeverExceedsMaxSupply`).
**Not done — and I want to be direct about this rather than paper over it:**
- **I could not run these tests.** This sandbox has no network path to
install the `forge` binary (Foundry isn't on npm/pip/cargo registries in
a form I could reach). I *did* compile the test file against real
forge-std + real OZ v5.1.0 fetched from GitHub, so the API calls
(`vm.prank`, `vm.expectRevert`, `bound`, etc.) are syntactically correct
and type-check — but "compiles" is not "passes." Run `forge test -vvv`
yourself before trusting any of it.
- **No measured coverage number.** I'm not going to write "95% coverage
achieved" without having run `forge coverage` — that would be a made-up
number on a contract that holds real user funds. Get the real number
yourself once you can run the suite.
- **No invariant tests written yet** (e.g. "global collateralization ratio
never drops below 100% across any sequence of mint/redeem/seize calls").
These need a Foundry `Handler` contract that bounds random call sequences
— a real but distinct piece of work from unit tests. Listed as a TODO at
the bottom of the test file rather than faked.
- **No professional audit.** Nothing above substitutes for one, and you
should not deploy this to mainnet — especially with upgradeability and
multi-collateral risk — without a paid third-party audit. That's true
regardless of how much AI-assisted review any of this gets.
---
# # Deployment Checklist (Testnet)
1. Deploy your `TimelockController` (OZ stock contract, no changes needed):
`minDelay`, `proposers = [your multisig]`, `executors = [your multisig]`
(or `address(0)` for "anyone can execute once ready"), `admin = address(0)`
after setup (renounce, standard practice).
2. Deploy `IsotopeStableCoinV2` logic contract.
3. Deploy `ERC1967Proxy(logic, abi.encodeCall(initialize, (timelock, pauserMultisig, treasury, seizedVault)))`.
4. Deploy `ChainlinkOracleAdapter` (or a testnet mock) for each collateral.
5. Through the Timelock: `addCollateral(daiAddress, oracleAddress, maxAllocation, minOraclePriceBP)`.
6. Deploy `V1Migrator(v1Address, v2ProxyAddress, collateralAddress)`.
7. Through the Timelock on V2: `setMigrator(migratorAddress)`.
8. (Optional, on V1) propose `redeemFeeBP = 0` for the migration window.
# # Testing Checklist (run yourself)
```bash
forge install foundry-rs/forge-std OpenZeppelin/[email protected] OpenZeppelin/[email protected]
forge test -vvv
forge coverage # get the REAL number
forge test --match-test testFuzz --fuzz-runs 10000
```
# # Known Constraint
`IsotopeStableCoinV2` compiles to **21,067 bytes** against the EIP-170
**24,576 byte** deployment limit (86% used, ~3.5KB headroom). Fine for now
— just something to watch if you add more functions in a future upgrade;
you may eventually need to split logic into a second facet/library.