Feature/platform upgradeable#13
Conversation
Add @openzeppelin/hardhat-upgrades and contracts-upgradeable, register the plugin, and introduce shared deploy helpers for Transparent Proxy deployment. Make sepolia_op_devnet network config conditional so compile works without local .env credentials.
Replace constructor and immutable OWNER with initializer setup, ERC20Upgradeable, and a one-time setCoordinator wiring step for proxy deployment order.
Accept an externally deployed DinToken proxy in initialize instead of deploying inline, and replace immutable dinToken with storage.
Replace constructor with initialize, move DIN_TOKEN and DIN_COORDINATOR from immutables into storage, and adopt OwnableUpgradeable.
Replace constructor and daoAdmin with OwnableUpgradeable, move fee defaults into initialize, and drop setDAOAdmin in favor of transferOwnership.
Add deploy-platform and upgrade-platform scripts with npm shortcuts, wire the four contracts in dependency order, and annotate constructors for hardhat-upgrades validation.
Cover state persistence, access control, storage layout validation, and cross-contract wiring across transparent proxy upgrades for all four platform contracts.
|
Please can you review this PR. Thank you |
- Replace transfer() with call{value:} in DINModelRegistry.withdrawFees()
and add TransferFailed error to match DinCoordinator's pattern
- Change DinToken.initialize() visibility from public to external for
consistency with the other three platform contracts
- Add explanatory comments to all four V2 contracts justifying the
@Custom:oz-upgrades-unsafe-allow missing-initializer annotation
- Track implementation addresses in deployments JSON after each upgrade:
add implementations map to PlatformAddresses type and write it in
upgrade-platform.ts so the deployment record stays accurate post-upgrade
PR Review — Robbert AbimbolaPR: #13 1. Overall AssessmentStrong, well-scoped submission. Robbert read the codebase carefully, made defensible architecture decisions, documented them clearly, and shipped a clean implementation with tests. The storage layout reasoning, deployment order, and proxy pattern justification all show genuine understanding — not copy-paste from a tutorial. Abraham's independent read: "If I were reviewing this PR and the code matched the write-up, I'd probably approve it after a small compatibility discussion around DINModelRegistry." His strongest positive signals: correct proxy choice, proper handling of immutables and constructors, explicit deployment wiring, state-persistence tests rather than superficial upgrade tests, awareness of future governance migration. His only flagged weakness: the unnecessary API break from removing Verdict: Hold — and to be clear, this is not a reflection on the quality of Robbert's work. The Solidity implementation is solid and the hold is not on anything he did wrong architecturally. The hold exists because of a gap on our side: a 1a. Follow-up Commit (4c58299 — pushed after initial review)Robbert pushed a second commit on 2026-06-26 titled "fix(contracts): address upgrade review concerns" before being sent this review. It proactively addressed several minor items:
What this commit does NOT address (all items from §3–§5 below remain open):
The commit shows Robbert is actively iterating on feedback — positive signal. 2. Outstanding WorkArchitecture / Design
Implementation
Tests
Documentation
3. Required Fix Before MergePre-existing
|
| Deliverable | Status | Notes |
|---|---|---|
| Proxy pattern chosen and justified | ✅ | Transparent, well-argued |
| All 4 contracts converted to initializer pattern | ✅ | All 4 clean |
| Storage-layout safe | ✅ | immutables removed, defaults in initialize(), validateUpgrade() run |
| Hardhat deploy scripts for proxy deploy | ✅ | deploy-platform.ts with correct wiring order |
| Hardhat deploy scripts for upgrade flow | ✅ | upgrade-platform.ts with CONTRACT= env var |
| Upgrade-safety tests per contract | ✅ | 12 tests, 3 per contract |
| State persistence tests | ✅ | All 4 |
| Access control tests | ✅ | All 4 |
| Storage layout validation | ✅ | validateUpgrade() in all 4 suites |
| Write-up of changes and rationale | ✅ | Thorough PR description |
| Doc-inaccuracy notes | ✅ | 8 items, all accurate |
| Pre-existing tests still pass | ❌ | DinValidatorStake.test.ts broken (4 tests) — not fixed in 4c58299 |
Tests for _disableInitializers() |
❌ | Not present — implementation init not tested |
initialize() visibility consistent |
✅ | Fixed in 4c58299 |
| Implementation address tracked post-upgrade | ✅ | Added in 4c58299 |
withdrawFees uses safe call pattern |
✅ | Fixed in 4c58299 |
7. Questions for Robbert
-
Broken test (required):
DinValidatorStake.test.tsfails with "incorrect number of arguments to constructor" — did you run the full test suite before submitting? The fix is straightforward (usedeployPlatform()helper) but must be in the PR. -
daoAdminshim (design discussion): Why removedaoAdmin()/setDAOAdmin()entirely rather than adding thin compatibility wrappers overowner()/transferOwnership()? Was the clean break intentional, or was the shim option not considered? If intentional, thedincliABI update should be a tracked pre-condition before testnet deployment, not just a "with more time" item. -
Coordinator replacement (clarification):
DinToken.setCoordinator()is one-shot — once the coordinator address is set it can never change. Is this intentional? With Transparent Proxy, coordinator proxy addresses are stable so this is safe today, but it's worth a comment in the code confirming it's a deliberate design constraint. -
_disableInitializers()tests (gap):_disableInitializers()is present in all 4 constructors — confirmed. But there are no tests proving implementation contracts actually revert on direct initialization. Can you add one per contract? This turns a code-review check into a tested security property. -
ProxyAdmin recording: The deploy script saves the ProxyAdmin address to
deployments/${network}.json. Is that file committed, or gitignored? If gitignored, what's the plan for keeping the address safe for testnet? Losing it means reconstructing from transaction receipts. -
require()vs custom errors: Were the tworequire(condition, "string")calls inrequestModelRegistrationintentionally left, or missed? The rest of the contract uses custom errors; these are inconsistent. -
Testnet migration plan: The existing Optimism Sepolia deployment has live state (stakers, models). When we migrate to proxy-based contracts, is the plan a clean redeploy (accept state loss for devnet) or a migration script? Doesn't need to be in this PR, but worth aligning on now.
8. Merge Recommendation
Hold — three blockers before merge:
- Fix
DinValidatorStake.test.tsregression (§3) — straightforward, shouldn't take long. - Resolve the
daoAdmincompatibility question (§4) — either add the two-line shim or document the clean-break decision with a hard tracked prerequisite on thedincliside. - CLI-level integration tests must exist before this merges to
develop.
On point 3: the upgraded contracts change ABIs in ways that are almost certain to break dincli call sites:
daoAdmin()is gone fromDINModelRegistry— anydindao.pypath that calls it fails.DIN_TOKEN()/DIN_COORDINATOR()onDinValidatorStakeare now mutable storage rather thanimmutable— the getter ABI is the same but the behaviour at a freshly deployed address differs.DinCoordinatorno longer deploysDinTokenin its constructor — anydinclibootstrap flow that assumeddinToken = coordinator.dinToken()after a single deploy needs the new two-step wire.
There is currently no test harness at the dincli layer that exercises these paths against a local chain. Contract-level Hardhat tests prove the Solidity is correct; they say nothing about whether the Python CLI correctly handles the new contract interfaces. Until CLI-level tests exist — even minimal ones covering dindao calls and the token/coordinator bootstrap path against a local Hardhat node — we cannot safely merge this branch without risking a broken devnet CLI for everyone.
What "CLI-level tests" means here: tests in the dincli Python test suite (following the monkeypatch/CliRunner pattern in tests/) that spin up a local chain (or mock the contract layer), deploy the proxy contracts via the new scripts, and assert that the key dincli commands complete without error. This is a prerequisite that belongs on develop before or alongside this PR — it is not Robbert's work to own, but it is a gate on merging his branch.
Once blocker 3 is resolved (and 1 and 2 addressed), add the _disableInitializers() tests from §4 as a fast follow. The core Solidity work — proxy pattern, storage layout, deployment wiring, upgrade tests — is competent and ready to land once the CLI safety net is in place.
- Add DinValidatorStake.test.ts with 16 behavioural tests (staking, unbonding window, slash active/pending, blacklist, slasher management) using deployPlatform() proxy setup — fixes broken pre-existing test suite - Add _disableInitializers() guard test to all 4 upgrade test files, proving implementation contracts reject direct initialisation - Replace require() string errors in DINModelRegistry.requestModelRegistration with revert CoordinatorNoLongerSlasher / AuditorNoLongerSlasher to match the approval-time checks and drop string encoding cost - Add daoAdmin() / setDAOAdmin() backward-compat shims over OwnableUpgradeable so existing dincli ABI surface is preserved; emits DAOAdminUpdated event - Add uint256[50] __gap to all 4 platform contracts for safe future inheritance-level storage extension - Add comment to DinToken.setCoordinator() documenting the intentional one-shot constraint - Remove /deployments from hardhat/.gitignore so proxy addresses are committed alongside the code
|
Thanks for the thorough review, Umer. Broken test (§3) daoAdmin shim (§7 Q2) function daoAdmin() external view returns (address) { return owner(); } Coordinator replacement (§7 Q3) _disableInitializers() tests (§7 Q4) ProxyAdmin recording (§7 Q5) require() strings (§7 Q6) Testnet migration plan (§7 Q7) |
Follow-up — Code-Level Blockers Resolved ✅@robertocarlous — thanks for the fast and complete iteration. Checked commit b32b299 against all review items. Everything from §3–§5 is now addressed
Your responses to the review questions were clear and correct — in particular the testnet migration plan (clean redeploy) and the coordinator one-shot reasoning. One remaining gate before merge (team-side, not yours)The Solidity work is done and ready to land. The single remaining blocker is CLI-level integration tests in the Once that harness exists, this PR merges. We'll circle back when it's in place. Good work on this. The implementation is clean, the decisions are well-reasoned, and the follow-up was thorough. The hold is not on anything you did wrong — it's a gap in our own tooling we need to close before landing any ABI-changing contract PR. |
|
Understood, thanks for the clear sign-off. I'll keep the branch up to date with develop while WP 6.1 is in progress. You can always let me know whenever the CLI harness is ready and I'll rebase if needed. |
Option chosen
Option A — Upgradeable platform contracts
I chose Option A because it maps directly to Phase P3 ("contract upgrade flows") in
Developer/ROADMAP.md. The four platform contracts hold long-lived economic and coordination state; making them upgradeable preserves proxy addresses and on-chain state while allowing logic fixes over time. Task-level contracts (DINTaskCoordinator,DINTaskAuditor) were intentionally left as redeployable per the issue doc and task scope i was assigned toProxy pattern: Transparent Proxy
Chosen: OpenZeppelin Transparent Proxy +
ProxyAdmin(via@openzeppelin/hardhat-upgrades).Why not UUPS:
ProxyAdminlater without changing the pattern.Toolchain:
@openzeppelin/contracts-upgradeable^5.3.0@openzeppelin/hardhat-upgrades^3.9.1PROXY_KIND = "transparent"inhardhat/deploy/constants.tsStorage layout rules followed
immutablecross-contract references removed — proxies read storage by slot, not bytecode.constantvalues (MIN_STAKE,UNBONDING_PERIOD) unchanged — not stored in proxy slots.hardhat-upgradesvalidateUpgrade()run in tests before each V1 → V2 upgrade.Per-contract changes
DinTokenowner_initialize()immutable OWNERcoordinatorstorageonlyOwnerfor mintonlyCoordinatorfor mint;OwnableUpgradeablefor setupDinCoordinatorMint authority is wired post-deploy via
setCoordinator(address)(one-shot,onlyOwner).DinCoordinatornew DinToken(address(this))in constructorinitialize(address dinToken_)immutable dinTokendinTokenstorageOwnableconstructorOwnableUpgradeable+_disableInitializers()ReentrancyGuardTransientkept as-is (stateless / EIP-1153 — no proxy storage impact).DinValidatorStake(dinToken, dinCoordinator)initialize(dinToken, dinCoordinator)immutable DIN_TOKEN,immutable DIN_COORDINATOROwnableOwnableUpgradeableDINModelRegistrydaoAdmin = msg.senderinitialize(dinValidatorStake_)daoAdmin+onlyDAOAdmin+setDAOAdmin()OwnableUpgradeable+onlyOwner+transferOwnership()initialize()(proxy storage starts at zero)Access model deviation:
daoAdmin/setDAOAdmin/DAOAdminUpdatedremoved in favour of standard OZowner()/transferOwnership(). Functionally equivalent for admin migration;dincliABIs referencing the old interface are out of scope for this PR but will need a follow-up when testnet is redeployed.Deployment order
Proxies cannot use nested
new Contract()— wiring is explicit:DinTokenproxy →initialize()DinCoordinatorproxy →initialize(dinTokenAddress)DinToken.setCoordinator(dinCoordinatorAddress)DinValidatorStakeproxy →initialize(dinTokenAddress, dinCoordinatorAddress)DinCoordinator.updateValidatorStakeContract(stakeAddress)DINModelRegistryproxy →initialize(stakeAddress)Scripts:
cd hardhat && npm test