Source tree: foundry/src/ — the reference implementation
Toolchain: Foundry (forge), solc 0.8.28 with via_ir = true, openzeppelin-foundry-upgrades (runs @openzeppelin/upgrades-core through npx, so run npm ci in foundry/ first)
Proxy pattern: OpenZeppelin Transparent Proxy, one ProxyAdmin per proxy (OZ v5)
This document covers how the DIN platform contracts are made upgradeable, and the tooling that deploys, upgrades and validates them. For the older Hardhat version of the same material (four contracts, PR #13 era) see ../hardhat/README.md. The design decision for how dincli should eventually deploy proxies is recorded in proxy-deployment-architecture.md.
| Contract | Upgradeable | Rationale |
|---|---|---|
DinTreasury |
✅ Transparent Proxy | Holds protocol ETH/DIN; its address is wired into the fee router and stake contract |
DinToken |
✅ | Balances must survive logic upgrades |
DinCoordinator |
✅ | Faucet/emission minting hub and slasher administrator; referenced everywhere |
DinFeeRouter |
✅ | Fee split logic will evolve; accrued balances must persist |
DinValidatorStake |
✅ | Custodies all validator stake; slashing logic will evolve |
DINModelRegistry |
✅ | Long-lived model and fee state |
DinEmission |
✅ | Emission schedule state per task auditor |
DinFairLaunchDistributor |
✅ (separate script) | Vesting state; deployed by DeployFairLaunchDistributor |
DINTaskCoordinator, DINTaskAuditor |
❌ plain Ownable |
Deployed per model by the model owner; a redeploy is the upgrade path |
DINShared.sol |
n/a | Types, interfaces, errors only |
The dividing line is lifecycle: contracts deployed once per network and holding protocol-wide state get proxies; per-model contracts stay plain.
- A bad implementation cannot brick the proxy. Upgrade authority lives in a separate ProxyAdmin, not in implementation code that a buggy V2 could break.
- Governance fits. ProxyAdmins are owned by the DIN-Representative today and can be transferred to a multisig or timelock without touching implementations.
- Cost accepted. There is a small per-call admin-check overhead.
- Upgradeable bases:
Initializable,OwnableUpgradeable(+ERC20Upgradeablefor the token). OZ v5 stores their state in namespaced ERC-7201 slots. - Constructor only calls
_disableInitializers()(annotated@custom:oz-upgrades-unsafe-allow constructor); all setup is in a one-shotinitialize(...). - No
immutables:DinValidatorStake'sDIN_TOKEN/DIN_COORDINATORare storage with their old ALL_CAPS names kept. - Defaults (fees, rates, parameters) are assigned in
initialize, not at declaration. - A trailing
uint256[50] __gapin every platform contract. ReentrancyGuardTransient(EIP-1153 transient storage — no slots, no init call) where re-entrancy guards are needed.
Slot-by-slot inventories: storage_layout.md.
| Task | Tool | Doc |
|---|---|---|
| Deploy + wire all seven proxies | foundry/script/DeployPlatform.s.sol |
DeployPlatform |
| Import addresses into dincli | dincli system import-deployments (reads foundry/deployments/<network>.json) |
DeployPlatform — Usage |
Upgrade one proxy (to its <Name>V2) |
foundry/script/UpgradePlatform.s.sol |
UpgradePlatform |
| Per-network deployments file | foundry/script/DeploymentsPath.sol |
DeploymentsPath |
| V2 upgrade fixtures | foundry/src/upgrade/*V2.sol |
V2 fixtures |
The contract-by-contract behavior (initializers, wiring, access control, ownership planes) is documented in each contract's page under contracts/: DinToken, DinCoordinator, DinValidatorStake, DINModelRegistry.
| Test | What it checks |
|---|---|
foundry/test/UpgradeValidation.t.sol |
Upgrades.validateImplementation on all seven platform implementations (initializer, storage and unsafe-pattern rules) |
foundry/test/DeployPlatform.t.sol — ProxyWiringTest |
Every wiring edge from the deploy sequence; all ProxyAdmins owned by the deployer; faucet mint through the proxy |
foundry/test/DeployPlatformScript.t.sol — DeployPlatformScriptTest |
Runs the script's own deploy() with default and overridden tokenomics; every contract and ProxyAdmin owned by the deployer; invalid overrides revert the deploy |
DeployPlatform.t.sol — ReInitializerProtectionTest |
A second initialize on each proxy, and a direct initialize on each implementation, both revert |
DeployPlatform.t.sol — DinTokenUpgradeTest, DinCoordinatorUpgradeTest, DinValidatorStakeUpgradeTest, DINModelRegistryUpgradeTest |
Upgrade to the V2 fixture with Upgrades.upgradeProxy (validation included); state and access control survive |
cd foundry && npm ci
forge test --match-contract 'UpgradeValidationTest|ProxyWiringTest|ReInitializerProtectionTest|UpgradeTest'npm ci must come first: the upgrade validations shell out to npx @openzeppelin/upgrades-core. Without a populated node_modules, parallel tests race each other fetching it.
- Append only; never reorder, remove or retype state. Consume
__gapslots once a proxy holds live state (see storage_layout.md for the current pre-deployment practice). - Keep
_disableInitializers()in every implementation constructor. - New one-time setup needs
reinitializer(n), notinitializer. - Diff
forge inspect <Contract> storage-layoutold vs new, and runUpgrades.validateUpgradeagainst the live proxy before upgrading. - The ProxyAdmin owner is the real upgrade key. Custody is currently the DIN-Representative's single key; on-chain governance is deferred to post-mainnet.
UpgradePlatform.s.soland the V2 fixtures cover onlyDinToken,DinCoordinator,DinValidatorStakeandDINModelRegistry.DinTreasury,DinFeeRouterandDinEmissionhave implementation validation but no upgrade round-trip test or upgrade script mapping.- dincli cannot deploy proxies itself yet (planned: dincli-native-proxy-deployment.md).