diff --git a/.gitignore b/.gitignore index 59008c5..d7ef9b6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,8 +23,6 @@ tasks/local/0x4657105FC932625CD289107aAE7B2174a822b709/services/__pycache__/* /tasks/* /scripts/* /hardhat/ignition/* -/hardhat/scripts/* -/hardhat/test/* /hardhat/test-output.txt /dincli/config/accounts.json /dincli/config/din_info.json diff --git a/Developer/Documentation/technical/storage_layout.md b/Developer/Documentation/technical/storage_layout.md new file mode 100644 index 0000000..c3e4cfb --- /dev/null +++ b/Developer/Documentation/technical/storage_layout.md @@ -0,0 +1,133 @@ +# Storage Layout — DIN Platform Contracts + +This document covers the storage layout rules and variable inventory for the four +upgradeable platform contracts. All four follow the OpenZeppelin Transparent Proxy +pattern (`Initializable`, `OwnableUpgradeable`) and carry a `uint256[50] private __gap` +reservation at each inheritance level to allow safe future additions. + +--- + +## Core rules for upgradeable contracts + +**Append-only.** Every state variable occupies an absolute storage slot derived from +its declared position in the inheritance chain. Any upgrade that inserts, removes, or +reorders variables corrupts the storage of the live proxy. The only safe operation is +appending new variables at the end of a contract's own block, before the `__gap`. + +**Consume gap slots before adding post-gap variables.** When a new state variable is +needed, shrink `__gap` by the number of slots required and place the new variable +immediately above `__gap`. Never add variables after `__gap`. + +**Inherited slots are fixed.** Slots occupied by OpenZeppelin base contracts +(`_initialized`, `_owner`, etc.) are determined by their upstream storage layout and +must not be touched. + +**`ReentrancyGuardTransient` is slot-neutral.** `DinCoordinator` and +`DinValidatorStake` inherit from `ReentrancyGuardTransient`, which stores its lock in +EIP-1153 transient storage (cleared each transaction). It contributes zero persistent +storage slots. + +--- + +## DinToken + +``` +[Initializable] + _initialized : uint64 (packed with _initializing bool) +[OwnableUpgradeable] + _owner : address +[ERC20Upgradeable] + _balances : mapping(address => uint256) + _allowances : mapping(address => mapping(address => uint256)) + _totalSupply : uint256 + _name : string + _symbol : string +─────────────────────────────────── contract-own slots ─── + coordinator : address + __gap : uint256[50] ← 50 reserved slots +``` + +`setCoordinator` is one-shot; `coordinator` will not change after initial wiring. +Future variables must be inserted above `__gap`, reducing its size accordingly. + +--- + +## DinCoordinator + +``` +[Initializable] + _initialized : uint64 +[OwnableUpgradeable] + _owner : address +─────────────────────────────────── contract-own slots ─── + dinToken : address (DinToken proxy) + dinValidatorStakeContract : address (IDinValidatorStake) + dinPerEth : uint256 (exchange rate, 1e18-scaled) + __gap : uint256[50] +``` + +`dinValidatorStakeContract` is written once by `updateValidatorStakeContract`. +`dinPerEth` is mutable via `updateDinPerEth`. + +--- + +## DinValidatorStake + +``` +[Initializable] + _initialized : uint64 +[OwnableUpgradeable] + _owner : address +─────────────────────────────────── contract-own slots ─── + DIN_TOKEN : IERC20 (immutable after initialize) + DIN_COORDINATOR : address (immutable after initialize) + slasherContracts : mapping(address => bool) + validators : mapping(address => ValidatorInfo) + __gap : uint256[50] +``` + +`ValidatorInfo` is a struct packed into a single mapping entry; its internal layout +does not affect the contract's top-level slot numbering. + +--- + +## DINModelRegistry + +``` +[Initializable] + _initialized : uint64 +[OwnableUpgradeable] + _owner : address +─────────────────────────────────── contract-own slots ─── + daoAdmin : address + modelCount : uint256 + models : mapping(uint256 => Model) + modelIdByCoordinator: mapping(address => uint256) + modelIdByAuditor : mapping(address => uint256) + proprietaryFee : uint256 + pendingModels : mapping(uint256 => PendingModel) + pendingCount : uint256 + __gap : uint256[50] +``` + +`daoAdmin` exists alongside `_owner` to preserve backward compatibility with +external callers that used the old `daoAdmin()` accessor. `setDAOAdmin` updates +both `daoAdmin` and transfers OZ ownership simultaneously. + +--- + +## Upgrade checklist + +Before deploying an implementation upgrade to a proxy: + +1. Run `forge inspect storage-layout` on both the old and new + implementation and diff the output. No existing variable should change slot, + type, or size. +2. Any new variable must appear above `__gap` with `__gap` shrunk by the + corresponding number of slots. +3. Structs used in mappings may gain new fields only if they are appended at the + end of the struct definition and the mapping is not iterated in a way that + assumes fixed struct size. +4. Confirm `_disableInitializers()` remains in the implementation constructor. +5. Run `openzeppelin-foundry-upgrades` `validateUpgrade` against the live proxy + address before executing the upgrade on-chain. diff --git a/Developer/tasks/task_290626_2.md b/Developer/tasks/task_290626_2.md new file mode 100644 index 0000000..fbc7883 --- /dev/null +++ b/Developer/tasks/task_290626_2.md @@ -0,0 +1,278 @@ +# Task: PR #13 Final Fixes + Test Harness Integration + NatSpec + Foundry Migration Start + +**ID:** task_290626_2 +**Assigned to:** Robbert Abimbola +**Created:** 2026-06-29 +**Status:** Open +**Roadmap ref:** P3-PR13 · P3-6.3b · Foundry migration (PR #15) +**Branch:** `feature/platform-upgradeable` (PR #13 fixes) +**Week:** 1 (July 1–3, 2026) — short week, Wednesday to Friday + +--- + +## Context + +Three days, four focused pieces in priority order. Stop at the end of each part if time runs short — the ordering matters. + +--- + +## Environment Setup (do this before Day 1) + +All paths below are from the lead engineer's machine and **will not match yours**. Before running anything, set up two Python venvs and update the constants file. + +### Two required venvs + +**`pydin` venv** — used for all deploy/registry/wallet commands (no torch): + +```bash +python3 -m venv +source /bin/activate +cd +pip install -e . # installs dincli from your local clone — do NOT use the GitHub hash from the freeze below +pip install pytest # if not pulled in by editable install +``` + +Reference package list (versions to match — the `-e git+...` line becomes `pip install -e .` from your local clone): +``` +aiohappyeyeballs==2.6.2, aiohttp==3.14.1, aiosignal==1.4.0, annotated-types==0.7.0, +attrs==26.1.0, base58==2.1.1, bitarray==3.8.2, blake3==1.0.8, certifi==2026.6.17, +charset-normalizer==3.4.7, ckzg==2.1.7, click==8.4.1, cytoolz==1.1.0, +eth-account==0.13.7, eth-hash==0.8.0, eth-keyfile==0.8.1, eth-keys==0.7.0, +eth-rlp==2.2.0, eth-typing==6.0.0, eth-utils==6.0.0, eth_abi==5.2.0, +frozenlist==1.8.0, hexbytes==1.3.1, idna==3.18, markdown-it-py==4.2.0, +mdurl==0.1.2, mmh3==5.2.1, morphys==1.0, multidict==6.7.1, packaging==26.2, +parsimonious==0.10.0, platformdirs==4.5.0, pluggy==1.6.0, propcache==0.5.2, +py-cid==0.5.0, py-multibase==2.0.0, py-multicodec==1.0.0, py-multihash==3.0.0, +pycryptodome==3.23.0, pydantic==2.13.4, pydantic_core==2.46.4, Pygments==2.20.0, +pytest==9.1.1, python-baseconv==1.2.2, python-dotenv==1.0.1, pyunormalize==17.0.0, +regex==2026.5.9, requests==2.34.2, rich==15.0.0, rlp==4.1.0, shellingham==1.5.4, +six==1.17.0, toolz==1.1.0, typer==0.20.0, types-requests==2.33.0.20260518, +typing-inspection==0.4.2, typing_extensions==4.15.0, urllib3==2.7.0, varint==1.0.2, +web3==7.10.0, websockets==15.0.1, yarl==1.24.2 +``` + +**`torchenv` venv** — used for commands that invoke model training, aggregation, or auditor evaluation: + +```bash +python3 -m venv +source /bin/activate +pip install torch==2.6.0 torchvision==0.21.0 +pip install -e # same dincli editable install +``` + +Reference additional packages (above the base pydin list): +``` +filelock==3.29.4, fsspec==2026.6.0, Jinja2==3.1.6, MarkupSafe==3.0.3, +mpmath==1.3.0, networkx==3.6.1, numpy==2.4.6, pillow==12.2.0, +setuptools==82.0.1, sympy==1.13.1, torch==2.6.0, torchvision==0.21.0, +triton==3.2.0 +(+ nvidia-* CUDA packages if running on GPU) +``` + +### Update `tests/dincli/constants.py` + +The constants file has hardcoded paths from the lead engineer's machine. Update these four lines to match your setup before running the harness: + +```python +# tests/dincli/constants.py — set these to your own paths +PYDIN_PYTHON = "/bin/python" +TORCHENV_PYTHON = "/bin/python" +ARTIFACT_BASE = Path("/hardhat/artifacts/contracts") +DIN_INFO_PATH = Path("/dincli/config/din_info.json") +``` + +Also check and update in `tests/dincli/conftest.py`: +```python +DIN_TEMP = Path("") # e.g. Path.home() / "tempdir/dincli" +NPX_BIN = "" # find with: which npx +IPFS_BIN = "" # find with: which ipfs +``` + +### Check prerequisites are installed and running + +```bash +which npx # Node / Hardhat +which ipfs # IPFS daemon +docker info # Docker daemon running +``` + +--- + +## Part 1 — PR #13 Final Fixes + +IF you believe that these potential fixes are already addressed, stale requirements, non-issue plese point out + +Note: `DinValidatorStake.test.ts` (Fix 1 from the original review) is already addressed in your branch — 231 lines, 16 tests. The remaining items are: + +### Fix 1: `daoAdmin` / `owner` compatibility on `DINModelRegistry` + +Add `daoAdmin()` → `owner()` and `setDAOAdmin(address)` → `transferOwnership(address)` wrappers (shim approach), OR explicitly document the clean-break decision in the PR description with a hard tracked prerequisite on the `dincli` side. Pick whichever is cleaner; flag the decision and rationale. + +### Fix 2: `dincli` ABI call-site list in PR description + +Written list of every `dincli` call site that breaks under the ABI changes in this PR. Not fixes — enumeration only. At minimum: + +- `daoAdmin()` on `DINModelRegistry` — removed; `dincli` calls this today +- `DinCoordinator`/`DinToken` two-step bootstrap — any path that assumed `dinToken = coordinator.dinToken()` after a single deploy needs the new two-step wire +- `DAOAdminUpdated` event listeners — removed from `DINModelRegistry` + +### Fix 3: `_disableInitializers()` tests × 4 + +One test per contract proving that calling `initialize()` directly on the implementation contract (not the proxy) reverts. Add to the existing upgrade-safety suite. + +### Addition: `uint256[50] private __gap` × 4 platform contracts + +Add storage gap to `DinCoordinator`, `DinToken`, `DinValidatorStake`, `DINModelRegistry`. Known omission from the current branch — fix it before merge, not after. + +**Outcome:** PR #13 ready for merge gate review by end of July 1. + +--- + +## Part 2 — Test Harness Integration (Day 2: July 2) + +The `develop` branch now has a full CLI-level integration test harness at `tests/dincli/` (task_290626_1, merged). Your PR branch does not have this yet. + +### Step 1: Sync your branch with `develop` + +```bash +git fetch origin +git rebase origin/develop +# or merge if rebase produces conflicts that are harder to resolve +``` + +### Step 2: Run the harness against your upgradeable contracts + +Prerequisites (from `tests/dincli/conftest.py` — these are auto-managed but Docker must be running): + +```bash +# Ensure Docker daemon is running +docker info # if not running: start Docker Desktop or sudo systemctl start docker + +# Activate your pydin venv +source /bin/activate + +# From your devnet root +cd +pytest tests/dincli/ -v -x -m integration --tb=short \ + 2>&1 | tee /results/pr13_run.txt +``` + +The harness resolves paths from `tests/dincli/constants.py` and `conftest.py` — update those first (see Environment Setup above). The conftest auto-manages Hardhat node startup, contract compilation, and IPFS daemon; Docker must already be running. + +### Step 3: Re-upload IPFS artifacts + +Before running, ensure the IPFS daemon is running and re-upload manifests and service files so CIDs in `din_info.json` / `manifest.json` are current: + +```bash +# From your devnet root, with pydin venv active and IPFS daemon running +python -m dincli.main ipfs upload -f ./cache_model_0/manifest.json +python -m dincli.main ipfs upload -f ./cache_model_0/requirements.compact.txt +python -m dincli.main ipfs upload -f ./cache_model_0/requirements.txt +# All 6 service files under cache_model_0/services/ +python -m dincli.main ipfs upload -f ./cache_model_0/services/model.py +# (repeat for aggregator.py, auditor.py, client.py, modelowner.py, validator.py) + +# Task-level contract ABIs +python -m dincli.main ipfs upload -f ./hardhat/artifacts/contracts/DINTaskCoordinator.sol/DINTaskCoordinator.json +python -m dincli.main ipfs upload -f ./hardhat/artifacts/contracts/DINTaskAuditor.sol/DINTaskAuditor.json +``` + +### Step 4: Document failures in PR description + +For each test that fails against your upgradeable contracts, add it to the call-site list from Part 1. This is the evidence Umer needs to update `dincli/cli` before the merge gate is cleared. You are not fixing Python — you are documenting what breaks. + +**Outcome:** PR description has a complete, evidence-backed call-site list from a real harness run. + +--- + +## Part 3 — P3-6.3b NatSpec (Day 3 — first half: July 3) + +Add NatSpec to all public and external functions across all 6 contracts and `DINShared.sol`. Do this while the code is fresh. + +**Scope:** + +| File | `__gap` needed | Notes | +|------|---------------|-------| +| `DinCoordinator.sol` | ✅ Yes (upgradeable) | Already added in Part 1 | +| `DinToken.sol` | ✅ Yes (upgradeable) | Already added in Part 1 | +| `DinValidatorStake.sol` | ✅ Yes (upgradeable) | Already added in Part 1 | +| `DINModelRegistry.sol` | ✅ Yes (upgradeable) | Already added in Part 1 | +| `DINTaskCoordinator.sol` | ❌ No (redeployed per model) | NatSpec only | +| `DINTaskAuditor.sol` | ❌ No (redeployed per model) | NatSpec only | +| `DINShared.sol` | ❌ No (types/interfaces) | `@notice` on interfaces + `@dev` on custom errors | + +**NatSpec format:** +```solidity +/// @notice One-line description of what this function does for a caller. +/// @param paramName What this parameter controls. +/// @return What is returned and what it represents. +/// @dev Internal implementation note — invariants, edge cases, or non-obvious constraints. +``` + +Also write `Documentation/technical/storage_layout.md` documenting the append-only storage rules followed in the 4 upgradeable contracts — this is a required deliverable for P3-6.3b and feeds directly into audit prep. + +--- + +## Part 4 — Foundry Migration Start, per PR #15 (Day 3 — second half: July 3) + +PR #15 logs a conditional Foundry-only recommendation. Two checklist items remain open before the final call can be made. Work through them. + +### Gate 1: `openzeppelin-foundry-upgrades` storage-layout validation + +Install the plugin and run a dry-run upgrade-safety check against PR #13's Transparent Proxy contracts: + +```bash +cd foundry +forge install OpenZeppelin/openzeppelin-foundry-upgrades +# or: add to foundry.toml [dependencies] +``` + +Then write a minimal test (`.t.sol`) that calls `Upgrades.validateUpgrade(...)` (or equivalent) against each of the 4 proxy contracts and confirm it passes. This mirrors what `hardhat-upgrades validateUpgrade()` does in the existing Hardhat suite. If it passes, the primary gate is cleared. + +### Gate 2: `forge verify-contract` on Optimism Sepolia Blockscout + +If a deployed contract address is available (even a testnet one), verify: + +```bash +forge verify-contract
\ + --verifier blockscout \ + --verifier-url https://optimism-sepolia.blockscout.com/api +``` + +Document the result (success or exact failure) in the PR #15 description update. + +### Update stale `foundry/src/` contracts + +`foundry/src/` currently holds 7 pre-upgrade, non-proxy contracts that diverged from `hardhat/contracts/` before PR #13. Update them to match the upgradeable versions from your branch (`initialize()`, `OwnableUpgradeable`, removed `immutables`, `__gap`). No new Foundry test files required this week — that is Week 2 scope. + +**Outcome:** PR #15 checklist complete; conditional recommendation confirmed or revised with evidence; `foundry/src/` contracts current. + +--- + +## Deliverables + +**By end of July 1 (Part 1):** +- [ ] `daoAdmin` shim added OR clean-break decision documented in PR #13 +- [ ] `dincli` ABI call-site list written into PR #13 description +- [ ] `_disableInitializers()` tests × 4 added +- [ ] `uint256[50] private __gap` added to all 4 platform contracts + +**By end of July 2 (Part 2):** +- [ ] `feature/platform-upgradeable` rebased onto current `develop` +- [ ] Test harness run against upgradeable contracts, results logged to `/home/azureuser/tempdir/dincli/results/pr13_run.txt` +- [ ] Harness failures documented in PR #13 description (complete call-site list) + +**By end of July 3 (Parts 3–4):** +- [ ] NatSpec on all public/external functions — 6 contracts + DINShared.sol +- [ ] `Developer/Documentation/technical/storage_layout.md` written +- [ ] `openzeppelin-foundry-upgrades` installed; storage-layout gate run and result documented +- [ ] `forge verify-contract` result documented (pass or failure with exact error) +- [ ] `foundry/src/` contracts updated to match PR #13 upgradeable versions + +--- + +## Notes + +- All Solidity / Hardhat / Foundry. Python and `dincli` changes (CLI fixes for broken call sites) are Umer's scope — your job is to document what breaks, not fix it. +- PR #15 is pending Umer's review. Proceed on the assumption Hardhat remains active alongside Foundry until the migration decision is final — do not remove Hardhat tests. +- If Parts 3–4 spill into Monday, prioritize in order: NatSpec before Foundry gates. diff --git a/dincli/abis/DinCoordinator.json b/dincli/abis/DinCoordinator.json index 82f390d..c154528 100644 --- a/dincli/abis/DinCoordinator.json +++ b/dincli/abis/DinCoordinator.json @@ -10,6 +10,16 @@ "name": "InvalidAddress", "type": "error" }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, { "inputs": [ { @@ -90,6 +100,19 @@ "name": "EthDepositAndDINminted", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -207,6 +230,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "dinToken_", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "owner", diff --git a/dincli/abis/DinToken.json b/dincli/abis/DinToken.json index 1b7e09c..79ca2ce 100644 --- a/dincli/abis/DinToken.json +++ b/dincli/abis/DinToken.json @@ -1,16 +1,15 @@ { "abi": [ { - "inputs": [ - { - "internalType": "address", - "name": "owner_", - "type": "address" - } - ], + "inputs": [], "stateMutability": "nonpayable", "type": "constructor" }, + { + "inputs": [], + "name": "CoordinatorAlreadySet", + "type": "error" + }, { "inputs": [ { @@ -102,6 +101,38 @@ "name": "InvalidAddress", "type": "error" }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, { "inputs": [], "name": "Unauthorized", @@ -132,6 +163,51 @@ "name": "Approval", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "coordinator", + "type": "address" + } + ], + "name": "CoordinatorSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -176,19 +252,6 @@ "name": "Transfer", "type": "event" }, - { - "inputs": [], - "name": "OWNER", - "outputs": [ - { - "internalType": "address", - "name": "", - "type": "address" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -256,6 +319,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "coordinator", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "decimals", @@ -269,6 +345,13 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -300,6 +383,39 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "coordinator_", + "type": "address" + } + ], + "name": "setCoordinator", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "symbol", @@ -378,6 +494,19 @@ ], "stateMutability": "nonpayable", "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" } ] } \ No newline at end of file diff --git a/hardhat/.gitignore b/hardhat/.gitignore index 96cdab9..02602df 100644 --- a/hardhat/.gitignore +++ b/hardhat/.gitignore @@ -16,3 +16,5 @@ node_modules # Hardhat Ignition default folder for deployments against a local node ignition/deployments/chain-31337 +/deployments + diff --git a/hardhat/contracts/DINModelRegistry.sol b/hardhat/contracts/DINModelRegistry.sol index 3fcc489..3176333 100644 --- a/hardhat/contracts/DINModelRegistry.sol +++ b/hardhat/contracts/DINModelRegistry.sol @@ -1,10 +1,8 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.28; -/// @title DIN Model Registry (v2 - Request/Approval Based) -/// @author InfiniteZero Foundation -/// @notice Secure registry with approval layer for manifests -/// @dev Minimal, auditable, DAO-controlled primitive +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; interface IDinValidatorStake { function isSlasherContract( @@ -16,11 +14,10 @@ interface IOwnable { function owner() external view returns (address); } -contract DINModelRegistry { - /*////////////////////////////////////////////////////////////// - ERRORS - //////////////////////////////////////////////////////////////*/ - error NotDINDAOAdmin(); +/// @title DIN Model Registry +/// @notice Manages model registration requests, manifest updates, and per-model +/// lifecycle controls. Deployed once per network behind a Transparent Proxy. +contract DINModelRegistry is Initializable, OwnableUpgradeable { error NotModelOwner(); error InvalidModelId(); error InvalidRequestId(); @@ -33,22 +30,18 @@ contract DINModelRegistry { error TaskCoordinatorAlreadyRegistered(); error TaskAuditorAlreadyRegistered(); error ZeroAddress(); - // Approval-time revalidation errors error CoordinatorNoLongerSlasher(); error AuditorNoLongerSlasher(); error CoordinatorOwnershipChanged(); error AuditorOwnershipChanged(); + error TransferFailed(); - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ event ModelRegistrationRequested( uint256 indexed requestId, address indexed requester ); event ModelApproved(uint256 indexed requestId, uint256 indexed modelId); event ModelRejected(uint256 indexed requestId); - event ManifestUpdateRequested( uint256 indexed requestId, uint256 indexed modelId @@ -59,31 +52,21 @@ contract DINModelRegistry { bytes32 newCID ); event ManifestUpdateRejected(uint256 indexed requestId); - - // Kill-switch events event ModelDisabled(uint256 indexed modelId); event ModelEnabled(uint256 indexed modelId); - - // Individual fee events (granular tracking) event OpenSourceFeeUpdated(uint256 newFee); event ProprietaryFeeUpdated(uint256 newFee); event OpenSourceUpdateFeeUpdated(uint256 newFee); event ProprietaryUpdateFeeUpdated(uint256 newFee); - - // Combined fee event (atomic governance proposals) event FeesUpdated( uint256 openSourceFee, uint256 proprietaryFee, uint256 openSourceUpdateFee, uint256 proprietaryUpdateFee ); - event FeesWithdrawn(address indexed to, uint256 amount); event DAOAdminUpdated(address indexed oldAdmin, address indexed newAdmin); - /*////////////////////////////////////////////////////////////// - STRUCTS - //////////////////////////////////////////////////////////////*/ struct Model { address owner; bool isOpenSource; @@ -114,36 +97,40 @@ contract DINModelRegistry { bool approved; } - /*////////////////////////////////////////////////////////////// - STATE VARIABLES - //////////////////////////////////////////////////////////////*/ - address public daoAdmin; IDinValidatorStake public dinValidatorStake; - // Registration fees - uint256 public openSourceFee = 0.000001 ether; - uint256 public proprietaryFee = 0.00001 ether; - - // Manifest update fees - uint256 public openSourceUpdateFee = 0.0000001 ether; - uint256 public proprietaryUpdateFee = 0.000001 ether; + uint256 public openSourceFee; + uint256 public proprietaryFee; + uint256 public openSourceUpdateFee; + uint256 public proprietaryUpdateFee; Model[] private models; ModelRequest[] public modelRequests; ManifestUpdateRequest[] public manifestRequests; - mapping(address => uint256) private _modelIdByTaskCoordinator; // Stores modelId + 1 - mapping(address => uint256) private _modelIdByTaskAuditor; // Stores modelId + 1 - - // Kill-switch: disabled models cannot have manifests updated or participate + mapping(address => uint256) private _modelIdByTaskCoordinator; + mapping(address => uint256) private _modelIdByTaskAuditor; mapping(uint256 => bool) public modelDisabled; - /*////////////////////////////////////////////////////////////// - MODIFIERS - //////////////////////////////////////////////////////////////*/ - modifier onlyDAOAdmin() { - if (msg.sender != daoAdmin) revert NotDINDAOAdmin(); - _; + // Reserved for future state variables at this inheritance level. + uint256[50] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialises the proxy, wires the validator stake contract, and sets + /// default registration and update fees. + /// @param dinValidatorStake_ Address of the DinValidatorStake proxy. + function initialize(address dinValidatorStake_) external initializer { + if (dinValidatorStake_ == address(0)) revert ZeroAddress(); + __Ownable_init(msg.sender); + dinValidatorStake = IDinValidatorStake(dinValidatorStake_); + openSourceFee = 0.000001 ether; + proprietaryFee = 0.00001 ether; + openSourceUpdateFee = 0.0000001 ether; + proprietaryUpdateFee = 0.000001 ether; } modifier onlyModelOwner(uint256 modelId) { @@ -157,24 +144,14 @@ contract DINModelRegistry { _; } - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - constructor(address _dinValidatorStake) { - daoAdmin = msg.sender; // DIN DAO representative - dinValidatorStake = IDinValidatorStake(_dinValidatorStake); - } - - /*////////////////////////////////////////////////////////////// - MODEL REGISTRATION REQUEST - //////////////////////////////////////////////////////////////*/ - - /// @notice Submit a model registration request for DAO review - /// @param manifestCID IPFS CID (bytes32) containing the model manifest - /// @param taskCoordinator Address of the registered task coordinator slasher contract - /// @param taskAuditor Address of the registered task auditor slasher contract - /// @param isOpenSource Whether the model is open-source or proprietary - /// @return requestId ID of the pending request + /// @notice Submits a model registration request for DIN-Representative review. + /// @dev Both task contracts must be registered slashers and owned by msg.sender + /// at submission time. These conditions are re-validated at approval. + /// @param manifestCID IPFS CID of the model manifest. + /// @param taskCoordinator Address of the model's DINTaskCoordinator contract. + /// @param taskAuditor Address of the model's DINTaskAuditor contract. + /// @param isOpenSource True for open-source fee tier; false for proprietary. + /// @return requestId Index of the created request in the modelRequests array. function requestModelRegistration( bytes32 manifestCID, address taskCoordinator, @@ -184,14 +161,10 @@ contract DINModelRegistry { uint256 requiredFee = isOpenSource ? openSourceFee : proprietaryFee; if (msg.value < requiredFee) revert InsufficientFee(); - require( - dinValidatorStake.isSlasherContract(taskCoordinator), - "Invalid Coordinator" - ); - require( - dinValidatorStake.isSlasherContract(taskAuditor), - "Invalid Auditor" - ); + if (!dinValidatorStake.isSlasherContract(taskCoordinator)) + revert CoordinatorNoLongerSlasher(); + if (!dinValidatorStake.isSlasherContract(taskAuditor)) + revert AuditorNoLongerSlasher(); if (taskCoordinator == taskAuditor) revert TaskCoordinatorEqualsTaskAuditor(); @@ -220,24 +193,21 @@ contract DINModelRegistry { emit ModelRegistrationRequested(requestId, msg.sender); } - /*////////////////////////////////////////////////////////////// - APPROVE / REJECT MODEL - //////////////////////////////////////////////////////////////*/ - - /// @notice DAO approves a pending model registration - function approveModel(uint256 requestId) external onlyDAOAdmin { + /// @notice Approves a pending registration request and adds the model to the registry. + /// @dev Re-validates slasher status and ownership at approval time to guard against + /// state changes between submission and approval. + /// @param requestId Index into the modelRequests array. + function approveModel(uint256 requestId) external onlyOwner { if (requestId >= modelRequests.length) revert InvalidRequestId(); ModelRequest storage req = modelRequests[requestId]; if (req.processed) revert AlreadyProcessed(); - // Guard against reusing the same coordinator or auditor across models if (_modelIdByTaskCoordinator[req.taskCoordinator] != 0) revert TaskCoordinatorAlreadyRegistered(); if (_modelIdByTaskAuditor[req.taskAuditor] != 0) revert TaskAuditorAlreadyRegistered(); - // Revalidate: slasher status or ownership may have changed since the request was submitted if (!dinValidatorStake.isSlasherContract(req.taskCoordinator)) revert CoordinatorNoLongerSlasher(); if (!dinValidatorStake.isSlasherContract(req.taskAuditor)) @@ -260,7 +230,6 @@ contract DINModelRegistry { }) ); - // Store modelId + 1 to distinguish from the zero-value default _modelIdByTaskCoordinator[req.taskCoordinator] = modelId + 1; _modelIdByTaskAuditor[req.taskAuditor] = modelId + 1; @@ -270,8 +239,9 @@ contract DINModelRegistry { emit ModelApproved(requestId, modelId); } - /// @notice DAO rejects a pending model registration (fee is retained) - function rejectModel(uint256 requestId) external onlyDAOAdmin { + /// @notice Rejects a pending registration request without adding a model. + /// @param requestId Index into the modelRequests array. + function rejectModel(uint256 requestId) external onlyOwner { if (requestId >= modelRequests.length) revert InvalidRequestId(); ModelRequest storage req = modelRequests[requestId]; @@ -283,14 +253,10 @@ contract DINModelRegistry { emit ModelRejected(requestId); } - /*////////////////////////////////////////////////////////////// - MANIFEST UPDATE REQUEST - //////////////////////////////////////////////////////////////*/ - - /// @notice Submit a manifest update request for DAO review (model owner only) - /// @param modelId ID of the model to update - /// @param newManifestCID New IPFS CID (bytes32) for the model manifest - /// @return requestId ID of the pending manifest update request + /// @notice Submits a manifest update request for an existing model. + /// @param modelId ID of the model to update. + /// @param newManifestCID IPFS CID of the new manifest. + /// @return requestId Index of the created request in the manifestRequests array. function requestManifestUpdate( uint256 modelId, bytes32 newManifestCID @@ -325,18 +291,14 @@ contract DINModelRegistry { emit ManifestUpdateRequested(requestId, modelId); } - /*////////////////////////////////////////////////////////////// - APPROVE / REJECT MANIFEST UPDATE - //////////////////////////////////////////////////////////////*/ - - /// @notice DAO approves a pending manifest update - function approveManifestUpdate(uint256 requestId) external onlyDAOAdmin { + /// @notice Approves a manifest update and writes the new CID to the model record. + /// @param requestId Index into the manifestRequests array. + function approveManifestUpdate(uint256 requestId) external onlyOwner { if (requestId >= manifestRequests.length) revert InvalidRequestId(); ManifestUpdateRequest storage req = manifestRequests[requestId]; if (req.processed) revert AlreadyProcessed(); - // Prevent approving updates for a model that has since been disabled if (modelDisabled[req.modelId]) revert ModelIsDisabled(req.modelId); models[req.modelId].manifestCID = req.newManifestCID; @@ -347,8 +309,9 @@ contract DINModelRegistry { emit ManifestUpdated(requestId, req.modelId, req.newManifestCID); } - /// @notice DAO rejects a pending manifest update (fee is retained) - function rejectManifestUpdate(uint256 requestId) external onlyDAOAdmin { + /// @notice Rejects a pending manifest update request. + /// @param requestId Index into the manifestRequests array. + function rejectManifestUpdate(uint256 requestId) external onlyOwner { if (requestId >= manifestRequests.length) revert InvalidRequestId(); ManifestUpdateRequest storage req = manifestRequests[requestId]; @@ -360,10 +323,14 @@ contract DINModelRegistry { emit ManifestUpdateRejected(requestId); } - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - + /// @notice Returns the full record for a registered model. + /// @param modelId ID of the model to query. + /// @return owner Address of the model owner. + /// @return isOpenSource True if the model is open-source. + /// @return manifestCID IPFS CID of the current manifest. + /// @return createdAt Block timestamp at registration. + /// @return taskCoordinator Address of the model's DINTaskCoordinator. + /// @return taskAuditor Address of the model's DINTaskAuditor. function getModel( uint256 modelId ) @@ -390,93 +357,103 @@ contract DINModelRegistry { ); } + /// @notice Returns the total number of approved models in the registry. + /// @return Count of registered models. function totalModels() external view returns (uint256) { return models.length; } + /// @notice Returns the total number of model registration requests submitted. + /// @return Count of entries in the modelRequests array. function totalModelRequests() external view returns (uint256) { return modelRequests.length; } + /// @notice Returns the total number of manifest update requests submitted. + /// @return Count of entries in the manifestRequests array. function totalManifestRequests() external view returns (uint256) { return manifestRequests.length; } - /// @notice Look up the model ID registered for a given task coordinator + /// @notice Looks up the model ID associated with a given task coordinator address. + /// @param taskCoordinator Address to query. + /// @return exists True if a model is registered for this coordinator. + /// @return modelId The model's ID (only meaningful if exists is true). function getModelIdByTaskCoordinator( address taskCoordinator ) external view returns (bool exists, uint256 modelId) { uint256 val = _modelIdByTaskCoordinator[taskCoordinator]; if (val == 0) return (false, 0); - return (true, val - 1); // subtract 1 to convert back to 0-indexed modelId + return (true, val - 1); } - /// @notice Look up the model ID registered for a given task auditor + /// @notice Looks up the model ID associated with a given task auditor address. + /// @param taskAuditor Address to query. + /// @return exists True if a model is registered for this auditor. + /// @return modelId The model's ID (only meaningful if exists is true). function getModelIdByTaskAuditor( address taskAuditor ) external view returns (bool exists, uint256 modelId) { uint256 val = _modelIdByTaskAuditor[taskAuditor]; if (val == 0) return (false, 0); - return (true, val - 1); // subtract 1 to convert back to 0-indexed modelId + return (true, val - 1); } - /*////////////////////////////////////////////////////////////// - DAO ADMIN FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - // ── Kill switch ──────────────────────────────────────────────────────── - - /// @notice Immediately disable a model — blocks manifest updates and - /// any downstream contract that checks modelDisabled(modelId) - function disableModel(uint256 modelId) external onlyDAOAdmin { + /// @notice Disables a model, blocking manifest updates and new GI registrations. + /// @param modelId ID of the model to disable. + function disableModel(uint256 modelId) external onlyOwner { if (modelId >= models.length) revert InvalidModelId(); modelDisabled[modelId] = true; emit ModelDisabled(modelId); } - /// @notice Re-enable a previously disabled model - function enableModel(uint256 modelId) external onlyDAOAdmin { + /// @notice Re-enables a previously disabled model. + /// @param modelId ID of the model to enable. + function enableModel(uint256 modelId) external onlyOwner { if (modelId >= models.length) revert InvalidModelId(); modelDisabled[modelId] = false; emit ModelEnabled(modelId); } - // ── Individual fee setters ───────────────────────────────────────────── - - /// @notice Update the open-source model registration fee - function setOpenSourceFee(uint256 newFee) external onlyDAOAdmin { + /// @notice Updates the registration fee for open-source models. + /// @param newFee New fee in wei. + function setOpenSourceFee(uint256 newFee) external onlyOwner { openSourceFee = newFee; emit OpenSourceFeeUpdated(newFee); } - /// @notice Update the proprietary model registration fee - function setProprietaryFee(uint256 newFee) external onlyDAOAdmin { + /// @notice Updates the registration fee for proprietary models. + /// @param newFee New fee in wei. + function setProprietaryFee(uint256 newFee) external onlyOwner { proprietaryFee = newFee; emit ProprietaryFeeUpdated(newFee); } - /// @notice Update the open-source manifest update fee - function setOpenSourceUpdateFee(uint256 newFee) external onlyDAOAdmin { + /// @notice Updates the manifest update fee for open-source models. + /// @param newFee New fee in wei. + function setOpenSourceUpdateFee(uint256 newFee) external onlyOwner { openSourceUpdateFee = newFee; emit OpenSourceUpdateFeeUpdated(newFee); } - /// @notice Update the proprietary manifest update fee - function setProprietaryUpdateFee(uint256 newFee) external onlyDAOAdmin { + /// @notice Updates the manifest update fee for proprietary models. + /// @param newFee New fee in wei. + function setProprietaryUpdateFee(uint256 newFee) external onlyOwner { proprietaryUpdateFee = newFee; emit ProprietaryUpdateFeeUpdated(newFee); } - // ── Combined setter (atomic governance) ──────────────────────────────── - - /// @notice Atomically update all four protocol fees in a single transaction - /// @dev Prefer this for governance proposals to avoid inconsistent fee states + /// @notice Updates all four fee tiers in a single transaction. + /// @param _openSourceFee Open-source registration fee in wei. + /// @param _proprietaryFee Proprietary registration fee in wei. + /// @param _openSourceUpdateFee Open-source manifest update fee in wei. + /// @param _proprietaryUpdateFee Proprietary manifest update fee in wei. function setFees( uint256 _openSourceFee, uint256 _proprietaryFee, uint256 _openSourceUpdateFee, uint256 _proprietaryUpdateFee - ) external onlyDAOAdmin { + ) external onlyOwner { openSourceFee = _openSourceFee; proprietaryFee = _proprietaryFee; openSourceUpdateFee = _openSourceUpdateFee; @@ -490,21 +467,31 @@ contract DINModelRegistry { ); } - /// @notice Withdraw accumulated fees to a designated address - function withdrawFees(address payable to) external onlyDAOAdmin { + /// @notice Transfers the contract's entire ETH balance to the specified address. + /// @param to Destination address for the fee withdrawal. + function withdrawFees(address payable to) external onlyOwner { uint256 balance = address(this).balance; - to.transfer(balance); + (bool success, ) = to.call{value: balance}(""); + if (!success) revert TransferFailed(); emit FeesWithdrawn(to, balance); } - // ── DAO admin transfer ───────────────────────────────────────────────── + // Backward-compat shims — dincli calls daoAdmin() / setDAOAdmin(). + // Underlying auth model is OwnableUpgradeable; these are read-through facades. + + /// @notice Returns the current admin address. Delegates to OwnableUpgradeable.owner(). + /// @dev Compatibility shim preserving the pre-upgrade daoAdmin() ABI surface. + /// @return The current owner address. + function daoAdmin() external view returns (address) { + return owner(); + } - /// @notice Transfer DAO admin role (multisig / timelock migration path) - /// @dev Emits DAOAdminUpdated so indexers can track governance handover - function setDAOAdmin(address newAdmin) external onlyDAOAdmin { - if (newAdmin == address(0)) revert ZeroAddress(); - address old = daoAdmin; - daoAdmin = newAdmin; + /// @notice Transfers ownership and emits DAOAdminUpdated for off-chain indexers. + /// @dev Compatibility shim preserving the pre-upgrade setDAOAdmin() ABI surface. + /// @param newAdmin Address to transfer ownership to. + function setDAOAdmin(address newAdmin) external onlyOwner { + address old = owner(); + transferOwnership(newAdmin); emit DAOAdminUpdated(old, newAdmin); } } diff --git a/hardhat/contracts/DINShared.sol b/hardhat/contracts/DINShared.sol index 20fcd19..c677511 100644 --- a/hardhat/contracts/DINShared.sol +++ b/hardhat/contracts/DINShared.sol @@ -80,75 +80,145 @@ interface IDINTaskAuditor { // ───────────────────────────────────────────────────────────────────────────── // Custom errors — DINTaskAuditor // ───────────────────────────────────────────────────────────────────────────── -error TA_NotTaskCoordinator(); // "Audit: Not task coordinator" -error TA_AmountMustBePositive(); // "Audit: Amount must be positive" -error TA_InvalidPassScore(); // "Audit: Invalid pass score, Pass score must be between 0 and 100" -error TA_AuditorRegistrationNotOpen(); // "Audit: Auditor registration not open" -error TA_WrongGI(); // "Audit: Invalid Global Iteration" -error TA_AuditorAlreadyRegistered(); // "Audit: Auditor already registered" -error TA_InsufficientStake(); // "Audit: Insufficient stake" -error TA_LMSubmissionsNotOpen(); // "Audit: LM submissions not open" -error TA_AlreadySubmitted(); // "Audit: Already submitted" -error TA_MaxLMSubmissionsReached(); // "Audit: Max LM submissions reached" -error TA_NotEnoughAuditors(); // "Audit: Not enough auditors" -error TA_CannotCreateAuditorsBatches(); // "Audit: Cannot create auditors batches" -error TA_BatchNotFound(); // "AuditBatch: Batch not found" -error TA_BatchDoesNotExist(); // "AuditBatch: Batch does not exist" -error TA_BatchIDMismatch(); // "AuditBatch: Batch ID mismatch" -error TA_CannotSetTestDataAssignedFlag(); // "AuditBatch: Cannot set test data assigned flag" -error TA_FlagMustBeTrue(); // " Flag must be true" -error TA_FlagAlreadySet(); // "Flag already set" -error TA_NotAssignedAuditor(); // "Audit: Not assigned auditor" -error TA_InvalidModelIndex(); // "Audit: Invalid model index" -error TA_CannotSetAuditScore(); // "Audit: Cannot set audit score" -error TA_ScoreOutOfRange(); // "Audit: Score out of range" -error TA_AlreadyVoted(); // "Audit: Already voted" -error TA_CannotFinalizeEvaluation(); // "Audit: Cannot finalize evaluation" -error TA_AuditorNotActive(); // "Audit: Auditor is not active" + +/// @dev Caller is not the DINTaskCoordinator contract paired with this auditor. +error TA_NotTaskCoordinator(); +/// @dev Slash or fee amount must be greater than zero. +error TA_AmountMustBePositive(); +/// @dev Pass score must be in the range [0, 100]. +error TA_InvalidPassScore(); +/// @dev Auditor registration phase is not currently open. +error TA_AuditorRegistrationNotOpen(); +/// @dev The supplied GI index does not match the contract's current GI. +error TA_WrongGI(); +/// @dev This address has already registered as an auditor for the current GI. +error TA_AuditorAlreadyRegistered(); +/// @dev Caller's active stake is below the minimum required to register. +error TA_InsufficientStake(); +/// @dev Local model submission phase is not currently open. +error TA_LMSubmissionsNotOpen(); +/// @dev This client has already submitted a local model for the current GI. +error TA_AlreadySubmitted(); +/// @dev The maximum number of local model submissions for the current GI has been reached. +error TA_MaxLMSubmissionsReached(); +/// @dev Fewer auditors registered than the minimum required to proceed. +error TA_NotEnoughAuditors(); +/// @dev GI state does not permit auditor batch creation at this time. +error TA_CannotCreateAuditorsBatches(); +/// @dev The specified auditor batch index does not exist. +error TA_BatchNotFound(); +/// @dev Batch lookup returned an uninitialised entry. +error TA_BatchDoesNotExist(); +/// @dev The provided batch ID does not match the stored batch ID. +error TA_BatchIDMismatch(); +/// @dev GI state does not permit setting the test data assigned flag. +error TA_CannotSetTestDataAssignedFlag(); +/// @dev The flag value supplied must be true; false is not accepted here. +error TA_FlagMustBeTrue(); +/// @dev The test data assigned flag for this batch has already been set. +error TA_FlagAlreadySet(); +/// @dev Caller is not assigned to the target auditor batch. +error TA_NotAssignedAuditor(); +/// @dev The supplied model index is outside the range of submitted models. +error TA_InvalidModelIndex(); +/// @dev GI state does not permit setting an audit score at this time. +error TA_CannotSetAuditScore(); +/// @dev Audit score must be in the range [0, 100]. +error TA_ScoreOutOfRange(); +/// @dev This auditor has already cast a vote for the specified model. +error TA_AlreadyVoted(); +/// @dev Evaluation cannot be finalised; not all batches have been scored. +error TA_CannotFinalizeEvaluation(); +/// @dev Auditor's validator status is not Active. +error TA_AuditorNotActive(); // ───────────────────────────────────────────────────────────────────────────── // Custom errors — DINTaskCoordinator // ───────────────────────────────────────────────────────────────────────────── -error TC_TaskAuditorContractCannotBeSet(); // "Task Coordinator: Task auditor contract cannot be set" -error TC_CoordinatorCannotBeSetAsSlasher(); // "Task Coordinator cannot be set as slasher" -error TC_CoordinatorIsNotSlasher(); // "Task Coordinator is not slasher" -error TC_AuditorCannotBeSetAsSlasher(); // "Task Auditor cannot be set as slasher" -error TC_AuditorIsNotSlasher(); // "Task Auditor is not slasher" -error TC_GenesisModelHashCannotBeSet(); // "Task Coordinator: Genesis model hash cannot be set" -error TC_GICannotBeStarted(); // "Task Coordinator: GI cannot be started" -error TC_WrongGI(); // "Task Coordinator: Wrong GI" -error TC_AggregatorsRegistrationCannotBeStarted(); // "Task Coordinator: Aggregators registration cannot be started" -error TC_AggregatorsRegistrationNotOpen(); // "Task Coordinator: Aggregators registration not open" -error TC_InsufficientStake(); // "Task Coordinator: Insufficient stake" -error TC_AggregatorAlreadyRegistered(); // "Task Coordinator: Validator already registered" -error TC_AggregatorsRegistrationCannotBeFinished(); // "Task Coordinator: Aggregators registration cannot be finished" -error TC_AuditorsRegistrationCannotBeStarted(); // "Task Coordinator: Auditors registration cannot be started" -error TC_AuditorsRegistrationCannotBeFinished(); // "Task Coordinator: Auditors registration cannot be finished" -error TC_LMSubmissionsCannotBeStarted(); // "Task Coordinator: LM submissions cannot be started" -error TC_LMSubmissionsNotStarted(); // "Task Coordinator: LM submissions not started" -error TC_LMEvalCannotBeStarted(); // "Task Coordinator: LM eval cannot be started" -error TC_LMEvalCannotBeFinished(); // "Task Coordinator: LM eval cannot be finished" -error TC_FailedToCreateAuditorsBatches(); // "Task Coordinator: Failed to create auditors batches" -error TC_CannotSetTestDataAssignedFlag(); // "Task Coordinator: Cannot set test data assigned flag" -error TC_EvalPhaseNotClosed(); // "Task Coordinator: Eval phase not closed" -error TC_NotEnoughValidators(); // "Task Coordinator: Not enough validators" -error TC_NotEnoughApprovedModels(); // "Task Coordinator: Not enough approved models" -error TC_BatchNotFound(); // "Task Coordinator: Batch not found" -error TC_OnlyOneTier2Batch(); // "Task Coordinator: Only one tier 2 batch" -error TC_NotReadyForT1Aggregation(); // "Task Coordinator: Not ready for T1 aggregation" -error TC_T1AggregationNotStarted(); // "Task Coordinator: T1 aggregation not started" -error TC_InvalidBatch(); // "Task Coordinator: Invalid batch" -error TC_NotBatchAggregator(); // "Task Coordinator: Not batch aggregator" -error TC_AlreadySubmitted(); // "Task Coordinator: Already submitted" -error TC_NoSubmissions(); // "Task Coordinator: No submissions" -error TC_NotReadyToFinalizeT1(); // "Task Coordinator: Not ready to finalize T1" -error TC_NotReadyForT2Aggregation(); // "Task Coordinator: Not ready for T2 aggregation" -error TC_T2AggregationNotStarted(); // "Task Coordinator: T2 aggregation not started" -error TC_NotReadyToFinalizeT2(); // "Task Coordinator: Not ready to finalize T2" -error TC_NotReadyToSlashAuditors(); // "Task Coordinator: Not ready to slash auditors" -error TC_NotReadyToSlashAggregators(); // "Task Coordinator: Not ready to slash Aggregators" -error TC_NotReadyToSetTier2Score(); // "Task Coordinator: Not ready to set tier 2 score" -error TC_NotReadyToEndGI(); // "Task Coordinator: Not ready to end GI" -error TC_FailedToFinalizeEvaluation(); // "Task Coordinator: Failed to finalize LMS evaluation" -error TC_AggregatorNotActive(); // "Task Coordinator: Aggregator is not active" -error TC_FailedToSlashAuditors(); // "Task Coordinator: Failed to slash auditors" + +/// @dev The task auditor contract has already been set for this deployment. +error TC_TaskAuditorContractCannotBeSet(); +/// @dev GI state does not permit registering the coordinator as a slasher. +error TC_CoordinatorCannotBeSetAsSlasher(); +/// @dev The coordinator contract is not yet registered as a slasher. +error TC_CoordinatorIsNotSlasher(); +/// @dev GI state does not permit registering the auditor as a slasher. +error TC_AuditorCannotBeSetAsSlasher(); +/// @dev The auditor contract is not yet registered as a slasher. +error TC_AuditorIsNotSlasher(); +/// @dev GI state does not permit setting the genesis model hash. +error TC_GenesisModelHashCannotBeSet(); +/// @dev GI state does not permit starting a new Global Iteration. +error TC_GICannotBeStarted(); +/// @dev The supplied GI index does not match the contract's current GI. +error TC_WrongGI(); +/// @dev GI state does not permit opening aggregator registration. +error TC_AggregatorsRegistrationCannotBeStarted(); +/// @dev Aggregator registration phase is not currently open. +error TC_AggregatorsRegistrationNotOpen(); +/// @dev Caller's active stake is below the minimum required to register. +error TC_InsufficientStake(); +/// @dev This address has already registered as an aggregator for the current GI. +error TC_AggregatorAlreadyRegistered(); +/// @dev GI state does not permit closing aggregator registration. +error TC_AggregatorsRegistrationCannotBeFinished(); +/// @dev GI state does not permit opening auditor registration. +error TC_AuditorsRegistrationCannotBeStarted(); +/// @dev GI state does not permit closing auditor registration. +error TC_AuditorsRegistrationCannotBeFinished(); +/// @dev GI state does not permit opening local model submissions. +error TC_LMSubmissionsCannotBeStarted(); +/// @dev Local model submission phase has not been opened. +error TC_LMSubmissionsNotStarted(); +/// @dev GI state does not permit starting the LMS evaluation phase. +error TC_LMEvalCannotBeStarted(); +/// @dev GI state does not permit closing the LMS evaluation phase. +error TC_LMEvalCannotBeFinished(); +/// @dev The call to DINTaskAuditor.createAuditorsBatches() returned false. +error TC_FailedToCreateAuditorsBatches(); +/// @dev GI state does not permit setting the test data assigned flag. +error TC_CannotSetTestDataAssignedFlag(); +/// @dev LMS evaluation phase has not been closed yet. +error TC_EvalPhaseNotClosed(); +/// @dev Fewer active validators registered than required to form T1 batches. +error TC_NotEnoughValidators(); +/// @dev Fewer models passed evaluation than the minimum required for T2 aggregation. +error TC_NotEnoughApprovedModels(); +/// @dev The requested batch index does not exist. +error TC_BatchNotFound(); +/// @dev T2 always produces exactly one batch; index must be 0. +error TC_OnlyOneTier2Batch(); +/// @dev GI state does not permit starting T1 aggregation. +error TC_NotReadyForT1Aggregation(); +/// @dev T1 aggregation phase has not been started. +error TC_T1AggregationNotStarted(); +/// @dev The batch index or aggregator assignment is invalid. +error TC_InvalidBatch(); +/// @dev Caller is not the assigned aggregator for this batch. +error TC_NotBatchAggregator(); +/// @dev This aggregator has already submitted a result for this batch. +error TC_AlreadySubmitted(); +/// @dev No aggregation submissions have been received for this batch. +error TC_NoSubmissions(); +/// @dev Not all T1 batches have been finalised; cannot close T1 phase. +error TC_NotReadyToFinalizeT1(); +/// @dev GI state does not permit starting T2 aggregation. +error TC_NotReadyForT2Aggregation(); +/// @dev T2 aggregation phase has not been started. +error TC_T2AggregationNotStarted(); +/// @dev The T2 batch has not received its submission yet. +error TC_NotReadyToFinalizeT2(); +/// @dev GI state does not permit slashing auditors. +error TC_NotReadyToSlashAuditors(); +/// @dev GI state does not permit slashing aggregators. +error TC_NotReadyToSlashAggregators(); +/// @dev GI state does not permit recording the tier-2 score. +error TC_NotReadyToSetTier2Score(); +/// @dev GI state does not permit ending the Global Iteration. +error TC_NotReadyToEndGI(); +/// @dev The call to DINTaskAuditor.finalizeEvaluation() returned false. +error TC_FailedToFinalizeEvaluation(); +/// @dev Aggregator's validator status is not Active. +error TC_AggregatorNotActive(); +/// @dev The call to DINTaskAuditor.slashAuditors() returned false. +error TC_FailedToSlashAuditors(); diff --git a/hardhat/contracts/DINTaskAuditor.sol b/hardhat/contracts/DINTaskAuditor.sol index 30df932..5c14fee 100644 --- a/hardhat/contracts/DINTaskAuditor.sol +++ b/hardhat/contracts/DINTaskAuditor.sol @@ -4,6 +4,10 @@ pragma solidity ^0.8.28; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import "./DINShared.sol"; +/// @title DIN Task Auditor +/// @notice Handles auditor registration, local model submission, scoring, eligibility +/// determination, and auditor slashing for a single federated-learning model. +/// Deployed once per model alongside its paired DINTaskCoordinator. contract DINTaskAuditor is Ownable { IDinValidatorStake public dinvalidatorStakeContract; @@ -135,6 +139,12 @@ contract DINTaskAuditor is Ownable { uint256 actual ); + /// @notice Deploys the auditor, wiring it to the validator stake and coordinator contracts. + /// @dev Batch parameters are set to demo defaults (3 auditors/batch, 3 models/batch, + /// quorum of 2, pass score of 50). The model owner can adjust pass score via + /// updatePassScore. + /// @param _dinvalidatorStakeContract_address Address of the DinValidatorStake proxy. + /// @param _dintaskcoordinator_contract_address Address of the paired DINTaskCoordinator. constructor( address _dinvalidatorStakeContract_address, address _dintaskcoordinator_contract_address @@ -156,6 +166,10 @@ contract DINTaskAuditor is Ownable { }); } + /// @notice Updates the minimum average score a model must achieve to be approved. + /// @dev Restricted to the paired DINTaskCoordinator. Called during startGI when + /// the two-argument overload is used. + /// @param newPassScore New pass score in the range [0, 100]. function updatePassScore( uint256 newPassScore ) external onlyTaskCoordinator { @@ -167,6 +181,10 @@ contract DINTaskAuditor is Ownable { emit PassScoreUpdated(oldScore, newPassScore); } + /// @notice Registers the caller as an auditor for the current GI. + /// @dev Caller must be an active validator; duplicate registrations revert. + /// The coordinator's GI state must be DINauditorsRegistrationStarted. + /// @param _GI Current GI index. function registerDINAuditor(uint _GI) public onlyCurrentGI(_GI) { if ( dintaskcoordinatorContract.GIstate() != @@ -185,12 +203,20 @@ contract DINTaskAuditor is Ownable { emit DINAuditorRegistered(_GI, msg.sender); } + /// @notice Returns the list of auditors registered for the given GI. + /// @param _GI GI index to query. + /// @return Ordered array of auditor addresses in registration order. function getDINtaskAuditors( uint _GI ) public view returns (address[] memory) { return dinAuditors[_GI]; } + /// @notice Submits a local model CID for the current GI. + /// @dev Each address may submit at most once per GI. Reverts if the global + /// submission cap (MAX_LM_SUBMISSIONS) has been reached. + /// @param _clientModel IPFS CID of the locally trained model weights, encoded as bytes32. + /// @param _GI Current GI index. function submitLocalModel( bytes32 _clientModel, uint _GI @@ -218,6 +244,9 @@ contract DINTaskAuditor is Ownable { clientHasSubmitted[_GI][msg.sender] = true; } + /// @notice Returns all local model submissions for the given GI. + /// @param _GI GI index to query. + /// @return Array of LMSubmission structs in submission order. function getClientModels( uint _GI ) public view returns (LMSubmission[] memory) { @@ -278,6 +307,12 @@ contract DINTaskAuditor is Ownable { } } + /// @notice Partitions active auditors and submitted models into audit batches. + /// @dev Called by the paired DINTaskCoordinator. Auditors are filtered to those + /// still Active at call time, then shuffled with blockhash-based entropy. + /// Returns false is never reached; reverts on any failure condition. + /// @param _GI Current GI index. + /// @return True on success. function createAuditorsBatches( uint _GI ) external onlyTaskCoordinator onlyCurrentGI(_GI) returns (bool) { @@ -339,11 +374,21 @@ contract DINTaskAuditor is Ownable { return true; } + /// @notice Returns the number of audit batches created for the given GI. + /// @param _GI GI index to query. + /// @return Number of audit batches. function AuditorsBatchCount(uint _GI) external view returns (uint) { if (_GI > dintaskcoordinatorContract.GI()) revert TA_WrongGI(); return auditBatches[_GI].length; } + /// @notice Returns the full details of an audit batch. + /// @param _GI GI index. + /// @param _batchId Batch index within that GI. + /// @return batchId Canonical batch identifier. + /// @return auditors Auditors assigned to this batch. + /// @return modelIndexes Indexes into lmSubmissions[_GI] assigned to this batch. + /// @return testDataCID IPFS CID of the test dataset for this batch. function getAuditorsBatch( uint _GI, uint _batchId @@ -368,6 +413,11 @@ contract DINTaskAuditor is Ownable { ); } + /// @notice Records the test dataset CID for a specific audit batch. + /// @dev Must be called once per batch before setTestDataAssignedFlag is invoked. + /// @param gi Current GI index. + /// @param batchId Batch index to assign the test dataset to. + /// @param testDataCID IPFS CID of the test dataset, encoded as bytes32. function assignAuditTestDataset( uint256 gi, uint256 batchId, @@ -380,6 +430,12 @@ contract DINTaskAuditor is Ownable { auditBatches[gi][batchId].testDataCID = testDataCID; } + /// @notice Marks test dataset distribution as complete for the given GI. + /// @dev Restricted to the paired DINTaskCoordinator. flag must be true; + /// can only be set once per GI. + /// @param _GI Current GI index. + /// @param flag Must be true. + /// @return True on success. function setTestDataAssignedFlag( uint _GI, bool flag @@ -447,12 +503,21 @@ contract DINTaskAuditor is Ownable { ); } + /// @notice Submits an audit score and eligibility vote for a specific model. + /// @dev Caller must be the assigned auditor for this batch and model index. + /// Each auditor may vote at most once per model. If the eligibility quorum + /// is reached after this vote, eligibility is finalised immediately. + /// @param gi Current GI index. + /// @param batchId Batch index containing this model. + /// @param modelIndex Index into lmSubmissions[gi] for the model being scored. + /// @param score Audit score in the range [0, 100]. + /// @param vote True if the auditor deems the model eligible, false otherwise. function setAuditScorenEligibility( uint256 gi, uint batchId, uint modelIndex, uint256 score, - bool vote // true = eligible, false = not eligible + bool vote ) public onlyAssignedAuditor(gi, batchId, modelIndex) onlyCurrentGI(gi) { if ( dintaskcoordinatorContract.GIstate() != @@ -478,6 +543,12 @@ contract DINTaskAuditor is Ownable { _tryFinalizeEligibility(gi, batchId, modelIndex); } + /// @notice Computes final average scores and approval status for all submitted models. + /// @dev Iterates all batches and models; a model is approved if eligible and its + /// average score meets or exceeds passScore. Returns true if at least one + /// model was finalised; reverts if GI state is not LMSevaluationStarted. + /// @param _GI Current GI index. + /// @return True if at least one model reached score quorum and was finalised. function finalizeEvaluation( uint _GI ) public onlyTaskCoordinator onlyCurrentGI(_GI) returns (bool) { @@ -540,6 +611,12 @@ contract DINTaskAuditor is Ownable { return finalizedCount > 0; } + /// @notice Slashes any auditor who failed to vote on at least one model in their batch. + /// @dev Slash amount equals minStake() at call time. Each slashed auditor emits an + /// AuditorSlashed event. Always returns true; individual slash failures do not + /// halt the loop. + /// @param _GI Current GI index. + /// @return True on completion. function slashAuditors( uint _GI ) external onlyTaskCoordinator onlyCurrentGI(_GI) returns (bool) { @@ -581,6 +658,10 @@ contract DINTaskAuditor is Ownable { return true; } + /// @notice Returns the indexes of all models approved for aggregation in the given GI. + /// @dev A model is approved when both eligible == true and finalAvgScore >= passScore. + /// @param _GI GI index to query. + /// @return Array of approved model indexes into lmSubmissions[_GI]. function approvedModelIndexes( uint _GI ) public view returns (uint[] memory) { diff --git a/hardhat/contracts/DINTaskCoordinator.sol b/hardhat/contracts/DINTaskCoordinator.sol index b7ae6e5..c1497af 100644 --- a/hardhat/contracts/DINTaskCoordinator.sol +++ b/hardhat/contracts/DINTaskCoordinator.sol @@ -4,6 +4,11 @@ pragma solidity ^0.8.28; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import "./DINShared.sol"; +/// @title DIN Task Coordinator +/// @notice Orchestrates the full Global Iteration (GI) lifecycle for a single +/// federated-learning model: slasher setup, validator registration, +/// local model submissions, auditing, Tier-1/Tier-2 aggregation, and +/// validator slashing. Deployed once per model by the model owner. contract DINTaskCoordinator is Ownable { IDinValidatorStake public dinvalidatorStakeContract; IDINTaskAuditor public dinTaskAuditorContract; @@ -75,6 +80,10 @@ contract DINTaskCoordinator is Ownable { uint256 actual ); + /// @notice Deploys the coordinator and sets the validator stake contract. + /// @dev GI state is initialised to AwaitingDINTaskAuditorToBeSet; the model + /// owner must call setDINTaskAuditorContract before any other setup step. + /// @param dinvalidatorStakeContract_address Address of the DinValidatorStake proxy. constructor(address dinvalidatorStakeContract_address) Ownable(msg.sender) { dinvalidatorStakeContract = IDinValidatorStake( dinvalidatorStakeContract_address @@ -82,6 +91,9 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.AwaitingDINTaskAuditorToBeSet; } + /// @notice Sets the paired DINTaskAuditor contract for this model. + /// @dev One-shot: reverts if called after the initial setup step. + /// @param _dintaskauditor_contract_address Address of the DINTaskAuditor contract. function setDINTaskAuditorContract( address _dintaskauditor_contract_address ) public onlyOwner { @@ -93,6 +105,10 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.AwaitingDINTaskCoordinatorAsSlasher; } + /// @notice Confirms that this coordinator is registered as a slasher on the + /// validator stake contract, advancing the GI state. + /// @dev The DIN-Representative must have called DinCoordinator.addSlasherContract + /// for this address before this function is called. function setDINTaskCoordinatorAsSlasher() public onlyOwner { if (GIstate != GIstates.AwaitingDINTaskCoordinatorAsSlasher) revert TC_CoordinatorCannotBeSetAsSlasher(); @@ -101,6 +117,10 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.AwaitingDINTaskAuditorAsSlasher; } + /// @notice Confirms that the paired DINTaskAuditor is registered as a slasher, + /// completing the setup sequence and enabling model registration. + /// @dev The DIN-Representative must have called DinCoordinator.addSlasherContract + /// for the auditor address before this function is called. function setDINTaskAuditorAsSlasher() public onlyOwner { if (GIstate != GIstates.AwaitingDINTaskAuditorAsSlasher) revert TC_AuditorCannotBeSetAsSlasher(); @@ -112,6 +132,8 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.AwaitingGenesisModel; } + /// @notice Records the genesis model IPFS hash, enabling GI 1 to be started. + /// @param _genesisModelIpfsHash CID of the genesis model weights, encoded as bytes32. function setGenesisModelIpfsHash( bytes32 _genesisModelIpfsHash ) public onlyOwner { @@ -121,10 +143,15 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.GenesisModelCreated; } + /// @notice Starts the next Global Iteration and updates the auditor pass score. + /// @param _GI Expected next GI index (must equal current GI + 1). + /// @param score New pass score to set on the paired DINTaskAuditor. function startGI(uint _GI, uint score) public onlyOwner { _startGI(_GI, score, true); } + /// @notice Starts the next Global Iteration, retaining the existing pass score. + /// @param _GI Expected next GI index (must equal current GI + 1). function startGI(uint _GI) public onlyOwner { _startGI(_GI, 0, false); } @@ -142,6 +169,8 @@ contract DINTaskCoordinator is Ownable { GI++; } + /// @notice Opens the aggregator registration window for the current GI. + /// @param _GI Current GI index, used to guard against stale calls. function startDINaggregatorsRegistration( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { @@ -150,6 +179,9 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.DINaggregatorsRegistrationStarted; } + /// @notice Registers the caller as an aggregator for the current GI. + /// @dev Caller must be an active validator; duplicate registrations revert. + /// @param _GI Current GI index. function registerDINaggregator(uint _GI) public { if (GIstate != GIstates.DINaggregatorsRegistrationStarted) revert TC_AggregatorsRegistrationNotOpen(); @@ -167,6 +199,8 @@ contract DINTaskCoordinator is Ownable { emit DINValidatorRegistered(_GI, msg.sender); } + /// @notice Closes the aggregator registration window. + /// @param _GI Current GI index. function closeDINaggregatorsRegistration( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { @@ -175,12 +209,17 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.DINaggregatorsRegistrationClosed; } + /// @notice Returns the list of aggregators registered for the given GI. + /// @param _GI GI index to query. + /// @return Ordered array of aggregator addresses in registration order. function getDINtaskAggregators( uint _GI ) public view returns (address[] memory) { return dinAggregators[_GI]; } + /// @notice Opens the auditor registration window on the paired DINTaskAuditor. + /// @param _GI Current GI index. function startDINauditorsRegistration( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { @@ -189,6 +228,8 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.DINauditorsRegistrationStarted; } + /// @notice Closes the auditor registration window. + /// @param _GI Current GI index. function closeDINauditorsRegistration( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { @@ -197,17 +238,24 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.DINauditorsRegistrationClosed; } + /// @notice Opens the local model submission window for the current GI. + /// @param _GI Current GI index. function startLMsubmissions(uint _GI) public onlyOwner onlyCurrentGI(_GI) { if (GIstate != GIstates.DINauditorsRegistrationClosed) revert TC_LMSubmissionsCannotBeStarted(); GIstate = GIstates.LMSstarted; } + /// @notice Closes the local model submission window. + /// @param _GI Current GI index. function closeLMsubmissions(uint _GI) public onlyOwner onlyCurrentGI(_GI) { if (GIstate != GIstates.LMSstarted) revert TC_LMSubmissionsNotStarted(); GIstate = GIstates.LMSclosed; } + /// @notice Delegates auditor batch creation to DINTaskAuditor and advances GI state. + /// @dev Reverts if the auditor contract returns false (e.g. insufficient auditors). + /// @param _GI Current GI index. function createAuditorsBatches( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { @@ -219,6 +267,10 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.AuditorsBatchesCreated; } + /// @notice Propagates the test data assignment flag to DINTaskAuditor. + /// @dev Must be called while GI state is AuditorsBatchesCreated. + /// @param _GI Current GI index. + /// @param flag True once test datasets have been distributed to auditors. function setTestDataAssignedFlag( uint _GI, bool flag @@ -229,6 +281,8 @@ contract DINTaskCoordinator is Ownable { dinTaskAuditorContract.setTestDataAssignedFlag(_GI, flag); } + /// @notice Opens the LMS evaluation phase so auditors can begin scoring. + /// @param _GI Current GI index. function startLMsubmissionsEvaluation( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { @@ -237,6 +291,10 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.LMSevaluationStarted; } + /// @notice Closes the LMS evaluation phase and finalises audit results on + /// the paired DINTaskAuditor. + /// @dev Calls DINTaskAuditor.finalizeEvaluation; reverts if it returns false. + /// @param _GI Current GI index. function closeLMsubmissionsEvaluation( uint _GI ) public onlyOwner onlyCurrentGI(_GI) { @@ -247,8 +305,12 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.LMSevaluationClosed; } - /// @notice Build Tier‑1 and Tier‑2 batches automatically. - /// @dev REQUIRES: LM evaluation closed. Validators must already be registered in dinAggregators[_GI]. + /// @notice Partitions active aggregators and approved models into Tier-1 batches + /// and creates a single Tier-2 batch from the remaining validators. + /// @dev Aggregators are filtered to those still Active at call time and shuffled + /// using blockhash-based entropy. Reverts if fewer than T1_AGGREGATORS_PER_BATCH + /// active validators remain, or if fewer than T1_MODELS_PER_BATCH models passed. + /// @param _GI Current GI index. function autoCreateTier1AndTier2( uint _GI ) external onlyOwner onlyCurrentGI(_GI) { @@ -370,10 +432,21 @@ contract DINTaskCoordinator is Ownable { } // ──────────── read helpers ──────────── + /// @notice Returns the number of Tier-1 batches created for the given GI. + /// @param _GI GI index to query. + /// @return Number of Tier-1 batches. function tier1BatchCount(uint _GI) external view returns (uint) { return tier1Batches[_GI].length; } + /// @notice Returns the full details of a Tier-1 batch. + /// @param _GI GI index. + /// @param _id Batch index within that GI. + /// @return batchId Canonical batch identifier. + /// @return validators Aggregators assigned to this batch. + /// @return modelIndexes Indexes into the approved model list assigned to this batch. + /// @return finalized True once a majority CID has been determined. + /// @return finalCID The consensus aggregation CID. function getTier1Batch( uint _GI, uint _id @@ -400,6 +473,14 @@ contract DINTaskCoordinator is Ownable { ); } + /// @notice Returns the details of the single Tier-2 batch for the given GI. + /// @dev _id must be 0; there is always exactly one Tier-2 batch per GI. + /// @param _GI GI index. + /// @param _id Must be 0. + /// @return batchId Canonical batch identifier (always 0). + /// @return validators Aggregators assigned to the Tier-2 batch. + /// @return finalized True once a majority CID has been determined. + /// @return finalCID The consensus aggregation CID. function getTier2Batch( uint _GI, uint _id @@ -419,6 +500,9 @@ contract DINTaskCoordinator is Ownable { return (b.batchId, b.aggregators, b.finalized, b.finalCID); } + /// @notice Transitions GI state to T1AggregationStarted, opening the + /// Tier-1 submission window for assigned aggregators. + /// @param _GI Current GI index. function startT1Aggregation( uint _GI ) external onlyOwner onlyCurrentGI(_GI) { @@ -427,6 +511,12 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.T1AggregationStarted; } + /// @notice Submits an aggregation result CID for a Tier-1 batch. + /// @dev Caller must be an assigned, active aggregator who has not already submitted. + /// Votes are tallied per CID; the majority CID is selected at finalization. + /// @param _GI Current GI index. + /// @param _batchId Tier-1 batch index. + /// @param _aggregationCID IPFS CID of the aggregated model weights, encoded as bytes32. function submitT1Aggregation( uint _GI, uint _batchId, @@ -452,6 +542,10 @@ contract DINTaskCoordinator is Ownable { t1Votes[_GI][_batchId][_aggregationCID]++; } + /// @notice Closes the Tier-1 submission window and selects the majority CID + /// for every batch. + /// @dev Iterates all T1 batches; reverts on the first batch that has no submissions. + /// @param _GI Current GI index. function finalizeT1Aggregation( uint _GI ) external onlyOwner onlyCurrentGI(_GI) { @@ -487,6 +581,8 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.T1AggregationDone; } + /// @notice Opens the Tier-2 aggregation submission window. + /// @param _GI Current GI index. function startT2Aggregation( uint _GI ) external onlyOwner onlyCurrentGI(_GI) { @@ -495,6 +591,12 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.T2AggregationStarted; } + /// @notice Submits an aggregation result CID for the Tier-2 batch. + /// @dev _batchId must be 0. Caller must be an assigned, active aggregator who + /// has not already submitted. + /// @param _GI Current GI index. + /// @param _batchId Must be 0. + /// @param _aggregationCID IPFS CID of the final aggregated model, encoded as bytes32. function submitT2Aggregation( uint _GI, uint _batchId, @@ -519,6 +621,9 @@ contract DINTaskCoordinator is Ownable { t2Votes[_GI][_batchId][_aggregationCID]++; } + /// @notice Closes the Tier-2 submission window and selects the majority CID. + /// @dev Reverts if the Tier-2 batch has received no submissions. + /// @param _GI Current GI index. function finalizeT2Aggregation( uint _GI ) external onlyOwner onlyCurrentGI(_GI) { @@ -554,6 +659,9 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.T2AggregationDone; } + /// @notice Triggers auditor slashing on the paired DINTaskAuditor contract. + /// @dev Reverts if DINTaskAuditor.slashAuditors returns false. + /// @param _GI Current GI index. function slashAuditors(uint _GI) external onlyOwner onlyCurrentGI(_GI) { if (GIstate != GIstates.T2AggregationDone) revert TC_NotReadyToSlashAuditors(); @@ -562,6 +670,11 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.AuditorsSlashed; } + /// @notice Slashes aggregators in both Tier-1 and Tier-2 batches that failed + /// to submit or submitted a CID that did not match the consensus. + /// @dev Slash amount equals minStake() at call time. Each affected aggregator + /// emits an AggregatorSlashed event with the actual amount deducted. + /// @param _GI Current GI index. function slashAggregators(uint _GI) external onlyOwner onlyCurrentGI(_GI) { if (GIstate != GIstates.AuditorsSlashed) revert TC_NotReadyToSlashAggregators(); @@ -641,6 +754,10 @@ contract DINTaskCoordinator is Ownable { GIstate = GIstates.AggregatorsSlashed; } + /// @notice Records the Tier-2 aggregation quality score for the current GI. + /// @dev Permitted during T2AggregationDone or GenesisModelCreated states. + /// @param _GI Current GI index. + /// @param _score Quality score for the Tier-2 aggregated model. function setTier2Score( uint _GI, uint _score @@ -652,10 +769,17 @@ contract DINTaskCoordinator is Ownable { tier2Score[_GI] = _score; } + /// @notice Returns the Tier-2 quality score recorded for the given GI. + /// @param _GI GI index to query. + /// @return The score set via setTier2Score for that GI. function getTier2Score(uint _GI) external view returns (uint) { return tier2Score[_GI]; } + /// @notice Marks the current Global Iteration as complete. + /// @dev Must be called after slashAggregators. The next startGI call will + /// increment GI and transition state to GIstarted. + /// @param _GI Current GI index. function endGI(uint _GI) external onlyOwner onlyCurrentGI(_GI) { if (GIstate != GIstates.AggregatorsSlashed) revert TC_NotReadyToEndGI(); GIstate = GIstates.GIended; diff --git a/hardhat/contracts/DinCoordinator.sol b/hardhat/contracts/DinCoordinator.sol index 5c7ed47..eaee17b 100644 --- a/hardhat/contracts/DinCoordinator.sol +++ b/hardhat/contracts/DinCoordinator.sol @@ -1,8 +1,9 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.28; -import "./DinToken.sol"; // Import the DINToken contract interface -import "@openzeppelin/contracts/access/Ownable.sol"; +import "./DinToken.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; interface IDinValidatorStake { @@ -11,11 +12,22 @@ interface IDinValidatorStake { function removeSlasherContract(address slasherContract) external; } -contract DinCoordinator is Ownable, ReentrancyGuardTransient { - DinToken public immutable dinToken; +/// @title DIN Coordinator +/// @notice Central hub for ETH-to-DIN token exchange and slasher contract +/// administration. Deployed once per network behind a Transparent Proxy. +contract DinCoordinator is + Initializable, + OwnableUpgradeable, + ReentrancyGuardTransient +{ + DinToken public dinToken; IDinValidatorStake public dinValidatorStakeContract; - uint256 public dinPerEth = 1_000_000 * 1e18; // 1M DIN tokens (with 18 decimals) + uint256 public dinPerEth; + + // Reserved for future state variables at this inheritance level. + uint256[50] private __gap; + event EthDepositAndDINminted( address indexed user, uint256 ethAmount, @@ -31,28 +43,44 @@ contract DinCoordinator is Ownable, ReentrancyGuardTransient { error ZeroValue(); error TransferFailed(); - constructor() Ownable(msg.sender) { - // Deploy DINToken - dinToken = new DinToken(address(this)); + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialises the proxy with the DIN token address and sets the + /// default exchange rate to 1,000,000 DIN per ETH. + /// @param dinToken_ Address of the DinToken proxy. + function initialize(address dinToken_) external initializer { + if (dinToken_ == address(0)) revert InvalidAddress(); + __Ownable_init(msg.sender); + dinToken = DinToken(dinToken_); + dinPerEth = 1_000_000 * 1e18; } - /// @notice User deposits ETH → receives DIN tokens + /// @notice Deposits ETH and mints the equivalent amount of DIN tokens to + /// the caller at the current exchange rate. + /// @dev Rate is a fixed-point value scaled by 1e18. function depositAndMint() external payable nonReentrant { if (msg.value == 0) revert ZeroValue(); - uint256 mintAmount = (msg.value * dinPerEth) / 1e18; // ✅ Safe decimal math + uint256 mintAmount = (msg.value * dinPerEth) / 1e18; dinToken.mint(msg.sender, mintAmount); emit EthDepositAndDINminted(msg.sender, msg.value, mintAmount); } + /// @notice Withdraws the contract's entire ETH balance to the owner address. function withdraw() external onlyOwner nonReentrant { uint256 balance = address(this).balance; - if (balance == 0) return; // ✅ No-op if empty + if (balance == 0) return; (bool success, ) = payable(owner()).call{value: balance}(""); if (!success) revert TransferFailed(); } + /// @notice Registers a task contract as an authorised slasher on the + /// validator stake contract. + /// @param slasherContract Address of the task contract to authorise. function addSlasherContract(address slasherContract) external onlyOwner { if (slasherContract == address(0)) revert InvalidAddress(); if (address(dinValidatorStakeContract) == address(0)) @@ -61,6 +89,8 @@ contract DinCoordinator is Ownable, ReentrancyGuardTransient { emit SlasherContractAdded(slasherContract); } + /// @notice Removes a task contract's slasher authorisation. + /// @param slasherContract Address of the task contract to deauthorise. function removeSlasherContract(address slasherContract) external onlyOwner { if (slasherContract == address(0)) revert InvalidAddress(); if (address(dinValidatorStakeContract) == address(0)) @@ -69,6 +99,10 @@ contract DinCoordinator is Ownable, ReentrancyGuardTransient { emit SlasherContractRemoved(slasherContract); } + /// @notice Points the coordinator at the DinValidatorStake proxy address. + /// @dev Called once during initial deployment wiring. No guard prevents a + /// second call; the owner is responsible for operational safety. + /// @param validatorStakeContract Address of the DinValidatorStake proxy. function updateValidatorStakeContract( address validatorStakeContract ) external onlyOwner { @@ -77,7 +111,8 @@ contract DinCoordinator is Ownable, ReentrancyGuardTransient { emit ValidatorStakeContractUpdated(validatorStakeContract); } - // ✅ Optional: Update exchange rate (with safeguards) + /// @notice Updates the DIN-per-ETH exchange rate used by depositAndMint. + /// @param newRate New rate scaled by 1e18 (e.g. 1_000_000 * 1e18 = 1 M DIN per ETH). function updateDinPerEth(uint256 newRate) external onlyOwner { if (newRate == 0) revert ZeroValue(); dinPerEth = newRate; diff --git a/hardhat/contracts/DinToken.sol b/hardhat/contracts/DinToken.sol index d7f411b..84f26c0 100644 --- a/hardhat/contracts/DinToken.sol +++ b/hardhat/contracts/DinToken.sol @@ -1,40 +1,59 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.19; +pragma solidity ^0.8.28; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; /// @title DIN Token -/// @notice ERC20-compliant token for the DIN Protocol ecosystem. -/// @dev Minting is restricted to the owner (DinCoordinator), which can be updated through transfer of ownership. - -contract DinToken is ERC20 { +/// @notice ERC-20 token minted exclusively by the DinCoordinator in exchange +/// for ETH deposits. Deployed behind a Transparent Proxy. +contract DinToken is Initializable, ERC20Upgradeable, OwnableUpgradeable { error InvalidAddress(); error Unauthorized(); + error CoordinatorAlreadySet(); + + address public coordinator; + + // Reserved for future state variables at this inheritance level. + uint256[50] private __gap; - // Event for off-chain indexing event TokensMinted(address indexed to, uint256 amount); + event CoordinatorSet(address indexed coordinator); - /// @notice Immutable owner - DinCoordinator, set once at deployment - address public immutable OWNER; + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialises the proxy, registering the token as "DIN Token" (DIN). + function initialize() external initializer { + __ERC20_init("DIN Token", "DIN"); + __Ownable_init(msg.sender); + } - /// @notice Constructor initializes the token with name and symbol. - constructor(address owner_) ERC20("DIN Token", "DIN") { - OWNER = owner_; - // Optionally mint initial supply to the deployer if needed - // _mint(owner_, initialSupply * 10 ** decimals()); + /// @notice Wires the DinCoordinator proxy as the sole authorised minter. + /// @dev One-shot: reverts if called a second time. The coordinator sits behind + /// a Transparent Proxy so its address is stable across upgrades; changing + /// it would require deploying a new proxy, which is an intentional constraint. + /// @param coordinator_ Address of the DinCoordinator proxy. + function setCoordinator(address coordinator_) external onlyOwner { + if (coordinator != address(0)) revert CoordinatorAlreadySet(); + if (coordinator_ == address(0)) revert InvalidAddress(); + coordinator = coordinator_; + emit CoordinatorSet(coordinator_); } - /// @notice Modifier: restricts to immutable owner (gas-efficient, no SLOAD) - modifier onlyOwner() { - if (msg.sender != OWNER) revert Unauthorized(); + modifier onlyCoordinator() { + if (msg.sender != coordinator) revert Unauthorized(); _; } - /// @notice Mint new tokens — callable only by the contract owner. - /// @param to The address to receive minted tokens. - /// @param amount The number of tokens to mint (18 decimals). - function mint(address to, uint256 amount) external onlyOwner { + /// @notice Mints DIN tokens to the specified address. + /// @dev Restricted to the coordinator. Called by DinCoordinator.depositAndMint. + /// @param to Recipient address. + /// @param amount Token amount in wei (18 decimals). + function mint(address to, uint256 amount) external onlyCoordinator { if (to == address(0)) revert InvalidAddress(); _mint(to, amount); emit TokensMinted(to, amount); diff --git a/hardhat/contracts/DinValidatorStake.sol b/hardhat/contracts/DinValidatorStake.sol index fe9436f..98dccc0 100644 --- a/hardhat/contracts/DinValidatorStake.sol +++ b/hardhat/contracts/DinValidatorStake.sol @@ -1,12 +1,20 @@ // SPDX-License-Identifier: MIT -pragma solidity ^0.8.20; +pragma solidity ^0.8.28; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/utils/ReentrancyGuardTransient.sol"; -contract DinValidatorStake is Ownable, ReentrancyGuardTransient { +/// @title DIN Validator Stake +/// @notice Manages validator staking, unbonding, slashing, and blacklisting for +/// the DIN protocol. Deployed once per network behind a Transparent Proxy. +contract DinValidatorStake is + Initializable, + OwnableUpgradeable, + ReentrancyGuardTransient +{ error NotDINCoordinator(); error ValidatorIsBlacklisted(); error ValidatorNotBlacklisted(); @@ -22,8 +30,8 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { error NoPendingWithdrawal(); error WithdrawalNotReady(); - IERC20 public immutable DIN_TOKEN; - address public immutable DIN_COORDINATOR; + IERC20 public DIN_TOKEN; + address public DIN_COORDINATOR; using SafeERC20 for IERC20; @@ -67,10 +75,25 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { mapping(address => ValidatorInfo) public validators; - constructor(address dinToken, address dinCoordinator) Ownable(msg.sender) { + // Reserved for future state variables at this inheritance level. + uint256[50] private __gap; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /// @notice Initialises the proxy with the token and coordinator addresses. + /// @param dinToken Address of the DinToken proxy. + /// @param dinCoordinator Address of the DinCoordinator proxy. + function initialize( + address dinToken, + address dinCoordinator + ) external initializer { if (dinToken == address(0) || dinCoordinator == address(0)) { revert InvalidAddress(); } + __Ownable_init(msg.sender); DIN_TOKEN = IERC20(dinToken); DIN_COORDINATOR = dinCoordinator; } @@ -85,6 +108,8 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { _; } + /// @notice Stakes DIN tokens and registers the caller as an active validator. + /// @param amount Token amount to stake, must be at least MIN_STAKE. function stake(uint256 amount) external nonReentrant { if (amount < MIN_STAKE) revert AmountLessThanMinStake(); @@ -100,6 +125,9 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { emit ValidatorStaked(msg.sender, amount); } + /// @notice Registers a contract as authorised to call slash(). + /// @dev Restricted to the DIN_COORDINATOR address. Called via DinCoordinator.addSlasherContract. + /// @param slasherContract Address of the task contract to authorise. function addSlasherContract( address slasherContract ) external onlyDinCoordinator { @@ -112,6 +140,8 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { emit SlasherContractAdded(slasherContract); } + /// @notice Removes a contract's slasher authorisation. + /// @param slasherContract Address of the task contract to deauthorise. function removeSlasherContract( address slasherContract ) external onlyDinCoordinator { @@ -124,6 +154,14 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { emit SlasherContractRemoved(slasherContract); } + /// @notice Slashes a validator's stake by the requested amount. + /// @dev Active stake is consumed first; any remainder is taken from pending + /// withdrawals. If total slashable stake is less than amount, the actual + /// slashed amount is capped and returned rather than reverting. + /// @param validator Address of the validator to slash. + /// @param amount Maximum token amount to slash. + /// @param reason Arbitrary identifier for the slash event, emitted on-chain. + /// @return The actual amount slashed (may be less than amount if stake is insufficient). function slash( address validator, uint256 amount, @@ -159,6 +197,10 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { return actualAmount; } + /// @notice Moves stake into a pending withdrawal subject to the unbonding period. + /// @dev Only one pending withdrawal can exist at a time. The withdrawn amount + /// remains slashable during the unbonding window. + /// @param amount Token amount to unstake. function unstake(uint256 amount) external nonReentrant { ValidatorInfo storage validator = validators[msg.sender]; if (validator.status == ValidatorStatus.Blacklisted) { @@ -182,6 +224,8 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { ); } + /// @notice Transfers a matured pending withdrawal back to the caller. + /// @dev Reverts if the unbonding period has not elapsed since unstake() was called. function claimUnstaked() external nonReentrant { ValidatorInfo storage validator = validators[msg.sender]; if (validator.status == ValidatorStatus.Blacklisted) { @@ -202,12 +246,18 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { emit ValidatorWithdrawalClaimed(msg.sender, pendingAmount); } + /// @notice Permanently prevents a validator from staking or unstaking. + /// @param validator Address to blacklist. function blacklistValidator(address validator) external onlyOwner { if (validator == address(0)) revert InvalidAddress(); validators[validator].status = ValidatorStatus.Blacklisted; emit ValidatorBlacklisted(validator); } + /// @notice Lifts a blacklist, restoring the validator's prior status. + /// @dev If the validator was jailed before being blacklisted and the jail + /// period has not expired, status is restored to Jailed, not Active. + /// @param validator Address to unblacklist. function unblacklistValidator(address validator) external onlyOwner { if (validator == address(0)) revert InvalidAddress(); @@ -226,24 +276,39 @@ contract DinValidatorStake is Ownable, ReentrancyGuardTransient { emit ValidatorUnblacklisted(validator); } + /// @notice Returns the minimum token amount required to become an active validator. + /// @return The MIN_STAKE constant in wei. function minStake() external pure returns (uint256) { return MIN_STAKE; } + /// @notice Returns true if the validator's status is Active. + /// @param validator Address to query. + /// @return True if the validator is currently in the Active state. function isValidatorActive(address validator) public view returns (bool) { ValidatorInfo storage info = validators[validator]; return info.status == ValidatorStatus.Active; } + /// @notice Returns a validator's current active stake. + /// @param validator Address to query. + /// @return Active stake balance in wei. function getStake(address validator) public view returns (uint256) { return validators[validator].activeStake; } + /// @notice Returns the total amount that can be slashed from a validator, + /// including both active stake and any pending withdrawals. + /// @param validator Address to query. + /// @return Sum of activeStake and pendingWithdrawals in wei. function slashableStakeOf(address validator) public view returns (uint256) { ValidatorInfo storage info = validators[validator]; return info.activeStake + info.pendingWithdrawals; } + /// @notice Returns true if the given address is a registered slasher contract. + /// @param slasherContract Address to query. + /// @return True if the address has slasher authorisation. function isSlasherContract( address slasherContract ) public view returns (bool) { diff --git a/hardhat/contracts/mocks/MockSlasher.sol b/hardhat/contracts/mocks/MockSlasher.sol new file mode 100644 index 0000000..536ff25 --- /dev/null +++ b/hardhat/contracts/mocks/MockSlasher.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract MockSlasher is Ownable { + constructor(address initialOwner) Ownable(initialOwner) {} +} diff --git a/hardhat/contracts/upgrade/DINModelRegistryV2.sol b/hardhat/contracts/upgrade/DINModelRegistryV2.sol new file mode 100644 index 0000000..4e0f3ee --- /dev/null +++ b/hardhat/contracts/upgrade/DINModelRegistryV2.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "../DINModelRegistry.sol"; + +/// @custom:oz-upgrades-from DINModelRegistry +// missing-initializer is intentional: V2 adds no new state, so it reuses +// V1's initializer. A reinitializer would only be needed if V2 introduced +// new storage variables that require one-time setup. +/// @custom:oz-upgrades-unsafe-allow missing-initializer +contract DINModelRegistryV2 is DINModelRegistry { + function version() external pure returns (uint256) { + return 2; + } +} diff --git a/hardhat/contracts/upgrade/DinCoordinatorV2.sol b/hardhat/contracts/upgrade/DinCoordinatorV2.sol new file mode 100644 index 0000000..c5eb9ca --- /dev/null +++ b/hardhat/contracts/upgrade/DinCoordinatorV2.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.28; + +import "../DinCoordinator.sol"; + +/// @custom:oz-upgrades-from DinCoordinator +// missing-initializer is intentional: V2 adds no new state, so it reuses +// V1's initializer. A reinitializer would only be needed if V2 introduced +// new storage variables that require one-time setup. +/// @custom:oz-upgrades-unsafe-allow missing-initializer +contract DinCoordinatorV2 is DinCoordinator { + function version() external pure returns (uint256) { + return 2; + } +} diff --git a/hardhat/contracts/upgrade/DinTokenV2.sol b/hardhat/contracts/upgrade/DinTokenV2.sol new file mode 100644 index 0000000..44aac86 --- /dev/null +++ b/hardhat/contracts/upgrade/DinTokenV2.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "../DinToken.sol"; + +/// @custom:oz-upgrades-from DinToken +// missing-initializer is intentional: V2 adds no new state, so it reuses +// V1's initializer. A reinitializer would only be needed if V2 introduced +// new storage variables that require one-time setup. +/// @custom:oz-upgrades-unsafe-allow missing-initializer +contract DinTokenV2 is DinToken { + function version() external pure returns (uint256) { + return 2; + } +} diff --git a/hardhat/contracts/upgrade/DinValidatorStakeV2.sol b/hardhat/contracts/upgrade/DinValidatorStakeV2.sol new file mode 100644 index 0000000..02bf16f --- /dev/null +++ b/hardhat/contracts/upgrade/DinValidatorStakeV2.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "../DinValidatorStake.sol"; + +/// @custom:oz-upgrades-from DinValidatorStake +// missing-initializer is intentional: V2 adds no new state, so it reuses +// V1's initializer. A reinitializer would only be needed if V2 introduced +// new storage variables that require one-time setup. +/// @custom:oz-upgrades-unsafe-allow missing-initializer +contract DinValidatorStakeV2 is DinValidatorStake { + function version() external pure returns (uint256) { + return 2; + } +} diff --git a/hardhat/deploy/constants.ts b/hardhat/deploy/constants.ts new file mode 100644 index 0000000..56e7833 --- /dev/null +++ b/hardhat/deploy/constants.ts @@ -0,0 +1,10 @@ +export const PROXY_KIND = "transparent" as const; + +export const PLATFORM_CONTRACTS = [ + "DinToken", + "DinCoordinator", + "DinValidatorStake", + "DINModelRegistry", +] as const; + +export type PlatformContractName = (typeof PLATFORM_CONTRACTS)[number]; diff --git a/hardhat/deploy/helpers.ts b/hardhat/deploy/helpers.ts new file mode 100644 index 0000000..32fd4cc --- /dev/null +++ b/hardhat/deploy/helpers.ts @@ -0,0 +1,64 @@ +import { ContractFactory } from "ethers"; +import { upgrades } from "hardhat"; +import * as fs from "fs"; +import * as path from "path"; + +import { PROXY_KIND } from "./constants"; +import type { PlatformAddresses } from "./types"; + +const DEPLOYMENTS_DIR = path.join(__dirname, "..", "deployments"); + +export async function deployTransparentProxy( + factory: T, + initArgs: unknown[] = [], + initializer = "initialize" +) { + const proxy = await upgrades.deployProxy(factory, initArgs, { + kind: PROXY_KIND, + initializer, + }); + await proxy.waitForDeployment(); + return proxy; +} + +export async function upgradeTransparentProxy( + proxyAddress: string, + factory: T +) { + const upgraded = await upgrades.upgradeProxy(proxyAddress, factory, { + kind: PROXY_KIND, + }); + await upgraded.waitForDeployment(); + return upgraded; +} + +export function deploymentPath(network: string): string { + return path.join(DEPLOYMENTS_DIR, `${network}.json`); +} + +export function savePlatformAddresses( + network: string, + addresses: PlatformAddresses +): void { + fs.mkdirSync(DEPLOYMENTS_DIR, { recursive: true }); + fs.writeFileSync( + deploymentPath(network), + JSON.stringify(addresses, null, 2) + "\n" + ); +} + +export function loadPlatformAddresses(network: string): PlatformAddresses { + const file = deploymentPath(network); + if (!fs.existsSync(file)) { + throw new Error( + `No deployment file at ${file}. Run the platform deploy script first.` + ); + } + return JSON.parse(fs.readFileSync(file, "utf8")) as PlatformAddresses; +} + +export async function getProxyAdminAddress( + proxyAddress: string +): Promise { + return upgrades.erc1967.getAdminAddress(proxyAddress); +} diff --git a/hardhat/deploy/types.ts b/hardhat/deploy/types.ts new file mode 100644 index 0000000..f1c0489 --- /dev/null +++ b/hardhat/deploy/types.ts @@ -0,0 +1,13 @@ +export interface PlatformAddresses { + dinToken: string; + dinCoordinator: string; + dinValidatorStake: string; + dinModelRegistry: string; + proxyAdmin?: string; + implementations?: { + dinToken?: string; + dinCoordinator?: string; + dinValidatorStake?: string; + dinModelRegistry?: string; + }; +} diff --git a/hardhat/hardhat.config.ts b/hardhat/hardhat.config.ts index 1516ded..fb7628c 100644 --- a/hardhat/hardhat.config.ts +++ b/hardhat/hardhat.config.ts @@ -1,5 +1,6 @@ import { HardhatUserConfig } from "hardhat/config"; import "@nomicfoundation/hardhat-toolbox"; +import "@openzeppelin/hardhat-upgrades"; import * as dotenv from "dotenv"; import * as fs from "fs"; import "hardhat-contract-sizer"; @@ -7,10 +8,8 @@ import * as path from "path"; import { task } from "hardhat/config"; import { HardhatRuntimeEnvironment } from "hardhat/types"; -// Load base .env dotenv.config({ path: "../.env" }); -// Load network-specific .env const network = process.env.NETWORK || "local"; const sepolia_op_devnet_rpc_url = process.env.SEPOLIA_OP_DEVNET_RPC_URL; const envFile = `../.env.${network}`; @@ -32,34 +31,40 @@ const config: HardhatUserConfig = { runs: 200 }, viaIR: true, - // ✅ CRITICAL: Set EVM version to Cancun for TSTORE/TLOAD support evmVersion: "cancun" } }, networks: { hardhat: { - hardfork: "cancun", // ✅ Required for local testing + hardfork: "cancun", ...(process.env.ALCHEMY_API_KEY ? { forking: { url: `https://eth-mainnet.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`, - // Replace with your Alchemy API key - //blockNumber: 18700000, // Optional: specific block to fork from (or remove to fork latest) }, } : {}), accounts: { - count: 70, // Generate 70 accounts + count: 70, accountsBalance: "10000000000000000000000" }, chainId: 1337, allowUnlimitedContractSize: true, }, - sepolia_op_devnet: { - url: sepolia_op_devnet_rpc_url, - accounts: [process.env.ETH_PRIVATE_KEY_0!, process.env.ETH_PRIVATE_KEY_1!], - chainId: 11155420, - }, + ...(sepolia_op_devnet_rpc_url && + process.env.ETH_PRIVATE_KEY_0 && + process.env.ETH_PRIVATE_KEY_1 + ? { + sepolia_op_devnet: { + url: sepolia_op_devnet_rpc_url, + accounts: [ + process.env.ETH_PRIVATE_KEY_0, + process.env.ETH_PRIVATE_KEY_1, + ], + chainId: 11155420, + }, + } + : {}), localhost: { url: "http://127.0.0.1:8545", @@ -84,7 +89,7 @@ const config: HardhatUserConfig = { enabled: false }, mocha: { - timeout: 100000000 // Set a very high timeout for tests + timeout: 100000000 } }; diff --git a/hardhat/package-lock.json b/hardhat/package-lock.json index 53eb191..1d85771 100644 --- a/hardhat/package-lock.json +++ b/hardhat/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@openzeppelin/contracts": "^5.3.0", + "@openzeppelin/contracts-upgradeable": "^5.3.0", "chalk": "^4.1.2", "cli-table3": "^0.6.5", "dotenv": "^17.2.3", @@ -18,6 +19,7 @@ "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", "@nomicfoundation/hardhat-verify": "^2.1.3", + "@openzeppelin/hardhat-upgrades": "^3.9.1", "hardhat": "^2.23.0", "hardhat-contract-sizer": "^2.10.1" } @@ -30,6 +32,550 @@ "license": "MIT", "peer": true }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/supports-web-crypto/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/client-lambda": { + "version": "3.1075.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.1075.0.tgz", + "integrity": "sha512-S+sy0L7WEouGGys0kJZJ+br0sQGxgWC+0nR/4ySsym/CSg/k+VHXBdD697WJLli+X5a76tFBshx5bjgVKgLg4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/credential-provider-node": "^3.972.58", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.23.tgz", + "integrity": "sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@aws-sdk/xml-builder": "^3.972.31", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.49.tgz", + "integrity": "sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.51.tgz", + "integrity": "sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.56", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.56.tgz", + "integrity": "sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-login": "^3.972.55", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.55.tgz", + "integrity": "sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.58", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.58.tgz", + "integrity": "sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-ini": "^3.972.56", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.49.tgz", + "integrity": "sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.55.tgz", + "integrity": "sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/token-providers": "3.1074.0", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.55.tgz", + "integrity": "sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.23.tgz", + "integrity": "sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/signature-v4-multi-region": "^3.996.35", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.35.tgz", + "integrity": "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1074.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1074.0.tgz", + "integrity": "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.13.tgz", + "integrity": "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/util-utf8-browser": { + "version": "3.259.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8-browser/-/util-utf8-browser-3.259.0.tgz", + "integrity": "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/util-utf8-browser/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.31.tgz", + "integrity": "sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@bytecodealliance/preview2-shim": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@bytecodealliance/preview2-shim/-/preview2-shim-0.17.0.tgz", + "integrity": "sha512-JorcEwe4ud0x5BS/Ar2aQWOQoFzjq/7jcnxYXCvSMh0oRm0dQXzOA+hqLDBnOMks1LLBA7dmiLLsEBl09Yd6iQ==", + "dev": true, + "license": "(Apache-2.0 WITH LLVM-exception)" + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1430,6 +1976,16 @@ "dev": true, "peer": true }, + "node_modules/@nomicfoundation/slang": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/@nomicfoundation/slang/-/slang-0.18.3.tgz", + "integrity": "sha512-YqAWgckqbHM0/CZxi9Nlf4hjk9wUNLC9ngWCWBiqMxPIZmzsVKYuChdlrfeBPQyvQQBoOhbx+7C1005kLVQDZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bytecodealliance/preview2-shim": "0.17.0" + } + }, "node_modules/@nomicfoundation/solidity-analyzer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@nomicfoundation/solidity-analyzer/-/solidity-analyzer-0.1.2.tgz", @@ -1527,11 +2083,174 @@ } }, "node_modules/@openzeppelin/contracts": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.3.0.tgz", - "integrity": "sha512-zj/KGoW7zxWUE8qOI++rUM18v+VeLTTzKs/DJFkSzHpQFPD/jKKF0TrMxBfGLl3kpdELCNccvB3zmofSzm4nlA==", + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.6.1.tgz", + "integrity": "sha512-Ly6SlsVJ3mj+b18W3R8gNufB7dTICT105fJhodGAGgyC2oqnBAhqSiNDJ8V8DLY05cCz81GLI0CU5vNYA1EC/w==", "license": "MIT" }, + "node_modules/@openzeppelin/contracts-upgradeable": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts-upgradeable/-/contracts-upgradeable-5.6.1.tgz", + "integrity": "sha512-n4a/vfRs114lXyUdYg7pyY8LvFKWvCDF5lEcRRAVxap8g6ZEdLqm+9tmt2zTtRHcNMxTYp9y5t6KBof4tHp7Og==", + "license": "MIT", + "peerDependencies": { + "@openzeppelin/contracts": "5.6.1" + } + }, + "node_modules/@openzeppelin/defender-sdk-base-client": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-base-client/-/defender-sdk-base-client-2.7.1.tgz", + "integrity": "sha512-7gFCteA+V3396A3McgqzmirwmbPXuHJYN896O3AbsHX9XcxInN74C5Zv3tFHld0GmIX/VlaIvILNMhOpdISZjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-lambda": "^3.563.0", + "amazon-cognito-identity-js": "^6.3.6", + "async-retry": "^1.3.3", + "axios": "^1.7.4" + } + }, + "node_modules/@openzeppelin/defender-sdk-deploy-client": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-deploy-client/-/defender-sdk-deploy-client-2.7.1.tgz", + "integrity": "sha512-vFkDupn8ATW83KjZlY5U7UdsvSo9YZwOMQoVaHJO3S+Z6h0wa6cTzuQV9C0AKYq524quQkFsQ4AQq5CgsgdEkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openzeppelin/defender-sdk-base-client": "^2.7.1", + "axios": "^1.7.4", + "lodash": "^4.17.21" + } + }, + "node_modules/@openzeppelin/defender-sdk-network-client": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/defender-sdk-network-client/-/defender-sdk-network-client-2.7.1.tgz", + "integrity": "sha512-AWJKT9YKv9wH3/1AJZCztF3VIsg1sX+v8fjtyFLROqtVAzmhB8WKBRVt9GHAZ+PmsixAKDMOEbH6R1cipTIVHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openzeppelin/defender-sdk-base-client": "^2.7.1", + "axios": "^1.7.4", + "lodash": "^4.17.21" + } + }, + "node_modules/@openzeppelin/hardhat-upgrades": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@openzeppelin/hardhat-upgrades/-/hardhat-upgrades-3.9.1.tgz", + "integrity": "sha512-pSDjlOnIpP+PqaJVe144dK6VVKZw2v6YQusyt0OOLiCsl+WUzfo4D0kylax7zjrOxqy41EK2ipQeIF4T+cCn2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@openzeppelin/defender-sdk-base-client": "^2.1.0", + "@openzeppelin/defender-sdk-deploy-client": "^2.1.0", + "@openzeppelin/defender-sdk-network-client": "^2.1.0", + "@openzeppelin/upgrades-core": "^1.41.0", + "chalk": "^4.1.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.1.5", + "proper-lockfile": "^4.1.1", + "undici": "^6.11.1" + }, + "bin": { + "migrate-oz-cli-project": "dist/scripts/migrate-oz-cli-project.js" + }, + "peerDependencies": { + "@nomicfoundation/hardhat-ethers": "^3.0.6", + "@nomicfoundation/hardhat-verify": "^2.0.14", + "ethers": "^6.6.0", + "hardhat": "^2.24.1" + }, + "peerDependenciesMeta": { + "@nomicfoundation/hardhat-verify": { + "optional": true + } + } + }, + "node_modules/@openzeppelin/hardhat-upgrades/node_modules/undici": { + "version": "6.27.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz", + "integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@openzeppelin/upgrades-core": { + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@openzeppelin/upgrades-core/-/upgrades-core-1.46.0.tgz", + "integrity": "sha512-UFSeO/4r8eeXj0C/HAwV+J4b72sE1HX0aALQFs5S2RBOsfXvKweyjQf35vrK32LQiyHdP6IPShAsEBVvpSEgGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nomicfoundation/slang": "^0.18.3", + "bignumber.js": "^9.1.2", + "cbor": "^10.0.0", + "chalk": "^4.1.0", + "compare-versions": "^6.0.0", + "debug": "^4.1.1", + "ethereumjs-util": "^7.0.3", + "minimatch": "^10.2.5", + "minimist": "^1.2.7", + "proper-lockfile": "^4.1.1", + "solidity-ast": "^0.4.60" + }, + "bin": { + "openzeppelin-upgrades-core": "dist/cli/cli.js" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/cbor": { + "version": "10.0.12", + "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.12.tgz", + "integrity": "sha512-exQDevYd7ZQLP4moMQcZkKCVZsXLAtUSflObr3xTh4xzFIv/xBCdvCd6L259kQOUP2kcTC0jvC6PpZIf/WmRXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "nofilter": "^3.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@openzeppelin/upgrades-core/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@scure/base": { "version": "1.1.9", "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", @@ -1645,47 +2364,239 @@ "node": ">=6" } }, - "node_modules/@sentry/tracing": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", - "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", + "node_modules/@sentry/tracing": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-5.30.0.tgz", + "integrity": "sha512-dUFowCr0AIMwiLD7Fs314Mdzcug+gBVo/+NCMyDw8tFxJkwWAKl7Qa2OZxLQ0ZHjakcj1hNKfCQJ9rhyfOl4Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sentry/hub": "5.30.0", + "@sentry/minimal": "5.30.0", + "@sentry/types": "5.30.0", + "@sentry/utils": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/types": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", + "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=6" + } + }, + "node_modules/@sentry/utils": { + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", + "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sentry/types": "5.30.0", + "tslib": "^1.9.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@smithy/core": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.26.0.tgz", + "integrity": "sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.2.tgz", + "integrity": "sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.5.2.tgz", + "integrity": "sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/is-array-buffer/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz", + "integrity": "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@smithy/signature-v4": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.5.2.tgz", + "integrity": "sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "MIT", + "license": "0BSD" + }, + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sentry/hub": "5.30.0", - "@sentry/minimal": "5.30.0", - "@sentry/types": "5.30.0", - "@sentry/utils": "5.30.0", - "tslib": "^1.9.3" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6" + "node": ">=18.0.0" } }, - "node_modules/@sentry/types": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.30.0.tgz", - "integrity": "sha512-R8xOqlSTZ+htqrfteCWU5Nk0CDN5ApUTvrlvBuiH1DyP6czDZ4ktbZB0hAgBlVcK0U+qpD3ag3Tqqpa5Q67rPw==", + "node_modules/@smithy/types/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "BSD-3-Clause", + "license": "0BSD" + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6" + "node": ">=14.0.0" } }, - "node_modules/@sentry/utils": { - "version": "5.30.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.30.0.tgz", - "integrity": "sha512-zaYmoH0NWWtvnJjC9/CBseXMtKHm/tm40sz3YfJRxeQjyzRqNQPgivpd9R/oDJCYj999mzdW382p/qi2ypjLww==", + "node_modules/@smithy/util-buffer-from/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "BSD-3-Clause", + "license": "0BSD" + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sentry/types": "5.30.0", - "tslib": "^1.9.3" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6" + "node": ">=14.0.0" } }, + "node_modules/@smithy/util-utf8/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/@solidity-parser/parser": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/@solidity-parser/parser/-/parser-0.14.5.tgz", @@ -1811,7 +2722,6 @@ "integrity": "sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -1891,7 +2801,6 @@ "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1902,7 +2811,6 @@ "integrity": "sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -1929,7 +2837,6 @@ "integrity": "sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -2032,6 +2939,44 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/amazon-cognito-identity-js": { + "version": "6.3.18", + "resolved": "https://registry.npmjs.org/amazon-cognito-identity-js/-/amazon-cognito-identity-js-6.3.18.tgz", + "integrity": "sha512-tsFGh5sMWvcD0LzDqkfJLZeNAglswtFB2clep7E1xJZpo/djQgrDHvXTzgPmzZuA3oNF2UXynSuENEDNTS56Pg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "1.2.2", + "buffer": "4.9.2", + "fast-base64-decode": "^1.0.0", + "isomorphic-unfetch": "^3.0.0", + "js-cookie": "^3.0.7" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/@aws-crypto/sha256-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz", + "integrity": "sha512-Nr1QJIbW/afYYGzYvrF70LtaHrIRtd4TNAglX8BvlfxJLZ45SAmueIKYl5tWoNBPzp65ymXGFK0Bb1vZUpuc9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^1.2.2", + "@aws-sdk/types": "^3.1.0", + "tslib": "^1.11.1" + } + }, + "node_modules/amazon-cognito-identity-js/node_modules/@aws-crypto/util": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-1.2.2.tgz", + "integrity": "sha512-H8PjG5WJ4wz0UXAFXeJjWCW1vkvIJ3qUUD+rGRwJ2/hj+xT58Qle2MTql/2MGzkU+1JLAFuR6aJpLAjHwhmwwg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.1.0", + "@aws-sdk/util-utf8-browser": "^3.0.0", + "tslib": "^1.11.1" + } + }, "node_modules/amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", @@ -2224,13 +3169,22 @@ "license": "MIT", "peer": true }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -2249,7 +3203,6 @@ "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2269,11 +3222,31 @@ "integrity": "sha512-xz7wQ8xDhdyP7tQxwdteLYeFfS68tSMNCZ/Y37WJ4bhGfKPpqEIlmIyueQHqOyoPhE6xNUqjzRr8ra0eF9VRvA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "^5.0.1" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bech32": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", @@ -2282,6 +3255,16 @@ "license": "MIT", "peer": true }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -2300,8 +3283,7 @@ "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.2.1.tgz", "integrity": "sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/bn.js": { "version": "5.2.1", @@ -2310,6 +3292,13 @@ "dev": true, "license": "MIT" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, "node_modules/boxen": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz", @@ -2389,7 +3378,6 @@ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "buffer-xor": "^1.0.3", "cipher-base": "^1.0.0", @@ -2405,7 +3393,6 @@ "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "base-x": "^3.0.2" } @@ -2416,13 +3403,24 @@ "integrity": "sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "bs58": "^4.0.0", "create-hash": "^1.1.0", "safe-buffer": "^5.1.2" } }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2435,8 +3433,7 @@ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/bytes": { "version": "3.1.2", @@ -2454,7 +3451,6 @@ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2619,7 +3615,6 @@ "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.4", "safe-buffer": "^5.2.1" @@ -2713,7 +3708,6 @@ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2879,6 +3873,13 @@ "node": ">= 12" } }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==", + "dev": true, + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2964,7 +3965,6 @@ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cipher-base": "^1.0.1", "inherits": "^2.0.1", @@ -2979,7 +3979,6 @@ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cipher-base": "^1.0.3", "create-hash": "^1.1.0", @@ -3085,7 +4084,6 @@ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.4.0" } @@ -3155,7 +4153,6 @@ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -3224,7 +4221,6 @@ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -3235,7 +4231,6 @@ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -3246,7 +4241,6 @@ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -3260,7 +4254,6 @@ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", @@ -3562,7 +4555,6 @@ "integrity": "sha512-SDl5kKrQAudFBUe5OJM9Ac6WmMyYmXX/6sTmLZ3ffG2eY6ZIGBes3pEDxNN6V72WyOw4CPD5RomKdsa8DAAwLg==", "dev": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "@types/bn.js": "^5.1.0", "bn.js": "^5.1.2", @@ -3580,7 +4572,6 @@ "integrity": "sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/pbkdf2": "^3.0.0", "@types/secp256k1": "^4.0.1", @@ -3737,12 +4728,18 @@ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "md5.js": "^1.3.4", "safe-buffer": "^5.1.1" } }, + "node_modules/fast-base64-decode": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz", + "integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3900,7 +4897,6 @@ "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -3969,7 +4965,6 @@ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4001,7 +4996,6 @@ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -4038,7 +5032,6 @@ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -4287,7 +5280,6 @@ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -4434,7 +5426,6 @@ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" }, @@ -4448,7 +5439,6 @@ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-symbols": "^1.0.3" }, @@ -4465,7 +5455,6 @@ "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.4", "readable-stream": "^3.6.0", @@ -4492,7 +5481,6 @@ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -4610,6 +5598,27 @@ "node": ">=0.10.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4793,8 +5802,7 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", @@ -4804,6 +5812,24 @@ "license": "ISC", "peer": true }, + "node_modules/isomorphic-unfetch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/isomorphic-unfetch/-/isomorphic-unfetch-3.1.0.tgz", + "integrity": "sha512-geDJjpoZ8N0kWexiwkX8F9NkTsXhetLPVbZFQ+JTW239QNOwvB0gniuR1Wc6f0AMTn7/mFGyXvHTifrCp/GH8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "unfetch": "^4.2.0" + } + }, + "node_modules/js-cookie": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz", + "integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==", + "dev": true, + "license": "MIT" + }, "node_modules/js-sha3": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", @@ -5048,7 +6074,6 @@ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.4" } @@ -5059,7 +6084,6 @@ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1", @@ -5177,7 +6201,6 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.6" } @@ -5188,7 +6211,6 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5229,7 +6251,6 @@ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5421,6 +6442,27 @@ "lodash": "^4.17.21" } }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -5681,7 +6723,6 @@ "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "create-hash": "^1.1.2", "create-hmac": "^1.1.4", @@ -5785,13 +6826,34 @@ "node": ">= 6" } }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/qs": { "version": "6.14.0", @@ -6023,6 +7085,16 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6041,7 +7113,6 @@ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "hash-base": "^3.0.0", "inherits": "^2.0.1" @@ -6053,7 +7124,6 @@ "integrity": "sha512-d5gdPmgQ0Z+AklL2NVXr/IoSjNZFfTVvQWzL/AM2AOcSzYP2xjlb0AC8YyCLc41MSNf6P6QVtjgPdmVtzb+4lQ==", "dev": true, "license": "MPL-2.0", - "peer": true, "dependencies": { "bn.js": "^5.2.0" }, @@ -6265,8 +7335,7 @@ "resolved": "https://registry.npmjs.org/scrypt-js/-/scrypt-js-3.0.1.tgz", "integrity": "sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/secp256k1": { "version": "4.0.4", @@ -6275,7 +7344,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "elliptic": "^6.5.7", "node-addon-api": "^5.0.0", @@ -6290,8 +7358,7 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -6318,8 +7385,7 @@ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/setprototypeof": { "version": "1.2.0", @@ -6334,7 +7400,6 @@ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "license": "(MIT AND BSD-3-Clause)", - "peer": true, "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" @@ -6506,6 +7571,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -6575,6 +7647,13 @@ "semver": "bin/semver" } }, + "node_modules/solidity-ast": { + "version": "0.4.62", + "resolved": "https://registry.npmjs.org/solidity-ast/-/solidity-ast-0.4.62.tgz", + "integrity": "sha512-jSC7msQCkJXIzM8LlDjRZ5cif5w40g6THlXHFk3zchbL5dm3YLoBETvqPGo5KndYkftjhcs5kz1fnTu4d34lVQ==", + "dev": true, + "license": "MIT" + }, "node_modules/solidity-coverage": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/solidity-coverage/-/solidity-coverage-0.8.15.tgz", @@ -7087,6 +8166,13 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, "node_modules/ts-command-line-args": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/ts-command-line-args/-/ts-command-line-args-2.5.1.tgz", @@ -7379,8 +8465,14 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/unfetch": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/unfetch/-/unfetch-4.2.0.tgz", + "integrity": "sha512-F9p7yYCn6cIW9El1zi0HI6vqpeIvBsr3dSuRO6Xuppb1u5rXpCPmMvLSyECLhybr9isec8Ohl0hPekMVrEinDA==", + "dev": true, + "license": "MIT" }, "node_modules/universalify": { "version": "0.1.2", @@ -7559,6 +8651,24 @@ "@scure/bip39": "1.3.0" } }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", diff --git a/hardhat/package.json b/hardhat/package.json index 08c34ca..f8d9bc0 100644 --- a/hardhat/package.json +++ b/hardhat/package.json @@ -3,7 +3,10 @@ "version": "1.0.0", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "compile": "hardhat compile", + "test": "hardhat test", + "deploy:platform": "hardhat run scripts/deploy-platform.ts", + "upgrade:platform": "hardhat run scripts/upgrade-platform.ts" }, "keywords": [], "author": "", @@ -12,11 +15,13 @@ "devDependencies": { "@nomicfoundation/hardhat-toolbox": "^5.0.0", "@nomicfoundation/hardhat-verify": "^2.1.3", + "@openzeppelin/hardhat-upgrades": "^3.9.1", "hardhat": "^2.23.0", "hardhat-contract-sizer": "^2.10.1" }, "dependencies": { "@openzeppelin/contracts": "^5.3.0", + "@openzeppelin/contracts-upgradeable": "^5.3.0", "chalk": "^4.1.2", "cli-table3": "^0.6.5", "dotenv": "^17.2.3", diff --git a/hardhat/scripts/deploy-platform.ts b/hardhat/scripts/deploy-platform.ts new file mode 100644 index 0000000..bca1915 --- /dev/null +++ b/hardhat/scripts/deploy-platform.ts @@ -0,0 +1,66 @@ +import hre, { ethers } from "hardhat"; + +import { + deployTransparentProxy, + getProxyAdminAddress, + savePlatformAddresses, +} from "../deploy/helpers"; + +async function main() { + const [deployer] = await ethers.getSigners(); + + console.log("Network:", hre.network.name); + console.log("Deployer:", deployer.address); + + const DinToken = await ethers.getContractFactory("DinToken"); + const dinToken = await deployTransparentProxy(DinToken, []); + const dinTokenAddress = await dinToken.getAddress(); + console.log("DinToken:", dinTokenAddress); + + const DinCoordinator = await ethers.getContractFactory("DinCoordinator"); + const dinCoordinator = await deployTransparentProxy(DinCoordinator, [ + dinTokenAddress, + ]); + const dinCoordinatorAddress = await dinCoordinator.getAddress(); + console.log("DinCoordinator:", dinCoordinatorAddress); + + await (await dinToken.setCoordinator(dinCoordinatorAddress)).wait(); + console.log("DinToken coordinator wired"); + + const DinValidatorStake = await ethers.getContractFactory("DinValidatorStake"); + const dinValidatorStake = await deployTransparentProxy(DinValidatorStake, [ + dinTokenAddress, + dinCoordinatorAddress, + ]); + const dinValidatorStakeAddress = await dinValidatorStake.getAddress(); + console.log("DinValidatorStake:", dinValidatorStakeAddress); + + await ( + await dinCoordinator.updateValidatorStakeContract(dinValidatorStakeAddress) + ).wait(); + console.log("DinCoordinator stake contract wired"); + + const DINModelRegistry = await ethers.getContractFactory("DINModelRegistry"); + const dinModelRegistry = await deployTransparentProxy(DINModelRegistry, [ + dinValidatorStakeAddress, + ]); + const dinModelRegistryAddress = await dinModelRegistry.getAddress(); + console.log("DINModelRegistry:", dinModelRegistryAddress); + + const proxyAdmin = await getProxyAdminAddress(dinTokenAddress); + + savePlatformAddresses(hre.network.name, { + dinToken: dinTokenAddress, + dinCoordinator: dinCoordinatorAddress, + dinValidatorStake: dinValidatorStakeAddress, + dinModelRegistry: dinModelRegistryAddress, + proxyAdmin, + }); + + console.log(`Saved deployments/${hre.network.name}.json`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/hardhat/scripts/upgrade-platform.ts b/hardhat/scripts/upgrade-platform.ts new file mode 100644 index 0000000..b4f109a --- /dev/null +++ b/hardhat/scripts/upgrade-platform.ts @@ -0,0 +1,68 @@ +import hre, { ethers, upgrades } from "hardhat"; + +import { PLATFORM_CONTRACTS } from "../deploy/constants"; +import { + loadPlatformAddresses, + savePlatformAddresses, + upgradeTransparentProxy, +} from "../deploy/helpers"; + +type ContractProxyKey = "dinToken" | "dinCoordinator" | "dinValidatorStake" | "dinModelRegistry"; + +const PROXY_KEYS: Record<(typeof PLATFORM_CONTRACTS)[number], ContractProxyKey> = { + DinToken: "dinToken", + DinCoordinator: "dinCoordinator", + DinValidatorStake: "dinValidatorStake", + DINModelRegistry: "dinModelRegistry", +}; + +async function main() { + const contractName = process.env.CONTRACT; + if (!contractName) { + throw new Error( + `Set CONTRACT to one of: ${PLATFORM_CONTRACTS.join(", ")}` + ); + } + + if ( + !PLATFORM_CONTRACTS.includes( + contractName as (typeof PLATFORM_CONTRACTS)[number] + ) + ) { + throw new Error( + `Unknown CONTRACT "${contractName}". Expected one of: ${PLATFORM_CONTRACTS.join(", ")}` + ); + } + + const addresses = loadPlatformAddresses(hre.network.name); + const proxyKey = + PROXY_KEYS[contractName as (typeof PLATFORM_CONTRACTS)[number]]; + const proxyAddress = addresses[proxyKey]; + if (!proxyAddress) { + throw new Error(`No proxy address for ${contractName} in deployment file`); + } + + console.log("Network:", hre.network.name); + console.log("Upgrading:", contractName); + console.log("Proxy:", proxyAddress); + + const factory = await ethers.getContractFactory(contractName); + await upgradeTransparentProxy(proxyAddress, factory); + + const implementationAddress = + await upgrades.erc1967.getImplementationAddress(proxyAddress); + + console.log("Proxy:", proxyAddress); + console.log("Implementation:", implementationAddress); + + addresses.implementations = { + ...addresses.implementations, + [proxyKey]: implementationAddress, + }; + savePlatformAddresses(hre.network.name, addresses); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/hardhat/test/DINModelRegistry.upgrade.test.ts b/hardhat/test/DINModelRegistry.upgrade.test.ts new file mode 100644 index 0000000..2888ef1 --- /dev/null +++ b/hardhat/test/DINModelRegistry.upgrade.test.ts @@ -0,0 +1,126 @@ +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; + +import { PROXY_KIND } from "../deploy/constants"; +import { upgradeTransparentProxy } from "../deploy/helpers"; +import { deployPlatform } from "./helpers/platform"; + +describe("DINModelRegistry upgrade", function () { + it("preserves fee settings and model state across upgrade", async function () { + const { deployer, user, dinCoordinator, dinModelRegistry } = + await deployPlatform(); + const proxyAddress = await dinModelRegistry.getAddress(); + + const newProprietaryFee = 42n; + await dinModelRegistry.connect(deployer).setProprietaryFee(newProprietaryFee); + + const MockSlasher = await ethers.getContractFactory("MockSlasher"); + const coordinatorSlasher = await MockSlasher.deploy(user.address); + const auditorSlasher = await MockSlasher.deploy(user.address); + await coordinatorSlasher.waitForDeployment(); + await auditorSlasher.waitForDeployment(); + + const coordinatorSlasherAddress = await coordinatorSlasher.getAddress(); + const auditorSlasherAddress = await auditorSlasher.getAddress(); + + await dinCoordinator.addSlasherContract(coordinatorSlasherAddress); + await dinCoordinator.addSlasherContract(auditorSlasherAddress); + + const manifestCID = ethers.id("manifest-v1"); + const fee = await dinModelRegistry.proprietaryFee(); + await dinModelRegistry.connect(user).requestModelRegistration( + manifestCID, + coordinatorSlasherAddress, + auditorSlasherAddress, + false, + { value: fee } + ); + await dinModelRegistry.connect(deployer).approveModel(0); + + const DINModelRegistryV2 = await ethers.getContractFactory( + "DINModelRegistryV2" + ); + await upgrades.validateUpgrade(proxyAddress, DINModelRegistryV2, { + kind: PROXY_KIND, + }); + const upgraded = await upgradeTransparentProxy( + proxyAddress, + DINModelRegistryV2 + ); + + expect(await upgraded.proprietaryFee()).to.equal(newProprietaryFee); + expect(await upgraded.totalModels()).to.equal(1); + expect(await upgraded.version()).to.equal(2); + + const model = await upgraded.getModel(0); + expect(model.owner).to.equal(user.address); + expect(model.manifestCID).to.equal(manifestCID); + expect(model.taskCoordinator).to.equal(coordinatorSlasherAddress); + expect(model.taskAuditor).to.equal(auditorSlasherAddress); + }); + + it("keeps owner-only governance restricted after upgrade", async function () { + const { other, dinModelRegistry } = await deployPlatform(); + const proxyAddress = await dinModelRegistry.getAddress(); + + const DINModelRegistryV2 = await ethers.getContractFactory( + "DINModelRegistryV2" + ); + await upgradeTransparentProxy(proxyAddress, DINModelRegistryV2); + + await expect( + dinModelRegistry.connect(other).setProprietaryFee(1) + ).to.be.revertedWithCustomError( + dinModelRegistry, + "OwnableUnauthorizedAccount" + ); + }); + + it("keeps pending requests readable after upgrade", async function () { + const { deployer, user, dinCoordinator, dinModelRegistry } = + await deployPlatform(); + const proxyAddress = await dinModelRegistry.getAddress(); + + const MockSlasher = await ethers.getContractFactory("MockSlasher"); + const coordinatorSlasher = await MockSlasher.deploy(user.address); + const auditorSlasher = await MockSlasher.deploy(user.address); + await coordinatorSlasher.waitForDeployment(); + await auditorSlasher.waitForDeployment(); + + await dinCoordinator.addSlasherContract(await coordinatorSlasher.getAddress()); + await dinCoordinator.addSlasherContract(await auditorSlasher.getAddress()); + + const fee = await dinModelRegistry.openSourceFee(); + await dinModelRegistry.connect(user).requestModelRegistration( + ethers.id("manifest-pending"), + await coordinatorSlasher.getAddress(), + await auditorSlasher.getAddress(), + true, + { value: fee } + ); + + const DINModelRegistryV2 = await ethers.getContractFactory( + "DINModelRegistryV2" + ); + await upgradeTransparentProxy(proxyAddress, DINModelRegistryV2); + + expect(await dinModelRegistry.totalModelRequests()).to.equal(1); + + const request = await dinModelRegistry.modelRequests(0); + expect(request.requester).to.equal(user.address); + expect(request.processed).to.equal(false); + + await dinModelRegistry.connect(deployer).approveModel(0); + expect(await dinModelRegistry.totalModels()).to.equal(1); + }); + + it("implementation contract cannot be initialized directly", async function () { + const DINModelRegistry = await ethers.getContractFactory("DINModelRegistry"); + const impl = await DINModelRegistry.deploy(); + await impl.waitForDeployment(); + const [signer] = await ethers.getSigners(); + await expect( + impl.initialize(signer.address) + ).to.be.revertedWithCustomError(impl, "InvalidInitialization"); + }); +}); diff --git a/hardhat/test/DinCoordinator.upgrade.test.ts b/hardhat/test/DinCoordinator.upgrade.test.ts new file mode 100644 index 0000000..52d4ea8 --- /dev/null +++ b/hardhat/test/DinCoordinator.upgrade.test.ts @@ -0,0 +1,81 @@ +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; + +import { PROXY_KIND } from "../deploy/constants"; +import { upgradeTransparentProxy } from "../deploy/helpers"; +import { deployPlatform, mintDinViaDeposit } from "./helpers/platform"; + +describe("DinCoordinator upgrade", function () { + it("preserves exchange rate and token wiring across upgrade", async function () { + const { user, dinToken, dinCoordinator } = await deployPlatform(); + const proxyAddress = await dinCoordinator.getAddress(); + const tokenAddress = await dinToken.getAddress(); + const newRate = 2_000_000n * 10n ** 18n; + + await dinCoordinator.updateDinPerEth(newRate); + await mintDinViaDeposit(dinCoordinator, user, 10_000_000_000_000_000n); + const balanceBefore = await dinToken.balanceOf(user.address); + + const DinCoordinatorV2 = await ethers.getContractFactory("DinCoordinatorV2"); + await upgrades.validateUpgrade(proxyAddress, DinCoordinatorV2, { + kind: PROXY_KIND, + }); + const upgraded = await upgradeTransparentProxy( + proxyAddress, + DinCoordinatorV2 + ); + + expect(await upgraded.dinToken()).to.equal(tokenAddress); + expect(await upgraded.dinPerEth()).to.equal(newRate); + expect(await upgraded.version()).to.equal(2); + expect(await dinToken.balanceOf(user.address)).to.equal(balanceBefore); + }); + + it("keeps owner-only functions restricted after upgrade", async function () { + const { other, dinCoordinator } = await deployPlatform(); + const proxyAddress = await dinCoordinator.getAddress(); + + const DinCoordinatorV2 = await ethers.getContractFactory("DinCoordinatorV2"); + await upgradeTransparentProxy(proxyAddress, DinCoordinatorV2); + + await expect( + dinCoordinator.connect(other).updateDinPerEth(1) + ).to.be.revertedWithCustomError( + dinCoordinator, + "OwnableUnauthorizedAccount" + ); + }); + + it("keeps slasher management wired to stake contract after upgrade", async function () { + const { deployer, dinCoordinator, dinValidatorStake } = + await deployPlatform(); + const proxyAddress = await dinCoordinator.getAddress(); + + const MockSlasher = await ethers.getContractFactory("MockSlasher"); + const slasher = await MockSlasher.deploy(deployer.address); + await slasher.waitForDeployment(); + + await dinCoordinator.addSlasherContract(await slasher.getAddress()); + expect( + await dinValidatorStake.isSlasherContract(await slasher.getAddress()) + ).to.equal(true); + + const DinCoordinatorV2 = await ethers.getContractFactory("DinCoordinatorV2"); + await upgradeTransparentProxy(proxyAddress, DinCoordinatorV2); + + expect( + await dinValidatorStake.isSlasherContract(await slasher.getAddress()) + ).to.equal(true); + }); + + it("implementation contract cannot be initialized directly", async function () { + const DinCoordinator = await ethers.getContractFactory("DinCoordinator"); + const impl = await DinCoordinator.deploy(); + await impl.waitForDeployment(); + const [signer] = await ethers.getSigners(); + await expect(impl.initialize(signer.address)).to.be.revertedWithCustomError( + impl, + "InvalidInitialization" + ); + }); +}); diff --git a/hardhat/test/DinToken.upgrade.test.ts b/hardhat/test/DinToken.upgrade.test.ts new file mode 100644 index 0000000..8f2acab --- /dev/null +++ b/hardhat/test/DinToken.upgrade.test.ts @@ -0,0 +1,64 @@ +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; + +import { PROXY_KIND } from "../deploy/constants"; +import { upgradeTransparentProxy } from "../deploy/helpers"; +import { deployPlatform, mintDinViaDeposit } from "./helpers/platform"; + +describe("DinToken upgrade", function () { + it("preserves balances and coordinator wiring across upgrade", async function () { + const { deployer, user, dinToken, dinCoordinator } = await deployPlatform(); + const proxyAddress = await dinToken.getAddress(); + const coordinatorAddress = await dinCoordinator.getAddress(); + + await mintDinViaDeposit(dinCoordinator, user, 10_000_000_000_000_000n); + const balanceBefore = await dinToken.balanceOf(user.address); + + const DinTokenV2 = await ethers.getContractFactory("DinTokenV2"); + await upgrades.validateUpgrade(proxyAddress, DinTokenV2, { + kind: PROXY_KIND, + }); + const upgraded = await upgradeTransparentProxy(proxyAddress, DinTokenV2); + + expect(await upgraded.coordinator()).to.equal(coordinatorAddress); + expect(await upgraded.balanceOf(user.address)).to.equal(balanceBefore); + expect(await upgraded.version()).to.equal(2); + + await mintDinViaDeposit(dinCoordinator, user, 1_000_000_000_000_000n); + expect(await upgraded.balanceOf(user.address)).to.be.gt(balanceBefore); + }); + + it("keeps mint restricted to coordinator after upgrade", async function () { + const { user, dinToken } = await deployPlatform(); + const proxyAddress = await dinToken.getAddress(); + + const DinTokenV2 = await ethers.getContractFactory("DinTokenV2"); + await upgradeTransparentProxy(proxyAddress, DinTokenV2); + + await expect( + dinToken.connect(user).mint(user.address, 1) + ).to.be.revertedWithCustomError(dinToken, "Unauthorized"); + }); + + it("keeps coordinator setup restricted to owner after upgrade", async function () { + const { other, dinToken, dinCoordinator } = await deployPlatform(); + const proxyAddress = await dinToken.getAddress(); + + const DinTokenV2 = await ethers.getContractFactory("DinTokenV2"); + await upgradeTransparentProxy(proxyAddress, DinTokenV2); + + await expect( + dinToken.connect(other).setCoordinator(await dinCoordinator.getAddress()) + ).to.be.revertedWithCustomError(dinToken, "OwnableUnauthorizedAccount"); + }); + + it("implementation contract cannot be initialized directly", async function () { + const DinToken = await ethers.getContractFactory("DinToken"); + const impl = await DinToken.deploy(); + await impl.waitForDeployment(); + await expect(impl.initialize()).to.be.revertedWithCustomError( + impl, + "InvalidInitialization" + ); + }); +}); diff --git a/hardhat/test/DinValidatorStake.test.ts b/hardhat/test/DinValidatorStake.test.ts new file mode 100644 index 0000000..d58db20 --- /dev/null +++ b/hardhat/test/DinValidatorStake.test.ts @@ -0,0 +1,231 @@ +import { setBalance } from "@nomicfoundation/hardhat-network-helpers"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { expect } from "chai"; +import { ethers } from "hardhat"; + +import { deployPlatform, mintDinViaDeposit } from "./helpers/platform"; + +const MIN_STAKE = 10n * 10n ** 18n; +const UNBONDING_PERIOD = 7 * 24 * 60 * 60; // 7 days in seconds +const STAKE_DEPOSIT_ETH = 10_000_000_000_000_000n; // 0.01 ETH → ~10,000 DIN at default rate + +async function setup() { + const platform = await deployPlatform(); + const { deployer, user, dinToken, dinCoordinator, dinValidatorStake } = + platform; + const stakeAddr = await dinValidatorStake.getAddress(); + + const MockSlasher = await ethers.getContractFactory("MockSlasher"); + const mockSlasher = await MockSlasher.deploy(deployer.address); + await mockSlasher.waitForDeployment(); + const slasherAddr = await mockSlasher.getAddress(); + await dinCoordinator.addSlasherContract(slasherAddr); + + await mintDinViaDeposit(dinCoordinator, user, STAKE_DEPOSIT_ETH); + await dinToken.connect(user).approve(stakeAddr, ethers.MaxUint256); + + return { ...platform, slasherAddr, stakeAddr }; +} + +describe("DinValidatorStake", function () { + describe("staking", function () { + it("accepts MIN_STAKE and marks validator Active", async function () { + const { user, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + expect(await dinValidatorStake.getStake(user.address)).to.equal(MIN_STAKE); + expect(await dinValidatorStake.isValidatorActive(user.address)).to.equal( + true + ); + }); + + it("reverts when stake is below MIN_STAKE", async function () { + const { user, dinValidatorStake } = await setup(); + await expect( + dinValidatorStake.connect(user).stake(MIN_STAKE - 1n) + ).to.be.revertedWithCustomError( + dinValidatorStake, + "AmountLessThanMinStake" + ); + }); + + it("reverts when validator is blacklisted", async function () { + const { deployer, user, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(deployer).blacklistValidator(user.address); + await expect( + dinValidatorStake.connect(user).stake(MIN_STAKE) + ).to.be.revertedWithCustomError(dinValidatorStake, "ValidatorIsBlacklisted"); + }); + }); + + describe("unstake and unbonding window", function () { + it("moves stake to pending and sets withdrawAvailableAt", async function () { + const { user, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + await dinValidatorStake.connect(user).unstake(MIN_STAKE); + + const info = await dinValidatorStake.validators(user.address); + expect(info.activeStake).to.equal(0n); + expect(info.pendingWithdrawals).to.equal(MIN_STAKE); + expect(info.withdrawAvailableAt).to.be.gt(0n); + }); + + it("reverts claimUnstaked before unbonding period elapses", async function () { + const { user, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + await dinValidatorStake.connect(user).unstake(MIN_STAKE); + await expect( + dinValidatorStake.connect(user).claimUnstaked() + ).to.be.revertedWithCustomError(dinValidatorStake, "WithdrawalNotReady"); + }); + + it("allows claimUnstaked after unbonding period and returns tokens", async function () { + const { user, dinToken, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + const balanceBefore = await dinToken.balanceOf(user.address); + + await dinValidatorStake.connect(user).unstake(MIN_STAKE); + await time.increase(UNBONDING_PERIOD + 1); + await dinValidatorStake.connect(user).claimUnstaked(); + + expect(await dinToken.balanceOf(user.address)).to.equal( + balanceBefore + MIN_STAKE + ); + expect(await dinValidatorStake.getStake(user.address)).to.equal(0n); + }); + + it("reverts second unstake when a pending withdrawal exists", async function () { + const { user, dinToken, dinCoordinator, dinValidatorStake, stakeAddr } = + await setup(); + await mintDinViaDeposit(dinCoordinator, user, STAKE_DEPOSIT_ETH); + await dinToken.connect(user).approve(stakeAddr, ethers.MaxUint256); + + await dinValidatorStake.connect(user).stake(MIN_STAKE * 2n); + await dinValidatorStake.connect(user).unstake(MIN_STAKE); + await expect( + dinValidatorStake.connect(user).unstake(MIN_STAKE) + ).to.be.revertedWithCustomError( + dinValidatorStake, + "PendingWithdrawalExists" + ); + }); + }); + + describe("slashing", function () { + it("slashes active stake via an authorized slasher", async function () { + const { user, dinValidatorStake, slasherAddr } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + + await setBalance(slasherAddr, ethers.parseEther("1")); + const slasherSigner = await ethers.getImpersonatedSigner(slasherAddr); + + const slashAmount = MIN_STAKE / 2n; + await dinValidatorStake + .connect(slasherSigner) + .slash(user.address, slashAmount, ethers.id("test-reason")); + + expect(await dinValidatorStake.getStake(user.address)).to.equal( + MIN_STAKE - slashAmount + ); + }); + + it("slashes pending withdrawals when active stake is exhausted", async function () { + const { user, dinValidatorStake, slasherAddr } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + await dinValidatorStake.connect(user).unstake(MIN_STAKE); + + await setBalance(slasherAddr, ethers.parseEther("1")); + const slasherSigner = await ethers.getImpersonatedSigner(slasherAddr); + + await dinValidatorStake + .connect(slasherSigner) + .slash(user.address, MIN_STAKE, ethers.id("test-reason")); + + const info = await dinValidatorStake.validators(user.address); + expect(info.activeStake).to.equal(0n); + expect(info.pendingWithdrawals).to.equal(0n); + }); + + it("reverts slash from an unauthorized address", async function () { + const { user, other, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + await expect( + dinValidatorStake + .connect(other) + .slash(user.address, MIN_STAKE, ethers.id("test")) + ).to.be.revertedWithCustomError(dinValidatorStake, "NotSlasherContract"); + }); + }); + + describe("blacklisting", function () { + it("owner can blacklist a validator and block further activity", async function () { + const { deployer, user, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + + await dinValidatorStake.connect(deployer).blacklistValidator(user.address); + expect(await dinValidatorStake.isValidatorActive(user.address)).to.equal( + false + ); + await expect( + dinValidatorStake.connect(user).unstake(MIN_STAKE) + ).to.be.revertedWithCustomError(dinValidatorStake, "ValidatorIsBlacklisted"); + }); + + it("owner can unblacklist and restore Active status", async function () { + const { deployer, user, dinValidatorStake } = await setup(); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + await dinValidatorStake.connect(deployer).blacklistValidator(user.address); + + await dinValidatorStake + .connect(deployer) + .unblacklistValidator(user.address); + expect(await dinValidatorStake.isValidatorActive(user.address)).to.equal( + true + ); + }); + + it("non-owner cannot blacklist", async function () { + const { user, other, dinValidatorStake } = await setup(); + await expect( + dinValidatorStake.connect(other).blacklistValidator(user.address) + ).to.be.revertedWithCustomError( + dinValidatorStake, + "OwnableUnauthorizedAccount" + ); + }); + + it("reverts unblacklist on a validator that is not blacklisted", async function () { + const { deployer, user, dinValidatorStake } = await setup(); + await expect( + dinValidatorStake.connect(deployer).unblacklistValidator(user.address) + ).to.be.revertedWithCustomError( + dinValidatorStake, + "ValidatorNotBlacklisted" + ); + }); + }); + + describe("slasher contract management", function () { + it("only DIN_COORDINATOR can register slasher contracts", async function () { + const { other, dinValidatorStake } = await setup(); + await expect( + dinValidatorStake.connect(other).addSlasherContract(other.address) + ).to.be.revertedWithCustomError(dinValidatorStake, "NotDINCoordinator"); + }); + + it("reverts registering the same slasher twice", async function () { + const { deployer, dinCoordinator, dinValidatorStake } = await setup(); + const MockSlasher = await ethers.getContractFactory("MockSlasher"); + const extra = await MockSlasher.deploy(deployer.address); + await extra.waitForDeployment(); + const addr = await extra.getAddress(); + + await dinCoordinator.addSlasherContract(addr); + await expect( + dinCoordinator.addSlasherContract(addr) + ).to.be.revertedWithCustomError( + dinValidatorStake, + "SlasherContractAlreadyAdded" + ); + }); + }); +}); diff --git a/hardhat/test/DinValidatorStake.upgrade.test.ts b/hardhat/test/DinValidatorStake.upgrade.test.ts new file mode 100644 index 0000000..bb366ca --- /dev/null +++ b/hardhat/test/DinValidatorStake.upgrade.test.ts @@ -0,0 +1,96 @@ +import { expect } from "chai"; +import { ethers, upgrades } from "hardhat"; + +import { PROXY_KIND } from "../deploy/constants"; +import { upgradeTransparentProxy } from "../deploy/helpers"; +import { deployPlatform, mintDinViaDeposit } from "./helpers/platform"; + +const MIN_STAKE = 10n * 10n ** 18n; +const STAKE_DEPOSIT_ETH = 10_000_000_000_000_000n; + +describe("DinValidatorStake upgrade", function () { + it("preserves stake balances across upgrade", async function () { + const { user, dinToken, dinCoordinator, dinValidatorStake } = + await deployPlatform(); + const proxyAddress = await dinValidatorStake.getAddress(); + + await mintDinViaDeposit(dinCoordinator, user, STAKE_DEPOSIT_ETH); + await dinToken + .connect(user) + .approve(proxyAddress, MIN_STAKE); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + + const stakeBefore = await dinValidatorStake.getStake(user.address); + + const DinValidatorStakeV2 = await ethers.getContractFactory( + "DinValidatorStakeV2" + ); + await upgrades.validateUpgrade(proxyAddress, DinValidatorStakeV2, { + kind: PROXY_KIND, + }); + const upgraded = await upgradeTransparentProxy( + proxyAddress, + DinValidatorStakeV2 + ); + + expect(await upgraded.getStake(user.address)).to.equal(stakeBefore); + expect(await upgraded.isValidatorActive(user.address)).to.equal(true); + expect(await upgraded.version()).to.equal(2); + }); + + it("keeps slasher registration restricted to coordinator after upgrade", async function () { + const { deployer, other, dinValidatorStake } = await deployPlatform(); + const proxyAddress = await dinValidatorStake.getAddress(); + + const MockSlasher = await ethers.getContractFactory("MockSlasher"); + const slasher = await MockSlasher.deploy(deployer.address); + await slasher.waitForDeployment(); + const slasherAddress = await slasher.getAddress(); + + const DinValidatorStakeV2 = await ethers.getContractFactory( + "DinValidatorStakeV2" + ); + await upgradeTransparentProxy(proxyAddress, DinValidatorStakeV2); + + await expect( + dinValidatorStake.connect(other).addSlasherContract(slasherAddress) + ).to.be.revertedWithCustomError(dinValidatorStake, "NotDINCoordinator"); + }); + + it("keeps blacklist control with owner after upgrade", async function () { + const { deployer, other, user, dinToken, dinCoordinator, dinValidatorStake } = + await deployPlatform(); + const proxyAddress = await dinValidatorStake.getAddress(); + + await mintDinViaDeposit(dinCoordinator, user, STAKE_DEPOSIT_ETH); + await dinToken.connect(user).approve(proxyAddress, MIN_STAKE); + await dinValidatorStake.connect(user).stake(MIN_STAKE); + + const DinValidatorStakeV2 = await ethers.getContractFactory( + "DinValidatorStakeV2" + ); + await upgradeTransparentProxy(proxyAddress, DinValidatorStakeV2); + + await dinValidatorStake.connect(deployer).blacklistValidator(user.address); + expect(await dinValidatorStake.isValidatorActive(user.address)).to.equal( + false + ); + + await expect( + dinValidatorStake.connect(other).unblacklistValidator(user.address) + ).to.be.revertedWithCustomError( + dinValidatorStake, + "OwnableUnauthorizedAccount" + ); + }); + + it("implementation contract cannot be initialized directly", async function () { + const DinValidatorStake = await ethers.getContractFactory("DinValidatorStake"); + const impl = await DinValidatorStake.deploy(); + await impl.waitForDeployment(); + const [s1, s2] = await ethers.getSigners(); + await expect( + impl.initialize(s1.address, s2.address) + ).to.be.revertedWithCustomError(impl, "InvalidInitialization"); + }); +}); diff --git a/hardhat/test/helpers/platform.ts b/hardhat/test/helpers/platform.ts new file mode 100644 index 0000000..6bec152 --- /dev/null +++ b/hardhat/test/helpers/platform.ts @@ -0,0 +1,64 @@ +import { Contract } from "ethers"; +import { ethers } from "hardhat"; + +import { deployTransparentProxy } from "../../deploy/helpers"; + +export interface PlatformFixture { + deployer: Awaited>[0]; + user: Awaited>[0]; + other: Awaited>[0]; + dinToken: Contract; + dinCoordinator: Contract; + dinValidatorStake: Contract; + dinModelRegistry: Contract; +} + +export async function deployPlatform(): Promise { + const [deployer, user, other] = await ethers.getSigners(); + + const DinToken = await ethers.getContractFactory("DinToken"); + const dinToken = await deployTransparentProxy(DinToken, []); + const dinTokenAddress = await dinToken.getAddress(); + + const DinCoordinator = await ethers.getContractFactory("DinCoordinator"); + const dinCoordinator = await deployTransparentProxy(DinCoordinator, [ + dinTokenAddress, + ]); + const dinCoordinatorAddress = await dinCoordinator.getAddress(); + + await (await dinToken.setCoordinator(dinCoordinatorAddress)).wait(); + + const DinValidatorStake = await ethers.getContractFactory("DinValidatorStake"); + const dinValidatorStake = await deployTransparentProxy(DinValidatorStake, [ + dinTokenAddress, + dinCoordinatorAddress, + ]); + const dinValidatorStakeAddress = await dinValidatorStake.getAddress(); + + await ( + await dinCoordinator.updateValidatorStakeContract(dinValidatorStakeAddress) + ).wait(); + + const DINModelRegistry = await ethers.getContractFactory("DINModelRegistry"); + const dinModelRegistry = await deployTransparentProxy(DINModelRegistry, [ + dinValidatorStakeAddress, + ]); + + return { + deployer, + user, + other, + dinToken, + dinCoordinator, + dinValidatorStake, + dinModelRegistry, + }; +} + +export async function mintDinViaDeposit( + dinCoordinator: Contract, + user: Awaited>[0], + ethAmount: bigint +) { + await dinCoordinator.connect(user).depositAndMint({ value: ethAmount }); +}