From a10cf39dbff9d176f0e28caa86e2fef3ebdb8651 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Thu, 20 Mar 2025 16:01:16 -0700 Subject: [PATCH 001/114] add .gitattributes file --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto From e5c6bb6771dc1ad1751224705923a35e882e4b70 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Thu, 20 Mar 2025 16:02:12 -0700 Subject: [PATCH 002/114] renormalize line endings in the repo --- hardhat.config.ts | 310 +-- package.json | 166 +- src/deploy/db/mongo-adapter/get-adapter.ts | 132 +- src/deploy/run-campaign.ts | 74 +- test/ZNSAddressResolver.test.ts | 526 ++-- test/ZNSRootRegistrar.test.ts | 2728 ++++++++++---------- test/helpers/deploy-helpers.ts | 350 +-- 7 files changed, 2143 insertions(+), 2143 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 3259b20ae..d33955b2f 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,155 +1,155 @@ -/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-unused-vars */ - -import { mochaGlobalSetup, mochaGlobalTeardown } from "./test/mocha-global"; - -require("dotenv").config(); - -import * as tenderly from "@tenderly/hardhat-tenderly"; -import "@nomicfoundation/hardhat-toolbox"; -import "@nomicfoundation/hardhat-ethers"; -import "@nomicfoundation/hardhat-verify"; -import "@nomicfoundation/hardhat-toolbox/network-helpers"; -import "@nomicfoundation/hardhat-chai-matchers"; -import "@openzeppelin/hardhat-upgrades"; -import "solidity-coverage"; -import "solidity-docgen"; -import "hardhat-gas-reporter"; -import { HardhatUserConfig, subtask } from "hardhat/config"; -import { TASK_TEST_RUN_MOCHA_TESTS } from "hardhat/builtin-tasks/task-names"; - - -subtask(TASK_TEST_RUN_MOCHA_TESTS) - .setAction(async (args, hre, runSuper) => { - await mochaGlobalSetup(); - const testFailures = await runSuper(args); - await mochaGlobalTeardown(); - - return testFailures; - }); - -// This call is needed to initialize Tenderly with Hardhat, -// the automatic verifications, though, don't seem to work, -// needing us to verify explicitly in code, however, -// for Tenderly to work properly with Hardhat this method -// needs to be called. The call below is commented out -// because if we leave it here, solidity-coverage -// does not work properly locally or in CI, so we -// keep it commented out and uncomment when using DevNet -// locally. -// !!! Uncomment this when using Tenderly !!! -tenderly.setup({ automaticVerifications: false }); - -const config : HardhatUserConfig = { - solidity: { - compilers: [ - { - version: "0.8.18", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - { - version: "0.8.3", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - ], - overrides: { - "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol": { - version: "0.8.9", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol": { - version: "0.8.9", - settings: { - optimizer: { - enabled: true, - runs: 200, - }, - }, - }, - }, - }, - paths: { - sources: "./contracts", - tests: "./test", - cache: "./cache", - artifacts: "./artifacts", - }, - typechain: { - outDir: "typechain", - }, - mocha: { - timeout: 5000000, - }, - gasReporter: { - enabled: false, - }, - networks: { - mainnet: { - url: `${process.env.MAINNET_RPC_URL}`, - gasPrice: 80000000000, - }, - sepolia: { - url: `${process.env.SEPOLIA_RPC_URL}`, - timeout: 10000000, - // accounts: [ // Comment out for CI, uncomment this when using Sepolia - // `${process.env.TESTNET_PRIVATE_KEY_A}`, - // `${process.env.TESTNET_PRIVATE_KEY_B}`, - // `${process.env.TESTNET_PRIVATE_KEY_C}`, - // `${process.env.TESTNET_PRIVATE_KEY_D}`, - // `${process.env.TESTNET_PRIVATE_KEY_E}`, - // `${process.env.TESTNET_PRIVATE_KEY_F}`, - // ], - // // Must have to avoid instead failing as `invalid length for result data` error - // throwOnCallFailures: false, // not sure if this even works - }, - devnet: { - // Add current URL that you spawned if not using automated spawning - url: `${process.env.DEVNET_RPC_URL}`, - chainId: 1, - }, - }, - defender: { - useDefenderDeploy: false, - apiKey: `${process.env.DEFENDER_KEY}`, - apiSecret: `${process.env.DEFENDER_SECRET}`, - }, - etherscan: { - apiKey: `${process.env.ETHERSCAN_API_KEY}`, - }, - sourcify: { - // If set to "true", will try to verify the contracts after deployment - enabled: false, - }, - tenderly: { - project: `${process.env.TENDERLY_PROJECT_SLUG}`, - username: `${process.env.TENDERLY_ACCOUNT_ID}`, - }, - docgen: { - pages: "files", - templates: "docs/docgen-templates", - outputDir: "docs/contracts", - exclude: [ - "upgrade-test-mocks/", - "upgradeMocks/", - "token/mocks/", - "utils/", - "oz-proxies/", - ], - }, -}; - -export default config; +/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-unused-vars */ + +import { mochaGlobalSetup, mochaGlobalTeardown } from "./test/mocha-global"; + +require("dotenv").config(); + +import * as tenderly from "@tenderly/hardhat-tenderly"; +import "@nomicfoundation/hardhat-toolbox"; +import "@nomicfoundation/hardhat-ethers"; +import "@nomicfoundation/hardhat-verify"; +import "@nomicfoundation/hardhat-toolbox/network-helpers"; +import "@nomicfoundation/hardhat-chai-matchers"; +import "@openzeppelin/hardhat-upgrades"; +import "solidity-coverage"; +import "solidity-docgen"; +import "hardhat-gas-reporter"; +import { HardhatUserConfig, subtask } from "hardhat/config"; +import { TASK_TEST_RUN_MOCHA_TESTS } from "hardhat/builtin-tasks/task-names"; + + +subtask(TASK_TEST_RUN_MOCHA_TESTS) + .setAction(async (args, hre, runSuper) => { + await mochaGlobalSetup(); + const testFailures = await runSuper(args); + await mochaGlobalTeardown(); + + return testFailures; + }); + +// This call is needed to initialize Tenderly with Hardhat, +// the automatic verifications, though, don't seem to work, +// needing us to verify explicitly in code, however, +// for Tenderly to work properly with Hardhat this method +// needs to be called. The call below is commented out +// because if we leave it here, solidity-coverage +// does not work properly locally or in CI, so we +// keep it commented out and uncomment when using DevNet +// locally. +// !!! Uncomment this when using Tenderly !!! +tenderly.setup({ automaticVerifications: false }); + +const config : HardhatUserConfig = { + solidity: { + compilers: [ + { + version: "0.8.18", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + { + version: "0.8.3", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + ], + overrides: { + "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol": { + version: "0.8.9", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol": { + version: "0.8.9", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + }, + }, + }, + }, + paths: { + sources: "./contracts", + tests: "./test", + cache: "./cache", + artifacts: "./artifacts", + }, + typechain: { + outDir: "typechain", + }, + mocha: { + timeout: 5000000, + }, + gasReporter: { + enabled: false, + }, + networks: { + mainnet: { + url: `${process.env.MAINNET_RPC_URL}`, + gasPrice: 80000000000, + }, + sepolia: { + url: `${process.env.SEPOLIA_RPC_URL}`, + timeout: 10000000, + // accounts: [ // Comment out for CI, uncomment this when using Sepolia + // `${process.env.TESTNET_PRIVATE_KEY_A}`, + // `${process.env.TESTNET_PRIVATE_KEY_B}`, + // `${process.env.TESTNET_PRIVATE_KEY_C}`, + // `${process.env.TESTNET_PRIVATE_KEY_D}`, + // `${process.env.TESTNET_PRIVATE_KEY_E}`, + // `${process.env.TESTNET_PRIVATE_KEY_F}`, + // ], + // // Must have to avoid instead failing as `invalid length for result data` error + // throwOnCallFailures: false, // not sure if this even works + }, + devnet: { + // Add current URL that you spawned if not using automated spawning + url: `${process.env.DEVNET_RPC_URL}`, + chainId: 1, + }, + }, + defender: { + useDefenderDeploy: false, + apiKey: `${process.env.DEFENDER_KEY}`, + apiSecret: `${process.env.DEFENDER_SECRET}`, + }, + etherscan: { + apiKey: `${process.env.ETHERSCAN_API_KEY}`, + }, + sourcify: { + // If set to "true", will try to verify the contracts after deployment + enabled: false, + }, + tenderly: { + project: `${process.env.TENDERLY_PROJECT_SLUG}`, + username: `${process.env.TENDERLY_ACCOUNT_ID}`, + }, + docgen: { + pages: "files", + templates: "docs/docgen-templates", + outputDir: "docs/contracts", + exclude: [ + "upgrade-test-mocks/", + "upgradeMocks/", + "token/mocks/", + "utils/", + "oz-proxies/", + ], + }, +}; + +export default config; diff --git a/package.json b/package.json index 0bdc0ac49..b3f796e0f 100644 --- a/package.json +++ b/package.json @@ -1,83 +1,83 @@ -{ - "name": "@zero-tech/zns-contracts", - "version": "1.0.0", - "description": "Zero Name Service Smart Contracts", - "author": "Zero CPT", - "license": "ISC", - "repository": "https://github.com/zer0-os/zNS.git", - "engines": { - "node": ">=18", - "npm": ">=9" - }, - "scripts": { - "compile": "hardhat compile", - "lint-sol": "yarn solhint ./contracts/**/*.sol", - "lint-ts": "yarn eslint ./test/** ./src/**", - "lint": "yarn lint-sol & yarn lint-ts --no-error-on-unmatched-pattern", - "clean": "hardhat clean", - "build": "yarn run clean && yarn run compile", - "postbuild": "yarn save-tag", - "typechain": "hardhat typechain", - "pretest": "yarn mongo:start", - "test": "hardhat test", - "test-local": "yarn test", - "posttest": "yarn mongo:stop", - "semantic-release": "semantic-release --tag-format='v${version}-dev'", - "coverage": "hardhat coverage", - "check-coverage": "istanbul check-coverage --statements 90 --branches 87 --functions 89 --lines 90", - "devnet": "ts-node src/tenderly/devnet/devnet-execute.ts", - "gas-cost": "ts-node src/utils/gas-costs.ts", - "docgen": "hardhat docgen", - "save-tag": "chmod a+x ./src/utils/git-tag/save-tag.sh && bash ./src/utils/git-tag/save-tag.sh", - "base64": "ts-node src/utils/convert-base64.ts", - "mongo:start": "docker-compose up -d", - "mongo:stop": "docker-compose stop", - "mongo:down": "docker-compose down", - "mongo:drop": "ts-node src/utils/drop-db.ts", - "run-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia" - }, - "pre-commit": [ - "lint" - ], - "devDependencies": { - "@ensdomains/ensjs": "2.1.0", - "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", - "@nomicfoundation/hardhat-ethers": "^3.0.5", - "@nomicfoundation/hardhat-network-helpers": "^1.0.9", - "@nomicfoundation/hardhat-toolbox": "^4.0.0", - "@nomicfoundation/hardhat-verify": "^2.0.0", - "@openzeppelin/contracts": "4.9.3", - "@openzeppelin/contracts-400": "npm:@openzeppelin/contracts@4.0.0", - "@openzeppelin/contracts-upgradeable": "4.9.3", - "@openzeppelin/contracts-upgradeable-400": "npm:@openzeppelin/contracts-upgradeable@4.0.0", - "@openzeppelin/defender-sdk": "^1.7.0", - "@openzeppelin/hardhat-upgrades": "2.5.0", - "@semantic-release/git": "^10.0.1", - "@tenderly/hardhat-tenderly": "^2.0.1", - "@typechain/ethers-v6": "^0.5.1", - "@typechain/hardhat": "^9.1.0", - "@types/chai": "^4.3.11", - "@types/mocha": "^9.1.0", - "@types/node": "^18.15.11", - "@zero-tech/eslint-config-cpt": "0.2.7", - "@zero-tech/ztoken": "2.0.0", - "chai": "^4.3.10", - "eslint": "^8.37.0", - "ethers": "^6.9.0", - "hardhat": "^2.19.1", - "hardhat-gas-reporter": "^1.0.9", - "semantic-release": "^21.0.1", - "solhint": "^4.0.0", - "solidity-coverage": "^0.8.5", - "solidity-docgen": "^0.6.0-beta.36", - "ts-node": "10.9.1", - "typechain": "^8.3.2", - "typescript": "^5.0.2" - }, - "dependencies": { - "axios": "^1.4.0", - "dotenv": "16.0.3", - "mongodb": "^6.1.0", - "winston": "^3.11.0" - } -} +{ + "name": "@zero-tech/zns-contracts", + "version": "1.0.0", + "description": "Zero Name Service Smart Contracts", + "author": "Zero CPT", + "license": "ISC", + "repository": "https://github.com/zer0-os/zNS.git", + "engines": { + "node": ">=18", + "npm": ">=9" + }, + "scripts": { + "compile": "hardhat compile", + "lint-sol": "yarn solhint ./contracts/**/*.sol", + "lint-ts": "yarn eslint ./test/** ./src/**", + "lint": "yarn lint-sol & yarn lint-ts --no-error-on-unmatched-pattern", + "clean": "hardhat clean", + "build": "yarn run clean && yarn run compile", + "postbuild": "yarn save-tag", + "typechain": "hardhat typechain", + "pretest": "yarn mongo:start", + "test": "hardhat test", + "test-local": "yarn test", + "posttest": "yarn mongo:stop", + "semantic-release": "semantic-release --tag-format='v${version}-dev'", + "coverage": "hardhat coverage", + "check-coverage": "istanbul check-coverage --statements 90 --branches 87 --functions 89 --lines 90", + "devnet": "ts-node src/tenderly/devnet/devnet-execute.ts", + "gas-cost": "ts-node src/utils/gas-costs.ts", + "docgen": "hardhat docgen", + "save-tag": "chmod a+x ./src/utils/git-tag/save-tag.sh && bash ./src/utils/git-tag/save-tag.sh", + "base64": "ts-node src/utils/convert-base64.ts", + "mongo:start": "docker-compose up -d", + "mongo:stop": "docker-compose stop", + "mongo:down": "docker-compose down", + "mongo:drop": "ts-node src/utils/drop-db.ts", + "run-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia" + }, + "pre-commit": [ + "lint" + ], + "devDependencies": { + "@ensdomains/ensjs": "2.1.0", + "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", + "@nomicfoundation/hardhat-ethers": "^3.0.5", + "@nomicfoundation/hardhat-network-helpers": "^1.0.9", + "@nomicfoundation/hardhat-toolbox": "^4.0.0", + "@nomicfoundation/hardhat-verify": "^2.0.0", + "@openzeppelin/contracts": "4.9.3", + "@openzeppelin/contracts-400": "npm:@openzeppelin/contracts@4.0.0", + "@openzeppelin/contracts-upgradeable": "4.9.3", + "@openzeppelin/contracts-upgradeable-400": "npm:@openzeppelin/contracts-upgradeable@4.0.0", + "@openzeppelin/defender-sdk": "^1.7.0", + "@openzeppelin/hardhat-upgrades": "2.5.0", + "@semantic-release/git": "^10.0.1", + "@tenderly/hardhat-tenderly": "^2.0.1", + "@typechain/ethers-v6": "^0.5.1", + "@typechain/hardhat": "^9.1.0", + "@types/chai": "^4.3.11", + "@types/mocha": "^9.1.0", + "@types/node": "^18.15.11", + "@zero-tech/eslint-config-cpt": "0.2.7", + "@zero-tech/ztoken": "2.0.0", + "chai": "^4.3.10", + "eslint": "^8.37.0", + "ethers": "^6.9.0", + "hardhat": "^2.19.1", + "hardhat-gas-reporter": "^1.0.9", + "semantic-release": "^21.0.1", + "solhint": "^4.0.0", + "solidity-coverage": "^0.8.5", + "solidity-docgen": "^0.6.0-beta.36", + "ts-node": "10.9.1", + "typechain": "^8.3.2", + "typescript": "^5.0.2" + }, + "dependencies": { + "axios": "^1.4.0", + "dotenv": "16.0.3", + "mongodb": "^6.1.0", + "winston": "^3.11.0" + } +} diff --git a/src/deploy/db/mongo-adapter/get-adapter.ts b/src/deploy/db/mongo-adapter/get-adapter.ts index ea571f105..8c97b12d1 100644 --- a/src/deploy/db/mongo-adapter/get-adapter.ts +++ b/src/deploy/db/mongo-adapter/get-adapter.ts @@ -1,66 +1,66 @@ -import { MongoDBAdapter } from "./mongo-adapter"; -import { getLogger } from "../../logger/create-logger"; -import { DEFAULT_MONGO_DB_NAME, DEFAULT_MONGO_URI } from "./constants"; -import { TLogger } from "../../campaign/types"; - -let mongoAdapter : MongoDBAdapter | null = null; - -export const resetMongoAdapter = () => { - mongoAdapter = null; -}; - - -export const getMongoAdapter = async (logger ?: TLogger) : Promise => { - const checkParams = { - dbUri: process.env.MONGO_DB_URI - ? process.env.MONGO_DB_URI - : DEFAULT_MONGO_URI, - dbName: process.env.MONGO_DB_NAME - ? process.env.MONGO_DB_NAME - : DEFAULT_MONGO_DB_NAME, - }; - - logger = !logger ? getLogger() : logger; - - const params = { - logger, - clientOpts: process.env.MONGO_DB_CLIENT_OPTS - ? JSON.parse(process.env.MONGO_DB_CLIENT_OPTS) - : undefined, - version: process.env.MONGO_DB_VERSION - ? process.env.MONGO_DB_VERSION - : undefined, - archive: process.env.ARCHIVE_PREVIOUS_DB_VERSION === "true", - }; - - let createNew = false; - if (mongoAdapter) { - Object.values(checkParams).forEach( - ([key, value]) => { - if (key === "version") key = "curVersion"; - - // if the existing adapter was created with different options than the currently needed one - // we create a new one and overwrite - if (JSON.stringify(mongoAdapter?.[key]) !== JSON.stringify(value)) { - createNew = true; - return; - } - } - ); - } else { - createNew = true; - } - - if (createNew) { - logger.debug("Creating new MongoDBAdapter instance"); - mongoAdapter = new MongoDBAdapter({ - ...checkParams, - ...params, - }); - await mongoAdapter.initialize(params.version); - } else { - logger.debug("Returning existing MongoDBAdapter instance"); - } - - return mongoAdapter as MongoDBAdapter; -}; +import { MongoDBAdapter } from "./mongo-adapter"; +import { getLogger } from "../../logger/create-logger"; +import { DEFAULT_MONGO_DB_NAME, DEFAULT_MONGO_URI } from "./constants"; +import { TLogger } from "../../campaign/types"; + +let mongoAdapter : MongoDBAdapter | null = null; + +export const resetMongoAdapter = () => { + mongoAdapter = null; +}; + + +export const getMongoAdapter = async (logger ?: TLogger) : Promise => { + const checkParams = { + dbUri: process.env.MONGO_DB_URI + ? process.env.MONGO_DB_URI + : DEFAULT_MONGO_URI, + dbName: process.env.MONGO_DB_NAME + ? process.env.MONGO_DB_NAME + : DEFAULT_MONGO_DB_NAME, + }; + + logger = !logger ? getLogger() : logger; + + const params = { + logger, + clientOpts: process.env.MONGO_DB_CLIENT_OPTS + ? JSON.parse(process.env.MONGO_DB_CLIENT_OPTS) + : undefined, + version: process.env.MONGO_DB_VERSION + ? process.env.MONGO_DB_VERSION + : undefined, + archive: process.env.ARCHIVE_PREVIOUS_DB_VERSION === "true", + }; + + let createNew = false; + if (mongoAdapter) { + Object.values(checkParams).forEach( + ([key, value]) => { + if (key === "version") key = "curVersion"; + + // if the existing adapter was created with different options than the currently needed one + // we create a new one and overwrite + if (JSON.stringify(mongoAdapter?.[key]) !== JSON.stringify(value)) { + createNew = true; + return; + } + } + ); + } else { + createNew = true; + } + + if (createNew) { + logger.debug("Creating new MongoDBAdapter instance"); + mongoAdapter = new MongoDBAdapter({ + ...checkParams, + ...params, + }); + await mongoAdapter.initialize(params.version); + } else { + logger.debug("Returning existing MongoDBAdapter instance"); + } + + return mongoAdapter as MongoDBAdapter; +}; diff --git a/src/deploy/run-campaign.ts b/src/deploy/run-campaign.ts index 78ff2e36d..119a0928f 100644 --- a/src/deploy/run-campaign.ts +++ b/src/deploy/run-campaign.ts @@ -1,37 +1,37 @@ -import { getConfig } from "./campaign/environments"; -import { runZnsCampaign } from "./zns-campaign"; -import { Defender } from "@openzeppelin/defender-sdk"; - -import { getLogger } from "./logger/create-logger"; - -const logger = getLogger(); - -const runCampaign = async () => { - const credentials = { - apiKey: process.env.DEFENDER_KEY, - apiSecret: process.env.DEFENDER_SECRET, - relayerApiKey: process.env.RELAYER_KEY, - relayerApiSecret: process.env.RELAYER_SECRET, - }; - - const client = new Defender(credentials); - - const provider = client.relaySigner.getProvider(); - const deployer = client.relaySigner.getSigner(provider, { speed: "fast" }); - - const config = await getConfig({ - deployer, - }); - - await runZnsCampaign({ - config, - provider, - }); -}; - -runCampaign().catch(error => { - logger.error(error.stack); - process.exit(1); -}).finally(() => { - process.exit(0); -}); +import { getConfig } from "./campaign/environments"; +import { runZnsCampaign } from "./zns-campaign"; +import { Defender } from "@openzeppelin/defender-sdk"; + +import { getLogger } from "./logger/create-logger"; + +const logger = getLogger(); + +const runCampaign = async () => { + const credentials = { + apiKey: process.env.DEFENDER_KEY, + apiSecret: process.env.DEFENDER_SECRET, + relayerApiKey: process.env.RELAYER_KEY, + relayerApiSecret: process.env.RELAYER_SECRET, + }; + + const client = new Defender(credentials); + + const provider = client.relaySigner.getProvider(); + const deployer = client.relaySigner.getSigner(provider, { speed: "fast" }); + + const config = await getConfig({ + deployer, + }); + + await runZnsCampaign({ + config, + provider, + }); +}; + +runCampaign().catch(error => { + logger.error(error.stack); + process.exit(1); +}).finally(() => { + process.exit(0); +}); diff --git a/test/ZNSAddressResolver.test.ts b/test/ZNSAddressResolver.test.ts index 33d64c3aa..af096f9e2 100644 --- a/test/ZNSAddressResolver.test.ts +++ b/test/ZNSAddressResolver.test.ts @@ -1,263 +1,263 @@ -import * as hre from "hardhat"; -import { - ERC165__factory, - ZNSAddressResolver, - ZNSAddressResolver__factory, - ZNSAddressResolverUpgradeMock__factory, -} from "../typechain"; -import { DeployZNSParams, IZNSContracts } from "./helpers/types"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { hashDomainLabel, hashSubdomainName } from "./helpers/hashing"; -import { - ADMIN_ROLE, - DEFAULT_RESOLVER_TYPE, - GOVERNOR_ROLE, - REGISTRAR_ROLE, - deployZNS, - getAccessRevertMsg, - validateUpgrade, INITIALIZED_ERR, -} from "./helpers"; -import { getProxyImplAddress } from "./helpers/utils"; - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const { expect } = require("chai"); - -describe("ZNSAddressResolver", () => { - let deployer : SignerWithAddress; - let mockRegistrar : SignerWithAddress; - let user : SignerWithAddress; - let operator : SignerWithAddress; - let wilderDomainHash : string; - - let zns : IZNSContracts; - - beforeEach(async () => { - [ - deployer, - operator, - user, - mockRegistrar, - ] = await hre.ethers.getSigners(); - - const params : DeployZNSParams = { - deployer, - governorAddresses: [deployer.address], - adminAddresses: [deployer.address], - }; - zns = await deployZNS(params); - - // Have to get this value for every test, but can be fixed - wilderDomainHash = hashSubdomainName("wilder"); - - await zns.accessController.connect(deployer).grantRole(REGISTRAR_ROLE, mockRegistrar.address); - - await zns.registry.connect(deployer).addResolverType(DEFAULT_RESOLVER_TYPE, await zns.addressResolver.getAddress()); - - await zns.registry.connect(mockRegistrar) - .createDomainRecord( - wilderDomainHash, - deployer.address, - DEFAULT_RESOLVER_TYPE - ); - }); - - it("Should NOT let initialize the implementation contract", async () => { - const factory = new ZNSAddressResolver__factory(deployer); - const impl = await getProxyImplAddress(await zns.addressResolver.getAddress()); - const implContract = factory.attach(impl) as ZNSAddressResolver; - - await expect( - implContract.initialize( - operator.address, - mockRegistrar.address, - ) - ).to.be.revertedWith(INITIALIZED_ERR); - }); - - it("Should get the AddressResolver", async () => { // Copy of registry tests - // The domain exists - const existResolver = await zns.registry.getDomainResolver(wilderDomainHash); - expect(existResolver).to.eq(await zns.addressResolver.getAddress()); - }); - - it("Returns 0 when the domain doesnt exist", async () => { - // The domain does not exist - const someDomainHash = hashDomainLabel("random-record"); - const notExistResolver = await zns.registry.getDomainResolver(someDomainHash); - expect(notExistResolver).to.eq(hre.ethers.ZeroAddress); - }); - - it("Should have registry address correctly set", async () => { - expect(await zns.addressResolver.registry()).to.equal(await zns.registry.getAddress()); - }); - - it("Should setRegistry() correctly with ADMIN_ROLE", async () => { - await expect( - zns.addressResolver.connect(deployer).setRegistry(operator.address) - ) - .to.emit(zns.addressResolver, "RegistrySet") - .withArgs(operator.address); - - expect(await zns.addressResolver.registry()).to.equal(operator.address); - }); - - it("Should revert when setRegistry() without ADMIN_ROLE", async () => { - await expect( - zns.addressResolver.connect(operator).setRegistry(operator.address) - ).to.be.revertedWith( - getAccessRevertMsg(operator.address, ADMIN_ROLE) - ); - }); - - it("Should setAccessController() correctly with ADMIN_ROLE", async () => { - expect(await zns.addressResolver.connect(deployer).setAccessController(operator.address)) - .to.emit(zns.addressResolver, "AccessControllerSet") - .withArgs(operator.address); - - expect(await zns.addressResolver.getAccessController()).to.equal(operator.address); - }); - - it("Should revert when setAccessController() without ADMIN_ROLE", async () => { - await expect( - zns.addressResolver.connect(operator).setAccessController(operator.address) - ).to.be.revertedWith( - getAccessRevertMsg(operator.address, ADMIN_ROLE) - ); - }); - - it("Should not allow non-owner address to setAddress", async () => { - await expect( - zns.addressResolver.connect(user).setAddress(wilderDomainHash, user.address) - ).to.be.revertedWith("ZNSAddressResolver: Not authorized for this domain"); - }); - - it("Should allow owner to setAddress and emit event", async () => { - await expect( - zns.addressResolver.connect(deployer) - .setAddress(wilderDomainHash, user.address) - ) - .to.emit(zns.addressResolver, "AddressSet") - .withArgs(wilderDomainHash, user.address); - - const resolvedAddress = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); - expect(resolvedAddress).to.equal(user.address); - }); - - it("Should allow operator to setAddress and emit event", async () => { - await zns.registry.connect(deployer).setOwnersOperator(operator.address, true); - - await expect( - zns.addressResolver.connect(operator) - .setAddress(wilderDomainHash, user.address) - ) - .to.emit(zns.addressResolver, "AddressSet") - .withArgs(wilderDomainHash, user.address); - }); - - it("Should allow REGISTRAR_ROLE to setAddress and emit event", async () => { - await zns.accessController.connect(deployer).grantRole(REGISTRAR_ROLE, mockRegistrar.address); - - await expect( - zns.addressResolver.connect(mockRegistrar) - .setAddress(wilderDomainHash, hre.ethers.ZeroAddress) - ) - .to.emit(zns.addressResolver, "AddressSet") - .withArgs(wilderDomainHash, hre.ethers.ZeroAddress); - - const address = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); - expect(address).to.eq(hre.ethers.ZeroAddress); - - }); - - it("Should resolve address correctly", async () => { - await zns.addressResolver.connect(deployer).setAddress(wilderDomainHash, user.address); - - const resolvedAddress = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); - expect(resolvedAddress).to.equal(user.address); - }); - - it("Should support the IZNSAddressResolver interface ID", async () => { - const interfaceId = await zns.addressResolver.getInterfaceId(); - const supported = await zns.addressResolver.supportsInterface(interfaceId); - expect(supported).to.be.true; - }); - - it("Should support the ERC-165 interface ID", async () => { - const erc165Interface = ERC165__factory.createInterface(); - - const fragment = erc165Interface.getFunction("supportsInterface"); - - const supported = await zns.addressResolver.supportsInterface(fragment.selector); - expect(supported).to.be.true; - }); - - it("Should not support other interface IDs", async () => { - const notSupported = await zns.addressResolver.supportsInterface("0xffffffff"); - expect(notSupported).to.be.false; - }); - - it("Should support full discovery flow from zns.registry", async () => { - await zns.addressResolver.connect(deployer).setAddress(wilderDomainHash, user.address); - - const resolverAddress = await zns.registry.getDomainResolver(wilderDomainHash); - expect(resolverAddress).to.eq(await zns.addressResolver.getAddress()); - - const resolvedAddress = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); - expect(resolvedAddress).to.eq(user.address); - }); - - describe("UUPS", () => { - it("Allows an authorized user to upgrade the contract", async () => { - // AddressResolver to upgrade to - const factory = new ZNSAddressResolverUpgradeMock__factory(deployer); - const newAddressResolver = await factory.deploy(); - await newAddressResolver.waitForDeployment(); - - // Confirm the deployer is a governor - expect( - await zns.accessController.hasRole(GOVERNOR_ROLE, deployer.address) - ).to.be.true; - - const upgradeTx = zns.domainToken.connect(deployer).upgradeTo(await newAddressResolver.getAddress()); - - await expect(upgradeTx).to.not.be.reverted; - }); - - it("Fails to upgrade if the caller is not authorized", async () => { - const factory = new ZNSAddressResolverUpgradeMock__factory(deployer); - - // DomainToken to upgrade to - const newAddressResolver = await factory.deploy(); - await newAddressResolver.waitForDeployment(); - - // Confirm the operator is not a governor - await expect( - zns.accessController.checkGovernor(operator.address) - ).to.be.revertedWith( - getAccessRevertMsg(operator.address, GOVERNOR_ROLE) - ); - - const upgradeTx = zns.domainToken.connect(operator).upgradeTo(await newAddressResolver.getAddress()); - - await expect(upgradeTx).to.be.revertedWith( - getAccessRevertMsg(operator.address, GOVERNOR_ROLE) - ); - }); - - it("Verifies that variable values are not changed in the upgrade process", async () => { - // AddressResolver to upgrade to - const factory = new ZNSAddressResolverUpgradeMock__factory(deployer); - const newResolver = await factory.deploy(); - await newResolver.waitForDeployment(); - - await zns.addressResolver.connect(mockRegistrar).setAddress(wilderDomainHash, user.address); - - const contractCalls = [ - zns.addressResolver.registry(), - zns.addressResolver.resolveDomainAddress(wilderDomainHash), - ]; - - await validateUpgrade(deployer, zns.addressResolver, newResolver, factory, contractCalls); - }); - }); -}); +import * as hre from "hardhat"; +import { + ERC165__factory, + ZNSAddressResolver, + ZNSAddressResolver__factory, + ZNSAddressResolverUpgradeMock__factory, +} from "../typechain"; +import { DeployZNSParams, IZNSContracts } from "./helpers/types"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { hashDomainLabel, hashSubdomainName } from "./helpers/hashing"; +import { + ADMIN_ROLE, + DEFAULT_RESOLVER_TYPE, + GOVERNOR_ROLE, + REGISTRAR_ROLE, + deployZNS, + getAccessRevertMsg, + validateUpgrade, INITIALIZED_ERR, +} from "./helpers"; +import { getProxyImplAddress } from "./helpers/utils"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { expect } = require("chai"); + +describe("ZNSAddressResolver", () => { + let deployer : SignerWithAddress; + let mockRegistrar : SignerWithAddress; + let user : SignerWithAddress; + let operator : SignerWithAddress; + let wilderDomainHash : string; + + let zns : IZNSContracts; + + beforeEach(async () => { + [ + deployer, + operator, + user, + mockRegistrar, + ] = await hre.ethers.getSigners(); + + const params : DeployZNSParams = { + deployer, + governorAddresses: [deployer.address], + adminAddresses: [deployer.address], + }; + zns = await deployZNS(params); + + // Have to get this value for every test, but can be fixed + wilderDomainHash = hashSubdomainName("wilder"); + + await zns.accessController.connect(deployer).grantRole(REGISTRAR_ROLE, mockRegistrar.address); + + await zns.registry.connect(deployer).addResolverType(DEFAULT_RESOLVER_TYPE, await zns.addressResolver.getAddress()); + + await zns.registry.connect(mockRegistrar) + .createDomainRecord( + wilderDomainHash, + deployer.address, + DEFAULT_RESOLVER_TYPE + ); + }); + + it("Should NOT let initialize the implementation contract", async () => { + const factory = new ZNSAddressResolver__factory(deployer); + const impl = await getProxyImplAddress(await zns.addressResolver.getAddress()); + const implContract = factory.attach(impl) as ZNSAddressResolver; + + await expect( + implContract.initialize( + operator.address, + mockRegistrar.address, + ) + ).to.be.revertedWith(INITIALIZED_ERR); + }); + + it("Should get the AddressResolver", async () => { // Copy of registry tests + // The domain exists + const existResolver = await zns.registry.getDomainResolver(wilderDomainHash); + expect(existResolver).to.eq(await zns.addressResolver.getAddress()); + }); + + it("Returns 0 when the domain doesnt exist", async () => { + // The domain does not exist + const someDomainHash = hashDomainLabel("random-record"); + const notExistResolver = await zns.registry.getDomainResolver(someDomainHash); + expect(notExistResolver).to.eq(hre.ethers.ZeroAddress); + }); + + it("Should have registry address correctly set", async () => { + expect(await zns.addressResolver.registry()).to.equal(await zns.registry.getAddress()); + }); + + it("Should setRegistry() correctly with ADMIN_ROLE", async () => { + await expect( + zns.addressResolver.connect(deployer).setRegistry(operator.address) + ) + .to.emit(zns.addressResolver, "RegistrySet") + .withArgs(operator.address); + + expect(await zns.addressResolver.registry()).to.equal(operator.address); + }); + + it("Should revert when setRegistry() without ADMIN_ROLE", async () => { + await expect( + zns.addressResolver.connect(operator).setRegistry(operator.address) + ).to.be.revertedWith( + getAccessRevertMsg(operator.address, ADMIN_ROLE) + ); + }); + + it("Should setAccessController() correctly with ADMIN_ROLE", async () => { + expect(await zns.addressResolver.connect(deployer).setAccessController(operator.address)) + .to.emit(zns.addressResolver, "AccessControllerSet") + .withArgs(operator.address); + + expect(await zns.addressResolver.getAccessController()).to.equal(operator.address); + }); + + it("Should revert when setAccessController() without ADMIN_ROLE", async () => { + await expect( + zns.addressResolver.connect(operator).setAccessController(operator.address) + ).to.be.revertedWith( + getAccessRevertMsg(operator.address, ADMIN_ROLE) + ); + }); + + it("Should not allow non-owner address to setAddress", async () => { + await expect( + zns.addressResolver.connect(user).setAddress(wilderDomainHash, user.address) + ).to.be.revertedWith("ZNSAddressResolver: Not authorized for this domain"); + }); + + it("Should allow owner to setAddress and emit event", async () => { + await expect( + zns.addressResolver.connect(deployer) + .setAddress(wilderDomainHash, user.address) + ) + .to.emit(zns.addressResolver, "AddressSet") + .withArgs(wilderDomainHash, user.address); + + const resolvedAddress = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); + expect(resolvedAddress).to.equal(user.address); + }); + + it("Should allow operator to setAddress and emit event", async () => { + await zns.registry.connect(deployer).setOwnersOperator(operator.address, true); + + await expect( + zns.addressResolver.connect(operator) + .setAddress(wilderDomainHash, user.address) + ) + .to.emit(zns.addressResolver, "AddressSet") + .withArgs(wilderDomainHash, user.address); + }); + + it("Should allow REGISTRAR_ROLE to setAddress and emit event", async () => { + await zns.accessController.connect(deployer).grantRole(REGISTRAR_ROLE, mockRegistrar.address); + + await expect( + zns.addressResolver.connect(mockRegistrar) + .setAddress(wilderDomainHash, hre.ethers.ZeroAddress) + ) + .to.emit(zns.addressResolver, "AddressSet") + .withArgs(wilderDomainHash, hre.ethers.ZeroAddress); + + const address = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); + expect(address).to.eq(hre.ethers.ZeroAddress); + + }); + + it("Should resolve address correctly", async () => { + await zns.addressResolver.connect(deployer).setAddress(wilderDomainHash, user.address); + + const resolvedAddress = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); + expect(resolvedAddress).to.equal(user.address); + }); + + it("Should support the IZNSAddressResolver interface ID", async () => { + const interfaceId = await zns.addressResolver.getInterfaceId(); + const supported = await zns.addressResolver.supportsInterface(interfaceId); + expect(supported).to.be.true; + }); + + it("Should support the ERC-165 interface ID", async () => { + const erc165Interface = ERC165__factory.createInterface(); + + const fragment = erc165Interface.getFunction("supportsInterface"); + + const supported = await zns.addressResolver.supportsInterface(fragment.selector); + expect(supported).to.be.true; + }); + + it("Should not support other interface IDs", async () => { + const notSupported = await zns.addressResolver.supportsInterface("0xffffffff"); + expect(notSupported).to.be.false; + }); + + it("Should support full discovery flow from zns.registry", async () => { + await zns.addressResolver.connect(deployer).setAddress(wilderDomainHash, user.address); + + const resolverAddress = await zns.registry.getDomainResolver(wilderDomainHash); + expect(resolverAddress).to.eq(await zns.addressResolver.getAddress()); + + const resolvedAddress = await zns.addressResolver.resolveDomainAddress(wilderDomainHash); + expect(resolvedAddress).to.eq(user.address); + }); + + describe("UUPS", () => { + it("Allows an authorized user to upgrade the contract", async () => { + // AddressResolver to upgrade to + const factory = new ZNSAddressResolverUpgradeMock__factory(deployer); + const newAddressResolver = await factory.deploy(); + await newAddressResolver.waitForDeployment(); + + // Confirm the deployer is a governor + expect( + await zns.accessController.hasRole(GOVERNOR_ROLE, deployer.address) + ).to.be.true; + + const upgradeTx = zns.domainToken.connect(deployer).upgradeTo(await newAddressResolver.getAddress()); + + await expect(upgradeTx).to.not.be.reverted; + }); + + it("Fails to upgrade if the caller is not authorized", async () => { + const factory = new ZNSAddressResolverUpgradeMock__factory(deployer); + + // DomainToken to upgrade to + const newAddressResolver = await factory.deploy(); + await newAddressResolver.waitForDeployment(); + + // Confirm the operator is not a governor + await expect( + zns.accessController.checkGovernor(operator.address) + ).to.be.revertedWith( + getAccessRevertMsg(operator.address, GOVERNOR_ROLE) + ); + + const upgradeTx = zns.domainToken.connect(operator).upgradeTo(await newAddressResolver.getAddress()); + + await expect(upgradeTx).to.be.revertedWith( + getAccessRevertMsg(operator.address, GOVERNOR_ROLE) + ); + }); + + it("Verifies that variable values are not changed in the upgrade process", async () => { + // AddressResolver to upgrade to + const factory = new ZNSAddressResolverUpgradeMock__factory(deployer); + const newResolver = await factory.deploy(); + await newResolver.waitForDeployment(); + + await zns.addressResolver.connect(mockRegistrar).setAddress(wilderDomainHash, user.address); + + const contractCalls = [ + zns.addressResolver.registry(), + zns.addressResolver.resolveDomainAddress(wilderDomainHash), + ]; + + await validateUpgrade(deployer, zns.addressResolver, newResolver, factory, contractCalls); + }); + }); +}); diff --git a/test/ZNSRootRegistrar.test.ts b/test/ZNSRootRegistrar.test.ts index bbb3f2c91..41c0a6a4a 100644 --- a/test/ZNSRootRegistrar.test.ts +++ b/test/ZNSRootRegistrar.test.ts @@ -1,1364 +1,1364 @@ -import * as hre from "hardhat"; -import { expect } from "chai"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { - normalizeName, - validateUpgrade, - AccessType, - OwnerOf, - PaymentType, - getAccessRevertMsg, - hashDomainLabel, - DEFAULT_TOKEN_URI, - distrConfigEmpty, - INVALID_LENGTH_ERR, - INITIALIZED_ERR, - INVALID_TOKENID_ERC_ERR, - REGISTRAR_ROLE, - DEFAULT_PRECISION_MULTIPLIER, - DEFAULT_PRICE_CONFIG, - DEFAULT_PROTOCOL_FEE_PERCENT, - NOT_AUTHORIZED_REG_ERR, - NOT_BOTH_OWNER_RAR_ERR, - NOT_TOKEN_OWNER_RAR_ERR, - ONLY_NAME_OWNER_REG_ERR, - ONLY_OWNER_REGISTRAR_REG_ERR, - INVALID_NAME_ERR, - paymentConfigEmpty, -} from "./helpers"; -import { IDistributionConfig } from "./helpers/types"; -import * as ethers from "ethers"; -import { defaultRootRegistration } from "./helpers/register-setup"; -import { checkBalance } from "./helpers/balances"; -import { getPriceObject } from "./helpers/pricing"; -import { getDomainHashFromEvent } from "./helpers/events"; -import { IDeployCampaignConfig, TZNSContractState } from "../src/deploy/campaign/types"; -import { ADMIN_ROLE, GOVERNOR_ROLE } from "../src/deploy/constants"; -import { - IERC20, - ZNSRootRegistrar, - ZNSRootRegistrar__factory, - ZNSRootRegistrarUpgradeMock__factory, -} from "../typechain"; -import { PaymentConfigStruct } from "../typechain/contracts/treasury/IZNSTreasury"; -import { runZnsCampaign } from "../src/deploy/zns-campaign"; -import { getProxyImplAddress } from "./helpers/utils"; -import { upgrades } from "hardhat"; -import { MongoDBAdapter } from "../src/deploy/db/mongo-adapter/mongo-adapter"; -import { getConfig } from "../src/deploy/campaign/environments"; - -require("@nomicfoundation/hardhat-chai-matchers"); - - -// This is the only test converted to use the new Campaign, other -// contract specific tests are using `deployZNS()` helper -describe("ZNSRootRegistrar", () => { - let deployer : SignerWithAddress; - let user : SignerWithAddress; - let governor : SignerWithAddress; - let admin : SignerWithAddress; - let randomUser : SignerWithAddress; - - let zns : TZNSContractState; - let zeroVault : SignerWithAddress; - let operator : SignerWithAddress; - let userBalanceInitial : bigint; - - let mongoAdapter : MongoDBAdapter; - - const defaultDomain = normalizeName("wilder"); - - beforeEach(async () => { - // zeroVault address is used to hold the fee charged to the user when registering - [deployer, zeroVault, user, operator, governor, admin, randomUser] = await hre.ethers.getSigners(); - - const config : IDeployCampaignConfig = await getConfig({ - deployer, - zeroVaultAddress: zeroVault.address, - governors: [deployer.address, governor.address], - admins: [deployer.address, admin.address], - }); - - const campaign = await runZnsCampaign({ - config, - }); - - zns = campaign.state.contracts; - - mongoAdapter = campaign.dbAdapter; - - await zns.meowToken.connect(deployer).approve( - await zns.treasury.getAddress(), - ethers.MaxUint256 - ); - - userBalanceInitial = ethers.parseEther("100000000000"); - // Give funds to user - await zns.meowToken.connect(user).approve(await zns.treasury.getAddress(), ethers.MaxUint256); - await zns.meowToken.mint(user.address, userBalanceInitial); - }); - - afterEach(async () => { - await mongoAdapter.dropDB(); - }); - - it("Sets the payment config when provided with the domain registration", async () => { - const tokenURI = "https://example.com/817c64af"; - const distrConfig : IDistributionConfig = { - pricerContract: await zns.curvePricer.getAddress(), - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }; - - await zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - await zns.addressResolver.getAddress(), - tokenURI, - distrConfig, - { - token: await zns.meowToken.getAddress(), - beneficiary: user.address, - } - ); - - const domainHash = hashDomainLabel(defaultDomain); - const config = await zns.treasury.paymentConfigs(domainHash); - expect(config.token).to.eq(await zns.meowToken.getAddress()); - expect(config.beneficiary).to.eq(user.address); - }); - - it("Does not set the payment config when the beneficiary is the zero address", async () => { - const tokenURI = "https://example.com/817c64af"; - const distrConfig : IDistributionConfig = { - pricerContract: await zns.curvePricer.getAddress(), - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }; - - await zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - await zns.addressResolver.getAddress(), - tokenURI, - distrConfig, - paymentConfigEmpty - ); - - const domainHash = hashDomainLabel(defaultDomain); - const config = await zns.treasury.paymentConfigs(domainHash); - expect(config.token).to.eq(ethers.ZeroAddress); - expect(config.beneficiary).to.eq(ethers.ZeroAddress); - }); - - it("Gas tests", async () => { - const tokenURI = "https://example.com/817c64af"; - const distrConfig : IDistributionConfig = { - pricerContract: await zns.curvePricer.getAddress(), - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }; - - await zns.rootRegistrar.connect(deployer).registerRootDomain( - defaultDomain, - deployer.address, - tokenURI, - distrConfig, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - const domainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - - // Registering as deployer (owner of parent) and user is different gas values - await zns.subRegistrar.connect(deployer).registerSubdomain( - domainHash, - "subdomain", - deployer.address, - tokenURI, - distrConfigEmpty, - paymentConfigEmpty, - ); - - const candidates = [ - deployer.address, - user.address, - governor.address, - admin.address, - randomUser.address, - ]; - - const allowed = [ - true, - true, - true, - true, - true, - ]; - - await zns.subRegistrar.updateMintlistForDomain( - domainHash, - candidates, - allowed - ); - }); - - it("Should NOT initialize the implementation contract", async () => { - const factory = new ZNSRootRegistrar__factory(deployer); - const impl = await getProxyImplAddress(await zns.rootRegistrar.getAddress()); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion - const implContract = factory.attach(impl) as ZNSRootRegistrar; - - await expect( - implContract.initialize( - operator.address, - operator.address, - operator.address, - operator.address, - operator.address, - ) - ).to.be.revertedWith(INITIALIZED_ERR); - }); - - it("Allows transfer of 0x0 domain ownership after deployment", async () => { - await zns.registry.updateDomainOwner(ethers.ZeroHash, user.address); - expect(await zns.registry.getDomainOwner(ethers.ZeroHash)).to.equal(user.address); - }); - - it("Confirms a new 0x0 owner can modify the configs in the treasury and curve pricer", async () => { - await zns.registry.updateDomainOwner(ethers.ZeroHash, user.address); - - const newTreasuryConfig : PaymentConfigStruct = { - token: zeroVault.address, // Just needs to be a different address - beneficiary: user.address, - }; - - // Modify the treasury - const treasuryTx = await zns.treasury.connect(user).setPaymentConfig(ethers.ZeroHash, newTreasuryConfig); - - await expect(treasuryTx).to.emit( - zns.treasury, - "BeneficiarySet" - ).withArgs( - ethers.ZeroHash, - user.address - ); - await expect(treasuryTx).to.emit( - zns.treasury, - "PaymentTokenSet" - ).withArgs( - ethers.ZeroHash, - zeroVault.address - ); - - // Modify the curve pricer - const newPricerConfig = { - baseLength: BigInt("6"), - maxLength: BigInt("35"), - maxPrice: ethers.parseEther("150"), - minPrice: ethers.parseEther("10"), - precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, - feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, - isSet: true, - }; - - const pricerTx = await zns.curvePricer.connect(user).setPriceConfig( - ethers.ZeroHash, - newPricerConfig, - ); - - await expect(pricerTx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( - ethers.ZeroHash, - newPricerConfig.maxPrice, - newPricerConfig.minPrice, - newPricerConfig.maxLength, - newPricerConfig.baseLength, - newPricerConfig.precisionMultiplier, - newPricerConfig.feePercentage, - ); - }); - - it("Confirms a user has funds and allowance for the Registrar", async () => { - const balance = await zns.meowToken.balanceOf(user.address); - expect(balance).to.eq(userBalanceInitial); - - const allowance = await zns.meowToken.allowance(user.address, await zns.treasury.getAddress()); - expect(allowance).to.eq(ethers.MaxUint256); - }); - - it("Should revert when initialize() without ADMIN_ROLE", async () => { - const userHasAdmin = await zns.accessController.hasRole(ADMIN_ROLE, user.address); - expect(userHasAdmin).to.be.false; - - const registrarFactory = new ZNSRootRegistrar__factory(user); - - const tx = upgrades.deployProxy( - registrarFactory, - [ - await zns.accessController.getAddress(), - await zns.registry.getAddress(), - await zns.curvePricer.getAddress(), - await zns.treasury.getAddress(), - await zns.domainToken.getAddress(), - ], - { - kind: "uups", - } - ); - - await expect(tx).to.be.revertedWith(getAccessRevertMsg(user.address, ADMIN_ROLE)); - }); - - it("Should NOT initialize twice", async () => { - const tx = zns.rootRegistrar.connect(deployer).initialize( - await zns.accessController.getAddress(), - randomUser.address, - randomUser.address, - randomUser.address, - randomUser.address, - ); - - await expect(tx).to.be.revertedWith("Initializable: contract is already initialized"); - }); - - describe("General functionality", () => { - it("#coreRegister() should revert if called by address without REGISTRAR_ROLE", async () => { - const isRegistrar = await zns.accessController.hasRole(REGISTRAR_ROLE, randomUser.address); - expect(isRegistrar).to.be.false; - - await expect( - zns.rootRegistrar.connect(randomUser).coreRegister({ - parentHash: ethers.ZeroHash, - domainHash: ethers.ZeroHash, - label: "randomname", - registrant: ethers.ZeroAddress, - price: "0", - stakeFee: "0", - domainAddress: ethers.ZeroAddress, - tokenURI: "", - isStakePayment: false, - paymentConfig: paymentConfigEmpty, - }) - ).to.be.revertedWith( - getAccessRevertMsg(randomUser.address, REGISTRAR_ROLE) - ); - }); - - it("#isOwnerOf() returns correct bools", async () => { - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - const tokenId = BigInt(domainHash); - - const isOwnerOfBothUser = await zns.rootRegistrar.isOwnerOf( - domainHash, - user.address, - OwnerOf.BOTH - ); - expect(isOwnerOfBothUser).to.be.true; - - const isOwnerOfBothRandom = await zns.rootRegistrar.isOwnerOf( - domainHash, - randomUser.address, - OwnerOf.BOTH - ); - expect(isOwnerOfBothRandom).to.be.false; - - // transfer token - await zns.domainToken.connect(user).transferFrom(user.address, randomUser.address, tokenId); - const isOwnerOfTokenUser = await zns.rootRegistrar.isOwnerOf( - domainHash, - user.address, - OwnerOf.TOKEN - ); - expect(isOwnerOfTokenUser).to.be.false; - - const isOwnerOfTokenRandom = await zns.rootRegistrar.isOwnerOf( - domainHash, - randomUser.address, - OwnerOf.TOKEN - ); - expect(isOwnerOfTokenRandom).to.be.true; - - const isOwnerOfNameUser = await zns.rootRegistrar.isOwnerOf( - domainHash, - user.address, - OwnerOf.NAME - ); - expect(isOwnerOfNameUser).to.be.true; - - const isOwnerOfNameRandom = await zns.rootRegistrar.isOwnerOf( - domainHash, - randomUser.address, - OwnerOf.NAME - ); - expect(isOwnerOfNameRandom).to.be.false; - - await expect( - zns.rootRegistrar.isOwnerOf(domainHash, user.address, 3) - ).to.be.reverted; - }); - - it("#setSubRegistrar() should revert if called by address without ADMIN_ROLE", async () => { - const isAdmin = await zns.accessController.hasRole(ADMIN_ROLE, randomUser.address); - expect(isAdmin).to.be.false; - - await expect( - zns.rootRegistrar.connect(randomUser).setSubRegistrar(randomUser.address) - ).to.be.revertedWith( - getAccessRevertMsg(randomUser.address, ADMIN_ROLE) - ); - }); - - it("#setSubRegistrar() should set the correct address", async () => { - await zns.rootRegistrar.connect(admin).setSubRegistrar(randomUser.address); - - expect( - await zns.rootRegistrar.subRegistrar() - ).to.equal(randomUser.address); - }); - - it("#setSubRegistrar() should NOT set the address to zero address", async () => { - await expect( - zns.rootRegistrar.connect(admin).setSubRegistrar(ethers.ZeroAddress) - ).to.be.revertedWith( - "ZNSRootRegistrar: subRegistrar_ is 0x0 address" - ); - }); - }); - - describe("Registers a root domain", () => { - it("Can NOT register a TLD with an empty name", async () => { - const emptyName = ""; - - await expect( - defaultRootRegistration({ - user: deployer, - zns, - domainName: emptyName, - }) - ).to.be.revertedWith(INVALID_LENGTH_ERR); - }); - - it("Can register a TLD with characters [a-z0-9-]", async () => { - const letters = "world"; - const lettersHash = hashDomainLabel(letters); - - const alphaNumeric = "0x0dwidler0x0"; - const alphaNumericHash = hashDomainLabel(alphaNumeric); - - const withHyphen = "0x0-dwidler-0x0"; - const withHyphenHash = hashDomainLabel(withHyphen); - - const tx1 = zns.rootRegistrar.connect(deployer).registerRootDomain( - letters, - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, - distrConfigEmpty, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - await expect(tx1).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( - ethers.ZeroHash, - lettersHash, - letters, - BigInt(lettersHash), - DEFAULT_TOKEN_URI, - deployer.address, - ethers.ZeroAddress, - ); - - const tx2 = zns.rootRegistrar.connect(deployer).registerRootDomain( - alphaNumeric, - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, - distrConfigEmpty, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - await expect(tx2).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( - ethers.ZeroHash, - alphaNumericHash, - alphaNumeric, - BigInt(alphaNumericHash), - DEFAULT_TOKEN_URI, - deployer.address, - ethers.ZeroAddress, - ); - - const tx3 = zns.rootRegistrar.connect(deployer).registerRootDomain( - withHyphen, - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, - distrConfigEmpty, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - await expect(tx3).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( - ethers.ZeroHash, - withHyphenHash, - withHyphen, - BigInt(withHyphenHash), - DEFAULT_TOKEN_URI, - deployer.address, - ethers.ZeroAddress, - ); - }); - - it("Fails for domains that use any invalid character", async () => { - // Valid names must match the pattern [a-z0-9] - const nameA = "WILDER"; - const nameB = "!?w1Id3r!?"; - const nameC = "!%$#^*?!#👍3^29"; - const nameD = "wo.rld"; - - await expect( - defaultRootRegistration({ - user: deployer, - zns, - domainName: nameA, - }) - ).to.be.revertedWith(INVALID_NAME_ERR); - - await expect( - defaultRootRegistration({ - user: deployer, - zns, - domainName: nameB, - }) - ).to.be.revertedWith(INVALID_NAME_ERR); - - await expect( - defaultRootRegistration({ - user: deployer, - zns, - domainName: nameC, - }) - ).to.be.revertedWith(INVALID_NAME_ERR); - - await expect( - defaultRootRegistration({ - user: deployer, - zns, - domainName: nameD, - }) - ).to.be.revertedWith(INVALID_NAME_ERR); - }); - - // eslint-disable-next-line max-len - it("Successfully registers a domain without a resolver or resolver content and fires a #DomainRegistered event", async () => { - const tokenURI = "https://example.com/817c64af"; - const tx = await zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - ethers.ZeroAddress, - tokenURI, - distrConfigEmpty, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - const hashFromTS = hashDomainLabel(defaultDomain); - - await expect(tx).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( - ethers.ZeroHash, - hashFromTS, - defaultDomain, - BigInt(hashFromTS), - tokenURI, - user.address, - ethers.ZeroAddress, - ); - - const tokenURISC = await zns.domainToken.tokenURI(hashFromTS); - expect(tokenURISC).to.eq(tokenURI); - }); - - it("Successfully registers a domain with distrConfig and adds it to state properly", async () => { - const distrConfig = { - pricerContract: await zns.fixedPricer.getAddress(), - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }; - const tokenURI = "https://example.com/817c64af"; - - await zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - ethers.ZeroAddress, - tokenURI, - distrConfig, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - const { - pricerContract, - accessType, - paymentType, - } = await zns.subRegistrar.distrConfigs(domainHash); - - expect(pricerContract).to.eq(distrConfig.pricerContract); - expect(paymentType).to.eq(distrConfig.paymentType); - expect(accessType).to.eq(distrConfig.accessType); - - const tokenURISC = await zns.domainToken.tokenURI(domainHash); - expect(tokenURISC).to.eq(tokenURI); - }); - - it("Stakes and saves the correct amount and token, takes the correct fee and sends fee to Zero Vault", async () => { - const balanceBeforeUser = await zns.meowToken.balanceOf(user.address); - const balanceBeforeVault = await zns.meowToken.balanceOf(zeroVault.address); - - // Deploy "wilder" with default configuration - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - const { - totalPrice, - expectedPrice, - stakeFee, - } = getPriceObject(defaultDomain, DEFAULT_PRICE_CONFIG); - - await checkBalance({ - token: zns.meowToken as IERC20, - balanceBefore: balanceBeforeUser, - userAddress: user.address, - target: totalPrice, - }); - - await checkBalance({ - token: zns.meowToken as IERC20, - balanceBefore: balanceBeforeVault, - userAddress: zeroVault.address, - target: stakeFee, - shouldDecrease: false, - }); - - const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); - - expect(staked).to.eq(expectedPrice); - expect(token).to.eq(await zns.meowToken.getAddress()); - }); - - it("Sets the correct data in Registry", async () => { - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - - const namehashRef = hashDomainLabel(defaultDomain); - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - expect(domainHash).to.eq(namehashRef); - - const { - owner: ownerFromReg, - resolver: resolverFromReg, - } = await zns.registry.getDomainRecord(domainHash); - - expect(ownerFromReg).to.eq(user.address); - expect(resolverFromReg).to.eq(await zns.addressResolver.getAddress()); - }); - - it("Fails when the user does not have enough funds", async () => { - const balance = await zns.meowToken.balanceOf(user.address); - await zns.meowToken.connect(user).transfer(randomUser.address, balance); - - const tx = defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - await expect(tx).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - }); - - it("Disallows creation of a duplicate domain", async () => { - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - const failTx = defaultRootRegistration({ - user: deployer, - zns, - domainName: defaultDomain, - }); - - await expect(failTx).to.be.revertedWith("ZNSRootRegistrar: Domain already exists"); - }); - - it("Successfully registers a domain without resolver content", async () => { - const tx = zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, - distrConfigEmpty, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - await expect(tx).to.not.be.reverted; - }); - - it("Records the correct domain hash", async () => { - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - const exists = await zns.registry.exists(domainHash); - expect(exists).to.be.true; - expect(domainHash).to.eq(hashDomainLabel(defaultDomain)); - }); - - it("Creates and finds the correct tokenId", async () => { - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - - const tokenId = BigInt( - await getDomainHashFromEvent({ - zns, - user, - }) - ); - const owner = await zns.domainToken.ownerOf(tokenId); - expect(owner).to.eq(user.address); - }); - - it("Resolves the correct address from the domain", async () => { - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - domainContent: await zns.rootRegistrar.getAddress(), - }); - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - const resolvedAddress = await zns.addressResolver.resolveDomainAddress(domainHash); - expect(resolvedAddress).to.eq(await zns.rootRegistrar.getAddress()); - }); - - it("Should NOT charge any tokens if price and/or stake fee is 0", async () => { - // set config on CurvePricer for the price to be 0 - await zns.curvePricer.connect(deployer).setMaxPrice(ethers.ZeroHash, "0"); - await zns.curvePricer.connect(deployer).setMinPrice(ethers.ZeroHash, "0"); - - const userBalanceBefore = await zns.meowToken.balanceOf(user.address); - const vaultBalanceBefore = await zns.meowToken.balanceOf(zeroVault.address); - - // register a domain - await zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, - distrConfigEmpty, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - const userBalanceAfter = await zns.meowToken.balanceOf(user.address); - const vaultBalanceAfter = await zns.meowToken.balanceOf(zeroVault.address); - - expect(userBalanceBefore).to.eq(userBalanceAfter); - expect(vaultBalanceBefore).to.eq(vaultBalanceAfter); - - // check existence in Registry - const domainHash = hashDomainLabel(defaultDomain); - const exists = await zns.registry.exists(domainHash); - expect(exists).to.be.true; - - // make sure no transfers happened - const transferEventFilter = zns.meowToken.filters.Transfer( - user.address, - ); - const events = await zns.meowToken.queryFilter(transferEventFilter); - expect(events.length).to.eq(0); - }); - }); - - describe("Reclaiming Domains", () => { - it("Can reclaim name/stake if Token is owned", async () => { - // Register Top level - await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - const tokenId = BigInt(domainHash); - const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); - - // Transfer the domain token - await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); - - // Verify owner in registry - const originalOwner = await zns.registry.connect(deployer).getDomainOwner(domainHash); - expect(originalOwner).to.equal(deployer.address); - - // Reclaim the Domain - await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); - - // Verify domain token is still owned - const owner = await zns.domainToken.connect(user).ownerOf(tokenId); - expect(owner).to.equal(user.address); - - // Verify domain is owned in registry - const registryOwner = await zns.registry.connect(user).getDomainOwner(domainHash); - expect(registryOwner).to.equal(user.address); - - // Verify same amount is staked - const { amount: stakedAfterReclaim, token: tokenAfterReclaim } = await zns.treasury.stakedForDomain(domainHash); - expect(staked).to.equal(stakedAfterReclaim); - expect(tokenAfterReclaim).to.equal(await zns.meowToken.getAddress()); - expect(token).to.equal(tokenAfterReclaim); - }); - - it("Reclaiming domain token emits DomainReclaimed event", async () => { - await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - const tokenId = BigInt(domainHash); - - // Transfer the domain token - await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); - // Reclaim the Domain - const tx = zns.rootRegistrar.connect(user).reclaimDomain(domainHash); - await expect(tx).to.emit(zns.rootRegistrar, "DomainReclaimed").withArgs( - domainHash, - user.address - ); - }); - - it("Cannot reclaim name/stake if token is not owned", async () => { - await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - // Reclaim the Domain - const tx = zns.rootRegistrar.connect(user).reclaimDomain(domainHash); - - // Verify Domain is not reclaimed - await expect(tx).to.be.revertedWith(NOT_TOKEN_OWNER_RAR_ERR); - - // Verify domain is not owned in registrar - const registryOwner = await zns.registry.connect(user).getDomainOwner(domainHash); - expect(registryOwner).to.equal(deployer.address); - }); - - it("Cannot reclaim if domain does not exist", async () => { - const domainHash = "0xd34cfa279afd55afc6aa9c00aa5d01df60179840a93d10eed730058b8dd4146c"; - // Reclaim the Domain - const tx = zns.rootRegistrar.connect(user).reclaimDomain(domainHash); - - // Verify Domain is not reclaimed - await expect(tx).to.be.revertedWith(INVALID_TOKENID_ERC_ERR); - }); - - it("Domain Token can be reclaimed, transferred, and then reclaimed again", async () => { - // Register Top level - await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - const tokenId = BigInt(domainHash); - const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); - - // Transfer the domain token - await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); - - // Reclaim the Domain - await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); - // Verify domain token is still owned - let owner = await zns.domainToken.connect(user).ownerOf(tokenId); - expect(owner).to.equal(user.address); - - // Transfer the domain token back - await zns.domainToken.connect(user).transferFrom(user.address, deployer.address, tokenId); - - // Reclaim the Domain again - await zns.rootRegistrar.connect(deployer).reclaimDomain(domainHash); - - // Verify domain token is owned - owner = await zns.domainToken.connect(deployer).ownerOf(tokenId); - expect(owner).to.equal(deployer.address); - - // Verify domain is owned in registrar - const registryOwner = await zns.registry.connect(deployer).getDomainOwner(domainHash); - expect(registryOwner).to.equal(deployer.address); - - // Verify same amount is staked - const { amount: stakedAfterReclaim, token: tokenAfterReclaim } = await zns.treasury.stakedForDomain(domainHash); - expect(staked).to.equal(stakedAfterReclaim); - expect(tokenAfterReclaim).to.equal(await zns.meowToken.getAddress()); - expect(token).to.equal(tokenAfterReclaim); - }); - - it("Can revoke and unstake after reclaiming", async () => { - // Verify Balance - const balance = await zns.meowToken.balanceOf(user.address); - expect(balance).to.eq(userBalanceInitial); - - // Register Top level - await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - const tokenId = BigInt(domainHash); - - // Validated staked values - const { - expectedPrice: expectedStaked, - } = getPriceObject(defaultDomain, DEFAULT_PRICE_CONFIG); - const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); - expect(staked).to.eq(expectedStaked); - expect(token).to.eq(await zns.meowToken.getAddress()); - - // Transfer the domain token - await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); - - // Reclaim the Domain - await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); - - // Revoke the Domain - await zns.rootRegistrar.connect(user).revokeDomain(domainHash); - - // Validated funds are unstaked - const { amount: finalstaked, token: finalToken } = await zns.treasury.stakedForDomain(domainHash); - expect(finalstaked).to.equal(BigInt("0")); - expect(finalToken).to.equal(ethers.ZeroAddress); - - // Verify final balances - const computedFinalBalance = balance + staked; - const finalBalance = await zns.meowToken.balanceOf(user.address); - expect(computedFinalBalance).to.equal(finalBalance); - }); - }); - - describe("Revoking Domains", () => { - it("Revokes a Top level Domain, locks distribution and removes mintlist", async () => { - // Register Top level - await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - distrConfig: { - pricerContract: await zns.fixedPricer.getAddress(), - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - }); - - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - // add mintlist to check revocation - await zns.subRegistrar.connect(user).updateMintlistForDomain( - domainHash, - [user.address, zeroVault.address], - [true, true] - ); - - const ogPrice = BigInt(135); - await zns.fixedPricer.connect(user).setPriceConfig( - domainHash, - { - price: ogPrice, - feePercentage: BigInt(0), - isSet: true, - } - ); - expect(await zns.fixedPricer.getPrice(domainHash, defaultDomain, false)).to.eq(ogPrice); - - const tokenId = BigInt( - await getDomainHashFromEvent({ - zns, - user, - }) - ); - - // Revoke the domain and then verify - await zns.rootRegistrar.connect(user).revokeDomain(domainHash); - - // Verify token has been burned - const ownerOfTx = zns.domainToken.connect(user).ownerOf(tokenId); - await expect(ownerOfTx).to.be.revertedWith( - INVALID_TOKENID_ERC_ERR - ); - - // Verify Domain Record Deleted - const exists = await zns.registry.exists(domainHash); - expect(exists).to.be.false; - - // validate access type has been set to LOCKED - const { accessType } = await zns.subRegistrar.distrConfigs(domainHash); - expect(accessType).to.eq(AccessType.LOCKED); - - // validate mintlist has been removed - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, user.address)).to.be.false; - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, zeroVault.address)).to.be.false; - }); - - it("Cannot revoke a domain that doesnt exist", async () => { - // Register Top level - const fakeHash = "0xd34cfa279afd55afc6aa9c00aa5d01df60179840a93d10eed730058b8dd4146c"; - const exists = await zns.registry.exists(fakeHash); - expect(exists).to.be.false; - - // Verify transaction is reverted - const tx = zns.rootRegistrar.connect(user).revokeDomain(fakeHash); - await expect(tx).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); - }); - - it("Revoking domain unstakes", async () => { - // Verify Balance - const balance = await zns.meowToken.balanceOf(user.address); - expect(balance).to.eq(userBalanceInitial); - - // Register Top level - await defaultRootRegistration({ user, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - // Validated staked values - const { - expectedPrice: expectedStaked, - stakeFee: expectedStakeFee, - } = getPriceObject(defaultDomain, DEFAULT_PRICE_CONFIG); - const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); - expect(staked).to.eq(expectedStaked); - expect(token).to.eq(await zns.meowToken.getAddress()); - - // Get balance after staking - const balanceAfterStaking = await zns.meowToken.balanceOf(user.address); - - // Revoke the domain - await zns.rootRegistrar.connect(user).revokeDomain(domainHash); - - // Validated funds are unstaked - const { amount: finalstaked, token: finalToken } = await zns.treasury.stakedForDomain(domainHash); - expect(finalstaked).to.equal(BigInt("0")); - expect(finalToken).to.equal(ethers.ZeroAddress); - - // Verify final balances - const computedBalanceAfterStaking = balanceAfterStaking + staked; - const balanceMinusFee = balance - expectedStakeFee; - expect(computedBalanceAfterStaking).to.equal(balanceMinusFee); - const finalBalance = await zns.meowToken.balanceOf(user.address); - expect(computedBalanceAfterStaking).to.equal(finalBalance); - }); - - it("Cannot revoke if Name is owned by another user", async () => { - // Register Top level - await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const parentDomainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - const owner = await zns.registry.connect(user).getDomainOwner(parentDomainHash); - expect(owner).to.not.equal(user.address); - - // Try to revoke domain - const tx = zns.rootRegistrar.connect(user).revokeDomain(parentDomainHash); - await expect(tx).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); - }); - - it("No one can revoke if Token and Name have different owners", async () => { - // Register Top level - await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const parentDomainHash = await getDomainHashFromEvent({ - zns, - user: deployer, - }); - const owner = await zns.registry.connect(user).getDomainOwner(parentDomainHash); - expect(owner).to.not.equal(user.address); - - const tokenId = BigInt(parentDomainHash); - - await zns.domainToken.transferFrom(deployer.address, user.address, tokenId); - - // Try to revoke domain as a new owner of the token - const tx = zns.rootRegistrar.connect(user).revokeDomain(parentDomainHash); - await expect(tx).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); - - const tx2 = zns.rootRegistrar.connect(deployer).revokeDomain(parentDomainHash); - await expect(tx2).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); - }); - - it("After domain has been revoked, an old operator can NOT access Registry", async () => { - // Register Top level - await defaultRootRegistration({ user, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); - - // assign an operator - await zns.registry.connect(user).setOwnersOperator(operator.address, true); - - // Revoke the domain - await zns.rootRegistrar.connect(user).revokeDomain(domainHash); - - // check operator access to the revoked domain - const tx2 = zns.registry - .connect(operator) - .updateDomainOwner( - domainHash, - operator.address - ); - await expect(tx2).to.be.revertedWith( - ONLY_OWNER_REGISTRAR_REG_ERR - ); - - const tx3 = zns.registry - .connect(operator) - .updateDomainRecord( - domainHash, - user.address, - operator.address - ); - await expect(tx3).to.be.revertedWith( - ONLY_NAME_OWNER_REG_ERR - ); - - const tx4 = zns.registry - .connect(operator) - .updateDomainResolver( - domainHash, - zeroVault.address - ); - await expect(tx4).to.be.revertedWith( - NOT_AUTHORIZED_REG_ERR - ); - }); - }); - - describe("State Setters", () => { - describe("#setAccessController", () => { - it("Should set AccessController and fire AccessControllerSet event", async () => { - const currentAC = await zns.rootRegistrar.getAccessController(); - const tx = await zns.rootRegistrar.connect(deployer).setAccessController(randomUser.address); - const newAC = await zns.rootRegistrar.getAccessController(); - - await expect(tx).to.emit(zns.rootRegistrar, "AccessControllerSet").withArgs(randomUser.address); - - expect(newAC).to.equal(randomUser.address); - expect(currentAC).to.not.equal(newAC); - }); - - it("Should revert if not called by ADMIN", async () => { - const tx = zns.rootRegistrar.connect(user).setAccessController(randomUser.address); - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(user.address, ADMIN_ROLE) - ); - }); - - it("Should revert if new AccessController is address zero", async () => { - const tx = zns.rootRegistrar.connect(deployer).setAccessController(ethers.ZeroAddress); - await expect(tx).to.be.revertedWith("AC: _accessController is 0x0 address"); - }); - }); - - describe("#setRegistry", () => { - it("Should set ZNSRegistry and fire RegistrySet event", async () => { - const currentRegistry = await zns.rootRegistrar.registry(); - const tx = await zns.rootRegistrar.connect(deployer).setRegistry(randomUser.address); - const newRegistry = await zns.rootRegistrar.registry(); - - await expect(tx).to.emit(zns.rootRegistrar, "RegistrySet").withArgs(randomUser.address); - - expect(newRegistry).to.equal(randomUser.address); - expect(currentRegistry).to.not.equal(newRegistry); - }); - - it("Should revert if not called by ADMIN", async () => { - const tx = zns.rootRegistrar.connect(user).setRegistry(randomUser.address); - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(user.address, ADMIN_ROLE) - ); - }); - - it("Should revert if ZNSRegistry is address zero", async () => { - const tx = zns.rootRegistrar.connect(deployer).setRegistry(ethers.ZeroAddress); - await expect(tx).to.be.revertedWith("ARegistryWired: _registry can not be 0x0 address"); - }); - }); - - describe("#setTreasury", () => { - it("Should set Treasury and fire TreasurySet event", async () => { - const currentTreasury = await zns.rootRegistrar.treasury(); - const tx = await zns.rootRegistrar.connect(deployer).setTreasury(randomUser.address); - const newTreasury = await zns.rootRegistrar.treasury(); - - await expect(tx).to.emit(zns.rootRegistrar, "TreasurySet").withArgs(randomUser.address); - - expect(newTreasury).to.equal(randomUser.address); - expect(currentTreasury).to.not.equal(newTreasury); - }); - - it("Should revert if not called by ADMIN", async () => { - const tx = zns.rootRegistrar.connect(user).setTreasury(randomUser.address); - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(user.address, ADMIN_ROLE) - ); - }); - - it("Should revert if Treasury is address zero", async () => { - const tx = zns.rootRegistrar.connect(deployer).setTreasury(ethers.ZeroAddress); - await expect(tx).to.be.revertedWith("ZNSRootRegistrar: treasury_ is 0x0 address"); - }); - }); - - describe("#setDomainToken", () => { - it("Should set DomainToken and fire DomainTokenSet event", async () => { - const currentToken = await zns.rootRegistrar.domainToken(); - const tx = await zns.rootRegistrar.connect(deployer).setDomainToken(randomUser.address); - const newToken = await zns.rootRegistrar.domainToken(); - - await expect(tx).to.emit(zns.rootRegistrar, "DomainTokenSet").withArgs(randomUser.address); - - expect(newToken).to.equal(randomUser.address); - expect(currentToken).to.not.equal(newToken); - }); - - it("Should revert if not called by ADMIN", async () => { - const tx = zns.rootRegistrar.connect(user).setDomainToken(randomUser.address); - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(user.address, ADMIN_ROLE) - ); - }); - - it("Should revert if DomainToken is address zero", async () => { - const tx = zns.rootRegistrar.connect(deployer).setDomainToken(ethers.ZeroAddress); - await expect(tx).to.be.revertedWith("ZNSRootRegistrar: domainToken_ is 0x0 address"); - }); - }); - }); - - describe("UUPS", () => { - it("Allows an authorized user to upgrade the contract", async () => { - // Confirm deployer has the correct role first - await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; - - const registrarFactory = new ZNSRootRegistrar__factory(deployer); - const registrar = await registrarFactory.deploy(); - await registrar.waitForDeployment(); - - const upgradeTx = zns.rootRegistrar.connect(deployer).upgradeTo(await registrar.getAddress()); - await expect(upgradeTx).to.not.be.reverted; - }); - - it("Fails to upgrade when an unauthorized users calls", async () => { - const registrarFactory = new ZNSRootRegistrar__factory(deployer); - const registrar = await registrarFactory.deploy(); - await registrar.waitForDeployment(); - - const tx = zns.rootRegistrar.connect(randomUser).upgradeTo(await registrar.getAddress()); - - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(randomUser.address, GOVERNOR_ROLE) - ); - }); - - it("Verifies that variable values are not changed in the upgrade process", async () => { - // Confirm deployer has the correct role first - await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; - - const registrarFactory = new ZNSRootRegistrarUpgradeMock__factory(deployer); - const registrar = await registrarFactory.deploy(); - await registrar.waitForDeployment(); - - const domainName = "world"; - const domainHash = hashDomainLabel(domainName); - - await zns.meowToken.connect(randomUser).approve(await zns.treasury.getAddress(), ethers.MaxUint256); - await zns.meowToken.mint(randomUser.address, DEFAULT_PRICE_CONFIG.maxPrice); - - await zns.rootRegistrar.connect(randomUser).registerRootDomain( - domainName, - randomUser.address, - DEFAULT_TOKEN_URI, - distrConfigEmpty, - { - token: ethers.ZeroAddress, - beneficiary: ethers.ZeroAddress, - } - ); - - - const contractCalls = [ - zns.rootRegistrar.getAccessController(), - zns.rootRegistrar.registry(), - zns.rootRegistrar.treasury(), - zns.rootRegistrar.domainToken(), - zns.registry.exists(domainHash), - zns.treasury.stakedForDomain(domainHash), - zns.domainToken.name(), - zns.domainToken.symbol(), - zns.curvePricer.getPrice(ethers.ZeroHash, domainName, false), - ]; - - await validateUpgrade(deployer, zns.rootRegistrar, registrar, registrarFactory, contractCalls); - }); - }); -}); +import * as hre from "hardhat"; +import { expect } from "chai"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { + normalizeName, + validateUpgrade, + AccessType, + OwnerOf, + PaymentType, + getAccessRevertMsg, + hashDomainLabel, + DEFAULT_TOKEN_URI, + distrConfigEmpty, + INVALID_LENGTH_ERR, + INITIALIZED_ERR, + INVALID_TOKENID_ERC_ERR, + REGISTRAR_ROLE, + DEFAULT_PRECISION_MULTIPLIER, + DEFAULT_PRICE_CONFIG, + DEFAULT_PROTOCOL_FEE_PERCENT, + NOT_AUTHORIZED_REG_ERR, + NOT_BOTH_OWNER_RAR_ERR, + NOT_TOKEN_OWNER_RAR_ERR, + ONLY_NAME_OWNER_REG_ERR, + ONLY_OWNER_REGISTRAR_REG_ERR, + INVALID_NAME_ERR, + paymentConfigEmpty, +} from "./helpers"; +import { IDistributionConfig } from "./helpers/types"; +import * as ethers from "ethers"; +import { defaultRootRegistration } from "./helpers/register-setup"; +import { checkBalance } from "./helpers/balances"; +import { getPriceObject } from "./helpers/pricing"; +import { getDomainHashFromEvent } from "./helpers/events"; +import { IDeployCampaignConfig, TZNSContractState } from "../src/deploy/campaign/types"; +import { ADMIN_ROLE, GOVERNOR_ROLE } from "../src/deploy/constants"; +import { + IERC20, + ZNSRootRegistrar, + ZNSRootRegistrar__factory, + ZNSRootRegistrarUpgradeMock__factory, +} from "../typechain"; +import { PaymentConfigStruct } from "../typechain/contracts/treasury/IZNSTreasury"; +import { runZnsCampaign } from "../src/deploy/zns-campaign"; +import { getProxyImplAddress } from "./helpers/utils"; +import { upgrades } from "hardhat"; +import { MongoDBAdapter } from "../src/deploy/db/mongo-adapter/mongo-adapter"; +import { getConfig } from "../src/deploy/campaign/environments"; + +require("@nomicfoundation/hardhat-chai-matchers"); + + +// This is the only test converted to use the new Campaign, other +// contract specific tests are using `deployZNS()` helper +describe("ZNSRootRegistrar", () => { + let deployer : SignerWithAddress; + let user : SignerWithAddress; + let governor : SignerWithAddress; + let admin : SignerWithAddress; + let randomUser : SignerWithAddress; + + let zns : TZNSContractState; + let zeroVault : SignerWithAddress; + let operator : SignerWithAddress; + let userBalanceInitial : bigint; + + let mongoAdapter : MongoDBAdapter; + + const defaultDomain = normalizeName("wilder"); + + beforeEach(async () => { + // zeroVault address is used to hold the fee charged to the user when registering + [deployer, zeroVault, user, operator, governor, admin, randomUser] = await hre.ethers.getSigners(); + + const config : IDeployCampaignConfig = await getConfig({ + deployer, + zeroVaultAddress: zeroVault.address, + governors: [deployer.address, governor.address], + admins: [deployer.address, admin.address], + }); + + const campaign = await runZnsCampaign({ + config, + }); + + zns = campaign.state.contracts; + + mongoAdapter = campaign.dbAdapter; + + await zns.meowToken.connect(deployer).approve( + await zns.treasury.getAddress(), + ethers.MaxUint256 + ); + + userBalanceInitial = ethers.parseEther("100000000000"); + // Give funds to user + await zns.meowToken.connect(user).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + await zns.meowToken.mint(user.address, userBalanceInitial); + }); + + afterEach(async () => { + await mongoAdapter.dropDB(); + }); + + it("Sets the payment config when provided with the domain registration", async () => { + const tokenURI = "https://example.com/817c64af"; + const distrConfig : IDistributionConfig = { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }; + + await zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + await zns.addressResolver.getAddress(), + tokenURI, + distrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ); + + const domainHash = hashDomainLabel(defaultDomain); + const config = await zns.treasury.paymentConfigs(domainHash); + expect(config.token).to.eq(await zns.meowToken.getAddress()); + expect(config.beneficiary).to.eq(user.address); + }); + + it("Does not set the payment config when the beneficiary is the zero address", async () => { + const tokenURI = "https://example.com/817c64af"; + const distrConfig : IDistributionConfig = { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }; + + await zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + await zns.addressResolver.getAddress(), + tokenURI, + distrConfig, + paymentConfigEmpty + ); + + const domainHash = hashDomainLabel(defaultDomain); + const config = await zns.treasury.paymentConfigs(domainHash); + expect(config.token).to.eq(ethers.ZeroAddress); + expect(config.beneficiary).to.eq(ethers.ZeroAddress); + }); + + it("Gas tests", async () => { + const tokenURI = "https://example.com/817c64af"; + const distrConfig : IDistributionConfig = { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }; + + await zns.rootRegistrar.connect(deployer).registerRootDomain( + defaultDomain, + deployer.address, + tokenURI, + distrConfig, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + const domainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + + // Registering as deployer (owner of parent) and user is different gas values + await zns.subRegistrar.connect(deployer).registerSubdomain( + domainHash, + "subdomain", + deployer.address, + tokenURI, + distrConfigEmpty, + paymentConfigEmpty, + ); + + const candidates = [ + deployer.address, + user.address, + governor.address, + admin.address, + randomUser.address, + ]; + + const allowed = [ + true, + true, + true, + true, + true, + ]; + + await zns.subRegistrar.updateMintlistForDomain( + domainHash, + candidates, + allowed + ); + }); + + it("Should NOT initialize the implementation contract", async () => { + const factory = new ZNSRootRegistrar__factory(deployer); + const impl = await getProxyImplAddress(await zns.rootRegistrar.getAddress()); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + const implContract = factory.attach(impl) as ZNSRootRegistrar; + + await expect( + implContract.initialize( + operator.address, + operator.address, + operator.address, + operator.address, + operator.address, + ) + ).to.be.revertedWith(INITIALIZED_ERR); + }); + + it("Allows transfer of 0x0 domain ownership after deployment", async () => { + await zns.registry.updateDomainOwner(ethers.ZeroHash, user.address); + expect(await zns.registry.getDomainOwner(ethers.ZeroHash)).to.equal(user.address); + }); + + it("Confirms a new 0x0 owner can modify the configs in the treasury and curve pricer", async () => { + await zns.registry.updateDomainOwner(ethers.ZeroHash, user.address); + + const newTreasuryConfig : PaymentConfigStruct = { + token: zeroVault.address, // Just needs to be a different address + beneficiary: user.address, + }; + + // Modify the treasury + const treasuryTx = await zns.treasury.connect(user).setPaymentConfig(ethers.ZeroHash, newTreasuryConfig); + + await expect(treasuryTx).to.emit( + zns.treasury, + "BeneficiarySet" + ).withArgs( + ethers.ZeroHash, + user.address + ); + await expect(treasuryTx).to.emit( + zns.treasury, + "PaymentTokenSet" + ).withArgs( + ethers.ZeroHash, + zeroVault.address + ); + + // Modify the curve pricer + const newPricerConfig = { + baseLength: BigInt("6"), + maxLength: BigInt("35"), + maxPrice: ethers.parseEther("150"), + minPrice: ethers.parseEther("10"), + precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, + feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, + isSet: true, + }; + + const pricerTx = await zns.curvePricer.connect(user).setPriceConfig( + ethers.ZeroHash, + newPricerConfig, + ); + + await expect(pricerTx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( + ethers.ZeroHash, + newPricerConfig.maxPrice, + newPricerConfig.minPrice, + newPricerConfig.maxLength, + newPricerConfig.baseLength, + newPricerConfig.precisionMultiplier, + newPricerConfig.feePercentage, + ); + }); + + it("Confirms a user has funds and allowance for the Registrar", async () => { + const balance = await zns.meowToken.balanceOf(user.address); + expect(balance).to.eq(userBalanceInitial); + + const allowance = await zns.meowToken.allowance(user.address, await zns.treasury.getAddress()); + expect(allowance).to.eq(ethers.MaxUint256); + }); + + it("Should revert when initialize() without ADMIN_ROLE", async () => { + const userHasAdmin = await zns.accessController.hasRole(ADMIN_ROLE, user.address); + expect(userHasAdmin).to.be.false; + + const registrarFactory = new ZNSRootRegistrar__factory(user); + + const tx = upgrades.deployProxy( + registrarFactory, + [ + await zns.accessController.getAddress(), + await zns.registry.getAddress(), + await zns.curvePricer.getAddress(), + await zns.treasury.getAddress(), + await zns.domainToken.getAddress(), + ], + { + kind: "uups", + } + ); + + await expect(tx).to.be.revertedWith(getAccessRevertMsg(user.address, ADMIN_ROLE)); + }); + + it("Should NOT initialize twice", async () => { + const tx = zns.rootRegistrar.connect(deployer).initialize( + await zns.accessController.getAddress(), + randomUser.address, + randomUser.address, + randomUser.address, + randomUser.address, + ); + + await expect(tx).to.be.revertedWith("Initializable: contract is already initialized"); + }); + + describe("General functionality", () => { + it("#coreRegister() should revert if called by address without REGISTRAR_ROLE", async () => { + const isRegistrar = await zns.accessController.hasRole(REGISTRAR_ROLE, randomUser.address); + expect(isRegistrar).to.be.false; + + await expect( + zns.rootRegistrar.connect(randomUser).coreRegister({ + parentHash: ethers.ZeroHash, + domainHash: ethers.ZeroHash, + label: "randomname", + registrant: ethers.ZeroAddress, + price: "0", + stakeFee: "0", + domainAddress: ethers.ZeroAddress, + tokenURI: "", + isStakePayment: false, + paymentConfig: paymentConfigEmpty, + }) + ).to.be.revertedWith( + getAccessRevertMsg(randomUser.address, REGISTRAR_ROLE) + ); + }); + + it("#isOwnerOf() returns correct bools", async () => { + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + const tokenId = BigInt(domainHash); + + const isOwnerOfBothUser = await zns.rootRegistrar.isOwnerOf( + domainHash, + user.address, + OwnerOf.BOTH + ); + expect(isOwnerOfBothUser).to.be.true; + + const isOwnerOfBothRandom = await zns.rootRegistrar.isOwnerOf( + domainHash, + randomUser.address, + OwnerOf.BOTH + ); + expect(isOwnerOfBothRandom).to.be.false; + + // transfer token + await zns.domainToken.connect(user).transferFrom(user.address, randomUser.address, tokenId); + const isOwnerOfTokenUser = await zns.rootRegistrar.isOwnerOf( + domainHash, + user.address, + OwnerOf.TOKEN + ); + expect(isOwnerOfTokenUser).to.be.false; + + const isOwnerOfTokenRandom = await zns.rootRegistrar.isOwnerOf( + domainHash, + randomUser.address, + OwnerOf.TOKEN + ); + expect(isOwnerOfTokenRandom).to.be.true; + + const isOwnerOfNameUser = await zns.rootRegistrar.isOwnerOf( + domainHash, + user.address, + OwnerOf.NAME + ); + expect(isOwnerOfNameUser).to.be.true; + + const isOwnerOfNameRandom = await zns.rootRegistrar.isOwnerOf( + domainHash, + randomUser.address, + OwnerOf.NAME + ); + expect(isOwnerOfNameRandom).to.be.false; + + await expect( + zns.rootRegistrar.isOwnerOf(domainHash, user.address, 3) + ).to.be.reverted; + }); + + it("#setSubRegistrar() should revert if called by address without ADMIN_ROLE", async () => { + const isAdmin = await zns.accessController.hasRole(ADMIN_ROLE, randomUser.address); + expect(isAdmin).to.be.false; + + await expect( + zns.rootRegistrar.connect(randomUser).setSubRegistrar(randomUser.address) + ).to.be.revertedWith( + getAccessRevertMsg(randomUser.address, ADMIN_ROLE) + ); + }); + + it("#setSubRegistrar() should set the correct address", async () => { + await zns.rootRegistrar.connect(admin).setSubRegistrar(randomUser.address); + + expect( + await zns.rootRegistrar.subRegistrar() + ).to.equal(randomUser.address); + }); + + it("#setSubRegistrar() should NOT set the address to zero address", async () => { + await expect( + zns.rootRegistrar.connect(admin).setSubRegistrar(ethers.ZeroAddress) + ).to.be.revertedWith( + "ZNSRootRegistrar: subRegistrar_ is 0x0 address" + ); + }); + }); + + describe("Registers a root domain", () => { + it("Can NOT register a TLD with an empty name", async () => { + const emptyName = ""; + + await expect( + defaultRootRegistration({ + user: deployer, + zns, + domainName: emptyName, + }) + ).to.be.revertedWith(INVALID_LENGTH_ERR); + }); + + it("Can register a TLD with characters [a-z0-9-]", async () => { + const letters = "world"; + const lettersHash = hashDomainLabel(letters); + + const alphaNumeric = "0x0dwidler0x0"; + const alphaNumericHash = hashDomainLabel(alphaNumeric); + + const withHyphen = "0x0-dwidler-0x0"; + const withHyphenHash = hashDomainLabel(withHyphen); + + const tx1 = zns.rootRegistrar.connect(deployer).registerRootDomain( + letters, + ethers.ZeroAddress, + DEFAULT_TOKEN_URI, + distrConfigEmpty, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + await expect(tx1).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.ZeroHash, + lettersHash, + letters, + BigInt(lettersHash), + DEFAULT_TOKEN_URI, + deployer.address, + ethers.ZeroAddress, + ); + + const tx2 = zns.rootRegistrar.connect(deployer).registerRootDomain( + alphaNumeric, + ethers.ZeroAddress, + DEFAULT_TOKEN_URI, + distrConfigEmpty, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + await expect(tx2).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.ZeroHash, + alphaNumericHash, + alphaNumeric, + BigInt(alphaNumericHash), + DEFAULT_TOKEN_URI, + deployer.address, + ethers.ZeroAddress, + ); + + const tx3 = zns.rootRegistrar.connect(deployer).registerRootDomain( + withHyphen, + ethers.ZeroAddress, + DEFAULT_TOKEN_URI, + distrConfigEmpty, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + await expect(tx3).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.ZeroHash, + withHyphenHash, + withHyphen, + BigInt(withHyphenHash), + DEFAULT_TOKEN_URI, + deployer.address, + ethers.ZeroAddress, + ); + }); + + it("Fails for domains that use any invalid character", async () => { + // Valid names must match the pattern [a-z0-9] + const nameA = "WILDER"; + const nameB = "!?w1Id3r!?"; + const nameC = "!%$#^*?!#👍3^29"; + const nameD = "wo.rld"; + + await expect( + defaultRootRegistration({ + user: deployer, + zns, + domainName: nameA, + }) + ).to.be.revertedWith(INVALID_NAME_ERR); + + await expect( + defaultRootRegistration({ + user: deployer, + zns, + domainName: nameB, + }) + ).to.be.revertedWith(INVALID_NAME_ERR); + + await expect( + defaultRootRegistration({ + user: deployer, + zns, + domainName: nameC, + }) + ).to.be.revertedWith(INVALID_NAME_ERR); + + await expect( + defaultRootRegistration({ + user: deployer, + zns, + domainName: nameD, + }) + ).to.be.revertedWith(INVALID_NAME_ERR); + }); + + // eslint-disable-next-line max-len + it("Successfully registers a domain without a resolver or resolver content and fires a #DomainRegistered event", async () => { + const tokenURI = "https://example.com/817c64af"; + const tx = await zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + ethers.ZeroAddress, + tokenURI, + distrConfigEmpty, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + const hashFromTS = hashDomainLabel(defaultDomain); + + await expect(tx).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.ZeroHash, + hashFromTS, + defaultDomain, + BigInt(hashFromTS), + tokenURI, + user.address, + ethers.ZeroAddress, + ); + + const tokenURISC = await zns.domainToken.tokenURI(hashFromTS); + expect(tokenURISC).to.eq(tokenURI); + }); + + it("Successfully registers a domain with distrConfig and adds it to state properly", async () => { + const distrConfig = { + pricerContract: await zns.fixedPricer.getAddress(), + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }; + const tokenURI = "https://example.com/817c64af"; + + await zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + ethers.ZeroAddress, + tokenURI, + distrConfig, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + const { + pricerContract, + accessType, + paymentType, + } = await zns.subRegistrar.distrConfigs(domainHash); + + expect(pricerContract).to.eq(distrConfig.pricerContract); + expect(paymentType).to.eq(distrConfig.paymentType); + expect(accessType).to.eq(distrConfig.accessType); + + const tokenURISC = await zns.domainToken.tokenURI(domainHash); + expect(tokenURISC).to.eq(tokenURI); + }); + + it("Stakes and saves the correct amount and token, takes the correct fee and sends fee to Zero Vault", async () => { + const balanceBeforeUser = await zns.meowToken.balanceOf(user.address); + const balanceBeforeVault = await zns.meowToken.balanceOf(zeroVault.address); + + // Deploy "wilder" with default configuration + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + const { + totalPrice, + expectedPrice, + stakeFee, + } = getPriceObject(defaultDomain, DEFAULT_PRICE_CONFIG); + + await checkBalance({ + token: zns.meowToken as IERC20, + balanceBefore: balanceBeforeUser, + userAddress: user.address, + target: totalPrice, + }); + + await checkBalance({ + token: zns.meowToken as IERC20, + balanceBefore: balanceBeforeVault, + userAddress: zeroVault.address, + target: stakeFee, + shouldDecrease: false, + }); + + const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); + + expect(staked).to.eq(expectedPrice); + expect(token).to.eq(await zns.meowToken.getAddress()); + }); + + it("Sets the correct data in Registry", async () => { + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + + const namehashRef = hashDomainLabel(defaultDomain); + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + expect(domainHash).to.eq(namehashRef); + + const { + owner: ownerFromReg, + resolver: resolverFromReg, + } = await zns.registry.getDomainRecord(domainHash); + + expect(ownerFromReg).to.eq(user.address); + expect(resolverFromReg).to.eq(await zns.addressResolver.getAddress()); + }); + + it("Fails when the user does not have enough funds", async () => { + const balance = await zns.meowToken.balanceOf(user.address); + await zns.meowToken.connect(user).transfer(randomUser.address, balance); + + const tx = defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + await expect(tx).to.be.revertedWith("ERC20: transfer amount exceeds balance"); + }); + + it("Disallows creation of a duplicate domain", async () => { + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + const failTx = defaultRootRegistration({ + user: deployer, + zns, + domainName: defaultDomain, + }); + + await expect(failTx).to.be.revertedWith("ZNSRootRegistrar: Domain already exists"); + }); + + it("Successfully registers a domain without resolver content", async () => { + const tx = zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + ethers.ZeroAddress, + DEFAULT_TOKEN_URI, + distrConfigEmpty, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + await expect(tx).to.not.be.reverted; + }); + + it("Records the correct domain hash", async () => { + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + const exists = await zns.registry.exists(domainHash); + expect(exists).to.be.true; + expect(domainHash).to.eq(hashDomainLabel(defaultDomain)); + }); + + it("Creates and finds the correct tokenId", async () => { + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + + const tokenId = BigInt( + await getDomainHashFromEvent({ + zns, + user, + }) + ); + const owner = await zns.domainToken.ownerOf(tokenId); + expect(owner).to.eq(user.address); + }); + + it("Resolves the correct address from the domain", async () => { + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + domainContent: await zns.rootRegistrar.getAddress(), + }); + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + const resolvedAddress = await zns.addressResolver.resolveDomainAddress(domainHash); + expect(resolvedAddress).to.eq(await zns.rootRegistrar.getAddress()); + }); + + it("Should NOT charge any tokens if price and/or stake fee is 0", async () => { + // set config on CurvePricer for the price to be 0 + await zns.curvePricer.connect(deployer).setMaxPrice(ethers.ZeroHash, "0"); + await zns.curvePricer.connect(deployer).setMinPrice(ethers.ZeroHash, "0"); + + const userBalanceBefore = await zns.meowToken.balanceOf(user.address); + const vaultBalanceBefore = await zns.meowToken.balanceOf(zeroVault.address); + + // register a domain + await zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + ethers.ZeroAddress, + DEFAULT_TOKEN_URI, + distrConfigEmpty, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + const userBalanceAfter = await zns.meowToken.balanceOf(user.address); + const vaultBalanceAfter = await zns.meowToken.balanceOf(zeroVault.address); + + expect(userBalanceBefore).to.eq(userBalanceAfter); + expect(vaultBalanceBefore).to.eq(vaultBalanceAfter); + + // check existence in Registry + const domainHash = hashDomainLabel(defaultDomain); + const exists = await zns.registry.exists(domainHash); + expect(exists).to.be.true; + + // make sure no transfers happened + const transferEventFilter = zns.meowToken.filters.Transfer( + user.address, + ); + const events = await zns.meowToken.queryFilter(transferEventFilter); + expect(events.length).to.eq(0); + }); + }); + + describe("Reclaiming Domains", () => { + it("Can reclaim name/stake if Token is owned", async () => { + // Register Top level + await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + const tokenId = BigInt(domainHash); + const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); + + // Transfer the domain token + await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); + + // Verify owner in registry + const originalOwner = await zns.registry.connect(deployer).getDomainOwner(domainHash); + expect(originalOwner).to.equal(deployer.address); + + // Reclaim the Domain + await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); + + // Verify domain token is still owned + const owner = await zns.domainToken.connect(user).ownerOf(tokenId); + expect(owner).to.equal(user.address); + + // Verify domain is owned in registry + const registryOwner = await zns.registry.connect(user).getDomainOwner(domainHash); + expect(registryOwner).to.equal(user.address); + + // Verify same amount is staked + const { amount: stakedAfterReclaim, token: tokenAfterReclaim } = await zns.treasury.stakedForDomain(domainHash); + expect(staked).to.equal(stakedAfterReclaim); + expect(tokenAfterReclaim).to.equal(await zns.meowToken.getAddress()); + expect(token).to.equal(tokenAfterReclaim); + }); + + it("Reclaiming domain token emits DomainReclaimed event", async () => { + await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + const tokenId = BigInt(domainHash); + + // Transfer the domain token + await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); + // Reclaim the Domain + const tx = zns.rootRegistrar.connect(user).reclaimDomain(domainHash); + await expect(tx).to.emit(zns.rootRegistrar, "DomainReclaimed").withArgs( + domainHash, + user.address + ); + }); + + it("Cannot reclaim name/stake if token is not owned", async () => { + await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + // Reclaim the Domain + const tx = zns.rootRegistrar.connect(user).reclaimDomain(domainHash); + + // Verify Domain is not reclaimed + await expect(tx).to.be.revertedWith(NOT_TOKEN_OWNER_RAR_ERR); + + // Verify domain is not owned in registrar + const registryOwner = await zns.registry.connect(user).getDomainOwner(domainHash); + expect(registryOwner).to.equal(deployer.address); + }); + + it("Cannot reclaim if domain does not exist", async () => { + const domainHash = "0xd34cfa279afd55afc6aa9c00aa5d01df60179840a93d10eed730058b8dd4146c"; + // Reclaim the Domain + const tx = zns.rootRegistrar.connect(user).reclaimDomain(domainHash); + + // Verify Domain is not reclaimed + await expect(tx).to.be.revertedWith(INVALID_TOKENID_ERC_ERR); + }); + + it("Domain Token can be reclaimed, transferred, and then reclaimed again", async () => { + // Register Top level + await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + const tokenId = BigInt(domainHash); + const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); + + // Transfer the domain token + await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); + + // Reclaim the Domain + await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); + // Verify domain token is still owned + let owner = await zns.domainToken.connect(user).ownerOf(tokenId); + expect(owner).to.equal(user.address); + + // Transfer the domain token back + await zns.domainToken.connect(user).transferFrom(user.address, deployer.address, tokenId); + + // Reclaim the Domain again + await zns.rootRegistrar.connect(deployer).reclaimDomain(domainHash); + + // Verify domain token is owned + owner = await zns.domainToken.connect(deployer).ownerOf(tokenId); + expect(owner).to.equal(deployer.address); + + // Verify domain is owned in registrar + const registryOwner = await zns.registry.connect(deployer).getDomainOwner(domainHash); + expect(registryOwner).to.equal(deployer.address); + + // Verify same amount is staked + const { amount: stakedAfterReclaim, token: tokenAfterReclaim } = await zns.treasury.stakedForDomain(domainHash); + expect(staked).to.equal(stakedAfterReclaim); + expect(tokenAfterReclaim).to.equal(await zns.meowToken.getAddress()); + expect(token).to.equal(tokenAfterReclaim); + }); + + it("Can revoke and unstake after reclaiming", async () => { + // Verify Balance + const balance = await zns.meowToken.balanceOf(user.address); + expect(balance).to.eq(userBalanceInitial); + + // Register Top level + await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + const tokenId = BigInt(domainHash); + + // Validated staked values + const { + expectedPrice: expectedStaked, + } = getPriceObject(defaultDomain, DEFAULT_PRICE_CONFIG); + const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); + expect(staked).to.eq(expectedStaked); + expect(token).to.eq(await zns.meowToken.getAddress()); + + // Transfer the domain token + await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); + + // Reclaim the Domain + await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); + + // Revoke the Domain + await zns.rootRegistrar.connect(user).revokeDomain(domainHash); + + // Validated funds are unstaked + const { amount: finalstaked, token: finalToken } = await zns.treasury.stakedForDomain(domainHash); + expect(finalstaked).to.equal(BigInt("0")); + expect(finalToken).to.equal(ethers.ZeroAddress); + + // Verify final balances + const computedFinalBalance = balance + staked; + const finalBalance = await zns.meowToken.balanceOf(user.address); + expect(computedFinalBalance).to.equal(finalBalance); + }); + }); + + describe("Revoking Domains", () => { + it("Revokes a Top level Domain, locks distribution and removes mintlist", async () => { + // Register Top level + await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + distrConfig: { + pricerContract: await zns.fixedPricer.getAddress(), + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + }); + + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + // add mintlist to check revocation + await zns.subRegistrar.connect(user).updateMintlistForDomain( + domainHash, + [user.address, zeroVault.address], + [true, true] + ); + + const ogPrice = BigInt(135); + await zns.fixedPricer.connect(user).setPriceConfig( + domainHash, + { + price: ogPrice, + feePercentage: BigInt(0), + isSet: true, + } + ); + expect(await zns.fixedPricer.getPrice(domainHash, defaultDomain, false)).to.eq(ogPrice); + + const tokenId = BigInt( + await getDomainHashFromEvent({ + zns, + user, + }) + ); + + // Revoke the domain and then verify + await zns.rootRegistrar.connect(user).revokeDomain(domainHash); + + // Verify token has been burned + const ownerOfTx = zns.domainToken.connect(user).ownerOf(tokenId); + await expect(ownerOfTx).to.be.revertedWith( + INVALID_TOKENID_ERC_ERR + ); + + // Verify Domain Record Deleted + const exists = await zns.registry.exists(domainHash); + expect(exists).to.be.false; + + // validate access type has been set to LOCKED + const { accessType } = await zns.subRegistrar.distrConfigs(domainHash); + expect(accessType).to.eq(AccessType.LOCKED); + + // validate mintlist has been removed + expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, user.address)).to.be.false; + expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, zeroVault.address)).to.be.false; + }); + + it("Cannot revoke a domain that doesnt exist", async () => { + // Register Top level + const fakeHash = "0xd34cfa279afd55afc6aa9c00aa5d01df60179840a93d10eed730058b8dd4146c"; + const exists = await zns.registry.exists(fakeHash); + expect(exists).to.be.false; + + // Verify transaction is reverted + const tx = zns.rootRegistrar.connect(user).revokeDomain(fakeHash); + await expect(tx).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); + }); + + it("Revoking domain unstakes", async () => { + // Verify Balance + const balance = await zns.meowToken.balanceOf(user.address); + expect(balance).to.eq(userBalanceInitial); + + // Register Top level + await defaultRootRegistration({ user, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + // Validated staked values + const { + expectedPrice: expectedStaked, + stakeFee: expectedStakeFee, + } = getPriceObject(defaultDomain, DEFAULT_PRICE_CONFIG); + const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); + expect(staked).to.eq(expectedStaked); + expect(token).to.eq(await zns.meowToken.getAddress()); + + // Get balance after staking + const balanceAfterStaking = await zns.meowToken.balanceOf(user.address); + + // Revoke the domain + await zns.rootRegistrar.connect(user).revokeDomain(domainHash); + + // Validated funds are unstaked + const { amount: finalstaked, token: finalToken } = await zns.treasury.stakedForDomain(domainHash); + expect(finalstaked).to.equal(BigInt("0")); + expect(finalToken).to.equal(ethers.ZeroAddress); + + // Verify final balances + const computedBalanceAfterStaking = balanceAfterStaking + staked; + const balanceMinusFee = balance - expectedStakeFee; + expect(computedBalanceAfterStaking).to.equal(balanceMinusFee); + const finalBalance = await zns.meowToken.balanceOf(user.address); + expect(computedBalanceAfterStaking).to.equal(finalBalance); + }); + + it("Cannot revoke if Name is owned by another user", async () => { + // Register Top level + await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const parentDomainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + const owner = await zns.registry.connect(user).getDomainOwner(parentDomainHash); + expect(owner).to.not.equal(user.address); + + // Try to revoke domain + const tx = zns.rootRegistrar.connect(user).revokeDomain(parentDomainHash); + await expect(tx).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); + }); + + it("No one can revoke if Token and Name have different owners", async () => { + // Register Top level + await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const parentDomainHash = await getDomainHashFromEvent({ + zns, + user: deployer, + }); + const owner = await zns.registry.connect(user).getDomainOwner(parentDomainHash); + expect(owner).to.not.equal(user.address); + + const tokenId = BigInt(parentDomainHash); + + await zns.domainToken.transferFrom(deployer.address, user.address, tokenId); + + // Try to revoke domain as a new owner of the token + const tx = zns.rootRegistrar.connect(user).revokeDomain(parentDomainHash); + await expect(tx).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); + + const tx2 = zns.rootRegistrar.connect(deployer).revokeDomain(parentDomainHash); + await expect(tx2).to.be.revertedWith(NOT_BOTH_OWNER_RAR_ERR); + }); + + it("After domain has been revoked, an old operator can NOT access Registry", async () => { + // Register Top level + await defaultRootRegistration({ user, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + + // assign an operator + await zns.registry.connect(user).setOwnersOperator(operator.address, true); + + // Revoke the domain + await zns.rootRegistrar.connect(user).revokeDomain(domainHash); + + // check operator access to the revoked domain + const tx2 = zns.registry + .connect(operator) + .updateDomainOwner( + domainHash, + operator.address + ); + await expect(tx2).to.be.revertedWith( + ONLY_OWNER_REGISTRAR_REG_ERR + ); + + const tx3 = zns.registry + .connect(operator) + .updateDomainRecord( + domainHash, + user.address, + operator.address + ); + await expect(tx3).to.be.revertedWith( + ONLY_NAME_OWNER_REG_ERR + ); + + const tx4 = zns.registry + .connect(operator) + .updateDomainResolver( + domainHash, + zeroVault.address + ); + await expect(tx4).to.be.revertedWith( + NOT_AUTHORIZED_REG_ERR + ); + }); + }); + + describe("State Setters", () => { + describe("#setAccessController", () => { + it("Should set AccessController and fire AccessControllerSet event", async () => { + const currentAC = await zns.rootRegistrar.getAccessController(); + const tx = await zns.rootRegistrar.connect(deployer).setAccessController(randomUser.address); + const newAC = await zns.rootRegistrar.getAccessController(); + + await expect(tx).to.emit(zns.rootRegistrar, "AccessControllerSet").withArgs(randomUser.address); + + expect(newAC).to.equal(randomUser.address); + expect(currentAC).to.not.equal(newAC); + }); + + it("Should revert if not called by ADMIN", async () => { + const tx = zns.rootRegistrar.connect(user).setAccessController(randomUser.address); + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(user.address, ADMIN_ROLE) + ); + }); + + it("Should revert if new AccessController is address zero", async () => { + const tx = zns.rootRegistrar.connect(deployer).setAccessController(ethers.ZeroAddress); + await expect(tx).to.be.revertedWith("AC: _accessController is 0x0 address"); + }); + }); + + describe("#setRegistry", () => { + it("Should set ZNSRegistry and fire RegistrySet event", async () => { + const currentRegistry = await zns.rootRegistrar.registry(); + const tx = await zns.rootRegistrar.connect(deployer).setRegistry(randomUser.address); + const newRegistry = await zns.rootRegistrar.registry(); + + await expect(tx).to.emit(zns.rootRegistrar, "RegistrySet").withArgs(randomUser.address); + + expect(newRegistry).to.equal(randomUser.address); + expect(currentRegistry).to.not.equal(newRegistry); + }); + + it("Should revert if not called by ADMIN", async () => { + const tx = zns.rootRegistrar.connect(user).setRegistry(randomUser.address); + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(user.address, ADMIN_ROLE) + ); + }); + + it("Should revert if ZNSRegistry is address zero", async () => { + const tx = zns.rootRegistrar.connect(deployer).setRegistry(ethers.ZeroAddress); + await expect(tx).to.be.revertedWith("ARegistryWired: _registry can not be 0x0 address"); + }); + }); + + describe("#setTreasury", () => { + it("Should set Treasury and fire TreasurySet event", async () => { + const currentTreasury = await zns.rootRegistrar.treasury(); + const tx = await zns.rootRegistrar.connect(deployer).setTreasury(randomUser.address); + const newTreasury = await zns.rootRegistrar.treasury(); + + await expect(tx).to.emit(zns.rootRegistrar, "TreasurySet").withArgs(randomUser.address); + + expect(newTreasury).to.equal(randomUser.address); + expect(currentTreasury).to.not.equal(newTreasury); + }); + + it("Should revert if not called by ADMIN", async () => { + const tx = zns.rootRegistrar.connect(user).setTreasury(randomUser.address); + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(user.address, ADMIN_ROLE) + ); + }); + + it("Should revert if Treasury is address zero", async () => { + const tx = zns.rootRegistrar.connect(deployer).setTreasury(ethers.ZeroAddress); + await expect(tx).to.be.revertedWith("ZNSRootRegistrar: treasury_ is 0x0 address"); + }); + }); + + describe("#setDomainToken", () => { + it("Should set DomainToken and fire DomainTokenSet event", async () => { + const currentToken = await zns.rootRegistrar.domainToken(); + const tx = await zns.rootRegistrar.connect(deployer).setDomainToken(randomUser.address); + const newToken = await zns.rootRegistrar.domainToken(); + + await expect(tx).to.emit(zns.rootRegistrar, "DomainTokenSet").withArgs(randomUser.address); + + expect(newToken).to.equal(randomUser.address); + expect(currentToken).to.not.equal(newToken); + }); + + it("Should revert if not called by ADMIN", async () => { + const tx = zns.rootRegistrar.connect(user).setDomainToken(randomUser.address); + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(user.address, ADMIN_ROLE) + ); + }); + + it("Should revert if DomainToken is address zero", async () => { + const tx = zns.rootRegistrar.connect(deployer).setDomainToken(ethers.ZeroAddress); + await expect(tx).to.be.revertedWith("ZNSRootRegistrar: domainToken_ is 0x0 address"); + }); + }); + }); + + describe("UUPS", () => { + it("Allows an authorized user to upgrade the contract", async () => { + // Confirm deployer has the correct role first + await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; + + const registrarFactory = new ZNSRootRegistrar__factory(deployer); + const registrar = await registrarFactory.deploy(); + await registrar.waitForDeployment(); + + const upgradeTx = zns.rootRegistrar.connect(deployer).upgradeTo(await registrar.getAddress()); + await expect(upgradeTx).to.not.be.reverted; + }); + + it("Fails to upgrade when an unauthorized users calls", async () => { + const registrarFactory = new ZNSRootRegistrar__factory(deployer); + const registrar = await registrarFactory.deploy(); + await registrar.waitForDeployment(); + + const tx = zns.rootRegistrar.connect(randomUser).upgradeTo(await registrar.getAddress()); + + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(randomUser.address, GOVERNOR_ROLE) + ); + }); + + it("Verifies that variable values are not changed in the upgrade process", async () => { + // Confirm deployer has the correct role first + await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; + + const registrarFactory = new ZNSRootRegistrarUpgradeMock__factory(deployer); + const registrar = await registrarFactory.deploy(); + await registrar.waitForDeployment(); + + const domainName = "world"; + const domainHash = hashDomainLabel(domainName); + + await zns.meowToken.connect(randomUser).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + await zns.meowToken.mint(randomUser.address, DEFAULT_PRICE_CONFIG.maxPrice); + + await zns.rootRegistrar.connect(randomUser).registerRootDomain( + domainName, + randomUser.address, + DEFAULT_TOKEN_URI, + distrConfigEmpty, + { + token: ethers.ZeroAddress, + beneficiary: ethers.ZeroAddress, + } + ); + + + const contractCalls = [ + zns.rootRegistrar.getAccessController(), + zns.rootRegistrar.registry(), + zns.rootRegistrar.treasury(), + zns.rootRegistrar.domainToken(), + zns.registry.exists(domainHash), + zns.treasury.stakedForDomain(domainHash), + zns.domainToken.name(), + zns.domainToken.symbol(), + zns.curvePricer.getPrice(ethers.ZeroHash, domainName, false), + ]; + + await validateUpgrade(deployer, zns.rootRegistrar, registrar, registrarFactory, contractCalls); + }); + }); +}); diff --git a/test/helpers/deploy-helpers.ts b/test/helpers/deploy-helpers.ts index 5484981ab..c962aa1f4 100644 --- a/test/helpers/deploy-helpers.ts +++ b/test/helpers/deploy-helpers.ts @@ -1,176 +1,176 @@ - -// For use in inegration test of deployment campaign -import * as hre from "hardhat"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { IDeployCampaignConfig, TLogger, TZNSContractState } from "../../src/deploy/campaign/types"; -import { ethers } from "ethers"; -import { IDistributionConfig } from "./types"; -import { expect } from "chai"; -import { hashDomainLabel, paymentConfigEmpty } from "."; -import { ICurvePriceConfig } from "../../src/deploy/missions/types"; - -export const approveBulk = async ( - signers : Array, - zns : TZNSContractState, -) => { - for (const signer of signers) { - // if (hre.network.name === "hardhat") { - const hasApproval = await zns.meowToken.allowance(signer.address, await zns.treasury.getAddress()); - - // To avoid resending the approval repeatedly we first check the allowance - if (hasApproval === BigInt(0)) { - const tx = await zns.meowToken.connect(signer).approve( - await zns.treasury.getAddress(), - ethers.MaxUint256, - ); - - await tx.wait(); - } - } -}; - -export const mintBulk = async ( - signers : Array, - amount : bigint, - zns : TZNSContractState, -) => { - for (const signer of signers) { - await zns.meowToken.connect(signer).mint( - signer.address, - amount - ); - } -}; - -export const getPriceBulk = async ( - domains : Array, - zns : TZNSContractState, - parentHashes ?: Array, -) => { - let index = 0; - const prices = []; - - for (const domain of domains) { - let parent; - if (parentHashes) { - parent = parentHashes[index]; - } else { - parent = ethers.ZeroHash; - } - - // temp, can do one call `getPRiceAndFee` but debugging where failure occurs - const price = await zns.curvePricer.getPrice(parent, domain, true); - const stakeFee = await zns.curvePricer.getFeeForPrice(parent, price); - - // TODO fix this to be one if statement - if (parentHashes) { - const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price + stakeFee); - - prices.push(price + stakeFee + protocolFee); - } else { - const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price); - - prices.push(price + protocolFee); - } - - - index++; - } - - return prices; -}; - -export const registerRootDomainBulk = async ( - signers : Array, - domains : Array, - config : IDeployCampaignConfig, - tokenUri : string, - distConfig : IDistributionConfig, - priceConfig : ICurvePriceConfig, - zns : TZNSContractState, - logger : TLogger, -) : Promise => { - let index = 0; - - for(const domain of domains) { - const balanceBefore = await zns.meowToken.balanceOf(signers[index].address); - const tx = await zns.rootRegistrar.connect(signers[index]).registerRootDomain( - domain, - config.zeroVaultAddress, - `${tokenUri}${index}`, - distConfig, - { - token: await zns.meowToken.getAddress(), - beneficiary: config.zeroVaultAddress, - } - ); - logger.info("Deploy transaction submitted, waiting..."); - if (hre.network.name !== "hardhat") { - await tx.wait(3); - logger.info(`Registered '${domain}' for ${signers[index].address} at tx: ${tx.hash}`); - } - - const balanceAfter = await zns.meowToken.balanceOf(signers[index].address); - const [price, protocolFee] = await zns.curvePricer.getPriceAndFee(ethers.ZeroHash, domain, true); - expect(balanceAfter).to.be.eq(balanceBefore - price - protocolFee); - - const domainHash = hashDomainLabel(domain); - expect(await zns.registry.exists(domainHash)).to.be.true; - - // TODO figure out if we want to do this on prod? - // To mint subdomains from this domain we must first set the price config and the payment config - await zns.curvePricer.connect(signers[index]).setPriceConfig(domainHash, priceConfig); - - index++; - } -}; - -export const registerSubdomainBulk = async ( - signers : Array, - parents : Array, - subdomains : Array, - subdomainHashes : Array, - domainAddress : string, - tokenUri : string, - distConfig : IDistributionConfig, - zns : TZNSContractState, - logger : TLogger, -) => { - let index = 0; - - for (const subdomain of subdomains) { - const balanceBefore = await zns.meowToken.balanceOf(signers[index].address); - const tx = await zns.subRegistrar.connect(signers[index]).registerSubdomain( - parents[index], - subdomain, - domainAddress, - `${tokenUri}${index}`, - distConfig, - paymentConfigEmpty - ); - - logger.info("Deploy transaction submitted, waiting..."); - - if (hre.network.name !== "hardhat") { - await tx.wait(3); - logger.info(`registered '${subdomain}' for ${signers[index].address} at tx: ${tx.hash}`); - } - - const balanceAfter = await zns.meowToken.balanceOf(signers[index].address); - - const owner = await zns.registry.getDomainOwner(parents[index]); - if (signers[index].address === owner) { - expect(balanceAfter).to.be.eq(balanceBefore); - } else { - const [price, stakeFee] = await zns.curvePricer.getPriceAndFee(parents[index], subdomain, true); - const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price + stakeFee); - - expect(balanceAfter).to.be.eq(balanceBefore - price - stakeFee - protocolFee); - } - - - expect(await zns.registry.exists(subdomainHashes[index])).to.be.true; - - index++; - } + +// For use in inegration test of deployment campaign +import * as hre from "hardhat"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { IDeployCampaignConfig, TLogger, TZNSContractState } from "../../src/deploy/campaign/types"; +import { ethers } from "ethers"; +import { IDistributionConfig } from "./types"; +import { expect } from "chai"; +import { hashDomainLabel, paymentConfigEmpty } from "."; +import { ICurvePriceConfig } from "../../src/deploy/missions/types"; + +export const approveBulk = async ( + signers : Array, + zns : TZNSContractState, +) => { + for (const signer of signers) { + // if (hre.network.name === "hardhat") { + const hasApproval = await zns.meowToken.allowance(signer.address, await zns.treasury.getAddress()); + + // To avoid resending the approval repeatedly we first check the allowance + if (hasApproval === BigInt(0)) { + const tx = await zns.meowToken.connect(signer).approve( + await zns.treasury.getAddress(), + ethers.MaxUint256, + ); + + await tx.wait(); + } + } +}; + +export const mintBulk = async ( + signers : Array, + amount : bigint, + zns : TZNSContractState, +) => { + for (const signer of signers) { + await zns.meowToken.connect(signer).mint( + signer.address, + amount + ); + } +}; + +export const getPriceBulk = async ( + domains : Array, + zns : TZNSContractState, + parentHashes ?: Array, +) => { + let index = 0; + const prices = []; + + for (const domain of domains) { + let parent; + if (parentHashes) { + parent = parentHashes[index]; + } else { + parent = ethers.ZeroHash; + } + + // temp, can do one call `getPRiceAndFee` but debugging where failure occurs + const price = await zns.curvePricer.getPrice(parent, domain, true); + const stakeFee = await zns.curvePricer.getFeeForPrice(parent, price); + + // TODO fix this to be one if statement + if (parentHashes) { + const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price + stakeFee); + + prices.push(price + stakeFee + protocolFee); + } else { + const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price); + + prices.push(price + protocolFee); + } + + + index++; + } + + return prices; +}; + +export const registerRootDomainBulk = async ( + signers : Array, + domains : Array, + config : IDeployCampaignConfig, + tokenUri : string, + distConfig : IDistributionConfig, + priceConfig : ICurvePriceConfig, + zns : TZNSContractState, + logger : TLogger, +) : Promise => { + let index = 0; + + for(const domain of domains) { + const balanceBefore = await zns.meowToken.balanceOf(signers[index].address); + const tx = await zns.rootRegistrar.connect(signers[index]).registerRootDomain( + domain, + config.zeroVaultAddress, + `${tokenUri}${index}`, + distConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: config.zeroVaultAddress, + } + ); + logger.info("Deploy transaction submitted, waiting..."); + if (hre.network.name !== "hardhat") { + await tx.wait(3); + logger.info(`Registered '${domain}' for ${signers[index].address} at tx: ${tx.hash}`); + } + + const balanceAfter = await zns.meowToken.balanceOf(signers[index].address); + const [price, protocolFee] = await zns.curvePricer.getPriceAndFee(ethers.ZeroHash, domain, true); + expect(balanceAfter).to.be.eq(balanceBefore - price - protocolFee); + + const domainHash = hashDomainLabel(domain); + expect(await zns.registry.exists(domainHash)).to.be.true; + + // TODO figure out if we want to do this on prod? + // To mint subdomains from this domain we must first set the price config and the payment config + await zns.curvePricer.connect(signers[index]).setPriceConfig(domainHash, priceConfig); + + index++; + } +}; + +export const registerSubdomainBulk = async ( + signers : Array, + parents : Array, + subdomains : Array, + subdomainHashes : Array, + domainAddress : string, + tokenUri : string, + distConfig : IDistributionConfig, + zns : TZNSContractState, + logger : TLogger, +) => { + let index = 0; + + for (const subdomain of subdomains) { + const balanceBefore = await zns.meowToken.balanceOf(signers[index].address); + const tx = await zns.subRegistrar.connect(signers[index]).registerSubdomain( + parents[index], + subdomain, + domainAddress, + `${tokenUri}${index}`, + distConfig, + paymentConfigEmpty + ); + + logger.info("Deploy transaction submitted, waiting..."); + + if (hre.network.name !== "hardhat") { + await tx.wait(3); + logger.info(`registered '${subdomain}' for ${signers[index].address} at tx: ${tx.hash}`); + } + + const balanceAfter = await zns.meowToken.balanceOf(signers[index].address); + + const owner = await zns.registry.getDomainOwner(parents[index]); + if (signers[index].address === owner) { + expect(balanceAfter).to.be.eq(balanceBefore); + } else { + const [price, stakeFee] = await zns.curvePricer.getPriceAndFee(parents[index], subdomain, true); + const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price + stakeFee); + + expect(balanceAfter).to.be.eq(balanceBefore - price - stakeFee - protocolFee); + } + + + expect(await zns.registry.exists(subdomainHashes[index])).to.be.true; + + index++; + } }; \ No newline at end of file From 38f7ce929bd0990e828eca65538fd91f2231679c Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 21 Mar 2025 15:38:53 -0700 Subject: [PATCH 003/114] make pausable registry --- .../registry/IZNSRegistryPausable.sol | 155 ++++++++ .../registry/ZNSRegistryPausable.sol | 337 ++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 contracts/zns-pausable/registry/IZNSRegistryPausable.sol create mode 100644 contracts/zns-pausable/registry/ZNSRegistryPausable.sol diff --git a/contracts/zns-pausable/registry/IZNSRegistryPausable.sol b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol new file mode 100644 index 000000000..363eba0eb --- /dev/null +++ b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + + +/** + * @notice The `DomainRecord` struct is meant to hold relevant information + * about a domain, such as its owner and resolver. + * - `owner` (address): The owner of the domain (also called the owner of the Name). + * - `resolver` (address): The address of the Resolver contract where this domain's source records are stored. + * + * In the future, there will be multiple Resolver contracts that support different types of sources. + * Currently, only the `ZNSAddressResolver` is implemented. + */ +interface IZNSRegistryPausable { + + /** + * @notice Description of a domain record, pointing to the + * owner address of that record as well as the address of + * its resolver + */ + struct DomainRecord { + address owner; + address resolver; + } + + /** + * @notice Emits when ownership of a domain is modified in ``records`` + * @param domainHash the hash of a domain's name + * @param owner The new domain owner + */ + event DomainOwnerSet( + bytes32 indexed domainHash, + address indexed owner + ); + + /** + * @notice Emit when a domain's resolver is modified in ``records`` + * @param domainHash the hash of a domain's name + * @param resolver The new resolver address + */ + event DomainResolverSet( + bytes32 indexed domainHash, + address indexed resolver + ); + + /** + * @notice Emits when a domain record is deleted + * @param domainHash The hash of a domain's name + */ + event DomainRecordDeleted( + bytes32 indexed domainHash + ); + + /** + * @notice Emit when an owner allows/disallows permissions for an operator + * @param owner Owner of the domain in question + * @param operator Address that was allowed/disallowed + * @param allowed Boolean status of their permission + */ + event OperatorPermissionSet( + address indexed owner, + address indexed operator, + bool allowed + ); + + /** + * @notice Emitted when a new resolver type is added to ZNS + * @param resolverType The name of the resolver type + * @param resolver The address of the resolver contract + */ + event ResolverAdded( + string resolverType, + address resolver + ); + + /** + * @notice Emitted when a resolver is deleted from ZNS + * @param resolverType The name of the resolver type + */ + event ResolverDeleted( + string resolverType + ); + + function initialize(address accessController) external; + + function exists(bytes32 domainHash) external view returns (bool); + + function isOwnerOrOperator( + bytes32 domainHash, + address candidate + ) external view returns (bool); + + function isOperatorFor( + address operator, + address owner + ) external view returns (bool); + + /** + * @notice Set an `operator` as `allowed` to give or remove permissions for all + * domains owned by `msg.sender` + * @param operator The account to allow/disallow + * @param allowed The true/false value to set + */ + function setOwnersOperator(address operator, bool allowed) external; + + function getDomainRecord( + bytes32 domainHash + ) external view returns (DomainRecord memory); + + function getDomainOwner( + bytes32 domainHash + ) external view returns (address); + + function getDomainResolver( + bytes32 domainHash + ) external view returns (address); + + function createDomainRecord( + bytes32 domainHash, + address owner, + string calldata resolverType + ) external; + + function getResolverType( + string calldata resolverType + ) external returns (address); + + function addResolverType( + string calldata resolverType, + address resolver + ) external; + + function deleteResolverType( + string calldata resolverType + ) external; + + function updateDomainRecord( + bytes32 domainHash, + address owner, + string calldata resolverType + ) external; + + function updateDomainOwner(bytes32 domainHash, address owner) external; + + function updateDomainResolver( + bytes32 domainHash, + string calldata resolverType + ) external; + + function deleteRecord(bytes32 domainHash) external; + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol new file mode 100644 index 000000000..c31253482 --- /dev/null +++ b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol @@ -0,0 +1,337 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSRegistryPausable } from "./IZNSRegistryPausable.sol"; +import { AAccessControlled } from "../access/AAccessControlled.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; + + +/** + * @title The main reference data contract in ZNS. Also, often, the last contract + * in the call chain of many operations where the most crucial Name owner data settles. + * Owner of a domain in this contract also serves as the owner of the stake in `ZNSTreasury`. + */ +contract ZNSRegistryPausable is + AAccessControlled, + UUPSUpgradeable, + PausableUpgradeable, + IZNSRegistryPausable { + + // Mapping of all approved resolvers + mapping(string resolverType => address resolver) internal resolvers; + + /** + * @notice Mapping of `domainHash` to [DomainRecord](./IZNSRegistry.md#iznsregistry) struct to hold information + * about each domain + */ + mapping(bytes32 domainHash => DomainRecord domainRecord) internal records; + + /** + * @notice Mapping of `owner` => `operator` => `bool` to show accounts that + * are or aren't allowed access to domains that `owner` has access to. + * Note that operators can NOT change the owner of the domain, but can change + * the resolver or resolver records. + */ + mapping(address owner => mapping(address operator => bool isOperator)) + internal operators; + + /** + * @notice Revert if `msg.sender` is not the owner or an operator allowed by the owner + * @param domainHash the hash of a domain's name + */ + modifier onlyOwnerOrOperator(bytes32 domainHash) { + require( + isOwnerOrOperator(domainHash, msg.sender), + "ZNSRegistry: Not authorized" + ); + _; + } + + /** + * @notice Revert if `msg.sender` is not the owner. Used for owner restricted functions. + * @param domainHash the hash of a domain's name + */ + modifier onlyOwner(bytes32 domainHash) { + require( + records[domainHash].owner == msg.sender, + "ZNSRegistry: Not the Name Owner" + ); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializer for the `ZNSRegistry` proxy. + * @param accessController_ The address of the `ZNSAccessController` contract + * @dev ! The owner of the 0x0 hash should be a multisig ideally, but EOA can be used to deploy ! + * > Admin account deploying the contract will be the owner of the 0x0 hash ! + */ + function initialize(address accessController_) external override initializer { + records[0x0].owner = msg.sender; + _setAccessController(accessController_); + } + + /** + * @notice Checks if a given domain exists + * @param domainHash The hash of a domain's name + */ + function exists(bytes32 domainHash) external view override returns (bool) { + return _exists(domainHash); + } + + /** + * @notice Checks if provided address is an owner or an operator of the provided domain + * @param domainHash The hash of a domain's name + * @param candidate The address for which we are checking access + */ + function isOwnerOrOperator( + bytes32 domainHash, + address candidate + ) public view override returns (bool) { + address owner = records[domainHash].owner; + return candidate == owner || operators[owner][candidate]; + } + + /** + * @notice External function that checks if provided address is an operator for the provided owner. + * @param operator The address for which we are checking access + * @param owner The owner of the domain(-s) in question + */ + function isOperatorFor( + address operator, + address owner + ) external view override returns (bool) { + return operators[owner][operator]; + } + + /** + * @notice Set an `operator` as `allowed` to give or remove permissions for ALL + * domains owned by the owner `msg.sender`. + * Emits an `OperatorPermissionSet` event. + * @param operator The account to allow/disallow + * @param allowed The true/false value to set + */ + function setOwnersOperator(address operator, bool allowed) external override whenNotPaused { + operators[msg.sender][operator] = allowed; + + emit OperatorPermissionSet(msg.sender, operator, allowed); + } + + /** + * @notice Gets a record for a domain (owner, resolver) from the internal mapping + * `records`. `records` maps a domain hash to a + * [DomainRecord](./IZNSRegistry.md#iznsregistry) struct. + * @param domainHash the hash of a domain's name + */ + function getDomainRecord( + bytes32 domainHash + ) external view override returns (DomainRecord memory) { + return records[domainHash]; + } + + /** + * @notice Gets the owner of the given domain + * @param domainHash the hash of a domain's name + */ + function getDomainOwner( + bytes32 domainHash + ) external view override returns (address) { + return records[domainHash].owner; + } + + /** + * @notice Gets the resolver set for the given domain. + * @param domainHash the hash of a domain's name + */ + function getDomainResolver( + bytes32 domainHash + ) external view override returns (address) { + return records[domainHash].resolver; + } + + /** + * @notice Creates a new domain record. Only callable by the `ZNSRootRegistrar.sol` + * or an address that has REGISTRAR_ROLE. This is one of the last calls in the Register + * flow that starts from `ZNSRootRegistrar.registerRootDomain()`. Calls 2 internal functions to set + * the owner and resolver of the domain separately. + * Can be called with `resolver` param as 0, which will exclude the call to set resolver. + * Emits `DomainOwnerSet` and possibly `DomainResolverSet` events. + * @param domainHash The hash of the domain name + * @param owner The owner of the new domain + * @param resolverType The string identifier of the resolver for the new domain, e.g. "address" + */ + function createDomainRecord( + bytes32 domainHash, + address owner, + string calldata resolverType + ) external override whenNotPaused onlyRegistrar { + _setDomainOwner(domainHash, owner); + + // We allow creation of partial domain data with no resolver address + if (bytes(resolverType).length != 0) { + _setDomainResolver(domainHash, resolverType); + } + } + + /** + * @notice Given a resolver type, returns the address of the resolver contract for that type or 0x0 if not found + * @param resolverType The resolver type as a string, e.g. "address" + */ + function getResolverType(string calldata resolverType) public view override returns(address) { + return resolvers[resolverType]; + } + + /** + * @notice Add a new resolver type option to the mapping of types + * This function can also be used to update the resolver mapping for an existing resolver + * simple by using an existing key like "address" with a new address + * @param resolverType The type of the resolver to add + * @param resolver The address of the new resolver contract + */ + function addResolverType(string calldata resolverType, address resolver) public override whenNotPaused onlyAdmin { + resolvers[resolverType] = resolver; + emit ResolverAdded(resolverType, resolver); + } + + /** + * @notice Delete a resolver type from the mapping of types + * @param resolverType The type to be removed + */ + function deleteResolverType(string calldata resolverType) public override whenNotPaused onlyAdmin { + delete resolvers[resolverType]; + emit ResolverDeleted(resolverType); + } + + /** + * @notice Updates an existing domain record's owner and resolver. + * Note that this function can ONLY be called by the Name owner of the domain. + * This is NOT used by the `ZNSRootRegistrar.sol` contract and serves as a user facing function + * for the owners of existing domains to change their data on this contract. A domain + * `operator` can NOT call this, since he is not allowed to change the owner. + * Emits `DomainOwnerSet` and `DomainResolverSet` events. + * @param domainHash The hash of the domain + * @param owner The owner or an allowed operator of that domain + * @param resolverType The resolver for the domain + */ + function updateDomainRecord( + bytes32 domainHash, + address owner, + string calldata resolverType + ) external override whenNotPaused onlyOwner(domainHash) { + // `exists` is checked implicitly through the modifier + _setDomainOwner(domainHash, owner); + _setDomainResolver(domainHash, resolverType); + } + + /** + * @notice Updates the owner of an existing domain. Can be called by either the Name owner + * on this contract OR the `ZNSRootRegistrar.sol` contract as part of the Reclaim flow + * that starts at `ZNSRootRegistrar.sol.reclaim()`. Emits an `DomainOwnerSet` event. + * @param domainHash the hash of a domain's name + * @param owner The account to transfer ownership to + */ + function updateDomainOwner( + bytes32 domainHash, + address owner + ) external override whenNotPaused { + require( + msg.sender == records[domainHash].owner || + accessController.isRegistrar(msg.sender), + "ZNSRegistry: Only Name Owner or Registrar allowed to call" + ); + + _setDomainOwner(domainHash, owner); + } + + /** + * @notice Updates the resolver of an existing domain in `records`. + * Can be called by either the owner of the Name or an allowed operator. + * @param domainHash the hash of a domain's name + * @param resolverType The new Resolver contract address + */ + function updateDomainResolver( + bytes32 domainHash, + string calldata resolverType + ) external override whenNotPaused onlyOwnerOrOperator(domainHash) { + // `exists` is checked implicitly through the modifier + _setDomainResolver(domainHash, resolverType); + } + + /** + * @notice Deletes a domain's record from this contract's state. + * This can ONLY be called by the `ZNSRootRegistrar.sol` contract as part of the Revoke flow + * or any address holding the `REGISTRAR_ROLE`. Emits a `DomainRecordDeleted` event. + * @param domainHash The hash of the domain name + */ + function deleteRecord(bytes32 domainHash) external override whenNotPaused onlyRegistrar { + delete records[domainHash]; + + emit DomainRecordDeleted(domainHash); + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external onlyAdmin { + _pause(); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external onlyAdmin { + _unpause(); + } + + /** + * @notice Check if a domain exists. True if the owner is not `0x0` + * @param domainHash the hash of a domain's name + */ + function _exists(bytes32 domainHash) internal view returns (bool) { + return records[domainHash].owner != address(0); + } + + /** + * @notice Internal function to set a domain's owner in state `records`. + * Owner can NOT be set to 0, since we use delete operation as part of the + * ``deleteRecord()`` function. + * Emits a `DomainOwnerSet` event. + * @param domainHash the hash of a domain's name + * @param owner The owner to set + */ + function _setDomainOwner(bytes32 domainHash, address owner) internal { + require(owner != address(0), "ZNSRegistry: Owner cannot be zero address"); + records[domainHash].owner = owner; + emit DomainOwnerSet(domainHash, owner); + } + + /** + * @notice Internal function to set a domain's resolver in state `records`. + * Resolver can be set to 0, since we allow partial domain data. Emits a `DomainResolverSet` event. + * @param domainHash the hash of a domain's name + * @param resolverType The resolver to set + */ + function _setDomainResolver( + bytes32 domainHash, + string calldata resolverType + ) internal { + address resolver = resolvers[resolverType]; + + records[domainHash].resolver = resolver; + emit DomainResolverSet(domainHash, resolver); + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The implementation contract to upgrade to + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} From 964d7c63b38f550b01695c55a588d59ec8f91827 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 21 Mar 2025 17:22:53 -0700 Subject: [PATCH 004/114] rework pause functionality for Registry from inheritance to direct feature add to contract code --- .../registry/IZNSRegistryPausable.sol | 10 +++++++ .../registry/ZNSRegistryPausable.sol | 26 +++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/contracts/zns-pausable/registry/IZNSRegistryPausable.sol b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol index 363eba0eb..636a201ee 100644 --- a/contracts/zns-pausable/registry/IZNSRegistryPausable.sol +++ b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol @@ -81,6 +81,16 @@ interface IZNSRegistryPausable { string resolverType ); + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + function initialize(address accessController) external; function exists(bytes32 domainHash) external view returns (bool); diff --git a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol index c31253482..66764285a 100644 --- a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol +++ b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.18; import { IZNSRegistryPausable } from "./IZNSRegistryPausable.sol"; -import { AAccessControlled } from "../access/AAccessControlled.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/security/PausableUpgradeable.sol"; /** @@ -15,7 +14,6 @@ import { PausableUpgradeable } from "@openzeppelin/contracts-upgradeable/securit contract ZNSRegistryPausable is AAccessControlled, UUPSUpgradeable, - PausableUpgradeable, IZNSRegistryPausable { // Mapping of all approved resolvers @@ -36,6 +34,8 @@ contract ZNSRegistryPausable is mapping(address owner => mapping(address operator => bool isOperator)) internal operators; + bool private _paused; + /** * @notice Revert if `msg.sender` is not the owner or an operator allowed by the owner * @param domainHash the hash of a domain's name @@ -60,6 +60,16 @@ contract ZNSRegistryPausable is _; } + modifier whenNotPaused() { + require(!paused(), "ZNSRegistry: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSRegistry: Contract is not paused"); + _; + } + /// @custom:oz-upgrades-unsafe-allow constructor constructor() { _disableInitializers(); @@ -97,6 +107,10 @@ contract ZNSRegistryPausable is return candidate == owner || operators[owner][candidate]; } + function paused() public view returns (bool) { + return _paused; + } + /** * @notice External function that checks if provided address is an operator for the provided owner. * @param operator The address for which we are checking access @@ -278,14 +292,16 @@ contract ZNSRegistryPausable is * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. */ function pause() external onlyAdmin { - _pause(); + _paused = true; + emit Paused(msg.sender); } /** * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. */ function unpause() external onlyAdmin { - _unpause(); + _paused = false; + emit Unpaused(msg.sender); } /** From 90e58cd942a672fc930341be09d3feb1f73c00a8 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 21 Mar 2025 17:23:25 -0700 Subject: [PATCH 005/114] modify logger file to add new env var that block log file creation --- src/deploy/logger/create-logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/deploy/logger/create-logger.ts b/src/deploy/logger/create-logger.ts index df45e91ca..140e94f1c 100644 --- a/src/deploy/logger/create-logger.ts +++ b/src/deploy/logger/create-logger.ts @@ -28,7 +28,7 @@ export const getLogger = () : TLogger => { const logFileName = `deploy-${Date.now()}.log`; - if (process.env.ENV_LEVEL?.includes("prod") || process.env.ENV_LEVEL?.includes("test")) { + if (process.env.MAKE_LOG_FILE === "true") { logger.add( new winston.transports.File({ filename: logFileName }), ); From 8d268c9dc26d4a4d8defa49fee76bbec0bd98c9b Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 21 Mar 2025 17:23:56 -0700 Subject: [PATCH 006/114] create storage validation helpers --- src/upgrade/storage-data.ts | 72 +++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/upgrade/storage-data.ts diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts new file mode 100644 index 000000000..3d42b39de --- /dev/null +++ b/src/upgrade/storage-data.ts @@ -0,0 +1,72 @@ +import * as hre from "hardhat"; +import { expect } from "chai"; +import { Contract, ContractFactory } from "ethers"; +import { getStorageLayout, getUnlinkedBytecode, getVersion, StorageLayout } from "@openzeppelin/upgrades-core"; +import { readValidations } from "@openzeppelin/hardhat-upgrades/dist/utils/validations"; +import { ContractStorageData } from "./types"; +import { ZNSContract } from "../../test/helpers/types"; + + +// TODO utils: move thess helpers to protocol-utils repo when available +export const getContractStorageLayout = async ( + contractFactory : ContractFactory +) : Promise => { + const validations = await readValidations(hre); + const unlinkedBytecode = getUnlinkedBytecode(validations, contractFactory.bytecode); + const encodedArgs = contractFactory.interface.encodeDeploy(); + const version = getVersion(unlinkedBytecode, contractFactory.bytecode, encodedArgs); + + return getStorageLayout(validations, version); +}; + +export const readContractStorage = async ( + contractFactory : ContractFactory, + contractObj : ZNSContract +) : Promise => { + const layout = await getContractStorageLayout(contractFactory); + + return layout.storage.reduce( + async ( + acc : Promise, + { contract, label, type } + ) : Promise => { + const newAcc = await acc; + + if ( + (contract === "zStakePoolBase" || + contract === "zStakeCorePool" || + contract === "zStakeCorePoolMigration") && + !type.includes("mapping") + ) { + try { + // TODO upg: figure this out! + const value = await contractObj[label](); + + newAcc.push({ [label]: value }); + } catch (e : unknown) { + console.log(`Error on LABEL ${label}: ${(e as Error).message}`); + } + } + + return newAcc; + }, + Promise.resolve([]) + ); +}; + + +export const compareStorageData = ( + dataBefore : ContractStorageData, + dataAfter : ContractStorageData, +) => { + dataAfter.forEach( + (stateVar, idx) => { + const [key, value] = Object.entries(stateVar)[0]; + + expect(value).to.equal( + dataBefore[idx][key], + `Mismatch on state var ${key} at idx ${idx}! Prev value: ${dataBefore[idx][key]}, new value: ${value}` + ); + } + ); +}; From 7951552060cfc9fd9268795518fc895e212a2371 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 21 Mar 2025 17:24:12 -0700 Subject: [PATCH 007/114] create main upgrade helper --- src/upgrade/types.ts | 20 +++++++++ src/upgrade/upgrade.ts | 94 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/upgrade/types.ts create mode 100644 src/upgrade/upgrade.ts diff --git a/src/upgrade/types.ts b/src/upgrade/types.ts new file mode 100644 index 000000000..bb41ac8e9 --- /dev/null +++ b/src/upgrade/types.ts @@ -0,0 +1,20 @@ +import { ZNSRegistryPausable } from "../../typechain"; +import { Addressable } from "ethers"; + +export type ContractStorageData = Array<{ + [label : string] : string | number | Array; +}>; + +export interface IContractData { + contractName : string; + instanceName : keyof IZNSContractsUpgraded; + address : string | Addressable; +} + +export type ZNSContractUpgraded = + ZNSRegistryPausable; + +export interface IZNSContractsUpgraded { + [instanceName : string] : ZNSContractUpgraded; + registry : ZNSRegistryPausable; +} diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts new file mode 100644 index 000000000..1c8791044 --- /dev/null +++ b/src/upgrade/upgrade.ts @@ -0,0 +1,94 @@ +import * as hre from "hardhat"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { compareStorageData, readContractStorage } from "./storage-data"; +import { ZNSContract } from "../../test/helpers/types"; +import { getLogger } from "../deploy/logger/create-logger"; +import { TLogger } from "../deploy/campaign/types"; +import { IContractData, IZNSContractsUpgraded, ZNSContractUpgraded } from "./types"; +import { Addressable } from "ethers"; + + +export const upgradeZNS = async ({ + governorExt, + contractData, +} : { + governorExt : SignerWithAddress; + contractData : Array; +}) => { + let governor = governorExt; + if (!governor) { + [ governor ] = await hre.ethers.getSigners(); + } + + const logger = getLogger(); + + console.log(`Governor acquired as ${governor.address}`); + + const znsUpgraded = await contractData.reduce( + async ( + acc : Promise, + { contractName, instanceName, address } + ) => { + const upgradedContracts = await acc; + + const upgradedContract = await upgradeZNSContract({ + contractName, + contractAddress: address, + governor, + logger, + }); + + upgradedContracts[instanceName] = upgradedContract; + + return upgradedContracts; + }, + Promise.resolve({} as IZNSContractsUpgraded) + ); + + return znsUpgraded; +}; + +export const upgradeZNSContract = async ({ + contractName, + contractAddress, + governor, + logger, +} : { + contractName : string; + contractAddress : string | Addressable; + governor : SignerWithAddress; + logger : TLogger; +}) => { + const originalFactory = await hre.ethers.getContractFactory(contractName); + const originalContract = originalFactory.attach(contractAddress) as ZNSContract; + + const storageDataPreUpgrade = await readContractStorage( + originalFactory, + originalContract, + ); + logger.info(`Pre-upgrade storage data of ${contractName} acquired`); + + logger.info(`Initiating upgrade of ${contractName} at address ${contractAddress}`); + let upgradedFactory = await hre.ethers.getContractFactory(`${contractName}Pausable`); + upgradedFactory = upgradedFactory.connect(governor); + + const upgradedContract = await hre.upgrades.upgradeProxy( + contractAddress, + upgradedFactory + ) as unknown as ZNSContractUpgraded; + + const implAddress = await hre.upgrades.erc1967.getImplementationAddress(await upgradedContract.getAddress()); + + logger.info(`Upgraded ${contractName} to new implementation at: ${implAddress}`); + + const storageDataPostUpgrade = await readContractStorage( + upgradedFactory, + upgradedContract, + ); + + compareStorageData(storageDataPreUpgrade, storageDataPostUpgrade); + logger.info("Storage compared successfully. Values are unchanged after upgrade"); + logger.info(`Upgrade of ${contractName} finished successfully`); + + return upgradedContract; +}; From 1d1e8f593d9133d9896e2287cafc70b60f1a23bd Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 21 Mar 2025 17:24:32 -0700 Subject: [PATCH 008/114] create basic smoke test --- hardhat.config.ts | 1 + test/zns-v1-upgrade-lock-test.ts | 55 ++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 test/zns-v1-upgrade-lock-test.ts diff --git a/hardhat.config.ts b/hardhat.config.ts index d33955b2f..e251d8658 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -98,6 +98,7 @@ const config : HardhatUserConfig = { enabled: false, }, networks: { + // TODO upg: add forking, but make an env var to turn it on/off in the upgrade test mainnet: { url: `${process.env.MAINNET_RPC_URL}`, gasPrice: 80000000000, diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts new file mode 100644 index 000000000..a262ff431 --- /dev/null +++ b/test/zns-v1-upgrade-lock-test.ts @@ -0,0 +1,55 @@ +import * as hre from "hardhat"; +import { IDeployCampaignConfig, TZNSContractState } from "../src/deploy/campaign/types"; +import { getConfig } from "../src/deploy/campaign/environments"; +import { runZnsCampaign } from "../src/deploy/zns-campaign"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { upgradeZNS, upgradeZNSContract } from "../src/upgrade/upgrade"; +import { IContractData } from "../src/upgrade/types"; +import { znsNames } from "../src/deploy/missions/contracts/names"; +import { expect } from "chai"; + + +describe("ZNS V1 Upgrade and Lock Test", () => { + let deployer : SignerWithAddress; + let user : SignerWithAddress; + let governor : SignerWithAddress; + let admin : SignerWithAddress; + let randomUser : SignerWithAddress; + + let zns : TZNSContractState; + let zeroVault : SignerWithAddress; + + before(async () => { + [deployer, zeroVault, user, governor, admin, randomUser] = await hre.ethers.getSigners(); + + const config : IDeployCampaignConfig = await getConfig({ + deployer, + zeroVaultAddress: zeroVault.address, + governors: [deployer.address, governor.address], + admins: [deployer.address, admin.address], + }); + + const campaign = await runZnsCampaign({ + config, + }); + + zns = campaign.state.contracts; + }); + + it.only("should upgrade all necessary ZNS contracts to pausable versions", async () => { + const contractData : Array = [ + { + contractName: znsNames.registry.contract, + instanceName: znsNames.registry.instance, + address: zns.registry.target, + }, + ]; + + const znsUpgraded = await upgradeZNS({ + governorExt: governor, + contractData, + }); + + expect(znsUpgraded.registry.target).to.equal(zns.registry.target); + }); +}); From f801ef32a9725effd17c253f52895d52821b1ba9 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 13:37:04 -0700 Subject: [PATCH 009/114] add missing modifiers to pause functions in Registry --- contracts/zns-pausable/registry/ZNSRegistryPausable.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol index 66764285a..afbe94f66 100644 --- a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol +++ b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol @@ -291,7 +291,7 @@ contract ZNSRegistryPausable is /** * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. */ - function pause() external onlyAdmin { + function pause() external whenNotPaused onlyAdmin { _paused = true; emit Paused(msg.sender); } @@ -299,7 +299,7 @@ contract ZNSRegistryPausable is /** * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. */ - function unpause() external onlyAdmin { + function unpause() external whenPaused onlyAdmin { _paused = false; emit Unpaused(msg.sender); } From 28af0bbd827aa8e48a73d9473089c68c8d165af0 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 13:38:06 -0700 Subject: [PATCH 010/114] create DomainTokenPausable --- .../token/IZNSDomainTokenPausable.sol | 79 ++++++ .../token/ZNSDomainTokenPausable.sol | 242 ++++++++++++++++++ test/zns-v1-upgrade-lock-test.ts | 5 + 3 files changed, 326 insertions(+) create mode 100644 contracts/zns-pausable/token/IZNSDomainTokenPausable.sol create mode 100644 contracts/zns-pausable/token/ZNSDomainTokenPausable.sol diff --git a/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol new file mode 100644 index 000000000..0309b0583 --- /dev/null +++ b/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; +import { IERC2981Upgradeable } from "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + + +interface IZNSDomainTokenPausable is IERC2981Upgradeable, IERC721Upgradeable { + + /** + * @notice Emitted when a Default Royalty (for all tokens) is set. + */ + event DefaultRoyaltySet(uint96 indexed defaultRoyalty); + /** + * @notice Emitted when Token Royalty is set for individual tokens per tokenID. + */ + event TokenRoyaltySet(uint256 indexed tokenId, uint96 indexed royalty); + /** + * @notice Emitted when a Base URI is set for all tokens. + */ + event BaseURISet(string indexed baseURI); + /** + * @notice Emitted when a Token URI is set for individual tokens per tokenID. + * @dev Note that this event is fired ONLY when the tokenURI is set externally + * through an external setter and NOT during the registration. + */ + event TokenURISet(uint256 indexed tokenId, string indexed tokenURI); + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + function initialize( + address accessController, + string calldata tokenName, + string calldata tokenSymbol, + address defaultRoyaltyReceiver, + uint96 defaultRoyaltyFraction + ) external; + + function totalSupply() external view returns (uint256); + + function register( + address to, + uint256 tokenId, + string memory _tokenURI + ) external; + + function revoke(uint256 tokenId) external; + + function tokenURI(uint256 tokenId) + external + view + returns (string memory); + + function setBaseURI(string memory baseURI_) external; + + function setTokenURI(uint256 tokenId, string memory _tokenURI) external; + + function setDefaultRoyalty(address receiver, uint96 royaltyFraction) external; + + function setTokenRoyalty( + uint256 tokenId, + address receiver, + uint96 royaltyFraction + ) external; + + function pause() external; + + function unpause() external; + + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} diff --git a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol new file mode 100644 index 000000000..1b59ea354 --- /dev/null +++ b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol @@ -0,0 +1,242 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { ERC2981Upgradeable } from "@openzeppelin/contracts-upgradeable/token/common/ERC2981Upgradeable.sol"; +import { ERC721URIStorageUpgradeable } + from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; +import { IZNSDomainTokenPausable } from "./IZNSDomainTokenPausable.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; + + +/** + * @title A contract for tokenizing domains under ZNS. Every domain in ZNS has a corresponding token + * minted at register time. This token is also an NFT that is fully ERC-721 compliant. + * @dev Note that all ZNS related functions on this contract can ONLY be called by either + * the `ZNSRootRegistrar.sol` contract or any address holding a REGISTRAR_ROLE. + */ +contract ZNSDomainTokenPausable is + AAccessControlled, + ERC721Upgradeable, + ERC2981Upgradeable, + ERC721URIStorageUpgradeable, + UUPSUpgradeable, + IZNSDomainTokenPausable { + + /** + * @notice Base URI used for ALL tokens. Can be empty if individual URIs are set. + */ + string private baseURI; + + /** + * @dev Total supply of all tokens + */ + uint256 private _totalSupply; + + bool private _paused; + + modifier whenNotPaused() { + require(!paused(), "ZNSDomainToken: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSDomainToken: Contract is not paused"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializer for the `ZNSDomainToken` proxy. + * Note that this function does NOT have role protection enforced! + * @param accessController_ The address of the `ZNSAccessController` contract + * @param name_ The name of the token + * @param symbol_ The symbol of the token + * @param defaultRoyaltyReceiver The address that will receive default royalties + * @param defaultRoyaltyFraction The default royalty fraction (as a base of 10,000) + */ + function initialize( + address accessController_, + string memory name_, + string memory symbol_, + address defaultRoyaltyReceiver, + uint96 defaultRoyaltyFraction + ) external override initializer { + __ERC721_init(name_, symbol_); + _setAccessController(accessController_); + _setDefaultRoyalty(defaultRoyaltyReceiver, defaultRoyaltyFraction); + } + + /** + * @notice Returns the total supply of all tokens + */ + function totalSupply() external view override returns (uint256) { + return _totalSupply; + } + + /** + * @notice Mints a token with a specified tokenId, using _safeMint, and sends it to the given address. + * Used ONLY as a part of the Register flow that starts from `ZNSRootRegistrar.registerRootDomain()` + * or `ZNSSubRegistrar.registerSubdomain()` and sets the individual tokenURI for the token minted. + * > TokenId is created as a hash of the domain name casted to uint256. + * @param to The address that will recieve the newly minted domain token (new domain owner) + * @param tokenId The TokenId that the caller wishes to mint/register. + * @param _tokenURI The tokenURI to be set for the token minted. + */ + function register( + address to, + uint256 tokenId, + string memory _tokenURI + ) external override whenNotPaused onlyRegistrar { + ++_totalSupply; + _safeMint(to, tokenId); + _setTokenURI(tokenId, _tokenURI); + } + + /** + * @notice Burns the token with the specified tokenId and removes the royalty information for this tokenID. + * Used ONLY as a part of the Revoke flow that starts from `ZNSRootRegistrar.revokeDomain()`. + * @param tokenId The tokenId (as `uint256(domainHash)`) that the caller wishes to burn/revoke + */ + function revoke(uint256 tokenId) external override whenNotPaused onlyRegistrar { + _burn(tokenId); + _resetTokenRoyalty(tokenId); + } + + /** + * @notice Returns the tokenURI for the given tokenId. + */ + function tokenURI(uint256 tokenId) + public + view + override(ERC721URIStorageUpgradeable, ERC721Upgradeable, IZNSDomainTokenPausable) + returns (string memory) + { + return super.tokenURI(tokenId); + } + + /** + * @notice Sets the tokenURI for the given tokenId. This is an external setter that can only + * be called by the ADMIN_ROLE of zNS. This functions is not a part of any flows and is here + * only to change faulty or outdated token URIs in case of corrupted metadata or other problems. + * Fires the `TokenURISet` event, which is NOT fired when tokenURI is set during the registration process. + * @param tokenId The tokenId (as `uint256(domainHash)`) that the caller wishes to set the tokenURI for + * @param _tokenURI The tokenURI to be set for the token with the given tokenId + */ + // TODO upg: figure out if onlyAdmin functions need to be only whenNotPaused or just keep them as is !!! + function setTokenURI(uint256 tokenId, string memory _tokenURI) external override onlyAdmin { + _setTokenURI(tokenId, _tokenURI); + emit TokenURISet(tokenId, _tokenURI); + } + + /** + * @notice Sets the baseURI for ALL tokens. Can only be called by the ADMIN_ROLE of zNS. + * Fires the `BaseURISet` event. + * @dev This contract supports both, baseURI and individual tokenURI that can be used + * interchangeably. + * > Note that if `baseURI` and `tokenURI` are set, the `tokenURI` will be appended to the `baseURI`! + * @param baseURI_ The baseURI to be set for all tokens + */ + function setBaseURI(string memory baseURI_) external override onlyAdmin { + baseURI = baseURI_; + emit BaseURISet(baseURI_); + } + + /** + * @notice Sets the default royalty for ALL tokens. Can only be called by the ADMIN_ROLE of zNS. + * Fires the `DefaultRoyaltySet` event. + * @dev This contract supports both, default royalties and individual token royalties per tokenID. + * @param receiver The address that will receive default royalties + * @param royaltyFraction The default royalty fraction (as a base of 10,000) + */ + function setDefaultRoyalty(address receiver, uint96 royaltyFraction) external override onlyAdmin { + _setDefaultRoyalty(receiver, royaltyFraction); + + emit DefaultRoyaltySet(royaltyFraction); + } + + /** + * @notice Sets the royalty for the given tokenId. Can only be called by the ADMIN_ROLE of zNS. + * Fires the `TokenRoyaltySet` event. + * @dev This contract supports both, default royalties and individual token royalties per tokenID. + * @param tokenId The tokenId (as `uint256(domainHash)`) that the caller wishes to set the royalty for + * @param receiver The address that will receive royalties for the given tokenId + * @param royaltyFraction The royalty fraction (as a base of 10,000) for the given tokenId + */ + function setTokenRoyalty( + uint256 tokenId, + address receiver, + uint96 royaltyFraction + ) external override onlyAdmin { + _setTokenRoyalty(tokenId, receiver, royaltyFraction); + + emit TokenRoyaltySet(tokenId, royaltyFraction); + } + + function paused() public view returns (bool) { + return _paused; + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice To allow for user extension of the protocol we have to + * enable checking acceptance of new interfaces to ensure they are supported + * @param interfaceId The interface ID + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC721Upgradeable, ERC721URIStorageUpgradeable, ERC2981Upgradeable, IZNSDomainTokenPausable) + returns (bool) { + return super.supportsInterface(interfaceId); + } + + /** + * @notice ERC721 `_burn` function + * @param tokenId The ID of the token to burn + */ + function _burn(uint256 tokenId) + internal + override(ERC721URIStorageUpgradeable, ERC721Upgradeable) + { + super._burn(tokenId); + --_totalSupply; + } + + /** + * @notice Return the baseURI + */ + function _baseURI() internal view override returns (string memory) { + return baseURI; + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The implementation contract to upgrade to + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index a262ff431..bd14d8768 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -43,6 +43,11 @@ describe("ZNS V1 Upgrade and Lock Test", () => { instanceName: znsNames.registry.instance, address: zns.registry.target, }, + { + contractName: znsNames.domainToken.contract, + instanceName: znsNames.domainToken.instance, + address: zns.domainToken.target, + }, ]; const znsUpgraded = await upgradeZNS({ From 71610661e263ed158f5af0d916ef05a00d29b01e Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 14:27:14 -0700 Subject: [PATCH 011/114] switch console log for proper logger call --- src/upgrade/upgrade.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index 1c8791044..f45d7d273 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -22,7 +22,7 @@ export const upgradeZNS = async ({ const logger = getLogger(); - console.log(`Governor acquired as ${governor.address}`); + logger.info(`Governor acquired as ${governor.address}`); const znsUpgraded = await contractData.reduce( async ( From d58ccc28110763c1642c69554ecd81bdbf825373 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 14:27:32 -0700 Subject: [PATCH 012/114] add TreasuryPausable --- .../treasury/IZNSTreasuryPausable.sol | 170 +++++++++ .../treasury/ZNSTreasuryPausable.sol | 340 ++++++++++++++++++ test/zns-v1-upgrade-lock-test.ts | 5 + 3 files changed, 515 insertions(+) create mode 100644 contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol create mode 100644 contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol diff --git a/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol new file mode 100644 index 000000000..e5e7bc7a9 --- /dev/null +++ b/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @notice The `PaymentConfig` struct describes the two pieces of information + * needed to create a payment configuration for a domain. The address of the + * user to send funds to in a sale, and what token those funds are in. + */ +struct PaymentConfig { + IERC20 token; + // feeBeneficiary if STAKE, paymentBeneficiary if DIRECT + address beneficiary; +} + + +/** + * @title IZNSTreasury.sol - Interface for the ZNSTreasury contract responsible for managing payments and staking. + * @dev Below are docs for the types in this file: + * - `PaymentConfig`: Struct containing data for the payment configuration of the parent distributing subdomains: + * + `token`: The address of the ERC-20 compliant payment token contract chosen by the parent + * + `beneficiary`: The address of the beneficiary contract or wallet that will receive payments or fees + * - `Stake`: Struct containing data for the staking of a domain written at the time of staking: + * + `token`: The address of the ERC-20 compliant staking token used to deposit a specific stake for domain + * + `amount`: The amount of the staking token above deposited by the user +*/ +interface IZNSTreasuryPausable { + /** + * @notice Describe a stake for a domain. This could be + * in any ERC20 token so the address of the specific token + * as well as the amount is required. + */ + struct Stake { + IERC20 token; + uint256 amount; + } + + /** + * @notice Emitted when a new stake is deposited upon registration of a new domain. + * @param domainHash The hash of the domain name + * @param depositor The address of the depositing user / new domain owner + * @param stakeAmount The amount they are depositing / price of the domain based on name length + * @param stakeFee The registration fee paid by the user on top of the staked amount + */ + event StakeDeposited( + bytes32 indexed parentHash, + bytes32 indexed domainHash, + address indexed depositor, + address stakingToken, + uint256 stakeAmount, + uint256 stakeFee, + uint256 protocolFee + ); + + /** + * @notice Emitted when a stake is withdrawn upon domain revocation. + * @param domainHash The hash of the domain name being revoked + * @param owner The owner of the domain being revoked + * @param stakeAmount The staked amount withdrawn to the user after revoking + */ + event StakeWithdrawn( + bytes32 indexed domainHash, + address indexed owner, + address indexed stakingToken, + uint256 stakeAmount + ); + + /** + * @notice Emitted when a direct payment is processed upon registration of a new domain. + * @param parentHash The hash of the parent domain + * @param domainHash The full namehash of the domain registered + * @param payer The address of the user who paid for the domain + * @param beneficiary The address of the beneficiary contract or wallet that received the payment + * @param amount The amount paid by the user + * @param protocolFee The protocol fee paid by the user to Zero + */ + event DirectPaymentProcessed( + bytes32 indexed parentHash, + bytes32 indexed domainHash, + address indexed payer, + address beneficiary, + uint256 amount, + uint256 protocolFee + ); + + /** + * @notice Emitted when `curvePricer` is set in state. + * @param curvePricer The new address of the CurvePricer contract + */ + event CurvePricerSet(address curvePricer); + + /** + * @notice Emitted when `stakingToken` is set in state. + * @param token The new address of the ERC-20 compliant payment token contract + */ + event PaymentTokenSet(bytes32 indexed domainHash, address indexed token); + + /** + * @notice Emitted when `zeroVault` is set in state. + * @param beneficiary The new address of the beneficiary contract or wallet + */ + event BeneficiarySet(bytes32 indexed domainHash, address indexed beneficiary); + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + function paymentConfigs( + bytes32 domainHash + ) external view returns ( + IERC20 token, + address beneficiary + ); + + function stakedForDomain(bytes32 domainHash) external view returns (IERC20, uint256); + + function stakeForDomain( + bytes32 parentHash, + bytes32 domainHash, + address depositor, + uint256 stakeAmount, + uint256 stakeFee, + uint256 protocolFee + ) external; + + function unstakeForDomain(bytes32 domainHash, address owner) external; + + function processDirectPayment( + bytes32 parentHash, + bytes32 domainHash, + address payer, + uint256 paymentAmount, + uint256 protocolFee + ) external; + + function setPaymentConfig( + bytes32 domainHash, + PaymentConfig memory paymentConfig + ) external; + + function setBeneficiary( + bytes32 domainHash, + address beneficiary + ) external; + + function setPaymentToken( + bytes32 domainHash, + address paymentToken + ) external; + + function setRegistry(address registry_) external; + + function initialize( + address accessController_, + address curvePricer_, + address stakingToken_, + address zeroVault_ + ) external; + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol new file mode 100644 index 000000000..5b9b60b05 --- /dev/null +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -0,0 +1,340 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSTreasuryPausable } from "./IZNSTreasuryPausable.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import { PaymentConfig } from "./IZNSTreasuryPausable.sol"; +import { ARegistryWired } from "../../registry/ARegistryWired.sol"; + + +/** + * @title IZNSTreasury.sol - Interface for the ZNSTreasury contract responsible for managing payments and staking. + * @dev This contract is not also the performer of all transfers, but it also stores staked funds for ALL domains + * that use PaymentType.STAKE. This is to ensure that the funds are not locked in the domain owner's wallet, + * but are held within the system and users do not have access to them while their respective domains are active. + * It also stores the payment configurations for all domains and staked amounts and token addresses which were used. + * This information is needed for revoking users to withdraw their stakes back when they exit the system. +*/ +contract ZNSTreasuryPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSTreasuryPausable { + using SafeERC20 for IERC20; + + /** + * @notice The mapping that stores the payment configurations for each domain. + * Zero's own configs for root domains is stored under 0x0 hash. + */ + mapping(bytes32 domainHash => PaymentConfig config) public override paymentConfigs; + + /** + * @notice The mapping that stores `Stake` struct mapped by domainHash. It stores the staking data for + * each domain in zNS. Note that there is no owner address to which the stake is tied to. Instead, the + * owner data from `ZNSRegistry` is used to identify a user who owns the stake. So the staking data is + * tied to the owner of the Name. This should be taken into account, since any transfer of the Token to + * another address, and the system, allowing them to Reclaim the Name, will also allow them to withdraw the stake. + * > Stake is owned by the owner of the Name in `ZNSRegistry` which the owner of the Token can reclaim! + */ + mapping(bytes32 domainHash => Stake stakeData) public override stakedForDomain; + + bool private _paused; + + modifier whenNotPaused() { + require(!paused(), "CONTRACTNAME: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "CONTRACTNAME: Contract is not paused"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice `ZNSTreasury` proxy state initializer. Note that setter functions are used + * instead of direct state variable assignments in order to use proper Access Control + * at initialization. Only ADMIN in `ZNSAccessController` can call this function. + * For this also, it is important that `ZNSAccessController` is deployed and initialized with role data + * before this contract is deployed. + * @param accessController_ The address of the `ZNSAccessController` contract. + * @param registry_ The address of the `ZNSRegistry` contract. + * @param paymentToken_ The address of the staking token (currently $ZERO). + * @param zeroVault_ The address of the Zero Vault - the wallet or contract to collect all the registration fees. + */ + function initialize( + address accessController_, + address registry_, + address paymentToken_, + address zeroVault_ + ) external override initializer { + _setAccessController(accessController_); + _setRegistry(registry_); + + require( + paymentToken_ != address(0), + "ZNSTreasury: paymentToken_ passed as 0x0 address" + ); + require( + zeroVault_ != address(0), + "ZNSTreasury: zeroVault_ passed as 0x0 address" + ); + + paymentConfigs[0x0] = PaymentConfig({ + token: IERC20(paymentToken_), + beneficiary : zeroVault_ + }); + } + + /** + * @notice Performs all the transfers for the staking payment. This function is called by `ZNSRootRegistrar.sol` + * when a user wants to register a domain. It transfers the stake amount and the registration fee + * to the contract from the user, and records the staked amount for the domain. + * Note that a user has to approve the correct amount of `domainPrice + stakeFee + protocolFee` + * for this function to not revert. + * + * Reads parent's payment config from state and transfers the stake amount and all fees to this contract. + * After that transfers the protocol fee to the Zero Vault from this contract to respective beneficiaries. + * After transfers have been performed, saves the staking data into `stakedForDomain[domainHash]` + * and fires a `StakeDeposited` event. + * @param parentHash The hash of the parent domain. + * @param domainHash The hash of the domain for which the stake is being deposited. + * @param depositor The address of the user who is depositing the stake. + * @param stakeAmount The amount of the staking token to be deposited. + * @param stakeFee The registration fee paid by the user on top of the staked amount to the parent domain owner. + * @param protocolFee The protocol fee paid by the user to Zero. + */ + function stakeForDomain( + bytes32 parentHash, + bytes32 domainHash, + address depositor, + uint256 stakeAmount, + uint256 stakeFee, + uint256 protocolFee + ) external override whenNotPaused onlyRegistrar { + PaymentConfig memory parentConfig = paymentConfigs[parentHash]; + + // Transfer stake amount and fees to this address + parentConfig.token.safeTransferFrom( + depositor, + address(this), + stakeAmount + stakeFee + protocolFee + ); + + // Transfer registration fee to the Zero Vault from this address + parentConfig.token.safeTransfer( + paymentConfigs[0x0].beneficiary, + protocolFee + ); + + // transfer stake fee to the parent beneficiary if it's > 0 + if (stakeFee > 0) { + require( + parentConfig.beneficiary != address(0), + "ZNSTreasury: parent domain has no beneficiary set" + ); + + parentConfig.token.safeTransfer( + parentConfig.beneficiary, + stakeFee + ); + } + + // Record staked amount for this domain + stakedForDomain[domainHash] = Stake({ + token: parentConfig.token, + amount: stakeAmount + }); + + emit StakeDeposited( + parentHash, + domainHash, + depositor, + address(parentConfig.token), + stakeAmount, + stakeFee, + protocolFee + ); + } + + /** + * @notice Withdraws the stake for a domain. This function is called by `ZNSRootRegistrar.sol` + * when a user wants to Revoke a domain. It transfers the stake amount from the contract back to the user, + * and deletes the stake data for the domain in state. Only REGISTRAR_ROLE can call this function. + * Emits a `StakeWithdrawn` event. + * Since we are clearing storage, gas refund from this operation makes Revoke transactions cheaper. + * @param domainHash The hash of the domain for which the stake is being withdrawn. + * @param owner The address of the user who is withdrawing the stake. + */ + function unstakeForDomain( + bytes32 domainHash, + address owner + ) external override whenNotPaused onlyRegistrar { + Stake memory stakeData = stakedForDomain[domainHash]; + delete stakedForDomain[domainHash]; + + stakeData.token.safeTransfer(owner, stakeData.amount); + + emit StakeWithdrawn( + domainHash, + owner, + address(stakeData.token), + stakeData.amount + ); + } + + /** + * @notice An alternative to `stakeForDomain()` for cases when a parent domain is using PaymentType.DIRECT. + * @dev Note that `stakeFee` transfers are NOT present here, since a fee on top of the price is ONLY supported + * for STAKE payment type. This function is called by `ZNSRootRegistrar.sol` when a user wants to register a domain. + * This function uses a different approach than `stakeForDomain()` as it performs 2 transfers from the user's + * wallet. Is uses `paymentConfigs[parentHash]` to get the token and beneficiary for the parent domain. + * Can be called ONLY by the REGISTRAR_ROLE. Fires a `DirectPaymentProcessed` event. + * @param parentHash The hash of the parent domain. + * @param domainHash The hash of the domain for which the stake is being deposited. + * @param payer The address of the user who is paying for the domain. + * @param paymentAmount The amount of the payment token to be deposited. + * @param protocolFee The protocol fee paid by the user to Zero. + */ + function processDirectPayment( + bytes32 parentHash, + bytes32 domainHash, + address payer, + uint256 paymentAmount, + uint256 protocolFee + ) external override whenNotPaused onlyRegistrar { + PaymentConfig memory parentConfig = paymentConfigs[parentHash]; + + require( + parentConfig.beneficiary != address(0), + "ZNSTreasury: parent domain has no beneficiary set" + ); + + // Transfer payment to parent beneficiary from payer + parentConfig.token.safeTransferFrom( + payer, + parentConfig.beneficiary, + paymentAmount + ); + + // Transfer registration fee to the Zero Vault from payer + parentConfig.token.safeTransferFrom( + payer, + paymentConfigs[0x0].beneficiary, + protocolFee + ); + + emit DirectPaymentProcessed( + parentHash, + domainHash, + payer, + parentConfig.beneficiary, + paymentAmount, + protocolFee + ); + } + + /** + * @notice Setter function for the `paymentConfig` chosen by domain owner. + * Only domain owner/operator can call this. + * @param domainHash The hash of the domain to set payment config for + * @param paymentConfig The payment config to be set for the domain (see IZNSTreasury.sol for details) + */ + function setPaymentConfig( + bytes32 domainHash, + PaymentConfig memory paymentConfig + ) external override whenNotPaused { + require( + registry.isOwnerOrOperator(domainHash, msg.sender) || accessController.isRegistrar(msg.sender), + "ZNSTreasury: Not authorized." + ); + _setBeneficiary(domainHash, paymentConfig.beneficiary); + _setPaymentToken(domainHash, address(paymentConfig.token)); + } + + /** + * @notice Setter function for the `PaymentConfig.beneficiary` address chosen by domain owner. + * Only domain owner/operator can call this. Fires a `BeneficiarySet` event. + * @param domainHash The hash of the domain to set beneficiary for + * @param beneficiary The address of the new beneficiary + * - the wallet or contract to collect all payments for the domain. + */ + function setBeneficiary( + bytes32 domainHash, + address beneficiary + ) public override whenNotPaused onlyOwnerOrOperator(domainHash) { + _setBeneficiary(domainHash, beneficiary); + } + + /** + * @notice Setter function for the `PaymentConfig.token` chosen by the domain owner. + * Only domain owner/operator can call this. Fires a `PaymentTokenSet` event. + * @param domainHash The hash of the domain to set payment token for + * @param paymentToken The address of the new payment/staking token + */ + function setPaymentToken( + bytes32 domainHash, + address paymentToken + ) public override whenNotPaused onlyOwnerOrOperator(domainHash) { + _setPaymentToken(domainHash, paymentToken); + } + + /** + * @notice Sets the registry address in state. + * @dev This function is required for all contracts inheriting `ARegistryWired`. + */ + function setRegistry( + address registry_ + ) external override(ARegistryWired, IZNSTreasuryPausable) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + function _setBeneficiary(bytes32 domainHash, address beneficiary) internal { + require(beneficiary != address(0), "ZNSTreasury: beneficiary passed as 0x0 address"); + + paymentConfigs[domainHash].beneficiary = beneficiary; + emit BeneficiarySet(domainHash, beneficiary); + } + + function _setPaymentToken(bytes32 domainHash, address paymentToken) internal { + require(paymentToken != address(0), "ZNSTreasury: paymentToken passed as 0x0 address"); + + paymentConfigs[domainHash].token = IERC20(paymentToken); + emit PaymentTokenSet(domainHash, paymentToken); + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The implementation contract to upgrade to + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index bd14d8768..62f7f8715 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -48,6 +48,11 @@ describe("ZNS V1 Upgrade and Lock Test", () => { instanceName: znsNames.domainToken.instance, address: zns.domainToken.target, }, + { + contractName: znsNames.treasury.contract, + instanceName: znsNames.treasury.instance, + address: zns.treasury.target, + }, ]; const znsUpgraded = await upgradeZNS({ From 3f2aec244f6ab6da27b0e783fb0b7bb76dc4e990 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 14:43:13 -0700 Subject: [PATCH 013/114] add RootRegistrarPausable and SubRegistrarPausable --- .../registrar/IZNSRootRegistrarPausable.sol | 171 +++++++ .../registrar/IZNSSubRegistrarPausable.sol | 140 ++++++ .../registrar/ZNSRootRegistrarPausable.sol | 444 ++++++++++++++++++ .../registrar/ZNSSubRegistrarPausable.sol | 398 ++++++++++++++++ test/zns-v1-upgrade-lock-test.ts | 10 + 5 files changed, 1163 insertions(+) create mode 100644 contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol create mode 100644 contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol create mode 100644 contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol create mode 100644 contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol diff --git a/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol new file mode 100644 index 000000000..9fb926061 --- /dev/null +++ b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol @@ -0,0 +1,171 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IDistributionConfig } from "../../types/IDistributionConfig.sol"; +import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; + + +/** + * @notice Stake fee is 0x0 for anything other than subdomain under a parent with Stake Payment + * parent hash will be 0x0 for root domain + */ +struct CoreRegisterArgs { + bytes32 parentHash; + bytes32 domainHash; + address registrant; + address domainAddress; + uint256 price; + uint256 stakeFee; + string label; + string tokenURI; + bool isStakePayment; + PaymentConfig paymentConfig; +} + +/** + * @title IZNSRootRegistrar.sol - Interface for the ZNSRootRegistrar contract resposible for registering root domains. + * @notice Below are docs for the types in this file: + * - `OwnerOf`: Enum signifying ownership of ZNS entities + * + NAME: The owner of the Name only + * + TOKEN: The owner of the Token only + * + BOTH: The owner of both the Name and the Token + * - `CoreRegisterArgs`: Struct containing all the arguments required to register a domain + * with ZNSRootRegistrar.coreRegister(): + * + `parentHash`: The hash of the parent domain (0x0 for root domains) + * + `domainHash`: The hash of the domain to be registered + * + `label`: The label of the domain to be registered + * + `registrant`: The address of the user who is registering the domain + * + `price`: The determined price for the domain to be registered based on parent rules + * + `stakeFee`: The determined stake fee for the domain to be registered (only for PaymentType.STAKE!) + * + `domainAddress`: The address to which the domain will be resolved to + * + `tokenURI`: The tokenURI for the domain to be registered + * + `isStakePayment`: A flag for whether the payment is a stake payment or not + */ +interface IZNSRootRegistrarPausable is IDistributionConfig { + + enum OwnerOf { + NAME, + TOKEN, + BOTH + } + + /** + * @notice Emitted when a NEW domain is registered. + * @dev `domainAddress` parameter is the address to which a domain name will relate to in ZNS. + * E.g. if a user made a domain for his wallet, the address of the wallet will be the `domainAddress`. + * This can be 0 as this variable is not required to perform registration process + * and can be set at a later time by the domain owner. + * @param parentHash The hash of the parent domain (0x0 for root domains) + * @param label The name as the last part of the full domain string (level) registered + * @param domainHash The hash of the domain registered + * @param tokenId The tokenId of the domain registered + * @param tokenURI The tokenURI of the domain registered + * @param registrant The address that called `ZNSRootRegistrar.registerRootDomain()` + * @param domainAddress The domain address of the domain registered + */ + event DomainRegistered( + bytes32 parentHash, + bytes32 indexed domainHash, + string label, + uint256 indexed tokenId, + string tokenURI, + address indexed registrant, + address domainAddress + ); + + /** + * @notice Emitted when a domain is revoked. + * @param domainHash The hash of the domain revoked + * @param owner The address that called `ZNSRootRegistrar.sol.revokeDomain()` and domain owner + * @param stakeRefunded A flag for whether the stake was refunded or not + */ + event DomainRevoked( + bytes32 indexed domainHash, + address indexed owner, + bool indexed stakeRefunded + ); + + /** + * @notice Emitted when an ownership of the Name is reclaimed by the Token owner. + * @param domainHash The hash of the domain reclaimed + * @param registrant The address that called `ZNSRootRegistrar.sol.reclaimDomain()` + */ + event DomainReclaimed( + bytes32 indexed domainHash, + address indexed registrant + ); + + /** + * @notice Emitted when the `rootPricer` address is set in state. + * @param rootPricer The new address of any IZNSPricer type contract + */ + event RootPricerSet(address rootPricer); + + /** + * @notice Emitted when the `treasury` address is set in state. + * @param treasury The new address of the Treasury contract + */ + event TreasurySet(address treasury); + + /** + * @notice Emitted when the `domainToken` address is set in state. + * @param domainToken The new address of the DomainToken contract + */ + event DomainTokenSet(address domainToken); + + /** + * @notice Emitted when the `subRegistrar` address is set in state. + * @param subRegistrar The new address of the SubRegistrar contract + */ + event SubRegistrarSet(address subRegistrar); + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + function initialize( + address accessController_, + address registry_, + address rootPricer_, + address treasury_, + address domainToken_ + ) external; + + function registerRootDomain( + string calldata name, + address domainAddress, + string calldata tokenURI, + DistributionConfig calldata distributionConfig, + PaymentConfig calldata paymentConfig + ) external returns (bytes32); + + function coreRegister( + CoreRegisterArgs memory args + ) external; + + function revokeDomain(bytes32 domainHash) external; + + function reclaimDomain(bytes32 domainHash) external; + + function isOwnerOf(bytes32 domainHash, address candidate, OwnerOf ownerOf) external view returns (bool); + + function setRegistry(address registry_) external; + + function setRootPricer(address rootPricer_) external; + + function setTreasury(address treasury_) external; + + function setDomainToken(address domainToken_) external; + + function setSubRegistrar(address subRegistrar_) external; + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol new file mode 100644 index 000000000..f9c894cf0 --- /dev/null +++ b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IDistributionConfig } from "../../types/IDistributionConfig.sol"; +import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { IZNSPricer } from "../../types/IZNSPricer.sol"; + + +/** + * @title IZNSSubRegistrar.sol - Interface for the ZNSSubRegistrar contract responsible for registering subdomains. +*/ +interface IZNSSubRegistrarPausable is IDistributionConfig { + + /** + * @notice Emitted when a new `DistributionConfig.pricerContract` is set for a domain. + */ + event PricerContractSet(bytes32 indexed domainHash, address indexed pricerContract); + + /** + * @notice Emitted when a new `DistributionConfig.paymentType` is set for a domain. + */ + event PaymentTypeSet(bytes32 indexed domainHash, PaymentType paymentType); + + /** + * @notice Emitted when a new `DistributionConfig.accessType` is set for a domain. + */ + event AccessTypeSet(bytes32 indexed domainHash, AccessType accessType); + + /** + * @notice Emitted when a new full `DistributionConfig` is set for a domain at once. + */ + event DistributionConfigSet( + bytes32 indexed domainHash, + IZNSPricer pricerContract, + PaymentType paymentType, + AccessType accessType + ); + + /** + * @notice Emitted when a `mintlist` is updated for a domain. + */ + event MintlistUpdated( + bytes32 indexed domainHash, + uint256 indexed ownerIndex, + address[] candidates, + bool[] allowed + ); + + /* + * @notice Emitted when a `mintlist` is removed for a domain by the owner or through + * `ZNSRootRegistrar.revokeDomain()`. + */ + event MintlistCleared(bytes32 indexed domainHash); + + /** + * @notice Emitted when the ZNSRootRegistrar address is set in state. + */ + event RootRegistrarSet(address registrar); + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + function distrConfigs( + bytes32 domainHash + ) external view returns ( + IZNSPricer pricerContract, + PaymentType paymentType, + AccessType accessType + ); + + function isMintlistedForDomain( + bytes32 domainHash, + address candidate + ) external view returns (bool); + + function initialize( + address _accessController, + address _registry, + address _rootRegistrar + ) external; + + function registerSubdomain( + bytes32 parentHash, + string calldata label, + address domainAddress, + string calldata tokenURI, + DistributionConfig calldata configForSubdomains, + PaymentConfig calldata paymentConfig + ) external returns (bytes32); + + function hashWithParent( + bytes32 parentHash, + string calldata label + ) external pure returns (bytes32); + + function setDistributionConfigForDomain( + bytes32 parentHash, + DistributionConfig calldata config + ) external; + + function setPricerContractForDomain( + bytes32 domainHash, + IZNSPricer pricerContract + ) external; + + function setPaymentTypeForDomain( + bytes32 domainHash, + PaymentType paymentType + ) external; + + function setAccessTypeForDomain( + bytes32 domainHash, + AccessType accessType + ) external; + + function updateMintlistForDomain( + bytes32 domainHash, + address[] calldata candidates, + bool[] calldata allowed + ) external; + + function clearMintlistForDomain(bytes32 domainHash) external; + + function clearMintlistAndLock(bytes32 domainHash) external; + + function setRegistry(address registry_) external; + + function setRootRegistrar(address registrar_) external; + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol new file mode 100644 index 000000000..ceae731e2 --- /dev/null +++ b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol @@ -0,0 +1,444 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { IZNSRootRegistrarPausable, CoreRegisterArgs } from "./IZNSRootRegistrarPausable.sol"; +import { IZNSTreasury, PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { IZNSDomainToken } from "../../token/IZNSDomainToken.sol"; +import { IZNSAddressResolver } from "../../resolver/IZNSAddressResolver.sol"; +import { IZNSSubRegistrarPausable } from "./IZNSSubRegistrarPausable.sol"; +import { IZNSPricer } from "../../types/IZNSPricer.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { StringUtils } from "../../utils/StringUtils.sol"; + + +/** + * @title Main entry point for the three main flows of ZNS - Register Root Domain, Reclaim and Revoke any domain. + * @notice This contract serves as the "umbrella" for many ZNS operations, it is given REGISTRAR_ROLE + * to combine multiple calls/operations between different modules to achieve atomic state changes + * and proper logic for the ZNS flows. You can see functions in other modules that are only allowed + * to be called by this contract to ensure proper management of ZNS data in multiple places. + * RRR - Register, Reclaim, Revoke start here and then call other modules to complete the flow. + * ZNSRootRegistrar.sol stores most of the other contract addresses and can communicate with other modules, + * but the relationship is one-sided, where other modules do not need to know about the ZNSRootRegistrar.sol, + * they only check REGISTRAR_ROLE that can, in theory, be assigned to any other address. + * @dev This contract is also called at the last stage of registering subdomains, since it has the common + * logic required to be performed for any level domains. + */ +contract ZNSRootRegistrarPausable is + UUPSUpgradeable, + AAccessControlled, + ARegistryWired, + IZNSRootRegistrarPausable { + using StringUtils for string; + + IZNSPricer public rootPricer; + IZNSTreasury public treasury; + IZNSDomainToken public domainToken; + IZNSSubRegistrarPausable public subRegistrar; + + bool private _paused; + + modifier whenNotPaused() { + require(!paused(), "ZNSRootRegistrar: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSRootRegistrar: Contract is not paused"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Create an instance of the ZNSRootRegistrar.sol + * for registering, reclaiming and revoking ZNS domains + * @dev Instead of direct assignments, we are calling the setter functions + * to apply Access Control and ensure only the ADMIN can set the addresses. + * @param accessController_ Address of the ZNSAccessController contract + * @param registry_ Address of the ZNSRegistry contract + * @param rootPricer_ Address of the IZNSPricer type contract that Zero chose to use for the root domains + * @param treasury_ Address of the ZNSTreasury contract + * @param domainToken_ Address of the ZNSDomainToken contract + */ + function initialize( + address accessController_, + address registry_, + address rootPricer_, + address treasury_, + address domainToken_ + ) external override initializer { + _setAccessController(accessController_); + setRegistry(registry_); + setRootPricer(rootPricer_); + setTreasury(treasury_); + setDomainToken(domainToken_); + } + + /** + * @notice This function is the main entry point for the Register Root Domain flow. + * Registers a new root domain such as `0://wilder`. + * Gets domain hash as a keccak256 hash of the domain label string casted to bytes32, + * checks existence of the domain in the registry and reverts if it exists. + * Calls `ZNSTreasury` to do the staking part, gets `tokenId` for the new token to be minted + * as domain hash casted to uint256, mints the token and sets the domain data in the `ZNSRegistry` + * and, possibly, `ZNSAddressResolver`. Emits a `DomainRegistered` event. + * @param name Name (label) of the domain to register + * @param domainAddress (optional) Address for the `ZNSAddressResolver` to return when requested + * @param tokenURI URI to assign to the Domain Token issued for the domain + * @param distributionConfig (optional) Distribution config for the domain to set in the same tx + * > Please note that passing distribution config will add more gas to the tx and most importantly - + * - the distributionConfig HAS to be passed FULLY filled or all zeros. It is optional as a whole, + * but all the parameters inside are required. + * @param paymentConfig (optional) Payment config for the domain to set on ZNSTreasury in the same tx + * > `paymentConfig` has to be fully filled or all zeros. It is optional as a whole, + * but all the parameters inside are required. + */ + function registerRootDomain( + string calldata name, + address domainAddress, + string calldata tokenURI, + DistributionConfig calldata distributionConfig, + PaymentConfig calldata paymentConfig + ) external override whenNotPaused returns (bytes32) { + // Confirms string values are only [a-z0-9-] + name.validate(); + + // Create hash for given domain name + bytes32 domainHash = keccak256(bytes(name)); + + require( + !registry.exists(domainHash), + "ZNSRootRegistrar: Domain already exists" + ); + + // Get price for the domain + uint256 domainPrice = rootPricer.getPrice(0x0, name, true); + + _coreRegister( + CoreRegisterArgs( + bytes32(0), + domainHash, + msg.sender, + domainAddress, + domainPrice, + 0, + name, + tokenURI, + true, + paymentConfig + ) + ); + + if (address(distributionConfig.pricerContract) != address(0)) { + // this adds additional gas to the register tx if passed + subRegistrar.setDistributionConfigForDomain(domainHash, distributionConfig); + } + + return domainHash; + } + + /** + * @notice External function used by `ZNSSubRegistrar` for the final stage of registering subdomains. + * @param args `CoreRegisterArgs`: Struct containing all the arguments required to register a domain + * with ZNSRootRegistrar.coreRegister(): + * + `parentHash`: The hash of the parent domain (0x0 for root domains) + * + `domainHash`: The hash of the domain to be registered + * + `label`: The label of the domain to be registered + * + `registrant`: The address of the user who is registering the domain + * + `price`: The determined price for the domain to be registered based on parent rules + * + `stakeFee`: The determined stake fee for the domain to be registered (only for PaymentType.STAKE!) + * + `domainAddress`: The address to which the domain will be resolved to + * + `tokenURI`: The tokenURI for the domain to be registered + * + `isStakePayment`: A flag for whether the payment is a stake payment or not + */ + function coreRegister( + CoreRegisterArgs memory args + ) external override onlyRegistrar { + _coreRegister( + args + ); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @dev Internal function that is called by this contract to finalize the registration of a domain. + * This function as also called by the external `coreRegister()` function as a part of + * registration of subdomains. + * This function kicks off payment processing logic, mints the token, sets the domain data in the `ZNSRegistry` + * and fires a `DomainRegistered` event. + * For params see external `coreRegister()` docs. + */ + function _coreRegister( + CoreRegisterArgs memory args + ) internal { + // payment part of the logic + if (args.price > 0) { + _processPayment(args); + } + + // Get tokenId for the new token to be minted for the new domain + uint256 tokenId = uint256(args.domainHash); + // mint token + domainToken.register(args.registrant, tokenId, args.tokenURI); + + // set data on Registry (for all) + Resolver (optional) + // If no domain address is given, only the domain owner is set, otherwise + // `ZNSAddressResolver` is called to assign an address to the newly registered domain. + // If the `domainAddress` is not provided upon registration, a user can call `ZNSAddressResolver.setAddress` + // to set the address themselves. + if (args.domainAddress != address(0)) { + registry.createDomainRecord(args.domainHash, args.registrant, "address"); + + IZNSAddressResolver(registry.getDomainResolver(args.domainHash)) + .setAddress(args.domainHash, args.domainAddress); + } else { + // By passing an empty string we tell the registry to not add a resolver + registry.createDomainRecord(args.domainHash, args.registrant, ""); + } + + // Because we check in the web app for the existance of both values in a payment config, + // it's fine to just check for one here + if (args.paymentConfig.beneficiary != address(0)) { + treasury.setPaymentConfig(args.domainHash, args.paymentConfig); + } + + emit DomainRegistered( + args.parentHash, + args.domainHash, + args.label, + tokenId, + args.tokenURI, + args.registrant, + args.domainAddress + ); + } + + /** + * @dev Internal function that is called by this contract to finalize the payment for a domain. + * Once the specific case is determined and `protocolFee` calculated, it calls ZNSTreasury to perform transfers. + */ + function _processPayment(CoreRegisterArgs memory args) internal { + // args.stakeFee can be 0 + uint256 protocolFee = rootPricer.getFeeForPrice(0x0, args.price + args.stakeFee); + + if (args.isStakePayment) { // for all root domains or subdomains with stake payment + treasury.stakeForDomain( + args.parentHash, + args.domainHash, + args.registrant, + args.price, + args.stakeFee, + protocolFee + ); + } else { // direct payment for subdomains + treasury.processDirectPayment( + args.parentHash, + args.domainHash, + args.registrant, + args.price, + protocolFee + ); + } + } + + /** + * @notice This function is the main entry point for the Revoke flow. + * Revokes a domain such as `0://wilder`. + * Gets `tokenId` from casted domain hash to uint256, calls `ZNSDomainToken` to burn the token, + * deletes the domain data from the `ZNSRegistry` and calls `ZNSTreasury` to unstake and withdraw funds + * user staked for the domain. Emits a `DomainRevoked` event. + * @dev > Note that we are not clearing the data in `ZNSAddressResolver` as it is considered not necessary + * since none other contracts will have the domain data on them. + * If we are not clearing `ZNSAddressResolver` state slots, we are making the next Register transaction + * for the same name cheaper, since SSTORE on a non-zero slot costs 5k gas, + * while SSTORE on a zero slot costs 20k gas. + * If a user wants to clear his data from `ZNSAddressResolver`, he can call `ZNSAddressResolver` directly himself + * BEFORE he calls to revoke, otherwise, `ZNSRegistry` owner check will fail, since the owner there + * will be 0x0 address. + * Also note that in order to Revoke, a caller has to be the owner of both: + * Name (in `ZNSRegistry`) and Token (in `ZNSDomainToken`). + * @param domainHash Hash of the domain to revoke + */ + function revokeDomain(bytes32 domainHash) + external + override + whenNotPaused + { + require( + isOwnerOf(domainHash, msg.sender, OwnerOf.BOTH), + "ZNSRootRegistrar: Not the owner of both Name and Token" + ); + + subRegistrar.clearMintlistAndLock(domainHash); + _coreRevoke(domainHash, msg.sender); + } + + /** + * @dev Internal part of the `revokeDomain()`. Called by this contract to finalize the Revoke flow of all domains. + * It calls `ZNSDomainToken` to burn the token, deletes the domain data from the `ZNSRegistry` and + * calls `ZNSTreasury` to unstake and withdraw funds user staked for the domain. Also emits + * a `DomainRevoked` event. + */ + function _coreRevoke(bytes32 domainHash, address owner) internal { + uint256 tokenId = uint256(domainHash); + domainToken.revoke(tokenId); + registry.deleteRecord(domainHash); + + // check if user registered a domain with the stake + (, uint256 stakedAmount) = treasury.stakedForDomain(domainHash); + bool stakeRefunded = false; + // send the stake back if it exists + if (stakedAmount > 0) { + treasury.unstakeForDomain(domainHash, owner); + stakeRefunded = true; + } + + emit DomainRevoked(domainHash, owner, stakeRefunded); + } + + /** + * @notice This function is the main entry point for the Reclaim flow. This flow is used to + * reclaim full ownership of a domain (through becoming the owner of the Name) from the ownership of the Token. + * This is used for different types of ownership transfers, such as: + * - domain sale - a user will sell the Token, then the new owner has to call this function to reclaim the Name + * - domain transfer - a user will transfer the Token, then the new owner + * has to call this function to reclaim the Name + * + * A user needs to only be the owner of the Token to be able to Reclaim. + * Updates the domain owner in the `ZNSRegistry` to the owner of the token and emits a `DomainReclaimed` event. + */ + function reclaimDomain(bytes32 domainHash) + external + override + whenNotPaused + { + require( + isOwnerOf(domainHash, msg.sender, OwnerOf.TOKEN), + "ZNSRootRegistrar: Not the owner of the Token" + ); + registry.updateDomainOwner(domainHash, msg.sender); + + emit DomainReclaimed(domainHash, msg.sender); + } + + /** + * @notice Function to validate that a given candidate is the owner of his Name, Token or both. + * @param domainHash Hash of the domain to check + * @param candidate Address of the candidate to check for ownership of the above domain's properties + * @param ownerOf Enum value to determine which ownership to check for: NAME, TOKEN, BOTH + */ + function isOwnerOf(bytes32 domainHash, address candidate, OwnerOf ownerOf) public view override returns (bool) { + if (ownerOf == OwnerOf.NAME) { + return candidate == registry.getDomainOwner(domainHash); + } else if (ownerOf == OwnerOf.TOKEN) { + return candidate == domainToken.ownerOf(uint256(domainHash)); + } else if (ownerOf == OwnerOf.BOTH) { + return candidate == registry.getDomainOwner(domainHash) + && candidate == domainToken.ownerOf(uint256(domainHash)); + } + + revert("Wrong enum value for `ownerOf`"); + } + + /** + * @notice Setter function for the `ZNSRegistry` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param registry_ Address of the `ZNSRegistry` contract + */ + function setRegistry(address registry_) public override(ARegistryWired, IZNSRootRegistrarPausable) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @notice Setter for the IZNSPricer type contract that Zero chooses to handle Root Domains. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param rootPricer_ Address of the IZNSPricer type contract to set as pricer of Root Domains + */ + function setRootPricer(address rootPricer_) public override onlyAdmin { + require( + rootPricer_ != address(0), + "ZNSRootRegistrar: rootPricer_ is 0x0 address" + ); + rootPricer = IZNSPricer(rootPricer_); + + emit RootPricerSet(rootPricer_); + } + + /** + * @notice Setter function for the `ZNSTreasury` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param treasury_ Address of the `ZNSTreasury` contract + */ + function setTreasury(address treasury_) public override onlyAdmin { + require( + treasury_ != address(0), + "ZNSRootRegistrar: treasury_ is 0x0 address" + ); + treasury = IZNSTreasury(treasury_); + + emit TreasurySet(treasury_); + } + + /** + * @notice Setter function for the `ZNSDomainToken` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param domainToken_ Address of the `ZNSDomainToken` contract + */ + function setDomainToken(address domainToken_) public override onlyAdmin { + require( + domainToken_ != address(0), + "ZNSRootRegistrar: domainToken_ is 0x0 address" + ); + domainToken = IZNSDomainToken(domainToken_); + + emit DomainTokenSet(domainToken_); + } + + /** + * @notice Setter for `ZNSSubRegistrar` contract. Only ADMIN in `ZNSAccessController` can call this function. + * @param subRegistrar_ Address of the `ZNSSubRegistrar` contract + */ + function setSubRegistrar(address subRegistrar_) external override onlyAdmin { + require(subRegistrar_ != address(0), "ZNSRootRegistrar: subRegistrar_ is 0x0 address"); + + subRegistrar = IZNSSubRegistrarPausable(subRegistrar_); + emit SubRegistrarSet(subRegistrar_); + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The implementation contract to upgrade to + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol new file mode 100644 index 000000000..4a16f848a --- /dev/null +++ b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol @@ -0,0 +1,398 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSPricer } from "../../types/IZNSPricer.sol"; +import { IZNSRootRegistrarPausable, CoreRegisterArgs } from "./IZNSRootRegistrarPausable.sol"; +import { IZNSSubRegistrarPausable } from "./IZNSSubRegistrarPausable.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { StringUtils } from "../../utils/StringUtils.sol"; +import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + + +/** + * @title ZNSSubRegistrar.sol - The contract for registering and revoking subdomains of zNS. + * @dev This contract has the entry point for registering subdomains, but calls + * the ZNSRootRegistrar back to finalize registration. Common logic for domains + * of any level is in the `ZNSRootRegistrar.coreRegister()`. +*/ +contract ZNSSubRegistrarPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSSubRegistrarPausable { + using StringUtils for string; + + /** + * @notice State var for the ZNSRootRegistrar contract that finalizes registration of subdomains. + */ + IZNSRootRegistrarPausable public rootRegistrar; + + /** + * @notice Mapping of domainHash to distribution config set by the domain owner/operator. + * These configs are used to determine how subdomains are distributed for every parent. + * @dev Note that the rules outlined in the DistributionConfig are only applied to direct children! + */ + mapping(bytes32 domainHash => DistributionConfig config) public override distrConfigs; + + struct Mintlist { + mapping(uint256 idx => mapping(address candidate => bool allowed)) list; + uint256 ownerIndex; + } + + /** + * @notice Mapping of domainHash to mintlist set by the domain owner/operator. + * These configs are used to determine who can register subdomains for every parent + * in the case where parent's DistributionConfig.AccessType is set to AccessType.MINTLIST. + */ + mapping(bytes32 domainHash => Mintlist mintStruct) public mintlist; + + bool private _paused; + + modifier whenNotPaused() { + require(!paused(), "ZNSSubRegistrar: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSSubRegistrar: Contract is not paused"); + _; + } + + modifier onlyOwnerOperatorOrRegistrar(bytes32 domainHash) { + require( + registry.isOwnerOrOperator(domainHash, msg.sender) + || accessController.isRegistrar(msg.sender), + "ZNSSubRegistrar: Not authorized" + ); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize( + address _accessController, + address _registry, + address _rootRegistrar + ) external override initializer { + _setAccessController(_accessController); + setRegistry(_registry); + setRootRegistrar(_rootRegistrar); + } + + /** + * @notice Entry point to register a subdomain under a parent domain specified. + * @dev Reads the `DistributionConfig` for the parent domain to determine how to distribute, + * checks if the sender is allowed to register, check if subdomain is available, + * acquires the price and other data needed to finalize the registration + * and calls the `ZNSRootRegistrar.coreRegister()` to finalize. + * @param parentHash The hash of the parent domain to register the subdomain under + * @param label The label of the subdomain to register (e.g. in 0://zero.child the label would be "child"). + * @param domainAddress (optional) The address to which the subdomain will be resolved to + * @param tokenURI (required) The tokenURI for the subdomain to be registered + * @param distrConfig (optional) The distribution config to be set for the subdomain to set rules for children + * @param paymentConfig (optional) Payment config for the domain to set on ZNSTreasury in the same tx + * > `paymentConfig` has to be fully filled or all zeros. It is optional as a whole, + * but all the parameters inside are required. + */ + function registerSubdomain( + bytes32 parentHash, + string calldata label, + address domainAddress, + string calldata tokenURI, + DistributionConfig calldata distrConfig, + PaymentConfig calldata paymentConfig + ) external override whenNotPaused returns (bytes32) { + // Confirms string values are only [a-z0-9-] + label.validate(); + + bytes32 domainHash = hashWithParent(parentHash, label); + require( + !registry.exists(domainHash), + "ZNSSubRegistrar: Subdomain already exists" + ); + + DistributionConfig memory parentConfig = distrConfigs[parentHash]; + + bool isOwnerOrOperator = registry.isOwnerOrOperator(parentHash, msg.sender); + require( + parentConfig.accessType != AccessType.LOCKED || isOwnerOrOperator, + "ZNSSubRegistrar: Parent domain's distribution is locked or parent does not exist" + ); + + if (parentConfig.accessType == AccessType.MINTLIST) { + require( + mintlist[parentHash] + .list + [mintlist[parentHash].ownerIndex] + [msg.sender], + "ZNSSubRegistrar: Sender is not approved for purchase" + ); + } + + CoreRegisterArgs memory coreRegisterArgs = CoreRegisterArgs({ + parentHash: parentHash, + domainHash: domainHash, + label: label, + registrant: msg.sender, + price: 0, + stakeFee: 0, + domainAddress: domainAddress, + tokenURI: tokenURI, + isStakePayment: parentConfig.paymentType == PaymentType.STAKE, + paymentConfig: paymentConfig + }); + + if (!isOwnerOrOperator) { + if (coreRegisterArgs.isStakePayment) { + (coreRegisterArgs.price, coreRegisterArgs.stakeFee) = IZNSPricer(address(parentConfig.pricerContract)) + .getPriceAndFee( + parentHash, + label, + true + ); + } else { + coreRegisterArgs.price = IZNSPricer(address(parentConfig.pricerContract)) + .getPrice( + parentHash, + label, + true + ); + } + } + + rootRegistrar.coreRegister(coreRegisterArgs); + + // ! note that the config is set ONLY if ALL values in it are set, specifically, + // without pricerContract being specified, the config will NOT be set + if (address(distrConfig.pricerContract) != address(0)) { + setDistributionConfigForDomain(coreRegisterArgs.domainHash, distrConfig); + } + + return domainHash; + } + + /** + * @notice Helper function to hash a child label with a parent domain hash. + */ + function hashWithParent( + bytes32 parentHash, + string calldata label + ) public pure override returns (bytes32) { + return keccak256( + abi.encodePacked( + parentHash, + keccak256(bytes(label)) + ) + ); + } + + /** + * @notice Setter for `distrConfigs[domainHash]`. + * Only domain owner/operator or ZNSRootRegistrar can call this function. + * @dev This config can be changed by the domain owner/operator at any time or be set + * after registration if the config was not provided during the registration. + * Fires `DistributionConfigSet` event. + * @param domainHash The domain hash to set the distribution config for + * @param config The new distribution config to set (for config fields see `IDistributionConfig.sol`) + */ + function setDistributionConfigForDomain( + bytes32 domainHash, + DistributionConfig calldata config + ) public override whenNotPaused onlyOwnerOperatorOrRegistrar(domainHash) { + require( + address(config.pricerContract) != address(0), + "ZNSSubRegistrar: pricerContract can not be 0x0 address" + ); + + distrConfigs[domainHash] = config; + + emit DistributionConfigSet( + domainHash, + config.pricerContract, + config.paymentType, + config.accessType + ); + } + + /** + * @notice One of the individual setters for `distrConfigs[domainHash]`. Sets `pricerContract` field of the struct. + * Made to be able to set the pricer contract for a domain without setting the whole config. + * Only domain owner/operator can call this function. + * Fires `PricerContractSet` event. + * @param domainHash The domain hash to set the pricer contract for + * @param pricerContract The new pricer contract to set + */ + function setPricerContractForDomain( + bytes32 domainHash, + IZNSPricer pricerContract + ) public override whenNotPaused { + require( + registry.isOwnerOrOperator(domainHash, msg.sender), + "ZNSSubRegistrar: Not authorized" + ); + + require( + address(pricerContract) != address(0), + "ZNSSubRegistrar: pricerContract can not be 0x0 address" + ); + + distrConfigs[domainHash].pricerContract = pricerContract; + + emit PricerContractSet(domainHash, address(pricerContract)); + } + + /** + * @notice One of the individual setters for `distrConfigs[domainHash]`. Sets `paymentType` field of the struct. + * Made to be able to set the payment type for a domain without setting the whole config. + * Only domain owner/operator can call this function. + * Fires `PaymentTypeSet` event. + * @param domainHash The domain hash to set the payment type for + * @param paymentType The new payment type to set + */ + function setPaymentTypeForDomain( + bytes32 domainHash, + PaymentType paymentType + ) public override whenNotPaused { + require( + registry.isOwnerOrOperator(domainHash, msg.sender), + "ZNSSubRegistrar: Not authorized" + ); + + distrConfigs[domainHash].paymentType = paymentType; + + emit PaymentTypeSet(domainHash, paymentType); + } + + /** + * @notice One of the individual setters for `distrConfigs[domainHash]`. Sets `accessType` field of the struct. + * Made to be able to set the access type for a domain without setting the whole config. + * Only domain owner/operator or ZNSRootRegistrar can call this function. + * Fires `AccessTypeSet` event. + * @param domainHash The domain hash to set the access type for + * @param accessType The new access type to set + */ + function setAccessTypeForDomain( + bytes32 domainHash, + AccessType accessType + ) public override whenNotPaused onlyOwnerOperatorOrRegistrar(domainHash) { + distrConfigs[domainHash].accessType = accessType; + emit AccessTypeSet(domainHash, accessType); + } + + /** + * @notice Setter for `mintlist[domainHash][candidate]`. Only domain owner/operator can call this function. + * Adds or removes candidates from the mintlist for a domain. Should only be used when the domain's owner + * wants to limit subdomain registration to a specific set of addresses. + * Can be used to add/remove multiple candidates at once. Can only be called by the domain owner/operator. + * Fires `MintlistUpdated` event. + * @param domainHash The domain hash to set the mintlist for + * @param candidates The array of candidates to add/remove + * @param allowed The array of booleans indicating whether to add or remove the candidate + */ + function updateMintlistForDomain( + bytes32 domainHash, + address[] calldata candidates, + bool[] calldata allowed + ) external override whenNotPaused { + require( + registry.isOwnerOrOperator(domainHash, msg.sender), + "ZNSSubRegistrar: Not authorized" + ); + + Mintlist storage mintlistForDomain = mintlist[domainHash]; + uint256 ownerIndex = mintlistForDomain.ownerIndex; + + for (uint256 i; i < candidates.length; i++) { + mintlistForDomain.list[ownerIndex][candidates[i]] = allowed[i]; + } + + emit MintlistUpdated(domainHash, ownerIndex, candidates, allowed); + } + + function isMintlistedForDomain( + bytes32 domainHash, + address candidate + ) external view override returns (bool) { + uint256 ownerIndex = mintlist[domainHash].ownerIndex; + return mintlist[domainHash].list[ownerIndex][candidate]; + } + + /* + * @notice Function to completely clear/remove the whole mintlist set for a given domain. + * Can only be called by the owner/operator of the domain or by `ZNSRootRegistrar` as a part of the + * `revokeDomain()` flow. + * Emits `MintlistCleared` event. + * @param domainHash The domain hash to clear the mintlist for + */ + function clearMintlistForDomain(bytes32 domainHash) + public + override + whenNotPaused + onlyOwnerOperatorOrRegistrar(domainHash) { + mintlist[domainHash].ownerIndex = mintlist[domainHash].ownerIndex + 1; + + emit MintlistCleared(domainHash); + } + + function clearMintlistAndLock(bytes32 domainHash) + external + override + whenNotPaused + onlyOwnerOperatorOrRegistrar(domainHash) { + setAccessTypeForDomain(domainHash, AccessType.LOCKED); + clearMintlistForDomain(domainHash); + } + + /** + * @notice Sets the registry address in state. + * @dev This function is required for all contracts inheriting `ARegistryWired`. + */ + function setRegistry(address registry_) public override(ARegistryWired, IZNSSubRegistrarPausable) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @notice Setter for `rootRegistrar`. Only admin can call this function. + * Fires `RootRegistrarSet` event. + * @param registrar_ The new address of the ZNSRootRegistrar contract + */ + function setRootRegistrar(address registrar_) public override onlyAdmin { + require(registrar_ != address(0), "ZNSSubRegistrar: _registrar can not be 0x0 address"); + rootRegistrar = IZNSRootRegistrarPausable(registrar_); + + emit RootRegistrarSet(registrar_); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The implementation contract to upgrade to + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 62f7f8715..42bc3d0fa 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -53,6 +53,16 @@ describe("ZNS V1 Upgrade and Lock Test", () => { instanceName: znsNames.treasury.instance, address: zns.treasury.target, }, + { + contractName: znsNames.rootRegistrar.contract, + instanceName: znsNames.rootRegistrar.instance, + address: zns.rootRegistrar.target, + }, + { + contractName: znsNames.subRegistrar.contract, + instanceName: znsNames.subRegistrar.instance, + address: zns.subRegistrar.target, + }, ]; const znsUpgraded = await upgradeZNS({ From fc15bc5a929c2109d639faf3504b86b31521cd4b Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 14:44:38 -0700 Subject: [PATCH 014/114] add proper contract name to revert messages in TreasuryPausable --- contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol index 5b9b60b05..d89c7c026 100644 --- a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -40,12 +40,12 @@ contract ZNSTreasuryPausable is AAccessControlled, ARegistryWired, UUPSUpgradeab bool private _paused; modifier whenNotPaused() { - require(!paused(), "CONTRACTNAME: Contract is paused"); + require(!paused(), "ZNSTreasury: Contract is paused"); _; } modifier whenPaused() { - require(paused(), "CONTRACTNAME: Contract is not paused"); + require(paused(), "ZNSTreasury: Contract is not paused"); _; } From bc1c82944782dcdbd1a4b48fa8597f52db4c32b6 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 14:53:51 -0700 Subject: [PATCH 015/114] add CurvePricePausable and FixedPricerPausable --- .../price/IZNSCurvePricerPausable.sol | 122 ++++++ .../price/IZNSFixedPricerPausable.sol | 83 ++++ .../price/ZNSCurvePricerPausable.sol | 390 ++++++++++++++++++ .../price/ZNSFixedPricerPausable.sol | 218 ++++++++++ test/zns-v1-upgrade-lock-test.ts | 10 + 5 files changed, 823 insertions(+) create mode 100644 contracts/zns-pausable/price/IZNSCurvePricerPausable.sol create mode 100644 contracts/zns-pausable/price/IZNSFixedPricerPausable.sol create mode 100644 contracts/zns-pausable/price/ZNSCurvePricerPausable.sol create mode 100644 contracts/zns-pausable/price/ZNSFixedPricerPausable.sol diff --git a/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol new file mode 100644 index 000000000..e7f91bfd7 --- /dev/null +++ b/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { ICurvePriceConfig } from "../../types/ICurvePriceConfig.sol"; +import { IZNSPricer } from "../../types/IZNSPricer.sol"; + + +interface IZNSCurvePricerPausable is ICurvePriceConfig, IZNSPricer { + + /** + * @notice Emitted when the `maxPrice` is set in `CurvePriceConfig` + * @param price The new maxPrice value + */ + event MaxPriceSet(bytes32 domainHash, uint256 price); + + /** + * @notice Emitted when the `minPrice` is set in `CurvePriceConfig` + * @param price The new minPrice value + */ + event MinPriceSet(bytes32 domainHash, uint256 price); + + /** + * @notice Emitted when the `baseLength` is set in `CurvePriceConfig` + * @param length The new baseLength value + */ + event BaseLengthSet(bytes32 domainHash, uint256 length); + + /** + * @notice Emitted when the `maxLength` is set in `CurvePriceConfig` + * @param length The new maxLength value + */ + event MaxLengthSet(bytes32 domainHash, uint256 length); + + /** + * @notice Emitted when the `precisionMultiplier` is set in `CurvePriceConfig` + * @param precision The new precisionMultiplier value + */ + event PrecisionMultiplierSet(bytes32 domainHash, uint256 precision); + + /** + * @notice Emitted when the `feePercentage` is set in state + * @param feePercentage The new feePercentage value + */ + event FeePercentageSet(bytes32 domainHash, uint256 feePercentage); + + /** + * @notice Emitted when the full `CurvePriceConfig` is set in state + * @param maxPrice The new `maxPrice` value + * @param minPrice The new `minPrice` value + * @param maxLength The new `maxLength` value + * @param baseLength The new `baseLength` value + * @param precisionMultiplier The new `precisionMultiplier` value + */ + event PriceConfigSet( + bytes32 domainHash, + uint256 maxPrice, + uint256 minPrice, + uint256 maxLength, + uint256 baseLength, + uint256 precisionMultiplier, + uint256 feePercentage + ); + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + function initialize( + address accessController_, + address registry_, + CurvePriceConfig calldata zeroPriceConfig_ + ) external; + + function getPrice( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view returns (uint256); + + function getFeeForPrice( + bytes32 parentHash, + uint256 price + ) external view returns (uint256); + + function getPriceAndFee( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view returns ( + uint256 price, + uint256 stakeFee + ); + + function setPriceConfig( + bytes32 domainHash, + CurvePriceConfig calldata priceConfig + ) external; + + function setMaxPrice(bytes32 domainHash, uint256 maxPrice) external; + + function setMinPrice(bytes32 domainHash, uint256 minPrice) external; + + function setBaseLength(bytes32 domainHash, uint256 length) external; + + function setMaxLength(bytes32 domainHash, uint256 length) external; + + function setPrecisionMultiplier(bytes32 domainHash, uint256 multiplier) external; + + function setFeePercentage(bytes32 domainHash, uint256 feePercentage) external; + + function setRegistry(address registry_) external; + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol b/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol new file mode 100644 index 000000000..d884f1d2f --- /dev/null +++ b/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSPricer } from "../../types/IZNSPricer.sol"; + + +/** + * @title IZNSFixedPricer.sol Below is the doc for PriceConfig struct. + * @notice Struct for price configurations per domainHash that is used in the `priceConfigs` mapping + * - price The value determining how much a subdomain under a particular parent would cost + * - feePercentage The value determining how much fee is charged for a subdomain registration + * @dev Please note that the `feePercentage` is set in the basis of 10,000 where 1% = 100 + * and feePercentage is NOT being read when used with PaymentType.DIRECT. This value is only + * used when PaymentType.STAKE is set in ZNSSubRegistrar. + */ +interface IZNSFixedPricerPausable is IZNSPricer { + /** + * @notice Emitted when the `PriceConfig.price` is set in state for a specific `domainHash` + * @param domainHash The hash of the domain who sets the price for subdomains + * @param newPrice The new price value set + */ + event PriceSet(bytes32 indexed domainHash, uint256 indexed newPrice); + + /** + * @notice Emitted when the `PriceConfig.feePercentage` is set in state for a specific `domainHash` + * @param domainHash The hash of the domain who sets the feePercentage for subdomains + * @param feePercentage The new feePercentage value set + */ + event FeePercentageSet(bytes32 indexed domainHash, uint256 indexed feePercentage); + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + struct PriceConfig { + uint256 price; + uint256 feePercentage; + bool isSet; + } + + function initialize(address _accessController, address _registry) external; + + function setPrice(bytes32 domainHash, uint256 _price) external; + + function getPrice( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view returns (uint256); + + function setFeePercentage( + bytes32 domainHash, + uint256 feePercentage + ) external; + + function getFeeForPrice( + bytes32 parentHash, + uint256 price + ) external view returns (uint256); + + function getPriceAndFee( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view returns (uint256 price, uint256 fee); + + function setPriceConfig( + bytes32 domainHash, + PriceConfig calldata priceConfig + ) external; + + function setRegistry(address registry_) external; + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol new file mode 100644 index 000000000..93509c25f --- /dev/null +++ b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IZNSCurvePricerPausable } from "./IZNSCurvePricerPausable.sol"; +import { StringUtils } from "../../utils/StringUtils.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWired } from "../../registry/ARegistryWired.sol"; + + +/** + * @title Implementation of the Curve Pricing, module that calculates the price of a domain + * based on its length and the rules set by Zero ADMIN. + * This module uses an asymptotic curve that starts from `maxPrice` for all domains <= `baseLength`. + * It then decreases in price, using the calculated price function below, until it reaches `minPrice` + * at `maxLength` length of the domain name. Price after `maxLength` is fixed and always equal to `minPrice`. + */ +contract ZNSCurvePricerPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSCurvePricerPausable { + using StringUtils for string; + + /** + * @notice Value used as a basis for percentage calculations, + * since Solidity does not support fractions. + */ + uint256 public constant PERCENTAGE_BASIS = 10000; + + /** + * @notice Mapping of domainHash to the price config for that domain set by the parent domain owner. + * @dev Zero, for pricing root domains, uses this mapping as well under 0x0 hash. + */ + mapping(bytes32 domainHash => CurvePriceConfig config) public priceConfigs; + + bool private _paused; + + modifier whenNotPaused() { + require(!paused(), "ZNSCurvePricer: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSCurvePricer: Contract is not paused"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Proxy initializer to set the initial state of the contract after deployment. + * Only Owner of the 0x0 hash (Zero owned address) can call this function. + * @dev > Note the for PriceConfig we set each value individually and calling + * 2 important functions that validate all of the config's values against the formula: + * - `setPrecisionMultiplier()` to validate precision multiplier + * - `_validateConfig()` to validate the whole config in order to avoid price spikes + * @param accessController_ the address of the ZNSAccessController contract. + * @param registry_ the address of the ZNSRegistry contract. + * @param zeroPriceConfig_ a number of variables that participate in the price calculation for subdomains. + */ + function initialize( + address accessController_, + address registry_, + CurvePriceConfig calldata zeroPriceConfig_ + ) external override initializer { + _setAccessController(accessController_); + _setRegistry(registry_); + + setPriceConfig(0x0, zeroPriceConfig_); + } + + /** + * @notice Get the price of a given domain name + * @dev `skipValidityCheck` param is added to provide proper revert when the user is + * calling this to find out the price of a domain that is not valid. But in Registrar contracts + * we want to do this explicitly and before we get the price to have lower tx cost for reverted tx. + * So Registrars will pass this bool as "true" to not repeat the validity check. + * Note that if calling this function directly to find out the price, a user should always pass "false" + * as `skipValidityCheck` param, otherwise, the price will be returned for an invalid label that is not + * possible to register. + * @param parentHash The hash of the parent domain under which price is determined + * @param label The label of the subdomain candidate to get the price for before/during registration + * @param skipValidityCheck If true, skips the validity check for the label + */ + function getPrice( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) public view override returns (uint256) { + require( + priceConfigs[parentHash].isSet, + "ZNSCurvePricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" + ); + + if (!skipValidityCheck) { + // Confirms string values are only [a-z0-9-] + label.validate(); + } + + uint256 length = label.strlen(); + // No pricing is set for 0 length domains + if (length == 0) return 0; + + return _getPrice(parentHash, length); + } + + /** + * @notice Part of the IZNSPricer interface - one of the functions required + * for any pricing contracts used with ZNS. It returns fee for a given price + * based on the value set by the owner of the parent domain. + * @param parentHash The hash of the parent domain under which fee is determined + * @param price The price to get the fee for + */ + function getFeeForPrice( + bytes32 parentHash, + uint256 price + ) public view override returns (uint256) { + return (price * priceConfigs[parentHash].feePercentage) / PERCENTAGE_BASIS; + } + + /** + * @notice Part of the IZNSPricer interface - one of the functions required + * for any pricing contracts used with ZNS. Returns both price and fee for a given label + * under the given parent. + * @param parentHash The hash of the parent domain under which price and fee are determined + * @param label The label of the subdomain candidate to get the price and fee for before/during registration + */ + function getPriceAndFee( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view override returns (uint256 price, uint256 stakeFee) { + price = getPrice(parentHash, label, skipValidityCheck); + stakeFee = getFeeForPrice(parentHash, price); + return (price, stakeFee); + } + + /** + * @notice Setter for `priceConfigs[domainHash]`. Only domain owner/operator can call this function. + * @dev Validates the value of the `precisionMultiplier` and the whole config in order to avoid price spikes, + * fires `PriceConfigSet` event. + * Only the owner of the domain or an allowed operator can call this function + * > This function should ALWAYS be used to set the config, since it's the only place where `isSet` is set to true. + * > Use the other individual setters to modify only, since they do not set this variable! + * @param domainHash The domain hash to set the price config for + * @param priceConfig The new price config to set + */ + function setPriceConfig( + bytes32 domainHash, + CurvePriceConfig calldata priceConfig + ) public override whenNotPaused { + setPrecisionMultiplier(domainHash, priceConfig.precisionMultiplier); + priceConfigs[domainHash].baseLength = priceConfig.baseLength; + priceConfigs[domainHash].maxPrice = priceConfig.maxPrice; + priceConfigs[domainHash].minPrice = priceConfig.minPrice; + priceConfigs[domainHash].maxLength = priceConfig.maxLength; + setFeePercentage(domainHash, priceConfig.feePercentage); + priceConfigs[domainHash].isSet = true; + + _validateConfig(domainHash); + + emit PriceConfigSet( + domainHash, + priceConfig.maxPrice, + priceConfig.minPrice, + priceConfig.maxLength, + priceConfig.baseLength, + priceConfig.precisionMultiplier, + priceConfig.feePercentage + ); + } + + /** + * @notice Sets the max price for domains. Validates the config with the new price. + * Fires `MaxPriceSet` event. + * Only domain owner can call this function. + * > `maxPrice` can be set to 0 along with `baseLength` or `minPrice` to make all domains free! + * @dev We are checking here for possible price spike at `maxLength` if the `maxPrice` values is NOT 0. + * In the case of 0 we do not validate, since setting it to 0 will make all subdomains free. + * @param maxPrice The maximum price to set + */ + function setMaxPrice( + bytes32 domainHash, + uint256 maxPrice + ) external override whenNotPaused onlyOwnerOrOperator(domainHash) { + priceConfigs[domainHash].maxPrice = maxPrice; + + if (maxPrice != 0) _validateConfig(domainHash); + + emit MaxPriceSet(domainHash, maxPrice); + } + + /** + * @notice Sets the minimum price for domains. Validates the config with the new price. + * Fires `MinPriceSet` event. + * Only domain owner/operator can call this function. + * @param domainHash The domain hash to set the `minPrice` for + * @param minPrice The minimum price to set in $ZERO + */ + function setMinPrice( + bytes32 domainHash, + uint256 minPrice + ) external override whenNotPaused onlyOwnerOrOperator(domainHash) { + priceConfigs[domainHash].minPrice = minPrice; + + _validateConfig(domainHash); + + emit MinPriceSet(domainHash, minPrice); + } + + /** + * @notice Set the value of the domain name length boundary where the `maxPrice` applies + * e.g. A value of '5' means all domains <= 5 in length cost the `maxPrice` price + * Validates the config with the new length. Fires `BaseLengthSet` event. + * Only domain owner/operator can call this function. + * > `baseLength` can be set to 0 to make all domains cost `maxPrice`! + * > This indicates to the system that we are + * > currently in a special phase where we define an exact price for all domains + * > e.g. promotions or sales + * @param domainHash The domain hash to set the `baseLength` for + * @param length Boundary to set + */ + function setBaseLength( + bytes32 domainHash, + uint256 length + ) external override whenNotPaused onlyOwnerOrOperator(domainHash) { + priceConfigs[domainHash].baseLength = length; + + _validateConfig(domainHash); + + emit BaseLengthSet(domainHash, length); + } + + /** + * @notice Set the maximum length of a domain name to which price formula applies. + * All domain names (labels) that are longer than this value will cost the fixed price of `minPrice`, + * and the pricing formula will not apply to them. + * Validates the config with the new length. + * Fires `MaxLengthSet` event. + * Only domain owner/operator can call this function. + * > `maxLength` can be set to 0 to make all domains cost `minPrice`! + * @param domainHash The domain hash to set the `maxLength` for + * @param length The maximum length to set + */ + function setMaxLength( + bytes32 domainHash, + uint256 length + ) external override whenNotPaused onlyOwnerOrOperator(domainHash) { + priceConfigs[domainHash].maxLength = length; + + if (length != 0) _validateConfig(domainHash); + + emit MaxLengthSet(domainHash, length); + } + + /** + * @notice Sets the precision multiplier for the price calculation. + * Multiplier This should be picked based on the number of token decimals + * to calculate properly. + * e.g. if we use a token with 18 decimals, and want precision of 2, + * our precision multiplier will be equal to `10^(18 - 2) = 10^16` + * Fires `PrecisionMultiplierSet` event. + * Only domain owner/operator can call this function. + * > Multiplier should be less or equal to 10^18 and greater than 0! + * @param multiplier The multiplier to set + */ + function setPrecisionMultiplier( + bytes32 domainHash, + uint256 multiplier + ) public override whenNotPaused onlyOwnerOrOperator(domainHash) { + require(multiplier != 0, "ZNSCurvePricer: precisionMultiplier cannot be 0"); + require(multiplier <= 10**18, "ZNSCurvePricer: precisionMultiplier cannot be greater than 10^18"); + priceConfigs[domainHash].precisionMultiplier = multiplier; + + emit PrecisionMultiplierSet(domainHash, multiplier); + } + + /** + * @notice Sets the fee percentage for domain registration. + * @dev Fee percentage is set according to the basis of 10000, outlined in `PERCENTAGE_BASIS`. + * Fires `FeePercentageSet` event. + * Only domain owner/operator can call this function. + * @param domainHash The domain hash to set the fee percentage for + * @param feePercentage The fee percentage to set + */ + function setFeePercentage(bytes32 domainHash, uint256 feePercentage) + public + override + whenNotPaused + onlyOwnerOrOperator(domainHash) { + require( + feePercentage <= PERCENTAGE_BASIS, + "ZNSCurvePricer: feePercentage cannot be greater than PERCENTAGE_BASIS" + ); + + priceConfigs[domainHash].feePercentage = feePercentage; + emit FeePercentageSet(domainHash, feePercentage); + } + + /** + * @notice Sets the registry address in state. + * @dev This function is required for all contracts inheriting `ARegistryWired`. + */ + function setRegistry(address registry_) external override(ARegistryWired, IZNSCurvePricerPausable) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice Internal function to calculate price based on the config set, + * and the length of the domain label. + * @dev Before we calculate the price, 4 different cases are possible: + * 1. `maxPrice` is 0, which means all subdomains under this parent are free + * 2. `baseLength` is 0, which means we are returning `maxPrice` as a specific price for all domains + * 3. `length` is less than or equal to `baseLength`, which means a domain will cost `maxPrice` + * 4. `length` is greater than `maxLength`, which means a domain will cost `minPrice` + * + * The formula itself creates an asymptotic curve that decreases in pricing based on domain name length, + * base length and max price, the result is divided by the precision multiplier to remove numbers beyond + * what we care about, then multiplied by the same precision multiplier to get the actual value + * with truncated values past precision. So having a value of `15.235234324234512365 * 10^18` + * with precision `2` would give us `15.230000000000000000 * 10^18` + * @param length The length of the domain name + */ + function _getPrice( + bytes32 parentHash, + uint256 length + ) internal view returns (uint256) { + CurvePriceConfig memory config = priceConfigs[parentHash]; + + // We use `maxPrice` as 0 to indicate free domains + if (config.maxPrice == 0) return 0; + + // Setting baseLength to 0 indicates to the system that we are + // currently in a special phase where we define an exact price for all domains + // e.g. promotions or sales + if (config.baseLength == 0) return config.maxPrice; + if (length <= config.baseLength) return config.maxPrice; + if (length > config.maxLength) return config.minPrice; + + return (config.baseLength * config.maxPrice / length) + / config.precisionMultiplier * config.precisionMultiplier; + } + + /** + * @notice Internal function called every time we set props of `priceConfigs[domainHash]` + * to make sure that values being set can not disrupt the price curve or zero out prices + * for domains. If this validation fails, the parent function will revert. + * @dev We are checking here for possible price spike at `maxLength` + * which can occur if some of the config values are not properly chosen and set. + */ + function _validateConfig(bytes32 domainHash) internal view { + uint256 prevToMinPrice = _getPrice(domainHash, priceConfigs[domainHash].maxLength); + require( + priceConfigs[domainHash].minPrice <= prevToMinPrice, + "ZNSCurvePricer: incorrect value set causes the price spike at maxLength." + ); + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The new implementation contract to upgrade to. + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol new file mode 100644 index 000000000..64a886737 --- /dev/null +++ b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol @@ -0,0 +1,218 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { IZNSFixedPricerPausable } from "./IZNSFixedPricerPausable.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { StringUtils } from "../../utils/StringUtils.sol"; + + +/** + * @notice Pricer contract that uses the most straightforward fixed pricing model + * that doesn't depend on the length of the label. +*/ +contract ZNSFixedPricerPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSFixedPricerPausable { + using StringUtils for string; + + uint256 public constant PERCENTAGE_BASIS = 10000; + + /** + * @notice Mapping of domainHash to price config set by the domain owner/operator + */ + mapping(bytes32 domainHash => PriceConfig config) public priceConfigs; + + bool private _paused; + + modifier whenNotPaused() { + require(!paused(), "ZNSFixedPricer: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSFixedPricer: Contract is not paused"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + function initialize(address _accessController, address _registry) external override initializer { + _setAccessController(_accessController); + setRegistry(_registry); + } + + /** + * @notice Sets the price for a domain. Only callable by domain owner/operator. Emits a `PriceSet` event. + * @param domainHash The hash of the domain who sets the price for subdomains + * @param _price The new price value set + */ + function setPrice( + bytes32 domainHash, + uint256 _price + ) public override whenNotPaused onlyOwnerOrOperator(domainHash) { + _setPrice(domainHash, _price); + } + + /** + * @notice Gets the price for a subdomain candidate label under the parent domain. + * @dev `skipValidityCheck` param is added to provide proper revert when the user is + * calling this to find out the price of a domain that is not valid. But in Registrar contracts + * we want to do this explicitly and before we get the price to have lower tx cost for reverted tx. + * So Registrars will pass this bool as "true" to not repeat the validity check. + * Note that if calling this function directly to find out the price, a user should always pass "false" + * as `skipValidityCheck` param, otherwise, the price will be returned for an invalid label that is not + * possible to register. + * @param parentHash The hash of the parent domain to check the price under + * @param label The label of the subdomain candidate to check the price for + * @param skipValidityCheck If true, skips the validity check for the label + */ + // solhint-disable-next-line no-unused-vars + function getPrice( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) public override view returns (uint256) { + require( + priceConfigs[parentHash].isSet, + "ZNSFixedPricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" + ); + + if (!skipValidityCheck) { + // Confirms string values are only [a-z0-9-] + label.validate(); + } + + return priceConfigs[parentHash].price; + } + + /** + * @notice Sets the feePercentage for a domain. Only callable by domain owner/operator. + * Emits a `FeePercentageSet` event. + * @dev `feePercentage` is set as a part of the `PERCENTAGE_BASIS` of 10,000 where 1% = 100 + * @param domainHash The hash of the domain who sets the feePercentage for subdomains + * @param feePercentage The new feePercentage value set + */ + function setFeePercentage( + bytes32 domainHash, + uint256 feePercentage + ) public override whenNotPaused onlyOwnerOrOperator(domainHash) { + _setFeePercentage(domainHash, feePercentage); + } + + /** + * @notice Setter for `priceConfigs[domainHash]`. Only domain owner/operator can call this function. + * @dev Sets both `PriceConfig.price` and `PriceConfig.feePercentage` in one call, fires `PriceSet` + * and `FeePercentageSet` events. + * > This function should ALWAYS be used to set the config, since it's the only place where `isSet` is set to true. + * > Use the other individual setters to modify only, since they do not set this variable! + * @param domainHash The domain hash to set the price config for + * @param priceConfig The new price config to set + */ + function setPriceConfig( + bytes32 domainHash, + PriceConfig calldata priceConfig + ) external override whenNotPaused { + setPrice(domainHash, priceConfig.price); + setFeePercentage(domainHash, priceConfig.feePercentage); + priceConfigs[domainHash].isSet = true; + } + + /** + * @notice Part of the IZNSPricer interface - one of the functions required + * for any pricing contracts used with ZNS. It returns fee for a given price + * based on the value set by the owner of the parent domain. + * @param parentHash The hash of the parent domain under which fee is determined + * @param price The price to get the fee for + */ + function getFeeForPrice( + bytes32 parentHash, + uint256 price + ) public view override returns (uint256) { + return (price * priceConfigs[parentHash].feePercentage) / PERCENTAGE_BASIS; + } + + /** + * @notice Part of the IZNSPricer interface - one of the functions required + * for any pricing contracts used with ZNS. Returns both price and fee for a given label + * under the given parent. + * @param parentHash The hash of the parent domain under which price and fee are determined + * @param label The label of the subdomain candidate to get the price and fee for before/during registration + * @param skipValidityCheck If true, skips the validity check for the label + */ + function getPriceAndFee( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view override returns (uint256 price, uint256 fee) { + price = getPrice(parentHash, label, skipValidityCheck); + fee = getFeeForPrice(parentHash, price); + return (price, fee); + } + + /** + * @notice Sets the registry address in state. + * @dev This function is required for all contracts inheriting `ARegistryWired`. + */ + function setRegistry(address registry_) public override(ARegistryWired, IZNSFixedPricerPausable) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice Internal function for set price + * @param domainHash The hash of the domain + * @param price The new price + */ + function _setPrice(bytes32 domainHash, uint256 price) internal { + priceConfigs[domainHash].price = price; + emit PriceSet(domainHash, price); + } + + /** + * @notice Internal function for setFeePercentage + * @param domainHash The hash of the domain + * @param feePercentage The new feePercentage + */ + function _setFeePercentage(bytes32 domainHash, uint256 feePercentage) internal { + require( + feePercentage <= PERCENTAGE_BASIS, + "ZNSFixedPricer: feePercentage cannot be greater than PERCENTAGE_BASIS" + ); + + priceConfigs[domainHash].feePercentage = feePercentage; + emit FeePercentageSet(domainHash, feePercentage); + } + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The new implementation contract to upgrade to. + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 42bc3d0fa..1d361a2b6 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -63,6 +63,16 @@ describe("ZNS V1 Upgrade and Lock Test", () => { instanceName: znsNames.subRegistrar.instance, address: zns.subRegistrar.target, }, + { + contractName: znsNames.curvePricer.contract, + instanceName: znsNames.curvePricer.instance, + address: zns.curvePricer.target, + }, + { + contractName: znsNames.fixedPricer.contract, + instanceName: znsNames.fixedPricer.instance, + address: zns.fixedPricer.target, + }, ]; const znsUpgraded = await upgradeZNS({ From fb1239a69c0eaeafccd8bda6af7d47a82aee9d16 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 15:16:38 -0700 Subject: [PATCH 016/114] add AddressResolverPausable --- .../resolver/IZNSAddressResolverPausable.sol | 41 +++++ .../resolver/ZNSAddressResolverPausable.sol | 154 ++++++++++++++++++ test/zns-v1-upgrade-lock-test.ts | 5 + 3 files changed, 200 insertions(+) create mode 100644 contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol create mode 100644 contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol diff --git a/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol new file mode 100644 index 000000000..7f40e688d --- /dev/null +++ b/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + + +interface IZNSAddressResolverPausable { + /** + * @dev Emit when ownership of a domain is modified + * @param newAddress The new domain owner + * @param domainHash The identifying hash of a domain's name + */ + event AddressSet(bytes32 indexed domainHash, address indexed newAddress); + + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + function resolveDomainAddress(bytes32 domainHash) external view returns (address); + + function setAddress( + bytes32 domainHash, + address newAddress + ) external; + + function getInterfaceId() external pure returns (bytes4); + + function setRegistry(address _registry) external; + + function initialize(address _accessController, address _registry) external; + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol new file mode 100644 index 000000000..15f852c9d --- /dev/null +++ b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol @@ -0,0 +1,154 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IZNSAddressResolverPausable } from "./IZNSAddressResolverPausable.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWired } from "../../registry/ARegistryWired.sol"; + + +/** + * @title The specific Resolver for ZNS that maps domain hashes to Ethereum addresses these domains were made for. + * @notice This Resolver supports ONLY the address type. Every domain in ZNS made for a contract or wallet address + * will have a corresponding record in this Resolver. + */ +contract ZNSAddressResolverPausable is + UUPSUpgradeable, + AAccessControlled, + ARegistryWired, + ERC165, + IZNSAddressResolverPausable { + /** + * @notice Mapping of domain hash to address used to bind domains + * to Ethereum wallets or contracts registered in ZNS. + */ + mapping(bytes32 domainHash => address resolvedAddress) + internal domainAddresses; + + bool private _paused; + + modifier whenNotPaused() { + require(!paused(), "ZNSAddressResolver: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSAddressResolver: Contract is not paused"); + _; + } + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializer for the `ZNSAddressResolver` proxy. + * Note that setter functions are used instead of direct state variable assignments + * to use access control at deploy time. Only ADMIN can call this function. + * @param accessController_ The address of the `ZNSAccessController` contract + * @param registry_ The address of the `ZNSRegistry` contract + */ + function initialize(address accessController_, address registry_) external override initializer { + _setAccessController(accessController_); + setRegistry(registry_); + } + + /** + * @dev Returns address associated with a given domain name hash. + * @param domainHash The identifying hash of a domain's name + */ + function resolveDomainAddress( + bytes32 domainHash + ) external view override returns (address) { + return domainAddresses[domainHash]; + } + + /** + * @dev Sets the address for a domain name hash. This function can only + * be called by the owner, operator of the domain OR by the `ZNSRootRegistrar.sol` + * as a part of the Register flow. + * Emits an `AddressSet` event. + * @param domainHash The identifying hash of a domain's name + * @param newAddress The new address to map the domain to + */ + function setAddress( + bytes32 domainHash, + address newAddress + ) external override whenNotPaused { + // only owner or operator of the current domain can set the address + // also, ZNSRootRegistrar.sol can set the address as part of the registration process + require( + registry.isOwnerOrOperator(domainHash, msg.sender) || + accessController.isRegistrar(msg.sender), + "ZNSAddressResolver: Not authorized for this domain" + ); + + domainAddresses[domainHash] = newAddress; + + emit AddressSet(domainHash, newAddress); + } + + /** + * @dev ERC-165 check for implementation identifier + * @dev Supports interfaces IZNSAddressResolver and IERC165 + * @param interfaceId ID to check, XOR of the first 4 bytes of each function signature + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165, IZNSAddressResolverPausable) returns (bool) { + return + interfaceId == getInterfaceId() || + super.supportsInterface(interfaceId); + } + + /** + * @dev Exposes IZNSAddressResolver interfaceId + */ + function getInterfaceId() public pure override returns (bytes4) { + return type(IZNSAddressResolverPausable).interfaceId; + } + + /** + * @dev Sets the address of the `ZNSRegistry` contract that holds all crucial data + * for every domain in the system. This function can only be called by the ADMIN. + * Emits a `RegistrySet` event. + * @param _registry The address of the `ZNSRegistry` contract + */ + function setRegistry(address _registry) public override(ARegistryWired, IZNSAddressResolverPausable) onlyAdmin { + _setRegistry(_registry); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view virtual returns (bool) { + return _paused; + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The implementation contract to upgrade to + */ + // solhint-disable-next-line no-unused-vars + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 1d361a2b6..10a8c1a53 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -73,6 +73,11 @@ describe("ZNS V1 Upgrade and Lock Test", () => { instanceName: znsNames.fixedPricer.instance, address: zns.fixedPricer.target, }, + { + contractName: znsNames.addressResolver.contract, + instanceName: znsNames.addressResolver.instance, + address: zns.addressResolver.target, + }, ]; const znsUpgraded = await upgradeZNS({ From 1c7c6eb9d81805d0bb4f70ff9ed2420617a0edb6 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 15:40:41 -0700 Subject: [PATCH 017/114] add ARegistryWiredPausable, change all type imports to new Pausable ones and update imports for ARegistryWiredPausable --- .../price/ZNSCurvePricerPausable.sol | 8 ++-- .../price/ZNSFixedPricerPausable.sol | 8 ++-- .../registrar/IZNSRootRegistrarPausable.sol | 2 +- .../registrar/IZNSSubRegistrarPausable.sol | 2 +- .../registrar/ZNSRootRegistrarPausable.sol | 23 ++++----- .../registrar/ZNSSubRegistrarPausable.sol | 10 ++-- .../registry/ARegistryWiredPausable.sol | 48 +++++++++++++++++++ .../resolver/ZNSAddressResolverPausable.sol | 6 +-- .../treasury/ZNSTreasuryPausable.sol | 8 ++-- 9 files changed, 82 insertions(+), 33 deletions(-) create mode 100644 contracts/zns-pausable/registry/ARegistryWiredPausable.sol diff --git a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol index 93509c25f..74e1dbb16 100644 --- a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol +++ b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol @@ -5,7 +5,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils import { IZNSCurvePricerPausable } from "./IZNSCurvePricerPausable.sol"; import { StringUtils } from "../../utils/StringUtils.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; -import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; /** @@ -15,7 +15,7 @@ import { ARegistryWired } from "../../registry/ARegistryWired.sol"; * It then decreases in price, using the calculated price function below, until it reaches `minPrice` * at `maxLength` length of the domain name. Price after `maxLength` is fixed and always equal to `minPrice`. */ -contract ZNSCurvePricerPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSCurvePricerPausable { +contract ZNSCurvePricerPausable is AAccessControlled, ARegistryWiredPausable, UUPSUpgradeable, IZNSCurvePricerPausable { using StringUtils for string; /** @@ -299,9 +299,9 @@ contract ZNSCurvePricerPausable is AAccessControlled, ARegistryWired, UUPSUpgrad /** * @notice Sets the registry address in state. - * @dev This function is required for all contracts inheriting `ARegistryWired`. + * @dev This function is required for all contracts inheriting `ARegistryWiredPausable`. */ - function setRegistry(address registry_) external override(ARegistryWired, IZNSCurvePricerPausable) onlyAdmin { + function setRegistry(address registry_) external override(ARegistryWiredPausable, IZNSCurvePricerPausable) onlyAdmin { _setRegistry(registry_); } diff --git a/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol index 64a886737..a56a5c5b3 100644 --- a/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol +++ b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.18; import { AAccessControlled } from "../../access/AAccessControlled.sol"; -import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; import { IZNSFixedPricerPausable } from "./IZNSFixedPricerPausable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { StringUtils } from "../../utils/StringUtils.sol"; @@ -12,7 +12,7 @@ import { StringUtils } from "../../utils/StringUtils.sol"; * @notice Pricer contract that uses the most straightforward fixed pricing model * that doesn't depend on the length of the label. */ -contract ZNSFixedPricerPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSFixedPricerPausable { +contract ZNSFixedPricerPausable is AAccessControlled, ARegistryWiredPausable, UUPSUpgradeable, IZNSFixedPricerPausable { using StringUtils for string; uint256 public constant PERCENTAGE_BASIS = 10000; @@ -154,9 +154,9 @@ contract ZNSFixedPricerPausable is AAccessControlled, ARegistryWired, UUPSUpgrad /** * @notice Sets the registry address in state. - * @dev This function is required for all contracts inheriting `ARegistryWired`. + * @dev This function is required for all contracts inheriting `ARegistryWiredPausable`. */ - function setRegistry(address registry_) public override(ARegistryWired, IZNSFixedPricerPausable) onlyAdmin { + function setRegistry(address registry_) public override(ARegistryWiredPausable, IZNSFixedPricerPausable) onlyAdmin { _setRegistry(registry_); } diff --git a/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol index 9fb926061..2777badf8 100644 --- a/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.18; import { IDistributionConfig } from "../../types/IDistributionConfig.sol"; -import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; /** diff --git a/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol index f9c894cf0..7f193d497 100644 --- a/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol @@ -2,7 +2,7 @@ pragma solidity 0.8.18; import { IDistributionConfig } from "../../types/IDistributionConfig.sol"; -import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; import { IZNSPricer } from "../../types/IZNSPricer.sol"; diff --git a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol index ceae731e2..6f2fd8af2 100644 --- a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol @@ -1,12 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; +// TODO upg: should we change imports and types for the new pausable ones ?!! import { AAccessControlled } from "../../access/AAccessControlled.sol"; -import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; import { IZNSRootRegistrarPausable, CoreRegisterArgs } from "./IZNSRootRegistrarPausable.sol"; -import { IZNSTreasury, PaymentConfig } from "../../treasury/IZNSTreasury.sol"; -import { IZNSDomainToken } from "../../token/IZNSDomainToken.sol"; -import { IZNSAddressResolver } from "../../resolver/IZNSAddressResolver.sol"; +import { IZNSTreasuryPausable, PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; +import { IZNSDomainTokenPausable } from "../token/IZNSDomainTokenPausable.sol"; +import { IZNSAddressResolverPausable } from "../resolver/IZNSAddressResolverPausable.sol"; import { IZNSSubRegistrarPausable } from "./IZNSSubRegistrarPausable.sol"; import { IZNSPricer } from "../../types/IZNSPricer.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -29,13 +30,13 @@ import { StringUtils } from "../../utils/StringUtils.sol"; contract ZNSRootRegistrarPausable is UUPSUpgradeable, AAccessControlled, - ARegistryWired, + ARegistryWiredPausable, IZNSRootRegistrarPausable { using StringUtils for string; IZNSPricer public rootPricer; - IZNSTreasury public treasury; - IZNSDomainToken public domainToken; + IZNSTreasuryPausable public treasury; + IZNSDomainTokenPausable public domainToken; IZNSSubRegistrarPausable public subRegistrar; bool private _paused; @@ -217,7 +218,7 @@ contract ZNSRootRegistrarPausable is if (args.domainAddress != address(0)) { registry.createDomainRecord(args.domainHash, args.registrant, "address"); - IZNSAddressResolver(registry.getDomainResolver(args.domainHash)) + IZNSAddressResolverPausable(registry.getDomainResolver(args.domainHash)) .setAddress(args.domainHash, args.domainAddress); } else { // By passing an empty string we tell the registry to not add a resolver @@ -373,7 +374,7 @@ contract ZNSRootRegistrarPausable is * Only ADMIN in `ZNSAccessController` can call this function. * @param registry_ Address of the `ZNSRegistry` contract */ - function setRegistry(address registry_) public override(ARegistryWired, IZNSRootRegistrarPausable) onlyAdmin { + function setRegistry(address registry_) public override(ARegistryWiredPausable, IZNSRootRegistrarPausable) onlyAdmin { _setRegistry(registry_); } @@ -402,7 +403,7 @@ contract ZNSRootRegistrarPausable is treasury_ != address(0), "ZNSRootRegistrar: treasury_ is 0x0 address" ); - treasury = IZNSTreasury(treasury_); + treasury = IZNSTreasuryPausable(treasury_); emit TreasurySet(treasury_); } @@ -417,7 +418,7 @@ contract ZNSRootRegistrarPausable is domainToken_ != address(0), "ZNSRootRegistrar: domainToken_ is 0x0 address" ); - domainToken = IZNSDomainToken(domainToken_); + domainToken = IZNSDomainTokenPausable(domainToken_); emit DomainTokenSet(domainToken_); } diff --git a/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol index 4a16f848a..1de0bbed2 100644 --- a/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol @@ -5,9 +5,9 @@ import { IZNSPricer } from "../../types/IZNSPricer.sol"; import { IZNSRootRegistrarPausable, CoreRegisterArgs } from "./IZNSRootRegistrarPausable.sol"; import { IZNSSubRegistrarPausable } from "./IZNSSubRegistrarPausable.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; -import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; import { StringUtils } from "../../utils/StringUtils.sol"; -import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -17,7 +17,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils * the ZNSRootRegistrar back to finalize registration. Common logic for domains * of any level is in the `ZNSRootRegistrar.coreRegister()`. */ -contract ZNSSubRegistrarPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSSubRegistrarPausable { +contract ZNSSubRegistrarPausable is AAccessControlled, ARegistryWiredPausable, UUPSUpgradeable, IZNSSubRegistrarPausable { using StringUtils for string; /** @@ -346,9 +346,9 @@ contract ZNSSubRegistrarPausable is AAccessControlled, ARegistryWired, UUPSUpgra /** * @notice Sets the registry address in state. - * @dev This function is required for all contracts inheriting `ARegistryWired`. + * @dev This function is required for all contracts inheriting `ARegistryWiredPausable`. */ - function setRegistry(address registry_) public override(ARegistryWired, IZNSSubRegistrarPausable) onlyAdmin { + function setRegistry(address registry_) public override(ARegistryWiredPausable, IZNSSubRegistrarPausable) onlyAdmin { _setRegistry(registry_); } diff --git a/contracts/zns-pausable/registry/ARegistryWiredPausable.sol b/contracts/zns-pausable/registry/ARegistryWiredPausable.sol new file mode 100644 index 000000000..ac176ec4b --- /dev/null +++ b/contracts/zns-pausable/registry/ARegistryWiredPausable.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSRegistryPausable } from "./IZNSRegistryPausable.sol"; + + +/** + * @title ARegistryWiredPausable.sol - Abstract contract, intdroducing ZNSRegistry to the storage + * of children contracts. Inheriting this contract means that child is connected to ZNSRegistry + * and is able to get AC and domain data from it or write to it. +*/ +abstract contract ARegistryWiredPausable { + + /** + * @notice Emitted when the ZNSRegistry address is set in state of the child contract. + */ + event RegistrySet(address registry); + + /** + * @notice ZNSRegistry address in the state of the child contract. + */ + IZNSRegistryPausable public registry; + + modifier onlyOwnerOrOperator(bytes32 domainHash) { + require( + registry.isOwnerOrOperator(domainHash, msg.sender), + "ARegistryWired: Not authorized. Only Owner or Operator allowed" + ); + _; + } + + /** + * @notice Internal function to set the ZNSRegistry address in the state of the child contract. + */ + function _setRegistry(address registry_) internal { + require(registry_ != address(0), "ARegistryWired: _registry can not be 0x0 address"); + registry = IZNSRegistryPausable(registry_); + emit RegistrySet(registry_); + } + + /** + * @notice Virtual function to make sure the setter is always implemented in children, + * otherwise we will not be able to reset the ZNSRegistry address in children + * @dev The reason this function is not implemented here is because it has to be + * implemented with Access Control that only child contract is connected to. + */ + function setRegistry(address registry_) external virtual; +} diff --git a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol index 15f852c9d..1f3d6c10e 100644 --- a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol +++ b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol @@ -5,7 +5,7 @@ import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { IZNSAddressResolverPausable } from "./IZNSAddressResolverPausable.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; -import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; /** @@ -16,7 +16,7 @@ import { ARegistryWired } from "../../registry/ARegistryWired.sol"; contract ZNSAddressResolverPausable is UUPSUpgradeable, AAccessControlled, - ARegistryWired, + ARegistryWiredPausable, ERC165, IZNSAddressResolverPausable { /** @@ -116,7 +116,7 @@ contract ZNSAddressResolverPausable is * Emits a `RegistrySet` event. * @param _registry The address of the `ZNSRegistry` contract */ - function setRegistry(address _registry) public override(ARegistryWired, IZNSAddressResolverPausable) onlyAdmin { + function setRegistry(address _registry) public override(ARegistryWiredPausable, IZNSAddressResolverPausable) onlyAdmin { _setRegistry(_registry); } diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol index d89c7c026..36c7e804d 100644 --- a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -7,7 +7,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; import { PaymentConfig } from "./IZNSTreasuryPausable.sol"; -import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; /** @@ -18,7 +18,7 @@ import { ARegistryWired } from "../../registry/ARegistryWired.sol"; * It also stores the payment configurations for all domains and staked amounts and token addresses which were used. * This information is needed for revoking users to withdraw their stakes back when they exit the system. */ -contract ZNSTreasuryPausable is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSTreasuryPausable { +contract ZNSTreasuryPausable is AAccessControlled, ARegistryWiredPausable, UUPSUpgradeable, IZNSTreasuryPausable { using SafeERC20 for IERC20; /** @@ -284,11 +284,11 @@ contract ZNSTreasuryPausable is AAccessControlled, ARegistryWired, UUPSUpgradeab /** * @notice Sets the registry address in state. - * @dev This function is required for all contracts inheriting `ARegistryWired`. + * @dev This function is required for all contracts inheriting `ARegistryWiredPausable`. */ function setRegistry( address registry_ - ) external override(ARegistryWired, IZNSTreasuryPausable) onlyAdmin { + ) external override(ARegistryWiredPausable, IZNSTreasuryPausable) onlyAdmin { _setRegistry(registry_); } From f20ecac396ed6711445a734a1831e3c381ec453f Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 16:51:11 -0700 Subject: [PATCH 018/114] remove pause locks on onlyAdmin functions in Registry --- contracts/zns-pausable/registry/ZNSRegistryPausable.sol | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol index afbe94f66..ce6be6bef 100644 --- a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol +++ b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol @@ -196,7 +196,7 @@ contract ZNSRegistryPausable is * @notice Given a resolver type, returns the address of the resolver contract for that type or 0x0 if not found * @param resolverType The resolver type as a string, e.g. "address" */ - function getResolverType(string calldata resolverType) public view override returns(address) { + function getResolverType(string calldata resolverType) public view override returns (address) { return resolvers[resolverType]; } @@ -207,7 +207,7 @@ contract ZNSRegistryPausable is * @param resolverType The type of the resolver to add * @param resolver The address of the new resolver contract */ - function addResolverType(string calldata resolverType, address resolver) public override whenNotPaused onlyAdmin { + function addResolverType(string calldata resolverType, address resolver) public override onlyAdmin { resolvers[resolverType] = resolver; emit ResolverAdded(resolverType, resolver); } @@ -216,7 +216,7 @@ contract ZNSRegistryPausable is * @notice Delete a resolver type from the mapping of types * @param resolverType The type to be removed */ - function deleteResolverType(string calldata resolverType) public override whenNotPaused onlyAdmin { + function deleteResolverType(string calldata resolverType) public override onlyAdmin { delete resolvers[resolverType]; emit ResolverDeleted(resolverType); } From 7acd98c49eadbf0b1946fb59ef3111b5d3916b7d Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 24 Mar 2025 16:51:43 -0700 Subject: [PATCH 019/114] add overrides of ERC721 functions to DomainToken to lock approvals and transfers --- .../token/ZNSDomainTokenPausable.sol | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol index 1b59ea354..81bcac7a0 100644 --- a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol +++ b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol @@ -2,6 +2,7 @@ pragma solidity 0.8.18; import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; +import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ERC2981Upgradeable } from "@openzeppelin/contracts-upgradeable/token/common/ERC2981Upgradeable.sol"; import { ERC721URIStorageUpgradeable } @@ -198,6 +199,45 @@ contract ZNSDomainTokenPausable is emit Unpaused(msg.sender); } + function approve( + address to, + uint256 tokenId + ) public override (ERC721Upgradeable, IERC721Upgradeable) whenNotPaused { + super.approve(to, tokenId); + } + + function setApprovalForAll( + address operator, + bool approved + ) public override (ERC721Upgradeable, IERC721Upgradeable) whenNotPaused { + super.setApprovalForAll(operator, approved); + } + + function transferFrom( + address from, + address to, + uint256 tokenId + ) public override (ERC721Upgradeable, IERC721Upgradeable) whenNotPaused { + super.transferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId + ) public override (ERC721Upgradeable, IERC721Upgradeable) whenNotPaused { + super.safeTransferFrom(from, to, tokenId); + } + + function safeTransferFrom( + address from, + address to, + uint256 tokenId, + bytes memory data + ) public override (ERC721Upgradeable, IERC721Upgradeable) whenNotPaused { + super.safeTransferFrom(from, to, tokenId, data); + } + /** * @notice To allow for user extension of the protocol we have to * enable checking acceptance of new interfaces to ensure they are supported From 9c598cfc7eb16338551083118d13a7883dfab28a Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 25 Mar 2025 16:09:50 -0700 Subject: [PATCH 020/114] refactor interfaces to inherit from a single IZNSPausable and have less code, fix imports in other contracts --- contracts/zns-pausable/IZNSPausable.sol | 19 ++ .../price/IZNSCurvePricerPausable.sol | 120 +------------ .../price/IZNSFixedPricerPausable.sol | 81 +-------- .../price/ZNSCurvePricerPausable.sol | 8 +- .../price/ZNSFixedPricerPausable.sol | 8 +- .../registrar/IZNSRootRegistrarPausable.sol | 169 +----------------- .../registrar/IZNSSubRegistrarPausable.sol | 138 +------------- .../registrar/ZNSRootRegistrarPausable.sol | 16 +- .../registrar/ZNSSubRegistrarPausable.sol | 13 +- .../registry/IZNSRegistryPausable.sol | 163 +---------------- .../resolver/IZNSAddressResolverPausable.sol | 39 +--- .../resolver/ZNSAddressResolverPausable.sol | 5 +- .../token/IZNSDomainTokenPausable.sol | 77 +------- .../token/ZNSDomainTokenPausable.sol | 16 +- .../treasury/IZNSTreasuryPausable.sol | 168 +---------------- .../treasury/ZNSTreasuryPausable.sol | 5 +- 16 files changed, 97 insertions(+), 948 deletions(-) create mode 100644 contracts/zns-pausable/IZNSPausable.sol diff --git a/contracts/zns-pausable/IZNSPausable.sol b/contracts/zns-pausable/IZNSPausable.sol new file mode 100644 index 000000000..a7a572795 --- /dev/null +++ b/contracts/zns-pausable/IZNSPausable.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + + +interface IZNSPausable { + /** + * @dev Emitted when the pause is triggered by `account`. + */ + event Paused(address account); + + /** + * @dev Emitted when the pause is lifted by `account`. + */ + event Unpaused(address account); + + function pause() external; + + function unpause() external; +} diff --git a/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol index e7f91bfd7..6404e087a 100644 --- a/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol +++ b/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol @@ -1,122 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -import { ICurvePriceConfig } from "../../types/ICurvePriceConfig.sol"; -import { IZNSPricer } from "../../types/IZNSPricer.sol"; +import { IZNSCurvePricer } from "../../price/IZNSCurvePricer.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -interface IZNSCurvePricerPausable is ICurvePriceConfig, IZNSPricer { - - /** - * @notice Emitted when the `maxPrice` is set in `CurvePriceConfig` - * @param price The new maxPrice value - */ - event MaxPriceSet(bytes32 domainHash, uint256 price); - - /** - * @notice Emitted when the `minPrice` is set in `CurvePriceConfig` - * @param price The new minPrice value - */ - event MinPriceSet(bytes32 domainHash, uint256 price); - - /** - * @notice Emitted when the `baseLength` is set in `CurvePriceConfig` - * @param length The new baseLength value - */ - event BaseLengthSet(bytes32 domainHash, uint256 length); - - /** - * @notice Emitted when the `maxLength` is set in `CurvePriceConfig` - * @param length The new maxLength value - */ - event MaxLengthSet(bytes32 domainHash, uint256 length); - - /** - * @notice Emitted when the `precisionMultiplier` is set in `CurvePriceConfig` - * @param precision The new precisionMultiplier value - */ - event PrecisionMultiplierSet(bytes32 domainHash, uint256 precision); - - /** - * @notice Emitted when the `feePercentage` is set in state - * @param feePercentage The new feePercentage value - */ - event FeePercentageSet(bytes32 domainHash, uint256 feePercentage); - - /** - * @notice Emitted when the full `CurvePriceConfig` is set in state - * @param maxPrice The new `maxPrice` value - * @param minPrice The new `minPrice` value - * @param maxLength The new `maxLength` value - * @param baseLength The new `baseLength` value - * @param precisionMultiplier The new `precisionMultiplier` value - */ - event PriceConfigSet( - bytes32 domainHash, - uint256 maxPrice, - uint256 minPrice, - uint256 maxLength, - uint256 baseLength, - uint256 precisionMultiplier, - uint256 feePercentage - ); - - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - function initialize( - address accessController_, - address registry_, - CurvePriceConfig calldata zeroPriceConfig_ - ) external; - - function getPrice( - bytes32 parentHash, - string calldata label, - bool skipValidityCheck - ) external view returns (uint256); - - function getFeeForPrice( - bytes32 parentHash, - uint256 price - ) external view returns (uint256); - - function getPriceAndFee( - bytes32 parentHash, - string calldata label, - bool skipValidityCheck - ) external view returns ( - uint256 price, - uint256 stakeFee - ); - - function setPriceConfig( - bytes32 domainHash, - CurvePriceConfig calldata priceConfig - ) external; - - function setMaxPrice(bytes32 domainHash, uint256 maxPrice) external; - - function setMinPrice(bytes32 domainHash, uint256 minPrice) external; - - function setBaseLength(bytes32 domainHash, uint256 length) external; - - function setMaxLength(bytes32 domainHash, uint256 length) external; - - function setPrecisionMultiplier(bytes32 domainHash, uint256 multiplier) external; - - function setFeePercentage(bytes32 domainHash, uint256 feePercentage) external; - - function setRegistry(address registry_) external; - - function pause() external; - - function unpause() external; -} +interface IZNSCurvePricerPausable is IZNSCurvePricer, IZNSPausable {} diff --git a/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol b/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol index d884f1d2f..23ac14c3d 100644 --- a/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol +++ b/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol @@ -1,83 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -import { IZNSPricer } from "../../types/IZNSPricer.sol"; +import { IZNSFixedPricer } from "../../price/IZNSFixedPricer.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -/** - * @title IZNSFixedPricer.sol Below is the doc for PriceConfig struct. - * @notice Struct for price configurations per domainHash that is used in the `priceConfigs` mapping - * - price The value determining how much a subdomain under a particular parent would cost - * - feePercentage The value determining how much fee is charged for a subdomain registration - * @dev Please note that the `feePercentage` is set in the basis of 10,000 where 1% = 100 - * and feePercentage is NOT being read when used with PaymentType.DIRECT. This value is only - * used when PaymentType.STAKE is set in ZNSSubRegistrar. - */ -interface IZNSFixedPricerPausable is IZNSPricer { - /** - * @notice Emitted when the `PriceConfig.price` is set in state for a specific `domainHash` - * @param domainHash The hash of the domain who sets the price for subdomains - * @param newPrice The new price value set - */ - event PriceSet(bytes32 indexed domainHash, uint256 indexed newPrice); - - /** - * @notice Emitted when the `PriceConfig.feePercentage` is set in state for a specific `domainHash` - * @param domainHash The hash of the domain who sets the feePercentage for subdomains - * @param feePercentage The new feePercentage value set - */ - event FeePercentageSet(bytes32 indexed domainHash, uint256 indexed feePercentage); - - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - struct PriceConfig { - uint256 price; - uint256 feePercentage; - bool isSet; - } - - function initialize(address _accessController, address _registry) external; - - function setPrice(bytes32 domainHash, uint256 _price) external; - - function getPrice( - bytes32 parentHash, - string calldata label, - bool skipValidityCheck - ) external view returns (uint256); - - function setFeePercentage( - bytes32 domainHash, - uint256 feePercentage - ) external; - - function getFeeForPrice( - bytes32 parentHash, - uint256 price - ) external view returns (uint256); - - function getPriceAndFee( - bytes32 parentHash, - string calldata label, - bool skipValidityCheck - ) external view returns (uint256 price, uint256 fee); - - function setPriceConfig( - bytes32 domainHash, - PriceConfig calldata priceConfig - ) external; - - function setRegistry(address registry_) external; - - function pause() external; - - function unpause() external; -} +interface IZNSFixedPricerPausable is IZNSFixedPricer, IZNSPausable {} diff --git a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol index 74e1dbb16..e8eb22025 100644 --- a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol +++ b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.18; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { IZNSCurvePricerPausable } from "./IZNSCurvePricerPausable.sol"; +import { IZNSCurvePricer } from "../../price/IZNSCurvePricer.sol"; import { StringUtils } from "../../utils/StringUtils.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; @@ -301,7 +302,12 @@ contract ZNSCurvePricerPausable is AAccessControlled, ARegistryWiredPausable, UU * @notice Sets the registry address in state. * @dev This function is required for all contracts inheriting `ARegistryWiredPausable`. */ - function setRegistry(address registry_) external override(ARegistryWiredPausable, IZNSCurvePricerPausable) onlyAdmin { + function setRegistry(address registry_) + external + override( + ARegistryWiredPausable, + IZNSCurvePricer + ) onlyAdmin { _setRegistry(registry_); } diff --git a/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol index a56a5c5b3..acb927081 100644 --- a/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol +++ b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.18; import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; import { IZNSFixedPricerPausable } from "./IZNSFixedPricerPausable.sol"; +import { IZNSFixedPricer } from "../../price/IZNSFixedPricer.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { StringUtils } from "../../utils/StringUtils.sol"; @@ -156,7 +157,12 @@ contract ZNSFixedPricerPausable is AAccessControlled, ARegistryWiredPausable, UU * @notice Sets the registry address in state. * @dev This function is required for all contracts inheriting `ARegistryWiredPausable`. */ - function setRegistry(address registry_) public override(ARegistryWiredPausable, IZNSFixedPricerPausable) onlyAdmin { + function setRegistry(address registry_) + public + override( + ARegistryWiredPausable, + IZNSFixedPricer + ) onlyAdmin { _setRegistry(registry_); } diff --git a/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol index 2777badf8..c055cea02 100644 --- a/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol @@ -1,171 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -import { IDistributionConfig } from "../../types/IDistributionConfig.sol"; -import { PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; +import { IZNSRootRegistrar } from "../../registrar/IZNSRootRegistrar.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -/** - * @notice Stake fee is 0x0 for anything other than subdomain under a parent with Stake Payment - * parent hash will be 0x0 for root domain - */ -struct CoreRegisterArgs { - bytes32 parentHash; - bytes32 domainHash; - address registrant; - address domainAddress; - uint256 price; - uint256 stakeFee; - string label; - string tokenURI; - bool isStakePayment; - PaymentConfig paymentConfig; -} - -/** - * @title IZNSRootRegistrar.sol - Interface for the ZNSRootRegistrar contract resposible for registering root domains. - * @notice Below are docs for the types in this file: - * - `OwnerOf`: Enum signifying ownership of ZNS entities - * + NAME: The owner of the Name only - * + TOKEN: The owner of the Token only - * + BOTH: The owner of both the Name and the Token - * - `CoreRegisterArgs`: Struct containing all the arguments required to register a domain - * with ZNSRootRegistrar.coreRegister(): - * + `parentHash`: The hash of the parent domain (0x0 for root domains) - * + `domainHash`: The hash of the domain to be registered - * + `label`: The label of the domain to be registered - * + `registrant`: The address of the user who is registering the domain - * + `price`: The determined price for the domain to be registered based on parent rules - * + `stakeFee`: The determined stake fee for the domain to be registered (only for PaymentType.STAKE!) - * + `domainAddress`: The address to which the domain will be resolved to - * + `tokenURI`: The tokenURI for the domain to be registered - * + `isStakePayment`: A flag for whether the payment is a stake payment or not - */ -interface IZNSRootRegistrarPausable is IDistributionConfig { - - enum OwnerOf { - NAME, - TOKEN, - BOTH - } - - /** - * @notice Emitted when a NEW domain is registered. - * @dev `domainAddress` parameter is the address to which a domain name will relate to in ZNS. - * E.g. if a user made a domain for his wallet, the address of the wallet will be the `domainAddress`. - * This can be 0 as this variable is not required to perform registration process - * and can be set at a later time by the domain owner. - * @param parentHash The hash of the parent domain (0x0 for root domains) - * @param label The name as the last part of the full domain string (level) registered - * @param domainHash The hash of the domain registered - * @param tokenId The tokenId of the domain registered - * @param tokenURI The tokenURI of the domain registered - * @param registrant The address that called `ZNSRootRegistrar.registerRootDomain()` - * @param domainAddress The domain address of the domain registered - */ - event DomainRegistered( - bytes32 parentHash, - bytes32 indexed domainHash, - string label, - uint256 indexed tokenId, - string tokenURI, - address indexed registrant, - address domainAddress - ); - - /** - * @notice Emitted when a domain is revoked. - * @param domainHash The hash of the domain revoked - * @param owner The address that called `ZNSRootRegistrar.sol.revokeDomain()` and domain owner - * @param stakeRefunded A flag for whether the stake was refunded or not - */ - event DomainRevoked( - bytes32 indexed domainHash, - address indexed owner, - bool indexed stakeRefunded - ); - - /** - * @notice Emitted when an ownership of the Name is reclaimed by the Token owner. - * @param domainHash The hash of the domain reclaimed - * @param registrant The address that called `ZNSRootRegistrar.sol.reclaimDomain()` - */ - event DomainReclaimed( - bytes32 indexed domainHash, - address indexed registrant - ); - - /** - * @notice Emitted when the `rootPricer` address is set in state. - * @param rootPricer The new address of any IZNSPricer type contract - */ - event RootPricerSet(address rootPricer); - - /** - * @notice Emitted when the `treasury` address is set in state. - * @param treasury The new address of the Treasury contract - */ - event TreasurySet(address treasury); - - /** - * @notice Emitted when the `domainToken` address is set in state. - * @param domainToken The new address of the DomainToken contract - */ - event DomainTokenSet(address domainToken); - - /** - * @notice Emitted when the `subRegistrar` address is set in state. - * @param subRegistrar The new address of the SubRegistrar contract - */ - event SubRegistrarSet(address subRegistrar); - - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - function initialize( - address accessController_, - address registry_, - address rootPricer_, - address treasury_, - address domainToken_ - ) external; - - function registerRootDomain( - string calldata name, - address domainAddress, - string calldata tokenURI, - DistributionConfig calldata distributionConfig, - PaymentConfig calldata paymentConfig - ) external returns (bytes32); - - function coreRegister( - CoreRegisterArgs memory args - ) external; - - function revokeDomain(bytes32 domainHash) external; - - function reclaimDomain(bytes32 domainHash) external; - - function isOwnerOf(bytes32 domainHash, address candidate, OwnerOf ownerOf) external view returns (bool); - - function setRegistry(address registry_) external; - - function setRootPricer(address rootPricer_) external; - - function setTreasury(address treasury_) external; - - function setDomainToken(address domainToken_) external; - - function setSubRegistrar(address subRegistrar_) external; - - function pause() external; - - function unpause() external; -} +interface IZNSRootRegistrarPausable is IZNSRootRegistrar, IZNSPausable {} diff --git a/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol index 7f193d497..174247f64 100644 --- a/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol @@ -1,140 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -import { IDistributionConfig } from "../../types/IDistributionConfig.sol"; -import { PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; -import { IZNSPricer } from "../../types/IZNSPricer.sol"; +import { IZNSSubRegistrar } from "../../registrar/IZNSSubRegistrar.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -/** - * @title IZNSSubRegistrar.sol - Interface for the ZNSSubRegistrar contract responsible for registering subdomains. -*/ -interface IZNSSubRegistrarPausable is IDistributionConfig { - - /** - * @notice Emitted when a new `DistributionConfig.pricerContract` is set for a domain. - */ - event PricerContractSet(bytes32 indexed domainHash, address indexed pricerContract); - - /** - * @notice Emitted when a new `DistributionConfig.paymentType` is set for a domain. - */ - event PaymentTypeSet(bytes32 indexed domainHash, PaymentType paymentType); - - /** - * @notice Emitted when a new `DistributionConfig.accessType` is set for a domain. - */ - event AccessTypeSet(bytes32 indexed domainHash, AccessType accessType); - - /** - * @notice Emitted when a new full `DistributionConfig` is set for a domain at once. - */ - event DistributionConfigSet( - bytes32 indexed domainHash, - IZNSPricer pricerContract, - PaymentType paymentType, - AccessType accessType - ); - - /** - * @notice Emitted when a `mintlist` is updated for a domain. - */ - event MintlistUpdated( - bytes32 indexed domainHash, - uint256 indexed ownerIndex, - address[] candidates, - bool[] allowed - ); - - /* - * @notice Emitted when a `mintlist` is removed for a domain by the owner or through - * `ZNSRootRegistrar.revokeDomain()`. - */ - event MintlistCleared(bytes32 indexed domainHash); - - /** - * @notice Emitted when the ZNSRootRegistrar address is set in state. - */ - event RootRegistrarSet(address registrar); - - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - function distrConfigs( - bytes32 domainHash - ) external view returns ( - IZNSPricer pricerContract, - PaymentType paymentType, - AccessType accessType - ); - - function isMintlistedForDomain( - bytes32 domainHash, - address candidate - ) external view returns (bool); - - function initialize( - address _accessController, - address _registry, - address _rootRegistrar - ) external; - - function registerSubdomain( - bytes32 parentHash, - string calldata label, - address domainAddress, - string calldata tokenURI, - DistributionConfig calldata configForSubdomains, - PaymentConfig calldata paymentConfig - ) external returns (bytes32); - - function hashWithParent( - bytes32 parentHash, - string calldata label - ) external pure returns (bytes32); - - function setDistributionConfigForDomain( - bytes32 parentHash, - DistributionConfig calldata config - ) external; - - function setPricerContractForDomain( - bytes32 domainHash, - IZNSPricer pricerContract - ) external; - - function setPaymentTypeForDomain( - bytes32 domainHash, - PaymentType paymentType - ) external; - - function setAccessTypeForDomain( - bytes32 domainHash, - AccessType accessType - ) external; - - function updateMintlistForDomain( - bytes32 domainHash, - address[] calldata candidates, - bool[] calldata allowed - ) external; - - function clearMintlistForDomain(bytes32 domainHash) external; - - function clearMintlistAndLock(bytes32 domainHash) external; - - function setRegistry(address registry_) external; - - function setRootRegistrar(address registrar_) external; - - function pause() external; - - function unpause() external; -} +interface IZNSSubRegistrarPausable is IZNSSubRegistrar, IZNSPausable {} diff --git a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol index 6f2fd8af2..e6de77e62 100644 --- a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -// TODO upg: should we change imports and types for the new pausable ones ?!! import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; -import { IZNSRootRegistrarPausable, CoreRegisterArgs } from "./IZNSRootRegistrarPausable.sol"; -import { IZNSTreasuryPausable, PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; +import { IZNSRootRegistrarPausable } from "./IZNSRootRegistrarPausable.sol"; +import { IZNSRootRegistrar } from "../../registrar/IZNSRootRegistrar.sol"; +import { IZNSFixedPricer } from "../../price/IZNSFixedPricer.sol"; +import { CoreRegisterArgs } from "../../registrar/IZNSRootRegistrar.sol"; +import { IZNSTreasuryPausable } from "../treasury/IZNSTreasuryPausable.sol"; +import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; import { IZNSDomainTokenPausable } from "../token/IZNSDomainTokenPausable.sol"; import { IZNSAddressResolverPausable } from "../resolver/IZNSAddressResolverPausable.sol"; import { IZNSSubRegistrarPausable } from "./IZNSSubRegistrarPausable.sol"; @@ -374,7 +377,12 @@ contract ZNSRootRegistrarPausable is * Only ADMIN in `ZNSAccessController` can call this function. * @param registry_ Address of the `ZNSRegistry` contract */ - function setRegistry(address registry_) public override(ARegistryWiredPausable, IZNSRootRegistrarPausable) onlyAdmin { + function setRegistry(address registry_) + public + override( + ARegistryWiredPausable, + IZNSRootRegistrar + ) onlyAdmin { _setRegistry(registry_); } diff --git a/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol index 1de0bbed2..bd0398238 100644 --- a/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol @@ -2,12 +2,14 @@ pragma solidity 0.8.18; import { IZNSPricer } from "../../types/IZNSPricer.sol"; -import { IZNSRootRegistrarPausable, CoreRegisterArgs } from "./IZNSRootRegistrarPausable.sol"; +import { IZNSRootRegistrarPausable } from "./IZNSRootRegistrarPausable.sol"; +import { IZNSSubRegistrar } from "../../registrar/IZNSSubRegistrar.sol"; +import { CoreRegisterArgs } from "../../registrar/IZNSRootRegistrar.sol"; import { IZNSSubRegistrarPausable } from "./IZNSSubRegistrarPausable.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; import { StringUtils } from "../../utils/StringUtils.sol"; -import { PaymentConfig } from "../treasury/IZNSTreasuryPausable.sol"; +import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -348,7 +350,12 @@ contract ZNSSubRegistrarPausable is AAccessControlled, ARegistryWiredPausable, U * @notice Sets the registry address in state. * @dev This function is required for all contracts inheriting `ARegistryWiredPausable`. */ - function setRegistry(address registry_) public override(ARegistryWiredPausable, IZNSSubRegistrarPausable) onlyAdmin { + function setRegistry(address registry_) + public + override( + ARegistryWiredPausable, + IZNSSubRegistrar + ) onlyAdmin { _setRegistry(registry_); } diff --git a/contracts/zns-pausable/registry/IZNSRegistryPausable.sol b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol index 636a201ee..b19d25f6f 100644 --- a/contracts/zns-pausable/registry/IZNSRegistryPausable.sol +++ b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol @@ -1,165 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; +import { IZNSRegistry } from "../../registry/IZNSRegistry.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -/** - * @notice The `DomainRecord` struct is meant to hold relevant information - * about a domain, such as its owner and resolver. - * - `owner` (address): The owner of the domain (also called the owner of the Name). - * - `resolver` (address): The address of the Resolver contract where this domain's source records are stored. - * - * In the future, there will be multiple Resolver contracts that support different types of sources. - * Currently, only the `ZNSAddressResolver` is implemented. - */ -interface IZNSRegistryPausable { - /** - * @notice Description of a domain record, pointing to the - * owner address of that record as well as the address of - * its resolver - */ - struct DomainRecord { - address owner; - address resolver; - } - - /** - * @notice Emits when ownership of a domain is modified in ``records`` - * @param domainHash the hash of a domain's name - * @param owner The new domain owner - */ - event DomainOwnerSet( - bytes32 indexed domainHash, - address indexed owner - ); - - /** - * @notice Emit when a domain's resolver is modified in ``records`` - * @param domainHash the hash of a domain's name - * @param resolver The new resolver address - */ - event DomainResolverSet( - bytes32 indexed domainHash, - address indexed resolver - ); - - /** - * @notice Emits when a domain record is deleted - * @param domainHash The hash of a domain's name - */ - event DomainRecordDeleted( - bytes32 indexed domainHash - ); - - /** - * @notice Emit when an owner allows/disallows permissions for an operator - * @param owner Owner of the domain in question - * @param operator Address that was allowed/disallowed - * @param allowed Boolean status of their permission - */ - event OperatorPermissionSet( - address indexed owner, - address indexed operator, - bool allowed - ); - - /** - * @notice Emitted when a new resolver type is added to ZNS - * @param resolverType The name of the resolver type - * @param resolver The address of the resolver contract - */ - event ResolverAdded( - string resolverType, - address resolver - ); - - /** - * @notice Emitted when a resolver is deleted from ZNS - * @param resolverType The name of the resolver type - */ - event ResolverDeleted( - string resolverType - ); - - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - function initialize(address accessController) external; - - function exists(bytes32 domainHash) external view returns (bool); - - function isOwnerOrOperator( - bytes32 domainHash, - address candidate - ) external view returns (bool); - - function isOperatorFor( - address operator, - address owner - ) external view returns (bool); - - /** - * @notice Set an `operator` as `allowed` to give or remove permissions for all - * domains owned by `msg.sender` - * @param operator The account to allow/disallow - * @param allowed The true/false value to set - */ - function setOwnersOperator(address operator, bool allowed) external; - - function getDomainRecord( - bytes32 domainHash - ) external view returns (DomainRecord memory); - - function getDomainOwner( - bytes32 domainHash - ) external view returns (address); - - function getDomainResolver( - bytes32 domainHash - ) external view returns (address); - - function createDomainRecord( - bytes32 domainHash, - address owner, - string calldata resolverType - ) external; - - function getResolverType( - string calldata resolverType - ) external returns (address); - - function addResolverType( - string calldata resolverType, - address resolver - ) external; - - function deleteResolverType( - string calldata resolverType - ) external; - - function updateDomainRecord( - bytes32 domainHash, - address owner, - string calldata resolverType - ) external; - - function updateDomainOwner(bytes32 domainHash, address owner) external; - - function updateDomainResolver( - bytes32 domainHash, - string calldata resolverType - ) external; - - function deleteRecord(bytes32 domainHash) external; - - function pause() external; - - function unpause() external; -} +interface IZNSRegistryPausable is IZNSRegistry, IZNSPausable {} diff --git a/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol index 7f40e688d..130a2ede7 100644 --- a/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol +++ b/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol @@ -1,41 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; +import { IZNSAddressResolver } from "../../resolver/IZNSAddressResolver.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -interface IZNSAddressResolverPausable { - /** - * @dev Emit when ownership of a domain is modified - * @param newAddress The new domain owner - * @param domainHash The identifying hash of a domain's name - */ - event AddressSet(bytes32 indexed domainHash, address indexed newAddress); - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - function supportsInterface(bytes4 interfaceId) external view returns (bool); - - function resolveDomainAddress(bytes32 domainHash) external view returns (address); - - function setAddress( - bytes32 domainHash, - address newAddress - ) external; - - function getInterfaceId() external pure returns (bytes4); - - function setRegistry(address _registry) external; - - function initialize(address _accessController, address _registry) external; - - function pause() external; - - function unpause() external; -} +interface IZNSAddressResolverPausable is IZNSAddressResolver, IZNSPausable {} diff --git a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol index 1f3d6c10e..9953bf9b7 100644 --- a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol +++ b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol @@ -4,6 +4,7 @@ pragma solidity 0.8.18; import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { IZNSAddressResolverPausable } from "./IZNSAddressResolverPausable.sol"; +import { IZNSAddressResolver } from "../../resolver/IZNSAddressResolver.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; @@ -97,7 +98,7 @@ contract ZNSAddressResolverPausable is */ function supportsInterface( bytes4 interfaceId - ) public view virtual override(ERC165, IZNSAddressResolverPausable) returns (bool) { + ) public view virtual override(ERC165, IZNSAddressResolver) returns (bool) { return interfaceId == getInterfaceId() || super.supportsInterface(interfaceId); @@ -116,7 +117,7 @@ contract ZNSAddressResolverPausable is * Emits a `RegistrySet` event. * @param _registry The address of the `ZNSRegistry` contract */ - function setRegistry(address _registry) public override(ARegistryWiredPausable, IZNSAddressResolverPausable) onlyAdmin { + function setRegistry(address _registry) public override(ARegistryWiredPausable, IZNSAddressResolver) onlyAdmin { _setRegistry(_registry); } diff --git a/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol index 0309b0583..c8b30297f 100644 --- a/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol +++ b/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol @@ -1,79 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; -import { IERC2981Upgradeable } from "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; +import { IZNSDomainToken } from "../../token/IZNSDomainToken.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -interface IZNSDomainTokenPausable is IERC2981Upgradeable, IERC721Upgradeable { - - /** - * @notice Emitted when a Default Royalty (for all tokens) is set. - */ - event DefaultRoyaltySet(uint96 indexed defaultRoyalty); - /** - * @notice Emitted when Token Royalty is set for individual tokens per tokenID. - */ - event TokenRoyaltySet(uint256 indexed tokenId, uint96 indexed royalty); - /** - * @notice Emitted when a Base URI is set for all tokens. - */ - event BaseURISet(string indexed baseURI); - /** - * @notice Emitted when a Token URI is set for individual tokens per tokenID. - * @dev Note that this event is fired ONLY when the tokenURI is set externally - * through an external setter and NOT during the registration. - */ - event TokenURISet(uint256 indexed tokenId, string indexed tokenURI); - - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - function initialize( - address accessController, - string calldata tokenName, - string calldata tokenSymbol, - address defaultRoyaltyReceiver, - uint96 defaultRoyaltyFraction - ) external; - - function totalSupply() external view returns (uint256); - - function register( - address to, - uint256 tokenId, - string memory _tokenURI - ) external; - - function revoke(uint256 tokenId) external; - - function tokenURI(uint256 tokenId) - external - view - returns (string memory); - - function setBaseURI(string memory baseURI_) external; - - function setTokenURI(uint256 tokenId, string memory _tokenURI) external; - - function setDefaultRoyalty(address receiver, uint96 royaltyFraction) external; - - function setTokenRoyalty( - uint256 tokenId, - address receiver, - uint96 royaltyFraction - ) external; - - function pause() external; - - function unpause() external; - - function supportsInterface(bytes4 interfaceId) external view returns (bool); -} +interface IZNSDomainTokenPausable is IZNSDomainToken, IZNSPausable {} diff --git a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol index 81bcac7a0..de28977f9 100644 --- a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol +++ b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol @@ -8,6 +8,7 @@ import { ERC2981Upgradeable } from "@openzeppelin/contracts-upgradeable/token/co import { ERC721URIStorageUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; import { IZNSDomainTokenPausable } from "./IZNSDomainTokenPausable.sol"; +import { IZNSDomainToken } from "../../token/IZNSDomainToken.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; @@ -115,8 +116,11 @@ contract ZNSDomainTokenPausable is function tokenURI(uint256 tokenId) public view - override(ERC721URIStorageUpgradeable, ERC721Upgradeable, IZNSDomainTokenPausable) - returns (string memory) + override( + ERC721URIStorageUpgradeable, + ERC721Upgradeable, + IZNSDomainToken + ) returns (string memory) { return super.tokenURI(tokenId); } @@ -247,8 +251,12 @@ contract ZNSDomainTokenPausable is public view virtual - override(ERC721Upgradeable, ERC721URIStorageUpgradeable, ERC2981Upgradeable, IZNSDomainTokenPausable) - returns (bool) { + override( + ERC721Upgradeable, + ERC721URIStorageUpgradeable, + ERC2981Upgradeable, + IZNSDomainToken + ) returns (bool) { return super.supportsInterface(interfaceId); } diff --git a/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol index e5e7bc7a9..0dfb9094b 100644 --- a/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol +++ b/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol @@ -1,170 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.18; -import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import { IZNSTreasury } from "../../treasury/IZNSTreasury.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; -/** - * @notice The `PaymentConfig` struct describes the two pieces of information - * needed to create a payment configuration for a domain. The address of the - * user to send funds to in a sale, and what token those funds are in. - */ -struct PaymentConfig { - IERC20 token; - // feeBeneficiary if STAKE, paymentBeneficiary if DIRECT - address beneficiary; -} - -/** - * @title IZNSTreasury.sol - Interface for the ZNSTreasury contract responsible for managing payments and staking. - * @dev Below are docs for the types in this file: - * - `PaymentConfig`: Struct containing data for the payment configuration of the parent distributing subdomains: - * + `token`: The address of the ERC-20 compliant payment token contract chosen by the parent - * + `beneficiary`: The address of the beneficiary contract or wallet that will receive payments or fees - * - `Stake`: Struct containing data for the staking of a domain written at the time of staking: - * + `token`: The address of the ERC-20 compliant staking token used to deposit a specific stake for domain - * + `amount`: The amount of the staking token above deposited by the user -*/ -interface IZNSTreasuryPausable { - /** - * @notice Describe a stake for a domain. This could be - * in any ERC20 token so the address of the specific token - * as well as the amount is required. - */ - struct Stake { - IERC20 token; - uint256 amount; - } - - /** - * @notice Emitted when a new stake is deposited upon registration of a new domain. - * @param domainHash The hash of the domain name - * @param depositor The address of the depositing user / new domain owner - * @param stakeAmount The amount they are depositing / price of the domain based on name length - * @param stakeFee The registration fee paid by the user on top of the staked amount - */ - event StakeDeposited( - bytes32 indexed parentHash, - bytes32 indexed domainHash, - address indexed depositor, - address stakingToken, - uint256 stakeAmount, - uint256 stakeFee, - uint256 protocolFee - ); - - /** - * @notice Emitted when a stake is withdrawn upon domain revocation. - * @param domainHash The hash of the domain name being revoked - * @param owner The owner of the domain being revoked - * @param stakeAmount The staked amount withdrawn to the user after revoking - */ - event StakeWithdrawn( - bytes32 indexed domainHash, - address indexed owner, - address indexed stakingToken, - uint256 stakeAmount - ); - - /** - * @notice Emitted when a direct payment is processed upon registration of a new domain. - * @param parentHash The hash of the parent domain - * @param domainHash The full namehash of the domain registered - * @param payer The address of the user who paid for the domain - * @param beneficiary The address of the beneficiary contract or wallet that received the payment - * @param amount The amount paid by the user - * @param protocolFee The protocol fee paid by the user to Zero - */ - event DirectPaymentProcessed( - bytes32 indexed parentHash, - bytes32 indexed domainHash, - address indexed payer, - address beneficiary, - uint256 amount, - uint256 protocolFee - ); - - /** - * @notice Emitted when `curvePricer` is set in state. - * @param curvePricer The new address of the CurvePricer contract - */ - event CurvePricerSet(address curvePricer); - - /** - * @notice Emitted when `stakingToken` is set in state. - * @param token The new address of the ERC-20 compliant payment token contract - */ - event PaymentTokenSet(bytes32 indexed domainHash, address indexed token); - - /** - * @notice Emitted when `zeroVault` is set in state. - * @param beneficiary The new address of the beneficiary contract or wallet - */ - event BeneficiarySet(bytes32 indexed domainHash, address indexed beneficiary); - - /** - * @dev Emitted when the pause is triggered by `account`. - */ - event Paused(address account); - - /** - * @dev Emitted when the pause is lifted by `account`. - */ - event Unpaused(address account); - - function paymentConfigs( - bytes32 domainHash - ) external view returns ( - IERC20 token, - address beneficiary - ); - - function stakedForDomain(bytes32 domainHash) external view returns (IERC20, uint256); - - function stakeForDomain( - bytes32 parentHash, - bytes32 domainHash, - address depositor, - uint256 stakeAmount, - uint256 stakeFee, - uint256 protocolFee - ) external; - - function unstakeForDomain(bytes32 domainHash, address owner) external; - - function processDirectPayment( - bytes32 parentHash, - bytes32 domainHash, - address payer, - uint256 paymentAmount, - uint256 protocolFee - ) external; - - function setPaymentConfig( - bytes32 domainHash, - PaymentConfig memory paymentConfig - ) external; - - function setBeneficiary( - bytes32 domainHash, - address beneficiary - ) external; - - function setPaymentToken( - bytes32 domainHash, - address paymentToken - ) external; - - function setRegistry(address registry_) external; - - function initialize( - address accessController_, - address curvePricer_, - address stakingToken_, - address zeroVault_ - ) external; - - function pause() external; - - function unpause() external; -} +interface IZNSTreasuryPausable is IZNSTreasury, IZNSPausable {} diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol index 36c7e804d..92addf82f 100644 --- a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -2,11 +2,12 @@ pragma solidity 0.8.18; import { IZNSTreasuryPausable } from "./IZNSTreasuryPausable.sol"; +import { IZNSTreasury } from "../../treasury/IZNSTreasury.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import { PaymentConfig } from "./IZNSTreasuryPausable.sol"; +import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.sol"; @@ -288,7 +289,7 @@ contract ZNSTreasuryPausable is AAccessControlled, ARegistryWiredPausable, UUPSU */ function setRegistry( address registry_ - ) external override(ARegistryWiredPausable, IZNSTreasuryPausable) onlyAdmin { + ) external override(ARegistryWiredPausable, IZNSTreasury) onlyAdmin { _setRegistry(registry_); } From 9bd7662f3f46208098277af8eb794f60ac1abb3b Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 25 Mar 2025 17:47:47 -0700 Subject: [PATCH 021/114] add many crucial tests for every contract + test for domain path pre and post upgrade, extend types for upgraded ZNS --- src/upgrade/types.ts | 23 +- src/upgrade/upgrade.ts | 1 + test/zns-v1-upgrade-lock-test.ts | 620 +++++++++++++++++++++++++++++-- 3 files changed, 605 insertions(+), 39 deletions(-) diff --git a/src/upgrade/types.ts b/src/upgrade/types.ts index bb41ac8e9..ca7b9f597 100644 --- a/src/upgrade/types.ts +++ b/src/upgrade/types.ts @@ -1,4 +1,9 @@ -import { ZNSRegistryPausable } from "../../typechain"; +import { + ZNSAddressResolverPausable, + ZNSCurvePricerPausable, + ZNSDomainTokenPausable, ZNSFixedPricerPausable, + ZNSRegistryPausable, ZNSRootRegistrarPausable, ZNSSubRegistrarPausable, ZNSTreasuryPausable, +} from "../../typechain"; import { Addressable } from "ethers"; export type ContractStorageData = Array<{ @@ -12,9 +17,23 @@ export interface IContractData { } export type ZNSContractUpgraded = - ZNSRegistryPausable; + ZNSRegistryPausable | + ZNSDomainTokenPausable | + ZNSAddressResolverPausable | + ZNSCurvePricerPausable | + ZNSFixedPricerPausable | + ZNSTreasuryPausable | + ZNSRootRegistrarPausable | + ZNSSubRegistrarPausable; export interface IZNSContractsUpgraded { [instanceName : string] : ZNSContractUpgraded; registry : ZNSRegistryPausable; + domainToken : ZNSDomainTokenPausable; + addressResolver : ZNSAddressResolverPausable; + curvePricer : ZNSCurvePricerPausable; + fixedPricer : ZNSFixedPricerPausable; + treasury : ZNSTreasuryPausable; + rootRegistrar : ZNSRootRegistrarPausable; + subRegistrar : ZNSSubRegistrarPausable; } diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index f45d7d273..eb832c98c 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -8,6 +8,7 @@ import { IContractData, IZNSContractsUpgraded, ZNSContractUpgraded } from "./typ import { Addressable } from "ethers"; +// TODO upg: add MongoAdapter here to update the Database with the new contract addresses export const upgradeZNS = async ({ governorExt, contractData, diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 10a8c1a53..a40628d6a 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -1,26 +1,84 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ import * as hre from "hardhat"; import { IDeployCampaignConfig, TZNSContractState } from "../src/deploy/campaign/types"; import { getConfig } from "../src/deploy/campaign/environments"; import { runZnsCampaign } from "../src/deploy/zns-campaign"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { upgradeZNS, upgradeZNSContract } from "../src/upgrade/upgrade"; -import { IContractData } from "../src/upgrade/types"; +import { upgradeZNS } from "../src/upgrade/upgrade"; +import { ContractStorageData, IContractData, IZNSContractsUpgraded } from "../src/upgrade/types"; import { znsNames } from "../src/deploy/missions/contracts/names"; import { expect } from "chai"; +import { + AccessType, + curvePriceConfigEmpty, + DEFAULT_PRICE_CONFIG, + distrConfigEmpty, + paymentConfigEmpty, + PaymentType, +} from "./helpers"; +import { registerDomainPath } from "./helpers/flows/registration"; +import { IDomainConfigForTest, IFixedPriceConfig } from "./helpers/types"; +import * as ethers from "ethers"; +import { readContractStorage } from "../src/upgrade/storage-data"; describe("ZNS V1 Upgrade and Lock Test", () => { let deployer : SignerWithAddress; - let user : SignerWithAddress; let governor : SignerWithAddress; let admin : SignerWithAddress; - let randomUser : SignerWithAddress; + let randomAcc : SignerWithAddress; + let rootOwner : SignerWithAddress; + let lvl2SubOwner : SignerWithAddress; + let lvl3SubOwner : SignerWithAddress; + let lvl4SubOwner : SignerWithAddress; + let lvl5SubOwner : SignerWithAddress; + let lvl6SubOwner : SignerWithAddress; + let branchLvl1Owner : SignerWithAddress; + let branchLvl2Owner : SignerWithAddress; let zns : TZNSContractState; let zeroVault : SignerWithAddress; + let domainConfigs : Array; + let domainHashes : Array; + + const fixedPrice = ethers.parseEther("1375.612"); + const fixedFeePercentage = BigInt(200); + + const contractNames = { ...znsNames }; + // @ts-ignore + delete contractNames.erc1967Proxy; + // @ts-ignore + delete contractNames.accessController; + // @ts-ignore + delete contractNames.meowToken; + + let contractData : Array; + let znsUpgraded : IZNSContractsUpgraded; + + let preUpgradeZnsStorage : Array; + + let methodCalls : { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key : string] : Array<{ method : string; args : Array; }>; + }; + before(async () => { - [deployer, zeroVault, user, governor, admin, randomUser] = await hre.ethers.getSigners(); + [ + deployer, + zeroVault, + governor, + admin, + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + branchLvl1Owner, + branchLvl2Owner, + randomAcc, + ] = await hre.ethers.getSigners(); const config : IDeployCampaignConfig = await getConfig({ deployer, @@ -34,57 +92,545 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); zns = campaign.state.contracts; - }); + zns.zeroVaultAddress = zeroVault.address; - it.only("should upgrade all necessary ZNS contracts to pausable versions", async () => { - const contractData : Array = [ - { - contractName: znsNames.registry.contract, - instanceName: znsNames.registry.instance, - address: zns.registry.target, - }, - { - contractName: znsNames.domainToken.contract, - instanceName: znsNames.domainToken.instance, - address: zns.domainToken.target, - }, + // get base contract level storage for each contract pre-upgrade + preUpgradeZnsStorage = await Object.values(contractNames).reduce( + async (acc : Promise>, { contract, instance }) => { + const newAcc = await acc; + + const contractFactory = await hre.ethers.getContractFactory(contract); + const contractObj = zns[instance]; + + const storage = await readContractStorage(contractFactory, contractObj); + + return [...newAcc, storage]; + }, Promise.resolve([]) + ); + + // Give funds to users + await Promise.all( + [ + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + branchLvl1Owner, + branchLvl2Owner, + ].map(async ({ address }) => + zns.meowToken.mint(address, ethers.parseEther("1000000"))) + ); + await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + + domainConfigs = [ { - contractName: znsNames.treasury.contract, - instanceName: znsNames.treasury.instance, - address: zns.treasury.target, + user: rootOwner, + domainLabel: "root", + fullConfig: { + distrConfig: { + pricerContract: await zns.fixedPricer.getAddress(), + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: rootOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: BigInt(0) }, + }, }, { - contractName: znsNames.rootRegistrar.contract, - instanceName: znsNames.rootRegistrar.instance, - address: zns.rootRegistrar.target, + user: lvl2SubOwner, + domainLabel: "lvltwo", + fullConfig: { + distrConfig: { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: lvl2SubOwner.address, + }, + priceConfig: DEFAULT_PRICE_CONFIG, + }, }, { - contractName: znsNames.subRegistrar.contract, - instanceName: znsNames.subRegistrar.instance, - address: zns.subRegistrar.target, + user: lvl3SubOwner, + domainLabel: "lvlthree", + fullConfig: { + distrConfig: { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: lvl3SubOwner.address, + }, + priceConfig: DEFAULT_PRICE_CONFIG, + }, }, { - contractName: znsNames.curvePricer.contract, - instanceName: znsNames.curvePricer.instance, - address: zns.curvePricer.target, + user: lvl4SubOwner, + domainLabel: "lvlfour", + fullConfig: { + distrConfig: { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: lvl4SubOwner.address, + }, + priceConfig: DEFAULT_PRICE_CONFIG, + }, }, { - contractName: znsNames.fixedPricer.contract, - instanceName: znsNames.fixedPricer.instance, - address: zns.fixedPricer.target, + user: lvl5SubOwner, + domainLabel: "lvlfive", + fullConfig: { + distrConfig: { + pricerContract: await zns.fixedPricer.getAddress(), + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: lvl5SubOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, }, { - contractName: znsNames.addressResolver.contract, - instanceName: znsNames.addressResolver.instance, - address: zns.addressResolver.target, + user: lvl6SubOwner, + domainLabel: "lvlsix", + fullConfig: { + distrConfig: { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: lvl6SubOwner.address, + }, + priceConfig: DEFAULT_PRICE_CONFIG, + }, }, ]; - const znsUpgraded = await upgradeZNS({ + const regResults = await registerDomainPath({ + zns, + domainConfigs, + }); + + domainHashes = regResults.map(({ domainHash }) => domainHash); + + // UPGRADE ZNS CONTRACTS + contractData = Object.entries(contractNames).map( + ([name, { contract, instance }]) => ({ + contractName: contract, + instanceName: instance, + address: zns[name].target, + })); + + // run the upgrade + znsUpgraded = await upgradeZNS({ governorExt: governor, contractData, }); + methodCalls = { + [znsNames.registry.instance]: [ + { + method: "setOwnersOperator", + args: [randomAcc.address, true], + }, + { + method: "createDomainRecord", + args: [hre.ethers.ZeroHash, randomAcc.address, "address"], + }, + { + method: "updateDomainRecord", + args: [hre.ethers.ZeroHash, randomAcc.address, "address"], + }, + { + method: "updateDomainOwner", + args: [hre.ethers.ZeroHash, randomAcc.address], + }, + { + method: "updateDomainResolver", + args: [hre.ethers.ZeroHash, "address"], + }, + { + method: "deleteRecord", + args: [hre.ethers.ZeroHash], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.domainToken.instance]: [ + { + method: "transferFrom", + args: [deployer.address, randomAcc.address, 1], + }, + { + // @ts-ignore + method: "safeTransferFrom(address,address,uint256)", + args: [deployer.address, randomAcc.address, "1"], + }, + { + // @ts-ignore + method: "safeTransferFrom(address,address,uint256,bytes)", + args: [deployer.address, randomAcc.address, "1", hre.ethers.ZeroHash], + }, + { + method: "approve", + args: [randomAcc.address, 1], + }, + { + method: "setApprovalForAll", + args: [randomAcc.address, true], + }, + { + method: "register", + args: [randomAcc.address, 123n, "dummyURI"], + }, + { + method: "revoke", + args: [123n], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.addressResolver.instance]: [ + { + method: "setAddress", + args: [hre.ethers.ZeroHash, randomAcc.address], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.curvePricer.instance]: [ + { + method: "setPriceConfig", + args: [hre.ethers.ZeroHash, curvePriceConfigEmpty], + }, + { + method: "setMaxPrice", + args: [hre.ethers.ZeroHash, 1], + }, + { + method: "setMinPrice", + args: [hre.ethers.ZeroHash, 1], + }, + { + method: "setBaseLength", + args: [hre.ethers.ZeroHash, 1], + }, + { + method: "setMaxLength", + args: [hre.ethers.ZeroHash, 1], + }, + { + method: "setPrecisionMultiplier", + args: [hre.ethers.ZeroHash, 1], + }, + { + method: "setFeePercentage", + args: [hre.ethers.ZeroHash, 1], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.fixedPricer.instance]: [ + { + method: "setPrice", + args: [hre.ethers.ZeroHash, 1n], + }, + { + method: "setFeePercentage", + args: [hre.ethers.ZeroHash, 1], + }, + { + method: "setPriceConfig", + args: [hre.ethers.ZeroHash, { price: 1n, feePercentage: 1n, isSet: true }], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.treasury.instance]: [ + { + method: "stakeForDomain", + args: [hre.ethers.ZeroHash, hre.ethers.ZeroHash, randomAcc.address, 1n, 1n, 1n], + }, + { + method: "unstakeForDomain", + args: [hre.ethers.ZeroHash, randomAcc.address], + }, + { + method: "processDirectPayment", + args: [hre.ethers.ZeroHash, hre.ethers.ZeroHash, randomAcc.address, 1n, 1n], + }, + { + method: "setPaymentConfig", + args: [hre.ethers.ZeroHash, paymentConfigEmpty], + }, + { + method: "setBeneficiary", + args: [hre.ethers.ZeroHash, randomAcc.address], + }, + { + method: "setPaymentToken", + args: [hre.ethers.ZeroHash, randomAcc.address], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.rootRegistrar.instance]: [ + { + method: "registerRootDomain", + args: ["domain", randomAcc.address, "uri", distrConfigEmpty, paymentConfigEmpty], + }, + { + method: "revokeDomain", + args: [hre.ethers.ZeroHash], + }, + { + method: "reclaimDomain", + args: [hre.ethers.ZeroHash], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.subRegistrar.instance]: [ + { + method: "registerSubdomain", + args: [hre.ethers.ZeroHash, "label", randomAcc.address, "uri", distrConfigEmpty, paymentConfigEmpty], + }, + { + method: "setDistributionConfigForDomain", + args: [hre.ethers.ZeroHash, distrConfigEmpty], + }, + { + method: "setPricerContractForDomain", + args: [hre.ethers.ZeroHash, randomAcc.address], + }, + { + method: "setPaymentTypeForDomain", + args: [hre.ethers.ZeroHash, 0], + }, + { + method: "setAccessTypeForDomain", + args: [hre.ethers.ZeroHash, 0], + }, + { + method: "updateMintlistForDomain", + args: [hre.ethers.ZeroHash, [randomAcc.address], [true]], + }, + { + method: "clearMintlistForDomain", + args: [hre.ethers.ZeroHash], + }, + { + method: "clearMintlistAndLock", + args: [hre.ethers.ZeroHash], + }, + { + method: "pause", + args: [], + }, + ], + }; + }); + + it("should keep the same proxy addresses for each contract", async () => { expect(znsUpgraded.registry.target).to.equal(zns.registry.target); + expect(znsUpgraded.domainToken.target).to.equal(zns.domainToken.target); + expect(znsUpgraded.addressResolver.target).to.equal(zns.addressResolver.target); + expect(znsUpgraded.curvePricer.target).to.equal(zns.curvePricer.target); + expect(znsUpgraded.fixedPricer.target).to.equal(zns.fixedPricer.target); + expect(znsUpgraded.treasury.target).to.equal(zns.treasury.target); + expect(znsUpgraded.rootRegistrar.target).to.equal(zns.rootRegistrar.target); + expect(znsUpgraded.subRegistrar.target).to.equal(zns.subRegistrar.target); + }); + + describe("Post upgrade storage tests", () => { + it("should be able to operate on pre-upgrade domains and properly reflect in storage", async () => { + await domainConfigs.reduce( + async ( + acc, + { user, fullConfig }, + idx + ) => { + await acc; + const domainHash = domainHashes[idx]; + + // check SubRegistrar storage + const newPricer = fullConfig.distrConfig.pricerContract === zns.curvePricer.target + ? zns.fixedPricer.target + : zns.curvePricer.target; + const newPaymentType = fullConfig.distrConfig.paymentType === PaymentType.DIRECT + ? PaymentType.STAKE + : PaymentType.DIRECT; + const newAccessType = fullConfig.distrConfig.accessType === AccessType.OPEN + ? AccessType.LOCKED + : AccessType.OPEN; + + // set new values + await zns.subRegistrar.connect(user).setDistributionConfigForDomain( + domainHash, + { + pricerContract: newPricer, + paymentType: newPaymentType, + accessType: newAccessType, + } + ); + // check new values + const domainConfig = await zns.subRegistrar.distrConfigs(domainHash); + expect(domainConfig.pricerContract).to.equal(newPricer); + expect(domainConfig.paymentType).to.equal(newPaymentType); + expect(domainConfig.accessType).to.equal(newAccessType); + + // check Treasury storage + // set new values + await zns.treasury.connect(user).setPaymentConfig( + domainHash, + { + token: randomAcc.address, + beneficiary: randomAcc.address, + } + ); + // check new values + const paymentConfig = await zns.treasury.paymentConfigs(domainHash); + expect(paymentConfig.token).to.equal(randomAcc.address); + expect(paymentConfig.beneficiary).to.equal(randomAcc.address); + + if ((fullConfig.priceConfig as IFixedPriceConfig).price) { + // check FixedPricer storage + const newPriceConfig = { + price: 111n, + feePercentage: 111n, + isSet: true, + }; + // set new values + await zns.fixedPricer.connect(user).setPriceConfig(domainHash, newPriceConfig); + + const priceConfig = await zns.fixedPricer.priceConfigs(domainHash); + expect(priceConfig.price).to.equal(newPriceConfig.price); + expect(priceConfig.feePercentage).to.equal(newPriceConfig.feePercentage); + } else { + // check CurvePricer storage + const newPriceConfig = { + maxPrice: hre.ethers.parseEther("1000"), + minPrice: hre.ethers.parseEther("100"), + maxLength: 100n, + baseLength: 10n, + precisionMultiplier: 10n ** 14n, + feePercentage: 100n, + isSet: true, + }; + // set new values + await zns.curvePricer.connect(user).setPriceConfig(domainHash, newPriceConfig); + // check new values + const priceConfig = await zns.curvePricer.priceConfigs(domainHash); + expect(priceConfig.maxPrice).to.equal(newPriceConfig.maxPrice); + expect(priceConfig.minPrice).to.equal(newPriceConfig.minPrice); + expect(priceConfig.maxLength).to.equal(newPriceConfig.maxLength); + expect(priceConfig.baseLength).to.equal(newPriceConfig.baseLength); + expect(priceConfig.precisionMultiplier).to.equal(newPriceConfig.precisionMultiplier); + expect(priceConfig.feePercentage).to.equal(newPriceConfig.feePercentage); + expect(priceConfig.isSet).to.equal(newPriceConfig.isSet); + } + + // check AddressResolver storage + // set new values + await zns.addressResolver.connect(user).setAddress(domainHash, randomAcc.address); + // check new values + const addr = await zns.addressResolver.resolveDomainAddress(domainHash); + expect(addr).to.equal(randomAcc.address); + + // check DomainToken storage + // set new values + await zns.domainToken.connect(user).transferFrom(user.address, randomAcc.address, BigInt(domainHash)); + // check new values + const tokenOwner = await zns.domainToken.ownerOf(BigInt(domainHash)); + expect(tokenOwner).to.equal(randomAcc.address); + + // check Registry storage + // set new values + await zns.registry.connect(user).updateDomainOwner(domainHash, randomAcc.address); + // check new values + const owner = await zns.registry.getDomainOwner(domainHash); + expect(owner).to.equal(randomAcc.address); + }, Promise.resolve() + ); + }); + + it("should NOT change any contract level storage variables", async () => { + const postUpgradeStorageData = await Object.values(contractNames).reduce( + async (acc : Promise>, { contract, instance }) => { + const newAcc = await acc; + + const contractFactory = await hre.ethers.getContractFactory(contract); + const contractObj = znsUpgraded[instance]; + + const storage = await readContractStorage(contractFactory, contractObj); + + return [...newAcc, storage]; + }, Promise.resolve([]) + ); + + preUpgradeZnsStorage.forEach((preUpgrade, idx) => { + const postUpgrade = postUpgradeStorageData[idx]; + + expect(preUpgrade.length).to.equal(postUpgrade.length); + + preUpgrade.forEach((pre, idx2) => { + const post = postUpgrade[idx2]; + + expect(pre).to.deep.equal(post); + }); + }); + }); + }); + + describe("Should pause contracts and lock all functions with `whenNotPaused` modifier", () => { + Object.values(contractNames).forEach( + ({ contract: name, instance }) => { + it(`${name}`, async () => { + const contract = znsUpgraded[instance]; + + await contract.connect(deployer).pause(); + + expect(await contract.paused()).to.equal(true); + + const methods = methodCalls[instance]; + + for (const { method, args } of methods) { + // @ts-ignore + await expect(contract[method](...args)).to.be.revertedWith(`${name}: Contract is paused`); + } + }); + } + ); }); }); From 37ba878a76a1d0a6ed0ad807621cc885bab1e6e1 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 25 Mar 2025 17:51:49 -0700 Subject: [PATCH 022/114] add some comments and rename vars --- test/zns-v1-upgrade-lock-test.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index a40628d6a..f58c7a4bd 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -124,6 +124,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { ); await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + // register a bunch of domains pre-upgrade domainConfigs = [ { user: rootOwner, @@ -231,6 +232,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { domainHashes = regResults.map(({ domainHash }) => domainHash); // UPGRADE ZNS CONTRACTS + + // get contract data for the upgrade helper contractData = Object.entries(contractNames).map( ([name, { contract, instance }]) => ({ contractName: contract, @@ -244,6 +247,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { contractData, }); + // list of all the methods that are blocked with `whenNotPaused` modifier + // along with arguments for calls methodCalls = { [znsNames.registry.instance]: [ { @@ -474,7 +479,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); describe("Post upgrade storage tests", () => { - it("should be able to operate on pre-upgrade domains and properly reflect in storage", async () => { + it("should be able to operate on pre-upgrade domains and properly reflect changes in storage", async () => { await domainConfigs.reduce( async ( acc, @@ -599,13 +604,13 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }, Promise.resolve([]) ); - preUpgradeZnsStorage.forEach((preUpgrade, idx) => { - const postUpgrade = postUpgradeStorageData[idx]; + preUpgradeZnsStorage.forEach((storagePre, idx) => { + const storagePost = postUpgradeStorageData[idx]; - expect(preUpgrade.length).to.equal(postUpgrade.length); + expect(storagePre.length).to.equal(storagePost.length); - preUpgrade.forEach((pre, idx2) => { - const post = postUpgrade[idx2]; + storagePre.forEach((pre, idx2) => { + const post = storagePost[idx2]; expect(pre).to.deep.equal(post); }); From 87d11593be17d036a567f2ea8f3160bbb1462b10 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 26 Mar 2025 16:55:56 -0700 Subject: [PATCH 023/114] add code for database updates --- src/deploy/db/mongo-adapter/get-adapter.ts | 2 +- src/upgrade/db.ts | 31 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/upgrade/db.ts diff --git a/src/deploy/db/mongo-adapter/get-adapter.ts b/src/deploy/db/mongo-adapter/get-adapter.ts index 8c97b12d1..0c2e01378 100644 --- a/src/deploy/db/mongo-adapter/get-adapter.ts +++ b/src/deploy/db/mongo-adapter/get-adapter.ts @@ -52,7 +52,7 @@ export const getMongoAdapter = async (logger ?: TLogger) : Promise { + const { abi, bytecode } = hre.artifacts.readArtifactSync(`${contractName}Pausable`); + + await dbAdapter.contracts.updateOne( + { + name: contractName, + }, + { + $set: { + abi: JSON.stringify(abi), + bytecode, + implementation: implAddress, + }, + }, + { + upsert: true, + } + ); +}; From 92a591f0df90b0e1a78ca9cd862862c6ebad60d1 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 26 Mar 2025 16:56:31 -0700 Subject: [PATCH 024/114] integrate db update code into the upgrade helper --- src/upgrade/storage-data.ts | 3 +-- src/upgrade/upgrade.ts | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index 3d42b39de..ab8097564 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -39,8 +39,7 @@ export const readContractStorage = async ( !type.includes("mapping") ) { try { - // TODO upg: figure this out! - const value = await contractObj[label](); + const value = await contractObj[(label as keyof ZNSContract)](); newAcc.push({ [label]: value }); } catch (e : unknown) { diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index eb832c98c..615c75d9c 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -6,9 +6,11 @@ import { getLogger } from "../deploy/logger/create-logger"; import { TLogger } from "../deploy/campaign/types"; import { IContractData, IZNSContractsUpgraded, ZNSContractUpgraded } from "./types"; import { Addressable } from "ethers"; +import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; +import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; +import { updateContractInDb } from "./db"; -// TODO upg: add MongoAdapter here to update the Database with the new contract addresses export const upgradeZNS = async ({ governorExt, contractData, @@ -25,6 +27,8 @@ export const upgradeZNS = async ({ logger.info(`Governor acquired as ${governor.address}`); + const dbAdapter = await getMongoAdapter(logger); + const znsUpgraded = await contractData.reduce( async ( acc : Promise, @@ -36,6 +40,7 @@ export const upgradeZNS = async ({ contractName, contractAddress: address, governor, + dbAdapter, logger, }); @@ -46,18 +51,25 @@ export const upgradeZNS = async ({ Promise.resolve({} as IZNSContractsUpgraded) ); - return znsUpgraded; + await dbAdapter.finalizeDeployedVersion(); + + return { + znsUpgraded, + dbAdapter, + }; }; export const upgradeZNSContract = async ({ contractName, contractAddress, governor, + dbAdapter, logger, } : { contractName : string; contractAddress : string | Addressable; governor : SignerWithAddress; + dbAdapter : MongoDBAdapter; logger : TLogger; }) => { const originalFactory = await hre.ethers.getContractFactory(contractName); @@ -82,6 +94,12 @@ export const upgradeZNSContract = async ({ logger.info(`Upgraded ${contractName} to new implementation at: ${implAddress}`); + await updateContractInDb({ + dbAdapter, + contractName, + implAddress, + }); + const storageDataPostUpgrade = await readContractStorage( upgradedFactory, upgradedContract, From e7086a5223ccdb11d710bb771266c580be343992 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 26 Mar 2025 16:56:48 -0700 Subject: [PATCH 025/114] add database tests --- test/zns-v1-upgrade-lock-test.ts | 76 ++++++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index f58c7a4bd..2a06dc6e2 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -17,9 +17,12 @@ import { PaymentType, } from "./helpers"; import { registerDomainPath } from "./helpers/flows/registration"; -import { IDomainConfigForTest, IFixedPriceConfig } from "./helpers/types"; +import { IDomainConfigForTest, IFixedPriceConfig, ZNSContract } from "./helpers/types"; import * as ethers from "ethers"; import { readContractStorage } from "../src/upgrade/storage-data"; +import { MongoDBAdapter } from "../src/deploy/db/mongo-adapter/mongo-adapter"; +import { IContractDbData } from "../src/deploy/db/types"; +import { IDBVersion } from "../src/deploy/db/mongo-adapter/types"; describe("ZNS V1 Upgrade and Lock Test", () => { @@ -63,6 +66,9 @@ describe("ZNS V1 Upgrade and Lock Test", () => { [key : string] : Array<{ method : string; args : Array; }>; }; + let dbVersionDeploy : IDBVersion; + let dbAdapterUpgrade : MongoDBAdapter; + before(async () => { [ deployer, @@ -94,13 +100,15 @@ describe("ZNS V1 Upgrade and Lock Test", () => { zns = campaign.state.contracts; zns.zeroVaultAddress = zeroVault.address; + const { dbAdapter: dbAdapterDeploy } = campaign; + // get base contract level storage for each contract pre-upgrade preUpgradeZnsStorage = await Object.values(contractNames).reduce( async (acc : Promise>, { contract, instance }) => { const newAcc = await acc; const contractFactory = await hre.ethers.getContractFactory(contract); - const contractObj = zns[instance]; + const contractObj = zns[instance] as ZNSContract; const storage = await readContractStorage(contractFactory, contractObj); @@ -241,11 +249,14 @@ describe("ZNS V1 Upgrade and Lock Test", () => { address: zns[name].target, })); + process.env.MONGO_DB_VERSION = dbAdapterDeploy.curVersion; + dbVersionDeploy = await dbAdapterDeploy.getLatestVersion() as IDBVersion; + // run the upgrade - znsUpgraded = await upgradeZNS({ + ({ znsUpgraded, dbAdapter: dbAdapterUpgrade } = await upgradeZNS({ governorExt: governor, contractData, - }); + })); // list of all the methods that are blocked with `whenNotPaused` modifier // along with arguments for calls @@ -467,6 +478,11 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }; }); + after(async () => { + await dbAdapterUpgrade.dropDB(); + process.env.MONGO_DB_VERSION = ""; + }); + it("should keep the same proxy addresses for each contract", async () => { expect(znsUpgraded.registry.target).to.equal(zns.registry.target); expect(znsUpgraded.domainToken.target).to.equal(zns.domainToken.target); @@ -478,6 +494,58 @@ describe("ZNS V1 Upgrade and Lock Test", () => { expect(znsUpgraded.subRegistrar.target).to.equal(zns.subRegistrar.target); }); + describe("Database tests", () => { + it("should have the same version in the database", async () => { + const { + dbVersion: curDbVersion, + type: curVersionType, + } = await dbAdapterUpgrade.getLatestVersion() as IDBVersion; + + expect(dbVersionDeploy.dbVersion).to.equal(curDbVersion); + expect(curDbVersion).to.equal(process.env.MONGO_DB_VERSION); + + expect(dbVersionDeploy.type).to.equal(curVersionType); + }); + + it("should update docs for each upgraded contract properly", async () => { + await Object.values(contractNames).reduce( + async (acc, { contract, instance }) => { + await acc; + + const { + abi: abiPreUpgrade, + bytecode: bytecodePreUpgrade, + } = hre.artifacts.readArtifactSync(contract); + const { + abi: abiPausable, + bytecode: bytecodePausable, + } = hre.artifacts.readArtifactSync(`${contract}Pausable`); + + const { + abi: abiPostUpgrade, + bytecode: bytecodePostUpgrade, + implementation: implPostUpgrade, + version: versionPostUpgrade, + } = await dbAdapterUpgrade.getContract(contract) as IContractDbData; + + const implAddress = await hre.upgrades.erc1967.getImplementationAddress( + znsUpgraded[instance].target as string + ); + + expect(implAddress).to.equal(implPostUpgrade); + + expect(JSON.stringify(abiPreUpgrade)).to.not.equal(abiPostUpgrade); + expect(abiPostUpgrade).to.equal(JSON.stringify(abiPausable)); + + expect(bytecodePreUpgrade).to.not.equal(bytecodePostUpgrade); + expect(bytecodePostUpgrade).to.equal(bytecodePausable); + + expect(versionPostUpgrade).to.equal(dbVersionDeploy.dbVersion); + }, Promise.resolve() + ); + }); + }); + describe("Post upgrade storage tests", () => { it("should be able to operate on pre-upgrade domains and properly reflect changes in storage", async () => { await domainConfigs.reduce( From 160ec3a401be51ea20d432b993e82b10a9dfff4b Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 26 Mar 2025 17:57:44 -0700 Subject: [PATCH 026/114] update getMongoAdapter() to return existing adapter if not reset through a function --- src/deploy/db/mongo-adapter/get-adapter.ts | 35 +++++----------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/src/deploy/db/mongo-adapter/get-adapter.ts b/src/deploy/db/mongo-adapter/get-adapter.ts index 0c2e01378..63dba9072 100644 --- a/src/deploy/db/mongo-adapter/get-adapter.ts +++ b/src/deploy/db/mongo-adapter/get-adapter.ts @@ -11,19 +11,16 @@ export const resetMongoAdapter = () => { export const getMongoAdapter = async (logger ?: TLogger) : Promise => { - const checkParams = { + logger = !logger ? getLogger() : logger; + + const params = { + logger, dbUri: process.env.MONGO_DB_URI ? process.env.MONGO_DB_URI : DEFAULT_MONGO_URI, dbName: process.env.MONGO_DB_NAME ? process.env.MONGO_DB_NAME : DEFAULT_MONGO_DB_NAME, - }; - - logger = !logger ? getLogger() : logger; - - const params = { - logger, clientOpts: process.env.MONGO_DB_CLIENT_OPTS ? JSON.parse(process.env.MONGO_DB_CLIENT_OPTS) : undefined, @@ -33,34 +30,16 @@ export const getMongoAdapter = async (logger ?: TLogger) : Promise { - if (key === "version") key = "curVersion"; - - // if the existing adapter was created with different options than the currently needed one - // we create a new one and overwrite - if (JSON.stringify(mongoAdapter?.[key]) !== JSON.stringify(value)) { - createNew = true; - return; - } - } - ); + logger.debug("Returning existing MongoDBAdapter instance"); + return mongoAdapter; } else { - createNew = true; - } - - if (createNew) { logger.debug(`Creating new MongoDBAdapter instance with version: ${params.version}`); mongoAdapter = new MongoDBAdapter({ - ...checkParams, ...params, }); await mongoAdapter.initialize(params.version); - } else { - logger.debug("Returning existing MongoDBAdapter instance"); } - return mongoAdapter as MongoDBAdapter; + return mongoAdapter ; }; From 87915e4283312ebf11713b656ecc325f6587b48d Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 26 Mar 2025 17:58:22 -0700 Subject: [PATCH 027/114] create contract data generator helpers and update upgrade helpers and tests to properly pass logger and dbAdapter --- src/upgrade/storage-data.ts | 2 +- src/upgrade/upgrade.ts | 54 ++++++++++++++++++++++++++------ test/zns-v1-upgrade-lock-test.ts | 34 +++++++++----------- 3 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index ab8097564..f0c6dba0b 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -1,6 +1,6 @@ import * as hre from "hardhat"; import { expect } from "chai"; -import { Contract, ContractFactory } from "ethers"; +import { ContractFactory } from "ethers"; import { getStorageLayout, getUnlinkedBytecode, getVersion, StorageLayout } from "@openzeppelin/upgrades-core"; import { readValidations } from "@openzeppelin/hardhat-upgrades/dist/utils/validations"; import { ContractStorageData } from "./types"; diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index 615c75d9c..bdf7eaac4 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -9,26 +9,28 @@ import { Addressable } from "ethers"; import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; import { updateContractInDb } from "./db"; +import { znsNames } from "../deploy/missions/contracts/names"; +import { IContractDbData } from "../deploy/db/types"; export const upgradeZNS = async ({ governorExt, contractData, + dbAdapter, + logger, } : { - governorExt : SignerWithAddress; + governorExt ?: SignerWithAddress; contractData : Array; + dbAdapter : MongoDBAdapter; + logger : TLogger; }) => { let governor = governorExt; if (!governor) { [ governor ] = await hre.ethers.getSigners(); } - const logger = getLogger(); - logger.info(`Governor acquired as ${governor.address}`); - const dbAdapter = await getMongoAdapter(logger); - const znsUpgraded = await contractData.reduce( async ( acc : Promise, @@ -39,7 +41,7 @@ export const upgradeZNS = async ({ const upgradedContract = await upgradeZNSContract({ contractName, contractAddress: address, - governor, + governor: governor as SignerWithAddress, dbAdapter, logger, }); @@ -53,10 +55,7 @@ export const upgradeZNS = async ({ await dbAdapter.finalizeDeployedVersion(); - return { - znsUpgraded, - dbAdapter, - }; + return znsUpgraded; }; export const upgradeZNSContract = async ({ @@ -111,3 +110,38 @@ export const upgradeZNSContract = async ({ return upgradedContract; }; + +export const getContractNamesToUpgrade = () : Partial => { + const contractNames = JSON.parse(JSON.stringify(znsNames)); + delete contractNames.erc1967Proxy; + delete contractNames.accessController; + delete contractNames.meowToken; + + return contractNames; +}; + +export const getContractDataForUpgrade = async ( + dbAdapter : MongoDBAdapter, +) : Promise> => { + const contractNames = getContractNamesToUpgrade(); + + return Object.values(contractNames).reduce( + async ( + acc : Promise>, + { contract, instance } + ) => { + const contractData = await acc; + + const { address } = await dbAdapter.getContract(contract) as IContractDbData; + + contractData.push({ + contractName: contract, + instanceName: instance, + address, + }); + + return contractData; + }, + Promise.resolve([]) + ); +}; diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 2a06dc6e2..808e4afb0 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -4,7 +4,7 @@ import { IDeployCampaignConfig, TZNSContractState } from "../src/deploy/campaign import { getConfig } from "../src/deploy/campaign/environments"; import { runZnsCampaign } from "../src/deploy/zns-campaign"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { upgradeZNS } from "../src/upgrade/upgrade"; +import { getContractDataForUpgrade, getContractNamesToUpgrade, upgradeZNS } from "../src/upgrade/upgrade"; import { ContractStorageData, IContractData, IZNSContractsUpgraded } from "../src/upgrade/types"; import { znsNames } from "../src/deploy/missions/contracts/names"; import { expect } from "chai"; @@ -23,9 +23,11 @@ import { readContractStorage } from "../src/upgrade/storage-data"; import { MongoDBAdapter } from "../src/deploy/db/mongo-adapter/mongo-adapter"; import { IContractDbData } from "../src/deploy/db/types"; import { IDBVersion } from "../src/deploy/db/mongo-adapter/types"; +import { getMongoAdapter, resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; +import { getLogger } from "../src/deploy/logger/create-logger"; -describe("ZNS V1 Upgrade and Lock Test", () => { +describe.only("ZNS V1 Upgrade and Lock Test", () => { let deployer : SignerWithAddress; let governor : SignerWithAddress; let admin : SignerWithAddress; @@ -48,19 +50,15 @@ describe("ZNS V1 Upgrade and Lock Test", () => { const fixedPrice = ethers.parseEther("1375.612"); const fixedFeePercentage = BigInt(200); - const contractNames = { ...znsNames }; - // @ts-ignore - delete contractNames.erc1967Proxy; - // @ts-ignore - delete contractNames.accessController; - // @ts-ignore - delete contractNames.meowToken; + const contractNames = getContractNamesToUpgrade(); let contractData : Array; let znsUpgraded : IZNSContractsUpgraded; let preUpgradeZnsStorage : Array; + const logger = getLogger(); + let methodCalls : { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key : string] : Array<{ method : string; args : Array; }>; @@ -239,24 +237,22 @@ describe("ZNS V1 Upgrade and Lock Test", () => { domainHashes = regResults.map(({ domainHash }) => domainHash); - // UPGRADE ZNS CONTRACTS - // get contract data for the upgrade helper - contractData = Object.entries(contractNames).map( - ([name, { contract, instance }]) => ({ - contractName: contract, - instanceName: instance, - address: zns[name].target, - })); + contractData = await getContractDataForUpgrade(dbAdapterDeploy); process.env.MONGO_DB_VERSION = dbAdapterDeploy.curVersion; dbVersionDeploy = await dbAdapterDeploy.getLatestVersion() as IDBVersion; + resetMongoAdapter(); + dbAdapterUpgrade = await getMongoAdapter(); + // run the upgrade - ({ znsUpgraded, dbAdapter: dbAdapterUpgrade } = await upgradeZNS({ + znsUpgraded = await upgradeZNS({ governorExt: governor, contractData, - })); + logger, + dbAdapter: dbAdapterUpgrade, + }); // list of all the methods that are blocked with `whenNotPaused` modifier // along with arguments for calls From e726ec9a62c8cc493a38a699a5a6b7e385a77b87 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 26 Mar 2025 17:58:38 -0700 Subject: [PATCH 028/114] create top level execute-upgrade script --- src/upgrade/execute-upgrade.ts | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/upgrade/execute-upgrade.ts diff --git a/src/upgrade/execute-upgrade.ts b/src/upgrade/execute-upgrade.ts new file mode 100644 index 000000000..3603c83cc --- /dev/null +++ b/src/upgrade/execute-upgrade.ts @@ -0,0 +1,35 @@ +import { getLogger } from "../deploy/logger/create-logger"; +import { getContractDataForUpgrade, upgradeZNS } from "./upgrade"; +import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; + + +const execute = async () => { + const logger = getLogger(); + const dbAdapter = await getMongoAdapter(logger); + + logger.info("Prepairing contract data for the upgrade..."); + + const contractData = await getContractDataForUpgrade(dbAdapter); + + logger.info("Contract data prepared. Starting the upgrade..."); + + const znsUpgraded = await upgradeZNS({ + contractData, + dbAdapter, + logger, + }); + + return znsUpgraded; +}; + +execute() + .then(znsUpgraded => { + const log = getLogger(); + log.info(`Upgraded ${Object.keys(znsUpgraded).length} ZNS contracts.`); + process.exit(0); + }) + .catch(e => { + const log = getLogger(); + log.error("Error during upgrade:", e); + process.exit(1); + }); From 9e5caad2d518d8fb1f9570745988d1dd955e7cc2 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 26 Mar 2025 18:06:35 -0700 Subject: [PATCH 029/114] remove .only --- test/zns-v1-upgrade-lock-test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 808e4afb0..52d5af4fb 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -27,7 +27,7 @@ import { getMongoAdapter, resetMongoAdapter } from "../src/deploy/db/mongo-adapt import { getLogger } from "../src/deploy/logger/create-logger"; -describe.only("ZNS V1 Upgrade and Lock Test", () => { +describe("ZNS V1 Upgrade and Lock Test", () => { let deployer : SignerWithAddress; let governor : SignerWithAddress; let admin : SignerWithAddress; From a8a24e1276a800afe05529574ec22a5f283b51d5 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 13:01:47 -0700 Subject: [PATCH 030/114] remove defender code from running the campaign --- src/deploy/run-campaign.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/deploy/run-campaign.ts b/src/deploy/run-campaign.ts index 119a0928f..731839d01 100644 --- a/src/deploy/run-campaign.ts +++ b/src/deploy/run-campaign.ts @@ -1,23 +1,14 @@ +import * as hre from "hardhat"; import { getConfig } from "./campaign/environments"; import { runZnsCampaign } from "./zns-campaign"; -import { Defender } from "@openzeppelin/defender-sdk"; - import { getLogger } from "./logger/create-logger"; + const logger = getLogger(); const runCampaign = async () => { - const credentials = { - apiKey: process.env.DEFENDER_KEY, - apiSecret: process.env.DEFENDER_SECRET, - relayerApiKey: process.env.RELAYER_KEY, - relayerApiSecret: process.env.RELAYER_SECRET, - }; - - const client = new Defender(credentials); + const [ deployer ] = await hre.ethers.getSigners(); - const provider = client.relaySigner.getProvider(); - const deployer = client.relaySigner.getSigner(provider, { speed: "fast" }); const config = await getConfig({ deployer, @@ -25,7 +16,6 @@ const runCampaign = async () => { await runZnsCampaign({ config, - provider, }); }; From 4f2bb39ab04e26aaf2cab93e5c91200e63890a59 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 13:02:23 -0700 Subject: [PATCH 031/114] add multi db update function --- src/deploy/zns-campaign.ts | 2 +- src/upgrade/db.ts | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/deploy/zns-campaign.ts b/src/deploy/zns-campaign.ts index e7e4bd1d1..c865345a9 100644 --- a/src/deploy/zns-campaign.ts +++ b/src/deploy/zns-campaign.ts @@ -14,7 +14,7 @@ import { import { getMongoAdapter } from "./db/mongo-adapter/get-adapter"; import { getLogger } from "./logger/create-logger"; -// TODO how do we mock certain things for tests + export const runZnsCampaign = async ({ config, provider, diff --git a/src/upgrade/db.ts b/src/upgrade/db.ts index bfab3e0b4..5be16f7db 100644 --- a/src/upgrade/db.ts +++ b/src/upgrade/db.ts @@ -1,7 +1,29 @@ import * as hre from "hardhat"; import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; +import { IContractData } from "./types"; +import { IDBVersion } from "../deploy/db/mongo-adapter/types"; +export const updateAllContractsInDb = async ({ + contractData, + dbAdapter, +} : { + contractData : Array; + dbAdapter : MongoDBAdapter; +}) => { + for (const { contractName, address } of contractData) { + const implAddress = await hre.upgrades.erc1967.getImplementationAddress( + address as string + ); + + await updateContractInDb({ + dbAdapter, + contractName, + implAddress, + }); + } +}; + export const updateContractInDb = async ({ dbAdapter, contractName, @@ -13,9 +35,12 @@ export const updateContractInDb = async ({ }) => { const { abi, bytecode } = hre.artifacts.readArtifactSync(`${contractName}Pausable`); + const { dbVersion: curVersion } = await dbAdapter.getLatestVersion() as IDBVersion; + await dbAdapter.contracts.updateOne( { name: contractName, + version: curVersion, }, { $set: { From e5ffb1d697e56cafc3f6f249fde427578ed0e5d4 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 13:03:11 -0700 Subject: [PATCH 032/114] move executable scripts to their own folder --- src/upgrade/{ => scripts}/execute-upgrade.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/upgrade/{ => scripts}/execute-upgrade.ts (69%) diff --git a/src/upgrade/execute-upgrade.ts b/src/upgrade/scripts/execute-upgrade.ts similarity index 69% rename from src/upgrade/execute-upgrade.ts rename to src/upgrade/scripts/execute-upgrade.ts index 3603c83cc..ae9582251 100644 --- a/src/upgrade/execute-upgrade.ts +++ b/src/upgrade/scripts/execute-upgrade.ts @@ -1,6 +1,6 @@ -import { getLogger } from "../deploy/logger/create-logger"; -import { getContractDataForUpgrade, upgradeZNS } from "./upgrade"; -import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; +import { getLogger } from "../../deploy/logger/create-logger"; +import { getContractDataForUpgrade, upgradeZNS } from "../upgrade"; +import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; const execute = async () => { @@ -30,6 +30,6 @@ execute() }) .catch(e => { const log = getLogger(); - log.error("Error during upgrade:", e); + log.error(`Error during upgrade! Message: ${e.message}, Stack: ${e.stack}`); process.exit(1); }); From 4427d0d60ff0a320f3914c4dee3037b6e7f8b716 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 13:03:38 -0700 Subject: [PATCH 033/114] create scripts to get implementation addresses and a separate script for updating the database --- src/upgrade/scripts/get-implementations.ts | 30 +++++++++++++++++++++ src/upgrade/scripts/update-db.ts | 31 ++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/upgrade/scripts/get-implementations.ts create mode 100644 src/upgrade/scripts/update-db.ts diff --git a/src/upgrade/scripts/get-implementations.ts b/src/upgrade/scripts/get-implementations.ts new file mode 100644 index 000000000..fd2da91ab --- /dev/null +++ b/src/upgrade/scripts/get-implementations.ts @@ -0,0 +1,30 @@ +import { getContractDataForUpgrade } from "../upgrade"; +import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; +import * as hre from "hardhat"; + + +export const getProxyImplementations = async () => { + const dbAdapter = await getMongoAdapter(); + const contractData = await getContractDataForUpgrade(dbAdapter); + + await Object.values(contractData).reduce( + async (acc, { contractName, address }) => { + await acc; + + const implAddress = await hre.upgrades.erc1967.getImplementationAddress( + address as string + ); + + console.log(`Implementation for ${contractName} is at: ${implAddress}`); + }, Promise.resolve() + ); +}; + +getProxyImplementations() + .then(() => { + process.exit(0); + }) + .catch(e => { + console.error(`Error getting proxy implementations: ${e.message}`); + process.exit(1); + }); diff --git a/src/upgrade/scripts/update-db.ts b/src/upgrade/scripts/update-db.ts new file mode 100644 index 000000000..c32f0f80b --- /dev/null +++ b/src/upgrade/scripts/update-db.ts @@ -0,0 +1,31 @@ +import { getLogger } from "../../deploy/logger/create-logger"; +import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; +import { getContractDataForUpgrade } from "../upgrade"; +import { updateAllContractsInDb } from "../db"; + + +const executeDbUpdate = async () => { + const logger = getLogger(); + const dbAdapter = await getMongoAdapter(logger); + + const contractData = await getContractDataForUpgrade(dbAdapter); + + logger.info( + `Updating DB with name ${dbAdapter.dbName} at version ${(await dbAdapter.getLatestVersion())?.dbVersion}` + ); + + await updateAllContractsInDb({ + contractData, + dbAdapter, + }); + + logger.info("DB update complete"); +}; + + +executeDbUpdate() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); From a66a8a0205a1a8ae78568aa36cf6d9a9c5b04d94 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 13:04:01 -0700 Subject: [PATCH 034/114] fix-extend storage data and upgrade helpers --- src/upgrade/storage-data.ts | 9 ++------- src/upgrade/upgrade.ts | 32 +++++++++----------------------- 2 files changed, 11 insertions(+), 30 deletions(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index f0c6dba0b..7b528047a 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -28,16 +28,11 @@ export const readContractStorage = async ( return layout.storage.reduce( async ( acc : Promise, - { contract, label, type } + { label, type } ) : Promise => { const newAcc = await acc; - if ( - (contract === "zStakePoolBase" || - contract === "zStakeCorePool" || - contract === "zStakeCorePoolMigration") && - !type.includes("mapping") - ) { + if (!type.includes("mapping")) { try { const value = await contractObj[(label as keyof ZNSContract)](); diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index bdf7eaac4..942d11919 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -2,26 +2,23 @@ import * as hre from "hardhat"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { compareStorageData, readContractStorage } from "./storage-data"; import { ZNSContract } from "../../test/helpers/types"; -import { getLogger } from "../deploy/logger/create-logger"; import { TLogger } from "../deploy/campaign/types"; import { IContractData, IZNSContractsUpgraded, ZNSContractUpgraded } from "./types"; import { Addressable } from "ethers"; -import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; -import { updateContractInDb } from "./db"; +import { updateAllContractsInDb } from "./db"; import { znsNames } from "../deploy/missions/contracts/names"; import { IContractDbData } from "../deploy/db/types"; +// TODO upg: add the ability to retry from where it left off/failed ! export const upgradeZNS = async ({ governorExt, contractData, - dbAdapter, logger, } : { governorExt ?: SignerWithAddress; contractData : Array; - dbAdapter : MongoDBAdapter; logger : TLogger; }) => { let governor = governorExt; @@ -42,7 +39,6 @@ export const upgradeZNS = async ({ contractName, contractAddress: address, governor: governor as SignerWithAddress, - dbAdapter, logger, }); @@ -53,8 +49,6 @@ export const upgradeZNS = async ({ Promise.resolve({} as IZNSContractsUpgraded) ); - await dbAdapter.finalizeDeployedVersion(); - return znsUpgraded; }; @@ -62,13 +56,11 @@ export const upgradeZNSContract = async ({ contractName, contractAddress, governor, - dbAdapter, logger, } : { contractName : string; contractAddress : string | Addressable; governor : SignerWithAddress; - dbAdapter : MongoDBAdapter; logger : TLogger; }) => { const originalFactory = await hre.ethers.getContractFactory(contractName); @@ -84,31 +76,25 @@ export const upgradeZNSContract = async ({ let upgradedFactory = await hre.ethers.getContractFactory(`${contractName}Pausable`); upgradedFactory = upgradedFactory.connect(governor); - const upgradedContract = await hre.upgrades.upgradeProxy( + let upgradedContract = await hre.upgrades.upgradeProxy( contractAddress, - upgradedFactory - ) as unknown as ZNSContractUpgraded; - - const implAddress = await hre.upgrades.erc1967.getImplementationAddress(await upgradedContract.getAddress()); + upgradedFactory, + ); - logger.info(`Upgraded ${contractName} to new implementation at: ${implAddress}`); + upgradedContract = await upgradedContract.waitForDeployment(); - await updateContractInDb({ - dbAdapter, - contractName, - implAddress, - }); + logger.info(`Upgraded ${contractName} to new implementation.`); const storageDataPostUpgrade = await readContractStorage( upgradedFactory, - upgradedContract, + upgradedContract as unknown as ZNSContractUpgraded, ); compareStorageData(storageDataPreUpgrade, storageDataPostUpgrade); logger.info("Storage compared successfully. Values are unchanged after upgrade"); logger.info(`Upgrade of ${contractName} finished successfully`); - return upgradedContract; + return upgradedContract as unknown as ZNSContractUpgraded; }; export const getContractNamesToUpgrade = () : Partial => { From 5fc419e7273821a8ae4f105eff2ef6a637d0cab5 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 13:04:27 -0700 Subject: [PATCH 035/114] add test to verify implementation addresses --- test/zns-v1-upgrade-lock-test.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 52d5af4fb..2469fae89 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -56,6 +56,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { let znsUpgraded : IZNSContractsUpgraded; let preUpgradeZnsStorage : Array; + let preUpgradeImpls : Array; const logger = getLogger(); @@ -243,6 +244,18 @@ describe("ZNS V1 Upgrade and Lock Test", () => { process.env.MONGO_DB_VERSION = dbAdapterDeploy.curVersion; dbVersionDeploy = await dbAdapterDeploy.getLatestVersion() as IDBVersion; + preUpgradeImpls = await Object.values(contractNames).reduce( + async (acc : Promise>, { instance }) => { + const newAcc = await acc; + + const implAddress = await hre.upgrades.erc1967.getImplementationAddress( + zns[instance].target as string + ); + + return [...newAcc, implAddress]; + }, Promise.resolve([]) + ); + resetMongoAdapter(); dbAdapterUpgrade = await getMongoAdapter(); @@ -490,6 +503,20 @@ describe("ZNS V1 Upgrade and Lock Test", () => { expect(znsUpgraded.subRegistrar.target).to.equal(zns.subRegistrar.target); }); + it("should upgrade each implementation to a new one", async () => { + await Object.values(contractNames).reduce( + async (acc, { instance }, idx) => { + await acc; + + const implAddressPostUpgrade = await hre.upgrades.erc1967.getImplementationAddress( + znsUpgraded[instance].target as string + ); + + expect(implAddressPostUpgrade).to.not.equal(preUpgradeImpls[idx]); + }, Promise.resolve() + ); + }); + describe("Database tests", () => { it("should have the same version in the database", async () => { const { From 0e6af10309012ffc03cefad23c3d623e193bbf6f Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 13:13:51 -0700 Subject: [PATCH 036/114] switch to proper logger in storage data helper, add new db update helper in test --- src/upgrade/scripts/execute-upgrade.ts | 1 - src/upgrade/storage-data.ts | 4 +++- test/zns-v1-upgrade-lock-test.ts | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/upgrade/scripts/execute-upgrade.ts b/src/upgrade/scripts/execute-upgrade.ts index ae9582251..054678427 100644 --- a/src/upgrade/scripts/execute-upgrade.ts +++ b/src/upgrade/scripts/execute-upgrade.ts @@ -15,7 +15,6 @@ const execute = async () => { const znsUpgraded = await upgradeZNS({ contractData, - dbAdapter, logger, }); diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index 7b528047a..8294a2853 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -5,6 +5,7 @@ import { getStorageLayout, getUnlinkedBytecode, getVersion, StorageLayout } from import { readValidations } from "@openzeppelin/hardhat-upgrades/dist/utils/validations"; import { ContractStorageData } from "./types"; import { ZNSContract } from "../../test/helpers/types"; +import { getLogger } from "../deploy/logger/create-logger"; // TODO utils: move thess helpers to protocol-utils repo when available @@ -23,6 +24,7 @@ export const readContractStorage = async ( contractFactory : ContractFactory, contractObj : ZNSContract ) : Promise => { + const logger = getLogger(); const layout = await getContractStorageLayout(contractFactory); return layout.storage.reduce( @@ -38,7 +40,7 @@ export const readContractStorage = async ( newAcc.push({ [label]: value }); } catch (e : unknown) { - console.log(`Error on LABEL ${label}: ${(e as Error).message}`); + logger.error(`Error on LABEL ${label}: ${(e as Error).message}`); } } diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 2469fae89..e806a42f7 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -25,6 +25,7 @@ import { IContractDbData } from "../src/deploy/db/types"; import { IDBVersion } from "../src/deploy/db/mongo-adapter/types"; import { getMongoAdapter, resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; import { getLogger } from "../src/deploy/logger/create-logger"; +import { updateAllContractsInDb } from "../src/upgrade/db"; describe("ZNS V1 Upgrade and Lock Test", () => { @@ -264,6 +265,11 @@ describe("ZNS V1 Upgrade and Lock Test", () => { governorExt: governor, contractData, logger, + }); + + // update database records to new implementations + await updateAllContractsInDb({ + contractData, dbAdapter: dbAdapterUpgrade, }); From 358dc26a3f45f2c6e7945aa3177aaee251517da7 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 15:14:37 -0700 Subject: [PATCH 037/114] modify registration helpers to accept confirmation number so they can run on actual networks --- test/helpers/flows/registration.ts | 3 ++ test/helpers/register-setup.ts | 47 +++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/test/helpers/flows/registration.ts b/test/helpers/flows/registration.ts index 58954e683..eafcda13c 100644 --- a/test/helpers/flows/registration.ts +++ b/test/helpers/flows/registration.ts @@ -11,9 +11,11 @@ import { getTokenContract } from "../tokens"; export const registerDomainPath = async ({ zns, domainConfigs, + confirmations, } : { zns : IZNSContracts; domainConfigs : Array; + confirmations ?: number; }) => domainConfigs.reduce( async ( acc : Promise>, @@ -62,6 +64,7 @@ export const registerDomainPath = async ({ const domainHash = await registrationWithSetup({ zns, parentHash, + confirmations, ...config, }); diff --git a/test/helpers/register-setup.ts b/test/helpers/register-setup.ts index 3a9c765bc..3f279944f 100644 --- a/test/helpers/register-setup.ts +++ b/test/helpers/register-setup.ts @@ -11,6 +11,7 @@ import { distrConfigEmpty, fullDistrConfigEmpty, DEFAULT_TOKEN_URI, paymentConfi import { getTokenContract } from "./tokens"; import { ICurvePriceConfig } from "../../src/deploy/missions/types"; import { expect } from "chai"; +import { hashDomainLabel } from "./hashing"; const { ZeroAddress } = ethers; @@ -18,6 +19,7 @@ const { ZeroAddress } = ethers; export const defaultRootRegistration = async ({ user, zns, + confirmations, domainName, domainContent = user.address, tokenURI = DEFAULT_TOKEN_URI, @@ -25,11 +27,12 @@ export const defaultRootRegistration = async ({ } : { user : SignerWithAddress; zns : IZNSContracts; + confirmations ?: number; domainName : string; domainContent ?: string; tokenURI ?: string; distrConfig ?: IDistributionConfig; -}) : Promise => { +}) => { const supplyBefore = await zns.domainToken.totalSupply(); const tx = await zns.rootRegistrar.connect(user).registerRootDomain( @@ -39,20 +42,21 @@ export const defaultRootRegistration = async ({ distrConfig, paymentConfigEmpty ); + await tx.wait(confirmations); const supplyAfter = await zns.domainToken.totalSupply(); expect(supplyAfter).to.equal(supplyBefore + BigInt(1)); - - return tx.wait(); }; export const approveForParent = async ({ zns, + confirmations, parentHash, user, domainLabel, } : { zns : IZNSContracts; + confirmations ?: number; parentHash : string; user : SignerWithAddress; domainLabel : string; @@ -72,7 +76,8 @@ export const approveForParent = async ({ const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price + parentFee); const toApprove = price + parentFee + protocolFee; - return tokenContract.connect(user).approve(await zns.treasury.getAddress(), toApprove); + const tx = await tokenContract.connect(user).approve(await zns.treasury.getAddress(), toApprove); + return tx.wait(confirmations); }; /** @@ -84,6 +89,7 @@ export const approveForParent = async ({ export const defaultSubdomainRegistration = async ({ user, zns, + confirmations, parentHash, subdomainLabel, domainContent = user.address, @@ -92,6 +98,7 @@ export const defaultSubdomainRegistration = async ({ } : { user : SignerWithAddress; zns : IZNSContracts; + confirmations ?: number; parentHash : string; subdomainLabel : string; domainContent ?: string; @@ -108,15 +115,15 @@ export const defaultSubdomainRegistration = async ({ distrConfig, paymentConfigEmpty ); + await tx.wait(confirmations); const supplyAfter = await zns.domainToken.totalSupply(); expect(supplyAfter).to.equal(supplyBefore + BigInt(1)); - - return tx.wait(); }; export const registrationWithSetup = async ({ zns, + confirmations, user, parentHash, domainLabel, @@ -126,6 +133,7 @@ export const registrationWithSetup = async ({ setConfigs = true, } : { zns : IZNSContracts; + confirmations ?: number; // how many confirmations to wait for user : SignerWithAddress; parentHash ?: string; domainLabel : string; @@ -144,6 +152,7 @@ export const registrationWithSetup = async ({ await defaultRootRegistration({ user, zns, + confirmations, domainName: domainLabel, domainContent, tokenURI, @@ -152,6 +161,7 @@ export const registrationWithSetup = async ({ } else { await approveForParent({ zns, + confirmations, parentHash, user, domainLabel, @@ -159,6 +169,7 @@ export const registrationWithSetup = async ({ await defaultSubdomainRegistration({ user, + confirmations, zns, parentHash, subdomainLabel: domainLabel, @@ -169,38 +180,48 @@ export const registrationWithSetup = async ({ } // get hash - const domainHash = await getDomainHashFromEvent({ - zns, - user, - }); + let domainHash; + try { + domainHash = await getDomainHashFromEvent({ + zns, + user, + }); + } catch (e) { + domainHash = !parentHash || parentHash === ethers.ZeroHash + ? hashDomainLabel(domainLabel) + : await zns.subRegistrar.hashWithParent(parentHash, domainLabel); + } if (!hasConfig) return domainHash; // set up prices if (fullConfig.distrConfig.pricerContract === await zns.fixedPricer.getAddress() && setConfigs) { - await zns.fixedPricer.connect(user).setPriceConfig( + const tx = await zns.fixedPricer.connect(user).setPriceConfig( domainHash, { ...fullConfig.priceConfig as IFixedPriceConfig, isSet: true, }, ); + if (confirmations) await tx.wait(confirmations); } else if (fullConfig.distrConfig.pricerContract === await zns.curvePricer.getAddress() && setConfigs) { - await zns.curvePricer.connect(user).setPriceConfig( + const tx = await zns.curvePricer.connect(user).setPriceConfig( domainHash, { ...fullConfig.priceConfig as ICurvePriceConfig, isSet: true, }, ); + if (confirmations) await tx.wait(confirmations); } if (fullConfig.paymentConfig.token !== ZeroAddress && setConfigs) { // set up payment config - await zns.treasury.connect(user).setPaymentConfig( + const tx = await zns.treasury.connect(user).setPaymentConfig( domainHash, fullConfig.paymentConfig, ); + if (confirmations) await tx.wait(confirmations); } return domainHash; From 7592b40bb531a84decdd855eca7207bfc99f4640 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 15:14:50 -0700 Subject: [PATCH 038/114] add exception to storage data helper --- src/upgrade/storage-data.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index 8294a2853..49840f54c 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -34,13 +34,13 @@ export const readContractStorage = async ( ) : Promise => { const newAcc = await acc; - if (!type.includes("mapping")) { + if (!type.includes("mapping") && !type.includes("array")) { try { const value = await contractObj[(label as keyof ZNSContract)](); newAcc.push({ [label]: value }); } catch (e : unknown) { - logger.error(`Error on LABEL ${label}: ${(e as Error).message}`); + logger.debug(`Error on LABEL ${label}: ${(e as Error).message}`); } } From bca2d53a79efb975a02fcc6a3700d8980fcee67d Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 15:15:38 -0700 Subject: [PATCH 039/114] make current test be able to run on actual networks --- test/zns-v1-upgrade-lock-test.ts | 141 ++++++++++++++++--------------- 1 file changed, 74 insertions(+), 67 deletions(-) diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index e806a42f7..5be62eb3e 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -30,20 +30,14 @@ import { updateAllContractsInDb } from "../src/upgrade/db"; describe("ZNS V1 Upgrade and Lock Test", () => { let deployer : SignerWithAddress; - let governor : SignerWithAddress; - let admin : SignerWithAddress; - let randomAcc : SignerWithAddress; let rootOwner : SignerWithAddress; let lvl2SubOwner : SignerWithAddress; let lvl3SubOwner : SignerWithAddress; let lvl4SubOwner : SignerWithAddress; let lvl5SubOwner : SignerWithAddress; let lvl6SubOwner : SignerWithAddress; - let branchLvl1Owner : SignerWithAddress; - let branchLvl2Owner : SignerWithAddress; let zns : TZNSContractState; - let zeroVault : SignerWithAddress; let domainConfigs : Array; let domainHashes : Array; @@ -61,6 +55,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { const logger = getLogger(); + const isRealNetwork = hre.network.name !== "hardhat"; + let methodCalls : { // eslint-disable-next-line @typescript-eslint/no-explicit-any [key : string] : Array<{ method : string; args : Array; }>; @@ -72,25 +68,19 @@ describe("ZNS V1 Upgrade and Lock Test", () => { before(async () => { [ deployer, - zeroVault, - governor, - admin, rootOwner, lvl2SubOwner, lvl3SubOwner, lvl4SubOwner, lvl5SubOwner, lvl6SubOwner, - branchLvl1Owner, - branchLvl2Owner, - randomAcc, ] = await hre.ethers.getSigners(); const config : IDeployCampaignConfig = await getConfig({ deployer, - zeroVaultAddress: zeroVault.address, - governors: [deployer.address, governor.address], - admins: [deployer.address, admin.address], + zeroVaultAddress: hre.network.name !== "hardhat" + ? process.env.ZERO_VAULT_ADDRESS as string + : deployer.address, }); const campaign = await runZnsCampaign({ @@ -98,7 +88,9 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); zns = campaign.state.contracts; - zns.zeroVaultAddress = zeroVault.address; + zns.zeroVaultAddress = hre.network.name !== "hardhat" + ? process.env.ZERO_VAULT_ADDRESS as string + : deployer.address; const { dbAdapter: dbAdapterDeploy } = campaign; @@ -116,21 +108,23 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }, Promise.resolve([]) ); + logger.debug("Funding users..."); // Give funds to users - await Promise.all( - [ - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - branchLvl1Owner, - branchLvl2Owner, - ].map(async ({ address }) => - zns.meowToken.mint(address, ethers.parseEther("1000000"))) + await [ + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + ].reduce(async (acc, { address }) => { + await acc; + const tx = await zns.meowToken.mint(address, ethers.parseEther("1000000")); + if (isRealNetwork) await tx.wait(2); + }, Promise.resolve() ); - await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + const tx = await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + if (isRealNetwork) await tx.wait(2); // register a bunch of domains pre-upgrade domainConfigs = [ @@ -232,9 +226,11 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }, ]; + logger.debug("Registering a path of domains..."); const regResults = await registerDomainPath({ zns, domainConfigs, + confirmations: isRealNetwork ? 2 : undefined, }); domainHashes = regResults.map(({ domainHash }) => domainHash); @@ -262,7 +258,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { // run the upgrade znsUpgraded = await upgradeZNS({ - governorExt: governor, + governorExt: deployer, contractData, logger, }); @@ -279,19 +275,19 @@ describe("ZNS V1 Upgrade and Lock Test", () => { [znsNames.registry.instance]: [ { method: "setOwnersOperator", - args: [randomAcc.address, true], + args: [deployer.address, true], }, { method: "createDomainRecord", - args: [hre.ethers.ZeroHash, randomAcc.address, "address"], + args: [hre.ethers.ZeroHash, deployer.address, "address"], }, { method: "updateDomainRecord", - args: [hre.ethers.ZeroHash, randomAcc.address, "address"], + args: [hre.ethers.ZeroHash, deployer.address, "address"], }, { method: "updateDomainOwner", - args: [hre.ethers.ZeroHash, randomAcc.address], + args: [hre.ethers.ZeroHash, deployer.address], }, { method: "updateDomainResolver", @@ -309,29 +305,29 @@ describe("ZNS V1 Upgrade and Lock Test", () => { [znsNames.domainToken.instance]: [ { method: "transferFrom", - args: [deployer.address, randomAcc.address, 1], + args: [deployer.address, rootOwner.address, 1], }, { // @ts-ignore method: "safeTransferFrom(address,address,uint256)", - args: [deployer.address, randomAcc.address, "1"], + args: [deployer.address, rootOwner.address, "1"], }, { // @ts-ignore method: "safeTransferFrom(address,address,uint256,bytes)", - args: [deployer.address, randomAcc.address, "1", hre.ethers.ZeroHash], + args: [deployer.address, rootOwner.address, "1", hre.ethers.ZeroHash], }, { method: "approve", - args: [randomAcc.address, 1], + args: [rootOwner.address, 1], }, { method: "setApprovalForAll", - args: [randomAcc.address, true], + args: [rootOwner.address, true], }, { method: "register", - args: [randomAcc.address, 123n, "dummyURI"], + args: [rootOwner.address, 123n, "dummyURI"], }, { method: "revoke", @@ -345,7 +341,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { [znsNames.addressResolver.instance]: [ { method: "setAddress", - args: [hre.ethers.ZeroHash, randomAcc.address], + args: [hre.ethers.ZeroHash, rootOwner.address], }, { method: "pause", @@ -407,15 +403,15 @@ describe("ZNS V1 Upgrade and Lock Test", () => { [znsNames.treasury.instance]: [ { method: "stakeForDomain", - args: [hre.ethers.ZeroHash, hre.ethers.ZeroHash, randomAcc.address, 1n, 1n, 1n], + args: [hre.ethers.ZeroHash, hre.ethers.ZeroHash, rootOwner.address, 1n, 1n, 1n], }, { method: "unstakeForDomain", - args: [hre.ethers.ZeroHash, randomAcc.address], + args: [hre.ethers.ZeroHash, rootOwner.address], }, { method: "processDirectPayment", - args: [hre.ethers.ZeroHash, hre.ethers.ZeroHash, randomAcc.address, 1n, 1n], + args: [hre.ethers.ZeroHash, hre.ethers.ZeroHash, rootOwner.address, 1n, 1n], }, { method: "setPaymentConfig", @@ -423,11 +419,11 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }, { method: "setBeneficiary", - args: [hre.ethers.ZeroHash, randomAcc.address], + args: [hre.ethers.ZeroHash, rootOwner.address], }, { method: "setPaymentToken", - args: [hre.ethers.ZeroHash, randomAcc.address], + args: [hre.ethers.ZeroHash, rootOwner.address], }, { method: "pause", @@ -437,7 +433,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { [znsNames.rootRegistrar.instance]: [ { method: "registerRootDomain", - args: ["domain", randomAcc.address, "uri", distrConfigEmpty, paymentConfigEmpty], + args: ["domain", rootOwner.address, "uri", distrConfigEmpty, paymentConfigEmpty], }, { method: "revokeDomain", @@ -455,7 +451,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { [znsNames.subRegistrar.instance]: [ { method: "registerSubdomain", - args: [hre.ethers.ZeroHash, "label", randomAcc.address, "uri", distrConfigEmpty, paymentConfigEmpty], + args: [hre.ethers.ZeroHash, "label", rootOwner.address, "uri", distrConfigEmpty, paymentConfigEmpty], }, { method: "setDistributionConfigForDomain", @@ -463,7 +459,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }, { method: "setPricerContractForDomain", - args: [hre.ethers.ZeroHash, randomAcc.address], + args: [hre.ethers.ZeroHash, rootOwner.address], }, { method: "setPaymentTypeForDomain", @@ -475,7 +471,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }, { method: "updateMintlistForDomain", - args: [hre.ethers.ZeroHash, [randomAcc.address], [true]], + args: [hre.ethers.ZeroHash, [rootOwner.address], [true]], }, { method: "clearMintlistForDomain", @@ -494,8 +490,10 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); after(async () => { - await dbAdapterUpgrade.dropDB(); - process.env.MONGO_DB_VERSION = ""; + if (hre.network.name === "hardhat") { + await dbAdapterUpgrade.dropDB(); + process.env.MONGO_DB_VERSION = ""; + } }); it("should keep the same proxy addresses for each contract", async () => { @@ -598,7 +596,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { : AccessType.OPEN; // set new values - await zns.subRegistrar.connect(user).setDistributionConfigForDomain( + let tx = await zns.subRegistrar.connect(user).setDistributionConfigForDomain( domainHash, { pricerContract: newPricer, @@ -606,6 +604,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { accessType: newAccessType, } ); + if (isRealNetwork) await tx.wait(2); // check new values const domainConfig = await zns.subRegistrar.distrConfigs(domainHash); expect(domainConfig.pricerContract).to.equal(newPricer); @@ -614,17 +613,18 @@ describe("ZNS V1 Upgrade and Lock Test", () => { // check Treasury storage // set new values - await zns.treasury.connect(user).setPaymentConfig( + tx = await zns.treasury.connect(user).setPaymentConfig( domainHash, { - token: randomAcc.address, - beneficiary: randomAcc.address, + token: rootOwner.address, + beneficiary: rootOwner.address, } ); + if (isRealNetwork) await tx.wait(2); // check new values const paymentConfig = await zns.treasury.paymentConfigs(domainHash); - expect(paymentConfig.token).to.equal(randomAcc.address); - expect(paymentConfig.beneficiary).to.equal(randomAcc.address); + expect(paymentConfig.token).to.equal(rootOwner.address); + expect(paymentConfig.beneficiary).to.equal(rootOwner.address); if ((fullConfig.priceConfig as IFixedPriceConfig).price) { // check FixedPricer storage @@ -634,7 +634,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { isSet: true, }; // set new values - await zns.fixedPricer.connect(user).setPriceConfig(domainHash, newPriceConfig); + tx = await zns.fixedPricer.connect(user).setPriceConfig(domainHash, newPriceConfig); + if (isRealNetwork) await tx.wait(2); const priceConfig = await zns.fixedPricer.priceConfigs(domainHash); expect(priceConfig.price).to.equal(newPriceConfig.price); @@ -651,7 +652,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { isSet: true, }; // set new values - await zns.curvePricer.connect(user).setPriceConfig(domainHash, newPriceConfig); + tx = await zns.curvePricer.connect(user).setPriceConfig(domainHash, newPriceConfig); + if (isRealNetwork) await tx.wait(2); // check new values const priceConfig = await zns.curvePricer.priceConfigs(domainHash); expect(priceConfig.maxPrice).to.equal(newPriceConfig.maxPrice); @@ -665,24 +667,28 @@ describe("ZNS V1 Upgrade and Lock Test", () => { // check AddressResolver storage // set new values - await zns.addressResolver.connect(user).setAddress(domainHash, randomAcc.address); + tx = await zns.addressResolver.connect(user).setAddress(domainHash, rootOwner.address); + if (isRealNetwork) await tx.wait(2); // check new values const addr = await zns.addressResolver.resolveDomainAddress(domainHash); - expect(addr).to.equal(randomAcc.address); + expect(addr).to.equal(rootOwner.address); // check DomainToken storage // set new values - await zns.domainToken.connect(user).transferFrom(user.address, randomAcc.address, BigInt(domainHash)); + tx = await zns.domainToken.connect(user).transferFrom(user.address, rootOwner.address, BigInt(domainHash)); + if (isRealNetwork) await tx.wait(2); // check new values const tokenOwner = await zns.domainToken.ownerOf(BigInt(domainHash)); - expect(tokenOwner).to.equal(randomAcc.address); + expect(tokenOwner).to.equal(rootOwner.address); // check Registry storage // set new values - await zns.registry.connect(user).updateDomainOwner(domainHash, randomAcc.address); + tx = await zns.registry.connect(user).updateDomainOwner(domainHash, rootOwner.address); + if (isRealNetwork) await tx.wait(2); + // check new values const owner = await zns.registry.getDomainOwner(domainHash); - expect(owner).to.equal(randomAcc.address); + expect(owner).to.equal(rootOwner.address); }, Promise.resolve() ); }); @@ -721,7 +727,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { it(`${name}`, async () => { const contract = znsUpgraded[instance]; - await contract.connect(deployer).pause(); + const tx = await contract.connect(deployer).pause(); + if (isRealNetwork) await tx.wait(2); expect(await contract.paused()).to.equal(true); From 5a405559c7f6a929d7bbaf45b019f578cb1f9b5c Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 18:06:36 -0700 Subject: [PATCH 040/114] update db and upgrade helpers to instead write to a new Upgraded version so we have 2 in parallel after the upgrade --- src/deploy/db/mongo-adapter/constants.ts | 1 + src/upgrade/db.ts | 78 +++++++++++++++++------- src/upgrade/scripts/execute-upgrade.ts | 7 ++- src/upgrade/scripts/update-db.ts | 10 --- src/upgrade/upgrade.ts | 44 ++++++------- 5 files changed, 83 insertions(+), 57 deletions(-) diff --git a/src/deploy/db/mongo-adapter/constants.ts b/src/deploy/db/mongo-adapter/constants.ts index 1b0e4ded9..bebd6492c 100644 --- a/src/deploy/db/mongo-adapter/constants.ts +++ b/src/deploy/db/mongo-adapter/constants.ts @@ -7,6 +7,7 @@ export const COLL_NAMES = { export const VERSION_TYPES = { temp: "TEMP", deployed: "DEPLOYED", + upgraded: "UPGRADED", archived: "ARCHIVED", }; diff --git a/src/upgrade/db.ts b/src/upgrade/db.ts index 5be16f7db..01374dec8 100644 --- a/src/upgrade/db.ts +++ b/src/upgrade/db.ts @@ -1,56 +1,92 @@ import * as hre from "hardhat"; import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; import { IContractData } from "./types"; -import { IDBVersion } from "../deploy/db/mongo-adapter/types"; +import { VERSION_TYPES } from "../deploy/db/mongo-adapter/constants"; +import { getLogger } from "../deploy/logger/create-logger"; +import { znsNames } from "../deploy/missions/contracts/names"; +import { getContractDataForUpgrade } from "./upgrade"; export const updateAllContractsInDb = async ({ - contractData, dbAdapter, } : { - contractData : Array; dbAdapter : MongoDBAdapter; }) => { + const logger = getLogger(); + + const contractNames = JSON.parse(JSON.stringify(znsNames)); + delete contractNames.erc1967Proxy; + contractNames.meowToken.contract = process.env.MOCK_MEOW_TOKEN === "true" + ? contractNames.meowToken.contractMock + : contractNames.meowToken.contract; + + const contractData = await getContractDataForUpgrade(dbAdapter, contractNames); + + const newDbVersion = Date.now().toString(); + const newContractsVersion = dbAdapter.getContractsVersionFromFile(); + logger.info( + `Updating DB "${dbAdapter.dbName}" with new version: ${newDbVersion} and contracts version: ${newContractsVersion}` + ); + for (const { contractName, address } of contractData) { - const implAddress = await hre.upgrades.erc1967.getImplementationAddress( - address as string - ); + let implAddress : string | null; + if (contractName === znsNames.accessController.contract) { + implAddress = null; + } else { + implAddress = await hre.upgrades.erc1967.getImplementationAddress( + address as string + ); + } await updateContractInDb({ dbAdapter, contractName, + proxyAddress: address as string, implAddress, + newDbVersion, }); } + + // Update the version in the DB + await dbAdapter.versions.insertOne({ + type: VERSION_TYPES.upgraded, + dbVersion: newDbVersion, + contractsVersion: newContractsVersion, + }); + + logger.info("DB update finished successfully."); }; export const updateContractInDb = async ({ dbAdapter, contractName, + proxyAddress, implAddress, + newDbVersion, } : { dbAdapter : MongoDBAdapter; contractName : string; - implAddress : string; + proxyAddress : string; + implAddress : string | null; + newDbVersion : string; }) => { - const { abi, bytecode } = hre.artifacts.readArtifactSync(`${contractName}Pausable`); + const artifactName = contractName === znsNames.accessController.contract + || contractName === znsNames.meowToken.contract + || contractName === znsNames.meowToken.contractMock + ? contractName + : `${contractName}Pausable`; - const { dbVersion: curVersion } = await dbAdapter.getLatestVersion() as IDBVersion; + const { abi, bytecode } = hre.artifacts.readArtifactSync(artifactName); - await dbAdapter.contracts.updateOne( + await dbAdapter.writeContract( + contractName, { name: contractName, - version: curVersion, - }, - { - $set: { - abi: JSON.stringify(abi), - bytecode, - implementation: implAddress, - }, + address: proxyAddress, + implementation: implAddress, + abi: JSON.stringify(abi), + bytecode, }, - { - upsert: true, - } + newDbVersion, ); }; diff --git a/src/upgrade/scripts/execute-upgrade.ts b/src/upgrade/scripts/execute-upgrade.ts index 054678427..45426ca3b 100644 --- a/src/upgrade/scripts/execute-upgrade.ts +++ b/src/upgrade/scripts/execute-upgrade.ts @@ -1,5 +1,5 @@ import { getLogger } from "../../deploy/logger/create-logger"; -import { getContractDataForUpgrade, upgradeZNS } from "../upgrade"; +import { getContractDataForUpgrade, getContractNamesToUpgrade, upgradeZNS } from "../upgrade"; import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; @@ -9,7 +9,10 @@ const execute = async () => { logger.info("Prepairing contract data for the upgrade..."); - const contractData = await getContractDataForUpgrade(dbAdapter); + const contractData = await getContractDataForUpgrade( + dbAdapter, + getContractNamesToUpgrade() + ); logger.info("Contract data prepared. Starting the upgrade..."); diff --git a/src/upgrade/scripts/update-db.ts b/src/upgrade/scripts/update-db.ts index c32f0f80b..1963e23ce 100644 --- a/src/upgrade/scripts/update-db.ts +++ b/src/upgrade/scripts/update-db.ts @@ -1,6 +1,5 @@ import { getLogger } from "../../deploy/logger/create-logger"; import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; -import { getContractDataForUpgrade } from "../upgrade"; import { updateAllContractsInDb } from "../db"; @@ -8,18 +7,9 @@ const executeDbUpdate = async () => { const logger = getLogger(); const dbAdapter = await getMongoAdapter(logger); - const contractData = await getContractDataForUpgrade(dbAdapter); - - logger.info( - `Updating DB with name ${dbAdapter.dbName} at version ${(await dbAdapter.getLatestVersion())?.dbVersion}` - ); - await updateAllContractsInDb({ - contractData, dbAdapter, }); - - logger.info("DB update complete"); }; diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index 942d11919..9d8e45167 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -6,7 +6,6 @@ import { TLogger } from "../deploy/campaign/types"; import { IContractData, IZNSContractsUpgraded, ZNSContractUpgraded } from "./types"; import { Addressable } from "ethers"; import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; -import { updateAllContractsInDb } from "./db"; import { znsNames } from "../deploy/missions/contracts/names"; import { IContractDbData } from "../deploy/db/types"; @@ -108,26 +107,23 @@ export const getContractNamesToUpgrade = () : Partial => { export const getContractDataForUpgrade = async ( dbAdapter : MongoDBAdapter, -) : Promise> => { - const contractNames = getContractNamesToUpgrade(); - - return Object.values(contractNames).reduce( - async ( - acc : Promise>, - { contract, instance } - ) => { - const contractData = await acc; - - const { address } = await dbAdapter.getContract(contract) as IContractDbData; - - contractData.push({ - contractName: contract, - instanceName: instance, - address, - }); - - return contractData; - }, - Promise.resolve([]) - ); -}; + contractNames : Partial, +) : Promise> => Object.values(contractNames).reduce( + async ( + acc : Promise>, + { contract, instance } + ) => { + const contractData = await acc; + + const { address } = await dbAdapter.getContract(contract) as IContractDbData; + + contractData.push({ + contractName: contract, + instanceName: instance, + address, + }); + + return contractData; + }, + Promise.resolve([]) +); From af2fd271103cde534585e7a4ba7a0c4ad616660d Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 28 Mar 2025 18:06:49 -0700 Subject: [PATCH 041/114] fix test for new changes --- .../token/ZNSDomainTokenPausable.sol | 1 - test/zns-v1-upgrade-lock-test.ts | 31 ++++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol index de28977f9..725a4f4d9 100644 --- a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol +++ b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol @@ -133,7 +133,6 @@ contract ZNSDomainTokenPausable is * @param tokenId The tokenId (as `uint256(domainHash)`) that the caller wishes to set the tokenURI for * @param _tokenURI The tokenURI to be set for the token with the given tokenId */ - // TODO upg: figure out if onlyAdmin functions need to be only whenNotPaused or just keep them as is !!! function setTokenURI(uint256 tokenId, string memory _tokenURI) external override onlyAdmin { _setTokenURI(tokenId, _tokenURI); emit TokenURISet(tokenId, _tokenURI); diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 5be62eb3e..991008b79 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -26,6 +26,8 @@ import { IDBVersion } from "../src/deploy/db/mongo-adapter/types"; import { getMongoAdapter, resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; import { getLogger } from "../src/deploy/logger/create-logger"; import { updateAllContractsInDb } from "../src/upgrade/db"; +import { VERSION_TYPES } from "../src/deploy/db/mongo-adapter/constants"; +import { getGitTag } from "../src/utils/git-tag/get-tag"; describe("ZNS V1 Upgrade and Lock Test", () => { @@ -236,7 +238,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { domainHashes = regResults.map(({ domainHash }) => domainHash); // get contract data for the upgrade helper - contractData = await getContractDataForUpgrade(dbAdapterDeploy); + contractData = await getContractDataForUpgrade(dbAdapterDeploy, getContractNamesToUpgrade()); process.env.MONGO_DB_VERSION = dbAdapterDeploy.curVersion; dbVersionDeploy = await dbAdapterDeploy.getLatestVersion() as IDBVersion; @@ -265,7 +267,6 @@ describe("ZNS V1 Upgrade and Lock Test", () => { // update database records to new implementations await updateAllContractsInDb({ - contractData, dbAdapter: dbAdapterUpgrade, }); @@ -522,16 +523,21 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); describe("Database tests", () => { - it("should have the same version in the database", async () => { - const { - dbVersion: curDbVersion, - type: curVersionType, - } = await dbAdapterUpgrade.getLatestVersion() as IDBVersion; + let upgradedDbVersion : string; + let contractsVersion : string; + + it("should create new version in the database", async () => { + ({ + dbVersion: upgradedDbVersion, + contractsVersion, + } = await dbAdapterUpgrade.versions.findOne({ + type: VERSION_TYPES.upgraded, + }) as IDBVersion); - expect(dbVersionDeploy.dbVersion).to.equal(curDbVersion); - expect(curDbVersion).to.equal(process.env.MONGO_DB_VERSION); + expect(dbVersionDeploy.dbVersion).to.not.equal(upgradedDbVersion); + expect(upgradedDbVersion).to.not.equal(process.env.MONGO_DB_VERSION); - expect(dbVersionDeploy.type).to.equal(curVersionType); + expect(contractsVersion).to.equal(getGitTag()); }); it("should update docs for each upgraded contract properly", async () => { @@ -553,7 +559,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { bytecode: bytecodePostUpgrade, implementation: implPostUpgrade, version: versionPostUpgrade, - } = await dbAdapterUpgrade.getContract(contract) as IContractDbData; + } = await dbAdapterUpgrade.getContract(contract, upgradedDbVersion) as IContractDbData; const implAddress = await hre.upgrades.erc1967.getImplementationAddress( znsUpgraded[instance].target as string @@ -567,7 +573,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { expect(bytecodePreUpgrade).to.not.equal(bytecodePostUpgrade); expect(bytecodePostUpgrade).to.equal(bytecodePausable); - expect(versionPostUpgrade).to.equal(dbVersionDeploy.dbVersion); + expect(versionPostUpgrade).to.not.equal(dbVersionDeploy.dbVersion); + expect(versionPostUpgrade).to.equal(upgradedDbVersion); }, Promise.resolve() ); }); From da9d5f8ba263c469c6467086ffce172e70cc21ff Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 31 Mar 2025 14:54:29 -0700 Subject: [PATCH 042/114] update storage checker script to acquire full diff before throwing error --- src/upgrade/storage-data.ts | 30 ++++++++++++++++++++++-------- src/upgrade/types.ts | 11 ++++++++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index 49840f54c..e04e0b3d7 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -3,7 +3,7 @@ import { expect } from "chai"; import { ContractFactory } from "ethers"; import { getStorageLayout, getUnlinkedBytecode, getVersion, StorageLayout } from "@openzeppelin/upgrades-core"; import { readValidations } from "@openzeppelin/hardhat-upgrades/dist/utils/validations"; -import { ContractStorageData } from "./types"; +import { ContractStorageData, ContractStorageDiff } from "./types"; import { ZNSContract } from "../../test/helpers/types"; import { getLogger } from "../deploy/logger/create-logger"; @@ -55,14 +55,28 @@ export const compareStorageData = ( dataBefore : ContractStorageData, dataAfter : ContractStorageData, ) => { - dataAfter.forEach( - (stateVar, idx) => { + const storageDiff = dataAfter.reduce( + (acc : ContractStorageDiff | undefined, stateVar, idx) => { const [key, value] = Object.entries(stateVar)[0]; - expect(value).to.equal( - dataBefore[idx][key], - `Mismatch on state var ${key} at idx ${idx}! Prev value: ${dataBefore[idx][key]}, new value: ${value}` - ); - } + if (value !== dataBefore[idx][key]) { + console.error( + `Mismatch on state var ${key} at idx ${idx}! Prev value: ${dataBefore[idx][key]}, new value: ${value}` + ); + + return [ + ...acc as ContractStorageDiff, + { + key, + valueBefore: dataBefore[idx][key], + valueAfter: value, + }, + ]; + } + }, [] ); + + if (storageDiff && storageDiff.length > 0) { + throw new Error(`Storage data mismatch: ${JSON.stringify(storageDiff)}`); + } }; diff --git a/src/upgrade/types.ts b/src/upgrade/types.ts index ca7b9f597..897ad3846 100644 --- a/src/upgrade/types.ts +++ b/src/upgrade/types.ts @@ -6,8 +6,17 @@ import { } from "../../typechain"; import { Addressable } from "ethers"; + +export type ContractStorageElement = string | number | Array<{}>; + export type ContractStorageData = Array<{ - [label : string] : string | number | Array; + [label : string] : ContractStorageElement; +}>; + +export type ContractStorageDiff = Array<{ + key : string; + valueBefore : ContractStorageElement; + valueAfter : ContractStorageElement; }>; export interface IContractData { From e44f956047c522b8da71bae8ea13a87f7e23a156 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 31 Mar 2025 15:38:42 -0700 Subject: [PATCH 043/114] extend db update script to also verify new implementations --- src/upgrade/db.ts | 9 +++++++-- src/upgrade/scripts/update-db.ts | 4 ++-- test/zns-v1-upgrade-lock-test.ts | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/upgrade/db.ts b/src/upgrade/db.ts index 01374dec8..3366aa600 100644 --- a/src/upgrade/db.ts +++ b/src/upgrade/db.ts @@ -1,13 +1,12 @@ import * as hre from "hardhat"; import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; -import { IContractData } from "./types"; import { VERSION_TYPES } from "../deploy/db/mongo-adapter/constants"; import { getLogger } from "../deploy/logger/create-logger"; import { znsNames } from "../deploy/missions/contracts/names"; import { getContractDataForUpgrade } from "./upgrade"; -export const updateAllContractsInDb = async ({ +export const updateAllContractsInDbAndVerify = async ({ dbAdapter, } : { dbAdapter : MongoDBAdapter; @@ -45,6 +44,12 @@ export const updateAllContractsInDb = async ({ implAddress, newDbVersion, }); + + if (hre.network.name !== "hardhat") { + await hre.run("verify:verify", { + address: implAddress, + }); + } } // Update the version in the DB diff --git a/src/upgrade/scripts/update-db.ts b/src/upgrade/scripts/update-db.ts index 1963e23ce..5ff1ce6be 100644 --- a/src/upgrade/scripts/update-db.ts +++ b/src/upgrade/scripts/update-db.ts @@ -1,13 +1,13 @@ import { getLogger } from "../../deploy/logger/create-logger"; import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; -import { updateAllContractsInDb } from "../db"; +import { updateAllContractsInDbAndVerify } from "../db"; const executeDbUpdate = async () => { const logger = getLogger(); const dbAdapter = await getMongoAdapter(logger); - await updateAllContractsInDb({ + await updateAllContractsInDbAndVerify({ dbAdapter, }); }; diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 991008b79..1eae6cb02 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -25,7 +25,7 @@ import { IContractDbData } from "../src/deploy/db/types"; import { IDBVersion } from "../src/deploy/db/mongo-adapter/types"; import { getMongoAdapter, resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; import { getLogger } from "../src/deploy/logger/create-logger"; -import { updateAllContractsInDb } from "../src/upgrade/db"; +import { updateAllContractsInDbAndVerify } from "../src/upgrade/db"; import { VERSION_TYPES } from "../src/deploy/db/mongo-adapter/constants"; import { getGitTag } from "../src/utils/git-tag/get-tag"; @@ -266,7 +266,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); // update database records to new implementations - await updateAllContractsInDb({ + await updateAllContractsInDbAndVerify({ dbAdapter: dbAdapterUpgrade, }); From 8ed4c24d88b2d646a45fd837f2d94cbad00d738e Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 31 Mar 2025 16:56:37 -0700 Subject: [PATCH 044/114] fix tests that use mongo adapter by resetting it in the appropriate places --- test/DeployCampaignInt.test.ts | 11 +++++++++++ test/ZNSRootRegistrar.test.ts | 2 ++ test/zns-v1-upgrade-lock-test.ts | 3 +++ 3 files changed, 16 insertions(+) diff --git a/test/DeployCampaignInt.test.ts b/test/DeployCampaignInt.test.ts index d2dded9d2..894f32ce4 100644 --- a/test/DeployCampaignInt.test.ts +++ b/test/DeployCampaignInt.test.ts @@ -88,6 +88,10 @@ describe("Deploy Campaign Test", () => { }; }); + afterEach(() => { + resetMongoAdapter(); + }); + it("should deploy new MeowTokenMock when `mockMeowToken` is true", async () => { const campaign = await runZnsCampaign({ config: campaignConfig, @@ -357,6 +361,7 @@ describe("Deploy Campaign Test", () => { afterEach(async () => { await mongoAdapter.dropDB(); + resetMongoAdapter(); }); // eslint-disable-next-line max-len @@ -847,6 +852,8 @@ describe("Deploy Campaign Test", () => { }, }; + resetMongoAdapter(); + campaign = await runZnsCampaign({ config: campaignConfig, }); @@ -884,6 +891,7 @@ describe("Deploy Campaign Test", () => { const initialArchiveVal = process.env.ARCHIVE_PREVIOUS_DB_VERSION; process.env.ARCHIVE_PREVIOUS_DB_VERSION = "true"; + resetMongoAdapter(); // run a new campaign const { dbAdapter: newDbAdapter } = await runZnsCampaign({ config: campaignConfig, @@ -932,6 +940,7 @@ describe("Deploy Campaign Test", () => { const initialArchiveVal = process.env.ARCHIVE_PREVIOUS_DB_VERSION; process.env.ARCHIVE_PREVIOUS_DB_VERSION = "false"; + resetMongoAdapter(); // run a new campaign const { dbAdapter: newDbAdapter } = await runZnsCampaign({ config: campaignConfig, @@ -971,6 +980,7 @@ describe("Deploy Campaign Test", () => { const initialDBVersionVal = process.env.MONGO_DB_VERSION; process.env.MONGO_DB_VERSION = initialDBVersion; + resetMongoAdapter(); // run a new campaign const { state: { contracts: newContracts } } = await runZnsCampaign({ config: campaignConfig, @@ -1031,6 +1041,7 @@ describe("Deploy Campaign Test", () => { afterEach(async () => { await mongoAdapter.dropDB(); + resetMongoAdapter(); }); it("should prepare the correct data for each contract when verifying on Etherscan", async () => { diff --git a/test/ZNSRootRegistrar.test.ts b/test/ZNSRootRegistrar.test.ts index 41c0a6a4a..baa756ed6 100644 --- a/test/ZNSRootRegistrar.test.ts +++ b/test/ZNSRootRegistrar.test.ts @@ -46,6 +46,7 @@ import { getProxyImplAddress } from "./helpers/utils"; import { upgrades } from "hardhat"; import { MongoDBAdapter } from "../src/deploy/db/mongo-adapter/mongo-adapter"; import { getConfig } from "../src/deploy/campaign/environments"; +import { resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; require("@nomicfoundation/hardhat-chai-matchers"); @@ -100,6 +101,7 @@ describe("ZNSRootRegistrar", () => { afterEach(async () => { await mongoAdapter.dropDB(); + resetMongoAdapter(); }); it("Sets the payment config when provided with the domain registration", async () => { diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 1eae6cb02..6c29d5db4 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -85,6 +85,8 @@ describe("ZNS V1 Upgrade and Lock Test", () => { : deployer.address, }); + resetMongoAdapter(); + const campaign = await runZnsCampaign({ config, }); @@ -493,6 +495,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { after(async () => { if (hre.network.name === "hardhat") { await dbAdapterUpgrade.dropDB(); + resetMongoAdapter(); process.env.MONGO_DB_VERSION = ""; } }); From da621d2b91716c3e34f99d23097f21aa16f2779f Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Apr 2025 14:28:55 -0700 Subject: [PATCH 045/114] linter fixes --- src/upgrade/storage-data.ts | 2 -- src/upgrade/types.ts | 2 +- src/utils/convert-base64.ts | 3 +-- test/helpers/register-setup.ts | 2 +- test/zns-v1-upgrade-lock-test.ts | 2 +- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index e04e0b3d7..d625078dc 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -1,5 +1,4 @@ import * as hre from "hardhat"; -import { expect } from "chai"; import { ContractFactory } from "ethers"; import { getStorageLayout, getUnlinkedBytecode, getVersion, StorageLayout } from "@openzeppelin/upgrades-core"; import { readValidations } from "@openzeppelin/hardhat-upgrades/dist/utils/validations"; @@ -8,7 +7,6 @@ import { ZNSContract } from "../../test/helpers/types"; import { getLogger } from "../deploy/logger/create-logger"; -// TODO utils: move thess helpers to protocol-utils repo when available export const getContractStorageLayout = async ( contractFactory : ContractFactory ) : Promise => { diff --git a/src/upgrade/types.ts b/src/upgrade/types.ts index 897ad3846..40a45d7ac 100644 --- a/src/upgrade/types.ts +++ b/src/upgrade/types.ts @@ -7,7 +7,7 @@ import { import { Addressable } from "ethers"; -export type ContractStorageElement = string | number | Array<{}>; +export type ContractStorageElement = string | number | Array; export type ContractStorageData = Array<{ [label : string] : ContractStorageElement; diff --git a/src/utils/convert-base64.ts b/src/utils/convert-base64.ts index 79a4a55b0..437952f22 100644 --- a/src/utils/convert-base64.ts +++ b/src/utils/convert-base64.ts @@ -1,5 +1,4 @@ - - +/* eslint-disable no-shadow */ const toBase64 = (str : string) => btoa(str); const fromBase64 = (str : string) => atob(str); diff --git a/test/helpers/register-setup.ts b/test/helpers/register-setup.ts index 3f279944f..ab51ef10e 100644 --- a/test/helpers/register-setup.ts +++ b/test/helpers/register-setup.ts @@ -5,7 +5,7 @@ import { IFullDistributionConfig, IZNSContracts, } from "./types"; -import { ContractTransactionReceipt, ethers } from "ethers"; +import { ethers } from "ethers"; import { getDomainHashFromEvent } from "./events"; import { distrConfigEmpty, fullDistrConfigEmpty, DEFAULT_TOKEN_URI, paymentConfigEmpty } from "./constants"; import { getTokenContract } from "./tokens"; diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 6c29d5db4..e1ba0e2f3 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/ban-ts-comment, no-shadow */ import * as hre from "hardhat"; import { IDeployCampaignConfig, TZNSContractState } from "../src/deploy/campaign/types"; import { getConfig } from "../src/deploy/campaign/environments"; From 78b35d93f037b725afd98b9bef4865ec29d7c660 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Apr 2025 14:30:01 -0700 Subject: [PATCH 046/114] make dbAdapter only drop when network is hardhat --- test/mocha-global.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/mocha-global.ts b/test/mocha-global.ts index 47592e86b..43c6b4e8f 100644 --- a/test/mocha-global.ts +++ b/test/mocha-global.ts @@ -1,3 +1,4 @@ +import * as hre from "hardhat"; import { getMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; @@ -7,7 +8,9 @@ export const mochaGlobalSetup = async () => { export const mochaGlobalTeardown = async () => { const mongoAdapter = await getMongoAdapter(); - // the next line can be commented out to leave the DB after test to manually test - await mongoAdapter.dropDB(); + if (hre.network.name === "hardhat") { + // the next line can be commented out to leave the DB after test to manually test + await mongoAdapter.dropDB(); + } await mongoAdapter.close(); }; From 09386dc953a358eecdb862a1b9572d1da5f6ec93 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Apr 2025 15:23:31 -0700 Subject: [PATCH 047/114] small fix --- src/upgrade/upgrade.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index 9d8e45167..7c2e393ab 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -10,7 +10,6 @@ import { znsNames } from "../deploy/missions/contracts/names"; import { IContractDbData } from "../deploy/db/types"; -// TODO upg: add the ability to retry from where it left off/failed ! export const upgradeZNS = async ({ governorExt, contractData, @@ -34,15 +33,13 @@ export const upgradeZNS = async ({ ) => { const upgradedContracts = await acc; - const upgradedContract = await upgradeZNSContract({ + upgradedContracts[instanceName] = await upgradeZNSContract({ contractName, contractAddress: address, governor: governor as SignerWithAddress, logger, }); - upgradedContracts[instanceName] = upgradedContract; - return upgradedContracts; }, Promise.resolve({} as IZNSContractsUpgraded) From 630a483a86f1a7f69cee9739df35ff697417a7fe Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Apr 2025 15:24:32 -0700 Subject: [PATCH 048/114] Revert "make dbAdapter only drop when network is hardhat" This reverts commit 78b35d93f037b725afd98b9bef4865ec29d7c660. --- test/mocha-global.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/mocha-global.ts b/test/mocha-global.ts index 43c6b4e8f..47592e86b 100644 --- a/test/mocha-global.ts +++ b/test/mocha-global.ts @@ -1,4 +1,3 @@ -import * as hre from "hardhat"; import { getMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; @@ -8,9 +7,7 @@ export const mochaGlobalSetup = async () => { export const mochaGlobalTeardown = async () => { const mongoAdapter = await getMongoAdapter(); - if (hre.network.name === "hardhat") { - // the next line can be commented out to leave the DB after test to manually test - await mongoAdapter.dropDB(); - } + // the next line can be commented out to leave the DB after test to manually test + await mongoAdapter.dropDB(); await mongoAdapter.close(); }; From e666caec713eb28dd3d100a4910257ce9b55dd79 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Apr 2025 15:55:12 -0700 Subject: [PATCH 049/114] skip pausable contracts from solidity coverage --- .solcover.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.solcover.js b/.solcover.js index d4924e9a2..4fc2d44f0 100644 --- a/.solcover.js +++ b/.solcover.js @@ -2,6 +2,7 @@ module.exports = { skipFiles: [ 'utils/StringUtils.sol', 'token/mocks', - 'upgrade-test-mocks' + 'upgrade-test-mocks', + 'contracts/zns-pausable' ] }; From 4241323dde2d0d06c5dbc68b4b5263db776ba59a Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Apr 2025 19:06:56 -0700 Subject: [PATCH 050/114] fix solcover config --- .solcover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.solcover.js b/.solcover.js index 4fc2d44f0..9f03e96e9 100644 --- a/.solcover.js +++ b/.solcover.js @@ -3,6 +3,6 @@ module.exports = { 'utils/StringUtils.sol', 'token/mocks', 'upgrade-test-mocks', - 'contracts/zns-pausable' + 'zns-pausable' ] }; From 00337a26a8bd456988f22e3de99a7e81f86b9a9b Mon Sep 17 00:00:00 2001 From: MichaelKorchagin Date: Mon, 14 Apr 2025 13:47:49 -0700 Subject: [PATCH 051/114] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2abca5351..d6b9c0b3b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage.json typechain typechain-types .idea +.dist # Hardhat files cache From 789826d53fe264d76d53f8c246cf3bd5a5485a3c Mon Sep 17 00:00:00 2001 From: MichaelKorchagin Date: Mon, 14 Apr 2025 13:55:11 -0700 Subject: [PATCH 052/114] Gitignore update --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d6b9c0b3b..772f0c6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,8 @@ coverage.json typechain typechain-types .idea -.dist +.vscode +dist # Hardhat files cache From 5f45fe1ce1935e3992c1b82804a79b08372b9f6e Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 14 Apr 2025 14:03:14 -0700 Subject: [PATCH 053/114] add `whenNotPaused` to new RootRegistrar to prevent possible calls to it from other contracts --- contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol index e6de77e62..c39d06742 100644 --- a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol @@ -163,7 +163,7 @@ contract ZNSRootRegistrarPausable is */ function coreRegister( CoreRegisterArgs memory args - ) external override onlyRegistrar { + ) external override whenNotPaused onlyRegistrar { _coreRegister( args ); From 98b1806c643ca6a405f99dae9db0e88f30538f04 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 14 Apr 2025 14:07:30 -0700 Subject: [PATCH 054/114] add pause() to the IZNSPausable interface --- contracts/zns-pausable/IZNSPausable.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/zns-pausable/IZNSPausable.sol b/contracts/zns-pausable/IZNSPausable.sol index a7a572795..d9f8aa67f 100644 --- a/contracts/zns-pausable/IZNSPausable.sol +++ b/contracts/zns-pausable/IZNSPausable.sol @@ -16,4 +16,6 @@ interface IZNSPausable { function pause() external; function unpause() external; + + function paused() external view returns (bool); } From 531df2a179ca35a24c0e5b5aa88858ebe2141e7c Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 14 Apr 2025 14:14:19 -0700 Subject: [PATCH 055/114] change virtual to override for all paused() instances --- .../zns-pausable/price/ZNSCurvePricerPausable.sol | 2 +- .../zns-pausable/price/ZNSFixedPricerPausable.sol | 2 +- .../registrar/ZNSRootRegistrarPausable.sol | 2 +- .../zns-pausable/registrar/ZNSSubRegistrarPausable.sol | 2 +- .../zns-pausable/registry/ZNSRegistryPausable.sol | 5 ++++- .../resolver/ZNSAddressResolverPausable.sol | 2 +- .../zns-pausable/token/ZNSDomainTokenPausable.sol | 10 +++++++--- .../zns-pausable/treasury/ZNSTreasuryPausable.sol | 2 +- 8 files changed, 17 insertions(+), 10 deletions(-) diff --git a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol index e8eb22025..7b5637458 100644 --- a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol +++ b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol @@ -314,7 +314,7 @@ contract ZNSCurvePricerPausable is AAccessControlled, ARegistryWiredPausable, UU /** * @dev Returns true if the contract is paused, and false otherwise. */ - function paused() public view virtual returns (bool) { + function paused() public view override returns (bool) { return _paused; } diff --git a/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol index acb927081..a6eb09423 100644 --- a/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol +++ b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol @@ -169,7 +169,7 @@ contract ZNSFixedPricerPausable is AAccessControlled, ARegistryWiredPausable, UU /** * @dev Returns true if the contract is paused, and false otherwise. */ - function paused() public view virtual returns (bool) { + function paused() public view override returns (bool) { return _paused; } diff --git a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol index c39d06742..28462bed3 100644 --- a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol @@ -172,7 +172,7 @@ contract ZNSRootRegistrarPausable is /** * @dev Returns true if the contract is paused, and false otherwise. */ - function paused() public view virtual returns (bool) { + function paused() public view override returns (bool) { return _paused; } diff --git a/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol index bd0398238..40313acd8 100644 --- a/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol +++ b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol @@ -374,7 +374,7 @@ contract ZNSSubRegistrarPausable is AAccessControlled, ARegistryWiredPausable, U /** * @dev Returns true if the contract is paused, and false otherwise. */ - function paused() public view virtual returns (bool) { + function paused() public view override returns (bool) { return _paused; } diff --git a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol index ce6be6bef..ca5a20106 100644 --- a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol +++ b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol @@ -107,7 +107,10 @@ contract ZNSRegistryPausable is return candidate == owner || operators[owner][candidate]; } - function paused() public view returns (bool) { + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override returns (bool) { return _paused; } diff --git a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol index 9953bf9b7..478c32f33 100644 --- a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol +++ b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol @@ -124,7 +124,7 @@ contract ZNSAddressResolverPausable is /** * @dev Returns true if the contract is paused, and false otherwise. */ - function paused() public view virtual returns (bool) { + function paused() public view override returns (bool) { return _paused; } diff --git a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol index 725a4f4d9..5f57dd4a1 100644 --- a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol +++ b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol @@ -5,8 +5,9 @@ import { ERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { ERC2981Upgradeable } from "@openzeppelin/contracts-upgradeable/token/common/ERC2981Upgradeable.sol"; -import { ERC721URIStorageUpgradeable } - from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; +import { + ERC721URIStorageUpgradeable +} from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol"; import { IZNSDomainTokenPausable } from "./IZNSDomainTokenPausable.sol"; import { IZNSDomainToken } from "../../token/IZNSDomainToken.sol"; import { AAccessControlled } from "../../access/AAccessControlled.sol"; @@ -182,7 +183,10 @@ contract ZNSDomainTokenPausable is emit TokenRoyaltySet(tokenId, royaltyFraction); } - function paused() public view returns (bool) { + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override returns (bool) { return _paused; } diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol index 92addf82f..fb16165bc 100644 --- a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -312,7 +312,7 @@ contract ZNSTreasuryPausable is AAccessControlled, ARegistryWiredPausable, UUPSU /** * @dev Returns true if the contract is paused, and false otherwise. */ - function paused() public view virtual returns (bool) { + function paused() public view override returns (bool) { return _paused; } From 98d753568576c1bfa3613e59c8be9ac9a753fb87 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 14 Apr 2025 14:22:38 -0700 Subject: [PATCH 056/114] simplify db helpers --- src/upgrade/db.ts | 8 +++----- src/upgrade/scripts/update-db.ts | 6 ++---- test/zns-v1-upgrade-lock-test.ts | 4 ++-- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/upgrade/db.ts b/src/upgrade/db.ts index 3366aa600..e2588775d 100644 --- a/src/upgrade/db.ts +++ b/src/upgrade/db.ts @@ -6,11 +6,9 @@ import { znsNames } from "../deploy/missions/contracts/names"; import { getContractDataForUpgrade } from "./upgrade"; -export const updateAllContractsInDbAndVerify = async ({ - dbAdapter, -} : { - dbAdapter : MongoDBAdapter; -}) => { +export const updateDbAndVerifyAll = async ( + dbAdapter : MongoDBAdapter, +) => { const logger = getLogger(); const contractNames = JSON.parse(JSON.stringify(znsNames)); diff --git a/src/upgrade/scripts/update-db.ts b/src/upgrade/scripts/update-db.ts index 5ff1ce6be..ae7d51324 100644 --- a/src/upgrade/scripts/update-db.ts +++ b/src/upgrade/scripts/update-db.ts @@ -1,15 +1,13 @@ import { getLogger } from "../../deploy/logger/create-logger"; import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; -import { updateAllContractsInDbAndVerify } from "../db"; +import { updateDbAndVerifyAll } from "../db"; const executeDbUpdate = async () => { const logger = getLogger(); const dbAdapter = await getMongoAdapter(logger); - await updateAllContractsInDbAndVerify({ - dbAdapter, - }); + await updateDbAndVerifyAll(dbAdapter); }; diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index e1ba0e2f3..4abea0fef 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -25,7 +25,7 @@ import { IContractDbData } from "../src/deploy/db/types"; import { IDBVersion } from "../src/deploy/db/mongo-adapter/types"; import { getMongoAdapter, resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; import { getLogger } from "../src/deploy/logger/create-logger"; -import { updateAllContractsInDbAndVerify } from "../src/upgrade/db"; +import { updateDbAndVerifyAll } from "../src/upgrade/db"; import { VERSION_TYPES } from "../src/deploy/db/mongo-adapter/constants"; import { getGitTag } from "../src/utils/git-tag/get-tag"; @@ -268,7 +268,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); // update database records to new implementations - await updateAllContractsInDbAndVerify({ + await updateDbAndVerifyAll({ dbAdapter: dbAdapterUpgrade, }); From 73f7ba6e5d92545c5f1d8ef5ba2544b844efb82d Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 14 Apr 2025 14:32:32 -0700 Subject: [PATCH 057/114] fix test --- test/zns-v1-upgrade-lock-test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/zns-v1-upgrade-lock-test.ts index 4abea0fef..da7860787 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/zns-v1-upgrade-lock-test.ts @@ -268,9 +268,7 @@ describe("ZNS V1 Upgrade and Lock Test", () => { }); // update database records to new implementations - await updateDbAndVerifyAll({ - dbAdapter: dbAdapterUpgrade, - }); + await updateDbAndVerifyAll(dbAdapterUpgrade); // list of all the methods that are blocked with `whenNotPaused` modifier // along with arguments for calls From 8a89546f9b1d399cee3bd43b0356616f9c0bf8c7 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 14 Apr 2025 14:34:26 -0700 Subject: [PATCH 058/114] change test name --- test/{zns-v1-upgrade-lock-test.ts => Pausable.upgrade.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/{zns-v1-upgrade-lock-test.ts => Pausable.upgrade.test.ts} (99%) diff --git a/test/zns-v1-upgrade-lock-test.ts b/test/Pausable.upgrade.test.ts similarity index 99% rename from test/zns-v1-upgrade-lock-test.ts rename to test/Pausable.upgrade.test.ts index da7860787..24e4c3caf 100644 --- a/test/zns-v1-upgrade-lock-test.ts +++ b/test/Pausable.upgrade.test.ts @@ -30,7 +30,7 @@ import { VERSION_TYPES } from "../src/deploy/db/mongo-adapter/constants"; import { getGitTag } from "../src/utils/git-tag/get-tag"; -describe("ZNS V1 Upgrade and Lock Test", () => { +describe("ZNS Upgrade and Pause Test", () => { let deployer : SignerWithAddress; let rootOwner : SignerWithAddress; let lvl2SubOwner : SignerWithAddress; From 46f10242625a496bb3f58831fd442063e49152c6 Mon Sep 17 00:00:00 2001 From: James Earle Date: Wed, 16 Apr 2025 14:21:24 -0400 Subject: [PATCH 059/114] WIP reconfigure to simplify process and reduce queries --- .gitignore | 2 + hardhat.config.ts | 4 + package.json | 3 + src/utils/migration/01_validation.ts | 128 +++++++++++++++++++ src/utils/migration/database.ts | 48 +++++++ src/utils/migration/subgraph/client.ts | 24 ++++ src/utils/migration/subgraph/queries.ts | 57 +++++++++ src/utils/migration/types.ts | 96 ++++++++++++++ src/utils/migration/validate.ts | 60 +++++++++ src/utils/migration/zns-contract-data.ts | 52 ++++++++ yarn.lock | 152 ++++++++++++++++++++++- 11 files changed, 624 insertions(+), 2 deletions(-) create mode 100644 src/utils/migration/01_validation.ts create mode 100644 src/utils/migration/database.ts create mode 100644 src/utils/migration/subgraph/client.ts create mode 100644 src/utils/migration/subgraph/queries.ts create mode 100644 src/utils/migration/types.ts create mode 100644 src/utils/migration/validate.ts create mode 100644 src/utils/migration/zns-contract-data.ts diff --git a/.gitignore b/.gitignore index 2abca5351..bda523d7d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ docker*.tgz # We don't ever use the generated manifests .openzeppelin + +output diff --git a/hardhat.config.ts b/hardhat.config.ts index d33955b2f..bab63f961 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -100,6 +100,10 @@ const config : HardhatUserConfig = { networks: { mainnet: { url: `${process.env.MAINNET_RPC_URL}`, + accounts: [ + // Read only + `${process.env.TESTNET_PRIVATE_KEY_A}`, + ], gasPrice: 80000000000, }, sepolia: { diff --git a/package.json b/package.json index b3f796e0f..97f19bebb 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "lint" ], "devDependencies": { + "@apollo/client": "^3.5.6", "@ensdomains/ensjs": "2.1.0", "@nomicfoundation/hardhat-chai-matchers": "^2.0.2", "@nomicfoundation/hardhat-ethers": "^3.0.5", @@ -57,6 +58,7 @@ "@typechain/ethers-v6": "^0.5.1", "@typechain/hardhat": "^9.1.0", "@types/chai": "^4.3.11", + "@types/graphql": "^14.5.0", "@types/mocha": "^9.1.0", "@types/node": "^18.15.11", "@zero-tech/eslint-config-cpt": "0.2.7", @@ -66,6 +68,7 @@ "ethers": "^6.9.0", "hardhat": "^2.19.1", "hardhat-gas-reporter": "^1.0.9", + "react": "^19.1.0", "semantic-release": "^21.0.1", "solhint": "^4.0.0", "solidity-coverage": "^0.8.5", diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts new file mode 100644 index 000000000..d8e1cb913 --- /dev/null +++ b/src/utils/migration/01_validation.ts @@ -0,0 +1,128 @@ +import * as hre from "hardhat"; +import * as fs from "fs"; +import { getUsersAndDomains } from "./subgraph"; +import { Domain } from "./types"; +import { getDBAdapter } from "./database"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { getZNS } from "./zns-contract-data"; +import { validateDomain } from "./validate" + +import assert from "assert"; + + +// For pagination of data in subgraph we use 'first' and 'skip' +const main = async () => { + const [ migrationAdmin ] = await hre.ethers.getSigners(); + + type User = { id: string, domains: Domain[] }; + type ValidatedUser = { address: string, validDomains: Domain[], invalidDomains: Domain[] }; + + const users = await getUsersAndDomains() as Array; + + console.log(`Found ${users.length} users`); + + const zns = await getZNS(migrationAdmin); + const validatedUsers : Array = []; + + // for each user, iterate list of domains + for(let i = 0; i < 5; i++) { + const user = users[i]; + + const validDomains : Array = [] + const invalidDomains : Array = []; + + for (let j = 0; j < user.domains.length; j++) { + const domain = user.domains[j]; + try { + await validateDomain(domain, zns, false); + validDomains.push(domain); + } catch (e) { + invalidDomains.push(domain); + // console.log("invalid domain found") + // throw (e as Error).message; // no point in try catch if we just throw error? + } + } + + validatedUsers.push({ + address: user.id, + validDomains, + invalidDomains + }); + + console.log(`Processed: ${i}`); + } + + const dbName = "zns-domain-migration"; + const uri = process.env.MONGO_DB_URI_WRITE; + + if (!uri) throw Error("No connection string provided"); + + let client = (await getDBAdapter(uri)).db(dbName); + + // To avoid duplicate data, we clear the DB before any inserts + await client.dropCollection("user-domains"); + await client.collection("user-domains").insertMany(validatedUsers); + + // HH not exiting process properly, exit manually + process.exit(0); +}; + +export const validateDomains = async ( + admin : SignerWithAddress, + users : Array // make user type +) : Promise>> => { + const start = Date.now(); + + // Get ZNS contracts from the MongoDB instance to validate against + const zns = await getZNS(admin); + + const invalidDomains : Array = []; + + // array instead? + const validatedUsers : Map> = new Map(); + + const validatedUsers2 = []; + // TODO remove subset when testedd + const subsetUsers = users.slice(0,3); + + let counter = 0; + for (let user of subsetUsers) { + const userDomains = users[counter] + + console.log(`USERDOMAINS_OBJ: ${userDomains}`); + const validDomains : Array = [] + + + if (!userDomains) continue; + + for (let domain of userDomains) { + try { + await validateDomain(domain, zns, false); + validDomains.push(domain); + } catch (e) { + console.log((e as Error).message); + invalidDomains.push(domain); + } + } + + validatedUsers.set(user, validDomains); + console.log(`Processed: ${++counter}`); + } + + if (invalidDomains.length > 0) { + fs.writeFileSync("output/invalid-domains.json", JSON.stringify(invalidDomains, undefined, 2)); + } + + // There should be no invalid domains for full run + assert.equal(invalidDomains.length, 0); + + const end = Date.now(); + console.log(`Validated all domains in ${end - start}ms`); + + return validatedUsers; +} + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/src/utils/migration/database.ts b/src/utils/migration/database.ts new file mode 100644 index 000000000..7e0b2f87d --- /dev/null +++ b/src/utils/migration/database.ts @@ -0,0 +1,48 @@ +import { MongoClient, ServerApiVersion } from "mongodb"; + +export let dbVersion : string; + +export const getDBAdapter = async ( + connectionString : string +): Promise => { + const mongoClient = new MongoClient( + connectionString, + { + serverApi: { + version: ServerApiVersion.v1, + strict: true, + deprecationErrors: true, + } + } + ); + + return await mongoClient.connect(); +} + +export const getZNSFromDB = async () => { + let version; + let uri; + let dbName; + + version = process.env.MONGO_DB_VERSION; + uri = process.env.MONGO_DB_URI; + dbName = process.env.MONGO_DB_NAME; + + if (!uri) { + throw new Error("Failed to connect: missing MongoDB URI or version"); + } + + let dbAdapter = await getDBAdapter(uri); + + if(!dbName) { + throw new Error(`Failed to connect: database "${dbName}" not found`); + } + + const db = await dbAdapter.db(dbName); + + let zns = await db.collection("contracts").find( + { version } + ).toArray(); + + return zns; +}; \ No newline at end of file diff --git a/src/utils/migration/subgraph/client.ts b/src/utils/migration/subgraph/client.ts new file mode 100644 index 000000000..6e6a12f19 --- /dev/null +++ b/src/utils/migration/subgraph/client.ts @@ -0,0 +1,24 @@ +import("@apollo/client"); + +// CommonJS error +import { + ApolloClient, + HttpLink, + InMemoryCache, + NormalizedCacheObject +} from "@apollo/client"; + +// const apollo = import("@apollo/client"); + +export const createClient = (subgraphUri ?: string) : ApolloClient => { + const uri = subgraphUri ? subgraphUri : process.env.SUBGRAPH_URL_DEV; + + if (!uri) throw Error("No Subgraph URI provided"); + + const client = new ApolloClient({ + link: new HttpLink({ uri: uri, fetch }), + cache: new InMemoryCache(), + }); + + return client; +}; diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts new file mode 100644 index 000000000..4734a04a0 --- /dev/null +++ b/src/utils/migration/subgraph/queries.ts @@ -0,0 +1,57 @@ +import { gql } from "@apollo/client/core"; + +export const getUsersAndDomains = gql` + query Domains($first: Int!, $skip: Int!) { + users(first: $first, skip: $skip) { + id + domains { + id + minter { + id + } + owner { + id + } + domainToken { + owner { + id + } + } + isRevoked + depth + label + isWorld + address + parentHash + parent { + id + isRevoked + label + } + accessType + pricerContract + paymentToken { + id + name + symbol + } + paymentType + curvePriceConfig { + id + } + fixedPriceConfig { + id + } + subdomainCount + address + tokenId + tokenURI + treasury { + id + beneficiaryAddress + } + creationBlock + } + } + } +`; diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts new file mode 100644 index 000000000..a51c4b289 --- /dev/null +++ b/src/utils/migration/types.ts @@ -0,0 +1,96 @@ +import { ContractTransactionReceipt } from "ethers"; +import { IDistributionConfig, IPaymentConfig } from "../../../test/helpers/types"; + +export interface Domain { + id : string; + minter : User; + owner : User; + domainToken : DomainToken; + depth : number; + label : string; + isRevoked : boolean; + isReclaimable : boolean; + reclaimableAddress : string; + isWorld : boolean; + address : string; + parentHash : string; + parent : Domain | null; + accessType : string; + paymentType : string; + pricerContract : string; + paymentToken : PaymentToken; + curvePriceConfig : CurvePriceConfig; + fixedPriceConfig : FixedPriceConfig; + subdomainCount : number; + tokenId : string; + tokenURI : string; + treasury : Treasury; + creationBlock : number; +} + +interface User { + id : string; + domains : Array; +} + +interface CurvePriceConfig { + id : string; + baseLength : string; + feePercentage : string; + maxLength : string; + maxPrice : string; + minPrice : string; + precisionMultiplier : string; +} + +interface FixedPriceConfig { + id : string; + feePercentage : string; + price : string; +} + +interface PaymentToken { + id : string; + name : string; + symbol : string; + decimals : string; +} + +interface Treasury { + id : string; + beneficiaryAddress : string; + domain : Domain; // cyclic? +} + +interface DomainToken { + baseURI : string; + defaultRoyalty : string; + owner : User; + royalty : string; + tokenId : string; + tokenName : string; + tokenSymbol : string; + tokenURI : string; +} + +export interface SubgraphError { + label : string; + hash : string; + parentHash : string; + parent : Domain | null; + error : string; +} + +export interface DomainData { + parentHash : string; + label : string; + domainAddress : string; + tokenUri : string; + distrConfig : IDistributionConfig; + paymentConfig : IPaymentConfig; +} + +export interface RegisteredDomains { + domainHashes : Array, + txHash : string +} \ No newline at end of file diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts new file mode 100644 index 000000000..a85449e57 --- /dev/null +++ b/src/utils/migration/validate.ts @@ -0,0 +1,60 @@ + +import { expect } from "chai"; +import { ZeroAddress, ZeroHash } from "ethers"; +import { Domain } from "./types"; +import { IZNSContracts } from "../../../test/helpers/types"; +import { + AccessType, + PaymentType, +} from "../../../test/helpers"; + +// TODO change to asserts + +export const validateDomain = async ( + domain : Domain, + zns : IZNSContracts, + postMigration : boolean = false +) => { + // Because we no longer delete from the store in the subgraph when a domain is revoked + // we have to first check `isRevoked` before checking the registry + if (!domain.isRevoked) { + expect(await zns.registry.exists(domain.id)).to.be.true; + expect( + (await zns.registry.getDomainOwner(domain.id)).toLowerCase()) + .to.equal(domain.owner.id.toLowerCase()); + expect( + (await zns.domainToken.ownerOf(domain.tokenId)).toLowerCase()) + .to.equal(domain.domainToken.owner.id.toLowerCase()); + + expect( + (await zns.addressResolver.resolveDomainAddress(domain.id)).toLowerCase()) + .to.equal(domain.address.toLowerCase()); + } + + const distrConfig = await zns.subRegistrar.distrConfigs(domain.id); + + // Props not yet set in the subgraph return null, but in the contract it will + // be 0 value, so we must mediate here + + if (postMigration) { + expect(distrConfig.accessType).to.equal(AccessType.OPEN); + expect(distrConfig.paymentType).to.equal(PaymentType.DIRECT); + expect(distrConfig.pricerContract.toLowerCase()).to.equal(ZeroAddress); + } else { + expect(distrConfig.accessType).to.equal(domain.accessType ?? 0n); + expect(distrConfig.paymentType).to.equal(domain.paymentType ?? 0n); + expect(distrConfig.pricerContract.toLowerCase()).to.equal(domain.pricerContract?.toLowerCase() ?? ZeroAddress); + } + + if (domain.isWorld) { + expect(domain.parentHash).to.equal(ZeroHash); + expect(!!domain.parent).to.be.false; + expect(domain.depth === 0); + } else { + // Because we no longer delete from the subgraph store on revoke, the domain is always present + // even if `isRevoked` is true + expect(!!domain.parent).to.be.true; + expect(domain.parentHash).to.not.equal(ZeroHash); + expect(domain.depth > 0); + } +} \ No newline at end of file diff --git a/src/utils/migration/zns-contract-data.ts b/src/utils/migration/zns-contract-data.ts new file mode 100644 index 000000000..84671bc4d --- /dev/null +++ b/src/utils/migration/zns-contract-data.ts @@ -0,0 +1,52 @@ +import { znsNames } from "../../deploy/missions/contracts/names"; +import { IZNSContracts } from "../../deploy/campaign/types"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { getZNSFromDB } from "./database"; +import { + ZNSAccessController__factory, + ZNSRegistry__factory, + MeowTokenMock__factory, + ZNSAddressResolver__factory, + ZNSCurvePricer__factory, + ZNSDomainToken__factory, + ZNSFixedPricer__factory, + ZNSRootRegistrar__factory, + ZNSSubRegistrar__factory, + ZNSTreasury__factory +} from "../../../typechain/index"; + +let znsCache : IZNSContracts | null = null; + +export const getZNS = async (signer : SignerWithAddress) => { + if (!znsCache || Object.values(znsCache).length < 10) { + const zns = await getZNSFromDB(); + + // Get each contract and manually connect to a factory. + // Using `getContractFactory()` returns an incorrect type of factory here + const acAddress = zns.find((contract) => contract.name === znsNames.accessController.contract); + const regAddress = zns.find((contract) => contract.name === znsNames.registry.contract); + const domainTokenAddress = zns.find((contract) => contract.name === znsNames.domainToken.contract); + const meowTokenAddress = zns.find((contract) => contract.name === znsNames.meowToken.contractMock); // contract on prod, contractMock on testnet + const addressResolverAddress = zns.find((contract) => contract.name === znsNames.addressResolver.contract); + const curvePricerAddress = zns.find((contract) => contract.name === znsNames.curvePricer.contract); + const treasuryAddress = zns.find((contract) => contract.name === znsNames.treasury.contract); + const rootRegistrarAddress = zns.find((contract) => contract.name === znsNames.rootRegistrar.contract); + const fixedPricerAddress = zns.find((contract) => contract.name === znsNames.fixedPricer.contract); + const subRegistrarAddress = zns.find((contract) => contract.name === znsNames.subRegistrar.contract); + + znsCache = { + accessController: ZNSAccessController__factory.connect(acAddress!.address, signer), + registry: ZNSRegistry__factory.connect(regAddress!.address, signer), + domainToken: ZNSDomainToken__factory.connect(domainTokenAddress!.address, signer), + meowToken: MeowTokenMock__factory.connect(meowTokenAddress!.address, signer), + addressResolver: ZNSAddressResolver__factory.connect(addressResolverAddress!.address, signer), + curvePricer: ZNSCurvePricer__factory.connect(curvePricerAddress!.address, signer), + treasury: ZNSTreasury__factory.connect(treasuryAddress!.address, signer), + rootRegistrar: ZNSRootRegistrar__factory.connect(rootRegistrarAddress!.address, signer), + fixedPricer: ZNSFixedPricer__factory.connect(fixedPricerAddress!.address, signer), + subRegistrar: ZNSSubRegistrar__factory.connect(subRegistrarAddress!.address, signer), + } + } + + return znsCache; +}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 9a1d19870..e70420249 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,25 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== +"@apollo/client@^3.5.6": + version "3.13.6" + resolved "https://registry.yarnpkg.com/@apollo/client/-/client-3.13.6.tgz#64db62d74b1aa78bfff50d995b3473e9a8c70ab7" + integrity sha512-G6A8uNb13V/Tv4TJQOs5PnxuE5Rf5D2dMnBQcg9mng1Eo4YBecwFEJ0L022mraq/dLB0jD5tiAESOD2bTyJ6gg== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + "@wry/caches" "^1.0.0" + "@wry/equality" "^0.5.6" + "@wry/trie" "^0.5.0" + graphql-tag "^2.12.6" + hoist-non-react-statics "^3.3.2" + optimism "^0.18.0" + prop-types "^15.7.2" + rehackt "^0.1.0" + symbol-observable "^4.0.0" + ts-invariant "^0.10.3" + tslib "^2.3.0" + zen-observable-ts "^1.2.5" + "@aws-crypto/sha256-js@1.2.2": version "1.2.2" resolved "https://registry.yarnpkg.com/@aws-crypto/sha256-js/-/sha256-js-1.2.2.tgz#02acd1a1fda92896fc5a28ec7c6e164644ea32fc" @@ -586,6 +605,11 @@ resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== +"@graphql-typed-document-node/core@^3.1.1": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861" + integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== + "@humanwhocodes/config-array@^0.11.13": version "0.11.13" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" @@ -1830,6 +1854,13 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/graphql@^14.5.0": + version "14.5.0" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.5.0.tgz#a545fb3bc8013a3547cf2f07f5e13a33642b75d6" + integrity sha512-MOkzsEp1Jk5bXuAsHsUi6BVv0zCO+7/2PTiZMXWDSsMXvNU6w/PLMQT2vHn8hy2i0JqojPz1Sz6rsFjHtsU0lA== + dependencies: + graphql "*" + "@types/http-cache-semantics@^4.0.2": version "4.0.4" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" @@ -2042,6 +2073,34 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== +"@wry/caches@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@wry/caches/-/caches-1.0.1.tgz#8641fd3b6e09230b86ce8b93558d44cf1ece7e52" + integrity sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA== + dependencies: + tslib "^2.3.0" + +"@wry/context@^0.7.0": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@wry/context/-/context-0.7.4.tgz#e32d750fa075955c4ab2cfb8c48095e1d42d5990" + integrity sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ== + dependencies: + tslib "^2.3.0" + +"@wry/equality@^0.5.6": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.5.7.tgz#72ec1a73760943d439d56b7b1e9985aec5d497bb" + integrity sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw== + dependencies: + tslib "^2.3.0" + +"@wry/trie@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@wry/trie/-/trie-0.5.0.tgz#11e783f3a53f6e4cd1d42d2d1323f5bc3fa99c94" + integrity sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA== + dependencies: + tslib "^2.3.0" + "@zero-tech/eslint-config-cpt@0.2.7": version "0.2.7" resolved "https://registry.yarnpkg.com/@zero-tech/eslint-config-cpt/-/eslint-config-cpt-0.2.7.tgz#ec1d94848737863a8c9b9e226fd586b7f8a97346" @@ -4835,6 +4894,18 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== +graphql-tag@^2.12.6: + version "2.12.6" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1" + integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== + dependencies: + tslib "^2.1.0" + +graphql@*: + version "16.10.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" + integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== + handlebars@^4.0.1, handlebars@^4.7.7: version "4.7.8" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.8.tgz#41c42c18b1be2365439188c77c6afae71c0cd9e9" @@ -5037,6 +5108,13 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + hook-std@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/hook-std/-/hook-std-3.0.0.tgz#47038a01981e07ce9d83a6a3b2eb98cad0f7bd58" @@ -5633,7 +5711,7 @@ js-sha3@^0.5.7: resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.5.7.tgz#0d4ffd8002d5333aabaf4a23eed2f6374c9f28e7" integrity sha512-GII20kjaPX0zJ8wzkTbNDYMY7msuZcTWk8S5UOh6806Jq/wz1J8/bnr8uGU0DAUmYDjj2Mr4X1cW8v/GLYnR+g== -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -6122,6 +6200,13 @@ logform@^2.3.2, logform@^2.4.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loupe@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -6997,7 +7082,7 @@ number-to-bn@1.7.0: bn.js "4.11.6" strip-hex-prefix "1.0.0" -object-assign@^4.1.0: +object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== @@ -7101,6 +7186,16 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +optimism@^0.18.0: + version "0.18.1" + resolved "https://registry.yarnpkg.com/optimism/-/optimism-0.18.1.tgz#5cf16847921413dbb0ac809907370388b9c6335f" + integrity sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ== + dependencies: + "@wry/caches" "^1.0.0" + "@wry/context" "^0.7.0" + "@wry/trie" "^0.5.0" + tslib "^2.3.0" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -7563,6 +7658,15 @@ promzard@^1.0.0: dependencies: read "^2.0.0" +prop-types@^15.7.2: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + proper-lockfile@^4.1.1: version "4.1.2" resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.2.tgz#c8b9de2af6b2f1601067f98e01ac66baa223141f" @@ -7646,6 +7750,16 @@ rc@1.2.8, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react@^19.1.0: + version "19.1.0" + resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75" + integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg== + read-cmd-shim@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/read-cmd-shim/-/read-cmd-shim-4.0.0.tgz#640a08b473a49043e394ae0c7a34dd822c73b9bb" @@ -7833,6 +7947,11 @@ registry-url@^6.0.0: dependencies: rc "1.2.8" +rehackt@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/rehackt/-/rehackt-0.1.0.tgz#a7c5e289c87345f70da8728a7eb878e5d03c696b" + integrity sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw== + req-cwd@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/req-cwd/-/req-cwd-2.0.0.tgz#d4082b4d44598036640fb73ddea01ed53db49ebc" @@ -8711,6 +8830,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +symbol-observable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-4.0.0.tgz#5b425f192279e87f2f9b937ac8540d1984b39205" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + sync-request@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/sync-request/-/sync-request-6.1.0.tgz#e96217565b5e50bbffe179868ba75532fb597e68" @@ -8909,6 +9033,13 @@ ts-essentials@^7.0.1: resolved "https://registry.yarnpkg.com/ts-essentials/-/ts-essentials-7.0.3.tgz#686fd155a02133eedcc5362dc8b5056cde3e5a38" integrity sha512-8+gr5+lqO3G84KdiTSMRLtuyJ+nTBVRKuCrK4lidMPdVeEp0uqC875uE5NMcaA7YYMN7XsNiFQuMvasF8HT/xQ== +ts-invariant@^0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.10.3.tgz#3e048ff96e91459ffca01304dbc7f61c1f642f6c" + integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ== + dependencies: + tslib "^2.1.0" + ts-node@10.9.1: version "10.9.1" resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b" @@ -8967,6 +9098,11 @@ tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== +tslib@^2.1.0, tslib@^2.3.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^2.3.1, tslib@^2.5.0: version "2.6.2" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" @@ -9630,6 +9766,18 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +zen-observable-ts@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz#6c6d9ea3d3a842812c6e9519209365a122ba8b58" + integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg== + dependencies: + zen-observable "0.8.15" + +zen-observable@0.8.15: + version "0.8.15" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== + zksync-web3@^0.14.3: version "0.14.4" resolved "https://registry.yarnpkg.com/zksync-web3/-/zksync-web3-0.14.4.tgz#0b70a7e1a9d45cc57c0971736079185746d46b1f" From 113e547e90f1329ad0555775446957f418932acd Mon Sep 17 00:00:00 2001 From: James Earle Date: Wed, 16 Apr 2025 14:23:12 -0400 Subject: [PATCH 060/114] WIP more changes, some EOL --- src/utils/migration/01_validation.ts | 125 ++++++------ src/utils/migration/02_registration.ts | 114 +++++++++++ src/utils/migration/registration.ts | 235 +++++++++++++++++++++++ src/utils/migration/subgraph/index.ts | 54 ++++++ src/utils/migration/validate.ts | 74 ++++--- src/utils/migration/zns-contract-data.ts | 2 +- test/helpers/types.ts | 2 +- 7 files changed, 521 insertions(+), 85 deletions(-) create mode 100644 src/utils/migration/02_registration.ts create mode 100644 src/utils/migration/registration.ts create mode 100644 src/utils/migration/subgraph/index.ts diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index d8e1cb913..31af4836d 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,13 +1,11 @@ import * as hre from "hardhat"; -import * as fs from "fs"; import { getUsersAndDomains } from "./subgraph"; import { Domain } from "./types"; import { getDBAdapter } from "./database"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate" - import assert from "assert"; +import { ZeroAddress } from "ethers"; // For pagination of data in subgraph we use 'first' and 'skip' @@ -15,7 +13,8 @@ const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); type User = { id: string, domains: Domain[] }; - type ValidatedUser = { address: string, validDomains: Domain[], invalidDomains: Domain[] }; + type InvalidDomain = { message: string, domain: Domain }; + type ValidatedUser = { address: string, validDomains: Domain[], invalidDomains: InvalidDomain[] }; const users = await getUsersAndDomains() as Array; @@ -25,11 +24,18 @@ const main = async () => { const validatedUsers : Array = []; // for each user, iterate list of domains - for(let i = 0; i < 5; i++) { + for(let i = 0; i < users.slice(0,5).length; i++) { const user = users[i]; const validDomains : Array = [] - const invalidDomains : Array = []; + const invalidDomains : Array = []; + + if (user.id != ZeroAddress) { + // As extra validation, be sure the on-chain domain token balance of a user + // matches the number of domains given by the subgraph + const domainBalance = await zns.domainToken.balanceOf(user.id); + assert.equal(domainBalance, user.domains.length); + } for (let j = 0; j < user.domains.length; j++) { const domain = user.domains[j]; @@ -37,19 +43,28 @@ const main = async () => { await validateDomain(domain, zns, false); validDomains.push(domain); } catch (e) { - invalidDomains.push(domain); - // console.log("invalid domain found") - // throw (e as Error).message; // no point in try catch if we just throw error? + // For debugging we keep invalid domains rather than throw + invalidDomains.push({ message: (e as Error).message, domain: domain }); } } - validatedUsers.push({ - address: user.id, - validDomains, - invalidDomains - }); + if (invalidDomains.length === 0 && validDomains.length > 0) { + validatedUsers.push({ + address: user.id, + validDomains, + invalidDomains + }); + } else { + console.log(`Empty or Invalid Domains found for user ${user.id}`); + console.log(invalidDomains.length); + console.log(validDomains.length); + } + // } else { + // // Shouldnt reach this + // // should skip if empty + // } - console.log(`Processed: ${i}`); + console.log(`Users Processed: ${i + 1}`); } const dbName = "zns-domain-migration"; @@ -67,60 +82,60 @@ const main = async () => { process.exit(0); }; -export const validateDomains = async ( - admin : SignerWithAddress, - users : Array // make user type -) : Promise>> => { - const start = Date.now(); +// export const validateDomains = async ( +// admin : SignerWithAddress, +// users : Array // make user type +// ) : Promise>> => { +// const start = Date.now(); - // Get ZNS contracts from the MongoDB instance to validate against - const zns = await getZNS(admin); +// // Get ZNS contracts from the MongoDB instance to validate against +// const zns = await getZNS(admin); - const invalidDomains : Array = []; +// const invalidDomains : Array = []; - // array instead? - const validatedUsers : Map> = new Map(); +// // array instead? +// const validatedUsers : Map> = new Map(); - const validatedUsers2 = []; - // TODO remove subset when testedd - const subsetUsers = users.slice(0,3); +// const validatedUsers2 = []; +// // TODO remove subset when testedd +// const subsetUsers = users.slice(0,3); - let counter = 0; - for (let user of subsetUsers) { - const userDomains = users[counter] +// let counter = 0; +// for (let user of subsetUsers) { +// const userDomains = users[counter] - console.log(`USERDOMAINS_OBJ: ${userDomains}`); - const validDomains : Array = [] +// console.log(`USERDOMAINS_OBJ: ${userDomains}`); +// const validDomains : Array = [] - if (!userDomains) continue; +// if (!userDomains) continue; - for (let domain of userDomains) { - try { - await validateDomain(domain, zns, false); - validDomains.push(domain); - } catch (e) { - console.log((e as Error).message); - invalidDomains.push(domain); - } - } +// for (let domain of userDomains) { +// try { +// await validateDomain(domain, zns, false); +// validDomains.push(domain); +// } catch (e) { +// console.log((e as Error).message); +// invalidDomains.push(domain); +// } +// } - validatedUsers.set(user, validDomains); - console.log(`Processed: ${++counter}`); - } +// validatedUsers.set(user, validDomains); +// console.log(`Processed: ${++counter}`); +// } - if (invalidDomains.length > 0) { - fs.writeFileSync("output/invalid-domains.json", JSON.stringify(invalidDomains, undefined, 2)); - } +// if (invalidDomains.length > 0) { +// fs.writeFileSync("output/invalid-domains.json", JSON.stringify(invalidDomains, undefined, 2)); +// } - // There should be no invalid domains for full run - assert.equal(invalidDomains.length, 0); +// // There should be no invalid domains for full run +// assert.equal(invalidDomains.length, 0); - const end = Date.now(); - console.log(`Validated all domains in ${end - start}ms`); +// const end = Date.now(); +// console.log(`Validated all domains in ${end - start}ms`); - return validatedUsers; -} +// return validatedUsers; +// } main().catch(error => { console.error(error); diff --git a/src/utils/migration/02_registration.ts b/src/utils/migration/02_registration.ts new file mode 100644 index 000000000..5c29375d0 --- /dev/null +++ b/src/utils/migration/02_registration.ts @@ -0,0 +1,114 @@ +import * as hre from "hardhat"; +import { Domain } from "./types"; +import * as fs from "fs"; +import { deployZNS } from "../../../test/helpers"; +import { postMigrationValidation, registerDomainsBulk } from "./registration"; +import { getZNS } from "./zns-contract-data"; +import { ROOTS_FILENAME, SUBS_FILENAME } from "./constants"; +import { IZNSContracts } from "../../deploy/campaign/types"; + +// Script #2 to be run AFTER validation of the domains with subgraph +const main = async () => { + const [ migrationAdmin, governor, admin ] = await hre.ethers.getSigners(); + + // Read domain data from file output of 01_validate.ts + const rootDomains = JSON.parse(fs.readFileSync(ROOTS_FILENAME, { + encoding: "utf8" + })) as Array; + + const subdomains = JSON.parse(fs.readFileSync(SUBS_FILENAME, { + encoding: "utf8" + })) as Array; + + let zns : IZNSContracts; + + if (hre.network.name === "hardhat") { + // Reset the network to be sure we aren't forking + await hre.network.provider.request({ + method: "hardhat_reset", + params: [], + }); + + const params = { + deployer: migrationAdmin, + governorAddresses: [migrationAdmin.address, governor.address], + adminAddresses: [migrationAdmin.address, admin.address], + }; + + // Recreate the domain tree with local ZNS + zns = await deployZNS(params); + } else if (hre.network.name === "sepolia") { + // Get instance of ZNS from DB + zns = await getZNS(migrationAdmin); + } else { + // TODO setup when zchain is deployed + throw new Error(`Network ${hre.network.name} not supported`); + } + + await zns.meowToken.connect(migrationAdmin).approve(await zns.treasury.getAddress(), hre.ethers.MaxUint256); + await zns.meowToken.connect(migrationAdmin).mint(migrationAdmin.address, hre.ethers.parseEther("8000000")); + + console.log( + `Balance of admin before: ${await zns.meowToken.balanceOf(migrationAdmin.address)}` + ); + + // Give approval to the RootRegistrar and SubRegistrar to transfer on behalf of the migration admin + await zns.domainToken.connect(migrationAdmin).setApprovalForAll(await zns.rootRegistrar.getAddress(), true); + await zns.domainToken.connect(migrationAdmin).setApprovalForAll(await zns.subRegistrar.getAddress(), true); + + const startTime = Date.now(); + + // How many domains we will register in a single transaction + const sliceSize = 50; + + // TODO because we no longer use the treasury at all we might be able to switch back to + // having owners just be the registrant for the domain, not the user then do a transfer + // would make it cheaper and faster, but on zchain gas probably won't matter + const start = 0; + + console.log(`Registering ${rootDomains.length - start} root domains with slice size ${sliceSize}`); + const registeredDomains = await registerDomainsBulk( + migrationAdmin, + rootDomains, + zns, + sliceSize, + start + ); + + console.log(`Registering ${subdomains.length} subdomains with slice size ${sliceSize}`); + const registeredSubdomains = await registerDomainsBulk( + migrationAdmin, + subdomains, + zns, + sliceSize, + 0 + ); + + // // ms -> s -> min + const totalTime = (Date.now() - startTime) / 1000 / 60; + console.log(`Registered ${rootDomains.length + subdomains.length} groups of domains in ${totalTime} minutes`); + console.log("Done") + + console.log(`txhash: ${registeredDomains[0].txHash}`); + console.log(`exists: ${await zns.registry.exists(registeredDomains[0].domainHashes[0])}`); + + console.log( + `Balance of admin after: ${await zns.meowToken.balanceOf(migrationAdmin.address)}` + ); + + // console.log("TEMP just doing postmigration validation for now to test") + console.log("Confirming with post-migration validation..."); + const fullDomains = rootDomains.concat(subdomains); + // await postMigrationValidation( + // zns, + // fullDomains + // ) + + // Manually exit here, HH runner doesn't exit properly + process.exit(0); +}; + +main().catch(error => { + console.error(error); + process.exitCode = 1; +}); \ No newline at end of file diff --git a/src/utils/migration/registration.ts b/src/utils/migration/registration.ts new file mode 100644 index 000000000..78dab6d8e --- /dev/null +++ b/src/utils/migration/registration.ts @@ -0,0 +1,235 @@ +import * as hre from "hardhat"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { RegisteredDomains, Domain } from "./types"; +import { IZNSContracts } from "../../deploy/campaign/types"; +import { paymentConfigEmpty } from "../../../test/helpers"; +import { IZNSContractsLocal } from "../../../test/helpers/types"; +import { DOMAIN_REGISTERED_TOPIC_SEPOLIA } from "./constants"; +import { expect } from "chai"; +import { validateDomain } from "./validate"; +import { ZeroAddress } from "ethers"; + + +export const registerDomainsBulk = async ( + regAdmin : SignerWithAddress, + domains : Array, // imagine this is ALL domains + zns : IZNSContracts, + sliceSize : number, + start : number, +) => { + const registeredDomains = Array(); + + // 'start' is used for retry logic and represents the number of domains + // we have already minted + + // The number of iterations to do based on the size of the incoming domain array + const numIters = Math.floor((domains.length - start) / sliceSize); + + // Because the terminator represents the *total* number of domains to register, + // we add `isStart` back in + const terminator = start + (sliceSize * numIters); + + for (let i = start; i < terminator; i += sliceSize) { + const domainsForTx = domains.slice(i, i + sliceSize); + + // If the domain hash is already in the registry we have already registered this + // batch of domains, so we skip it + if (!await zns.registry.exists(domainsForTx[0].id)) { + const { domainHashes, txHash, retryData } = await registerBase({ + regAdmin, + zns, + domains: domains.slice(i, i + sliceSize) + }) + + if (retryData) { + throw new Error("Error in registering domains") + } + + registeredDomains.push({ domainHashes, txHash }); + + console.log("Registered domains: ", i + sliceSize); + } else { + console.log(`Skipping already registered domains: ${i} to ${i + sliceSize}}`); + } + }; + + // In the likely case that the list of domains is not divisble by slice size, we + // want to make sure we do the last set of domains as well + const { domainHashes, txHash, retryData } = await registerBase({ + regAdmin, + zns, + domains: domains.slice(terminator) // terminator -> end of array + }) + + if (retryData) { + throw new Error("Error in registering domains") + } + + console.log("Registered additional domains: ", start + domainHashes.length); + + registeredDomains.push({ domainHashes, txHash }); + + return registeredDomains; +}; + +export const registerBase = async ({ + zns, + regAdmin, + domains +} : { + zns : IZNSContractsLocal | IZNSContracts; + regAdmin : SignerWithAddress; + domains : Array; +}) => { + + const tokenOwners = domains.map((domain) => { + if (domain.domainToken.owner.id === hre.ethers.ZeroAddress) { + // The ERC721 token has been burned, must mint with the record owner instead + // to recreate the tree fully + return domain.owner.id; + } else { + return domain.domainToken.owner.id; + } + }); + + const distConfigs = domains.map((domain) => { + let config = { + pricerContract: "", + paymentType: 0n, + accessType: 1n // Always use `open` access type for migration + }; + + // Get pricer contract + if (!domain.pricerContract) { + config.pricerContract = ZeroAddress; + } else { + config.pricerContract = domain.pricerContract; + } + + // Get payment type + if (!domain.paymentType || domain.paymentType === "0") { + config.paymentType = 0n; + } else { + config.paymentType = 1n; + } + + return config; + }); + + // Awaiting this promise within the `map` function below causes type problems + // when passing args to the function downstream. This is a workaround. + const tokenAddress = await zns.meowToken.getAddress(); + + const paymentConfigs = domains.map((domain) => { + return { + beneficiary: !domain.treasury.beneficiaryAddress + ? ZeroAddress + : domain.treasury.beneficiaryAddress, + token: tokenAddress + } + }); + + const recordOwners = domains.map((domain) => { return domain.owner.id }); + const parentHashes = domains.map((domain) => { return domain.parentHash }); + const labels = domains.map((domain) => { return domain.label }); + const domainAddresses = domains.map((domain) => {return domain.address }); + const tokenURIs = domains.map((domain) => { return domain.tokenURI }); + + let tx; + + try { + // Because we pre-filter using the query into sets of just root domains and just subdomains + // (ordered by depth) we know with certainty that if one parent hash is zero, they all are + if (parentHashes[0] === hre.ethers.ZeroHash) { + + const bulkMigrationArgs = { + tokenOwners: tokenOwners, + recordOwners: recordOwners, + names: labels, + domainAddresses: domainAddresses, + tokenURIs: tokenURIs, + distributionConfigs: distConfigs, + paymentConfigs: paymentConfigs, + } + + // It is by intention that we aren't recreating user configs + // We are just focusing on recreating the domain tree + tx = await zns.rootRegistrar.connect(regAdmin).registerRootDomainBulk( + bulkMigrationArgs, + // { + // gasLimit: 5000000 // TODO for debugging + // } + ); + } else { + const bulkMigrationArgs = { + domainToken: await zns.domainToken.getAddress(), + tokenOwners: tokenOwners, + recordOwners: recordOwners, + parentHashes: parentHashes, + labels: labels, + domainAddresses: domainAddresses, + tokenURIs: tokenURIs, + distributionConfigs: distConfigs, + paymentConfigs: paymentConfigs, + }; + + tx = await zns.subRegistrar.connect(regAdmin).registerSubdomainBulk( + bulkMigrationArgs + ); + } + } catch (e) { + console.log("Error registering domains: ", e); + // Return the domainData if something failed so we can log it + // for debugging purposes + return { + domainHash: undefined, + txHash: undefined, + retryData: domains + } + } + + // Providing a number on hardhat will cause it to hang + const blocks = hre.network.name === "hardhat" ? 0 : 5; + const txReceipt = await tx!.wait(blocks); + + if (!txReceipt) { + // Could this ever happen? Need this so downstream return states are never undefined + throw new Error("Transaction succeeded without receipt"); + } + + // Collected the registered domains + let domainHashes = Array(); + + const drEvents = txReceipt.logs.filter((log) => { + if (log.topics[0] === DOMAIN_REGISTERED_TOPIC_SEPOLIA) { + return log.topics[1]; // domainHash is always index 1 in this log + } + }) + // console.log(`DREVENTS: ${drEvents.length}`); + + drEvents.forEach((log) => { + domainHashes.push(log.topics[1]); + }); + + // console.log(`DOMAINHASHES: ${domainHashes.length}`); + + // console.log(txReceipt.hash); + + return { domainHashes, txHash: txReceipt.hash, retryData: undefined }; +}; + +export const postMigrationValidation = async ( + zns : IZNSContractsLocal | IZNSContracts, + domains : Array, +) => { + + // TODO figure out error with users who have not called to reclaim there domain + // after a transfer + for (const domain of domains) { + const error = await validateDomain(domain, zns, true); + + if (error) { + console.log("Error validating domain: ", error); + } + } +} \ No newline at end of file diff --git a/src/utils/migration/subgraph/index.ts b/src/utils/migration/subgraph/index.ts new file mode 100644 index 000000000..6cc345afa --- /dev/null +++ b/src/utils/migration/subgraph/index.ts @@ -0,0 +1,54 @@ +import { createClient } from "./client"; +import * as q from "./queries"; + + +// Grab domain data from the subgraph and validate against what's actually on mainnet + +export const getUsersAndDomains = async () => { + const first = 1000; + let skip = 0; + + let client = await createClient(); + + // First get all worlds + let result = await client.query({ + query: q.getUsersAndDomains, + variables: { + first, + skip, + }, + }); + + if (result.error) throw Error(`Error in graph query: ${result.error}`); + + const users = []; + + // We do this to collect ALL domains in a single array + while (result.data.users.length > 0) { + + // For each user, get every domain + for (const user of result.data.users) { + // user data from subgraph already has user and all domains + // so just return this + users.push(user); + } + + // Get next batch of domains + skip += 1000; + + // Refresh client + client = await createClient(); + + result = await client.query({ + query: q.getUsersAndDomains, + variables: { + first, + skip, + }, + }); + } + + return users; +}; + + diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index a85449e57..d8fe2eb9b 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -1,5 +1,4 @@ -import { expect } from "chai"; import { ZeroAddress, ZeroHash } from "ethers"; import { Domain } from "./types"; import { IZNSContracts } from "../../../test/helpers/types"; @@ -7,8 +6,7 @@ import { AccessType, PaymentType, } from "../../../test/helpers"; - -// TODO change to asserts +import assert from "assert"; export const validateDomain = async ( domain : Domain, @@ -18,43 +16,63 @@ export const validateDomain = async ( // Because we no longer delete from the store in the subgraph when a domain is revoked // we have to first check `isRevoked` before checking the registry if (!domain.isRevoked) { - expect(await zns.registry.exists(domain.id)).to.be.true; - expect( - (await zns.registry.getDomainOwner(domain.id)).toLowerCase()) - .to.equal(domain.owner.id.toLowerCase()); - expect( - (await zns.domainToken.ownerOf(domain.tokenId)).toLowerCase()) - .to.equal(domain.domainToken.owner.id.toLowerCase()); - - expect( - (await zns.addressResolver.resolveDomainAddress(domain.id)).toLowerCase()) - .to.equal(domain.address.toLowerCase()); + + // For speed in processing we group promises together + const promises = [ + zns.registry.exists(domain.id), + zns.registry.getDomainOwner(domain.id), + zns.domainToken.ownerOf(domain.tokenId), + zns.addressResolver.resolveDomainAddress(domain.id) + ] + + const [ + exists, + domainOwner, + domainTokenOwner, + domainAddress + ] = await Promise.all(promises) as unknown as [boolean, string, string, string]; + + assert.ok(!!exists); + + assert.equal( + domainOwner.toLowerCase(), + domain.owner.id.toLowerCase() + ); + + assert.equal( + domainTokenOwner.toLowerCase(), + domain.domainToken.owner.id.toLowerCase() + ); + + assert.equal( + domainAddress.toLowerCase(), + domain.address.toLowerCase() + ); } const distrConfig = await zns.subRegistrar.distrConfigs(domain.id); // Props not yet set in the subgraph return null, but in the contract it will // be 0 value, so we must mediate here - if (postMigration) { - expect(distrConfig.accessType).to.equal(AccessType.OPEN); - expect(distrConfig.paymentType).to.equal(PaymentType.DIRECT); - expect(distrConfig.pricerContract.toLowerCase()).to.equal(ZeroAddress); + assert.equal(distrConfig.accessType, AccessType.OPEN); + assert.equal(distrConfig.paymentType, PaymentType.DIRECT); + assert.equal(distrConfig.pricerContract.toLowerCase(), ZeroAddress); } else { - expect(distrConfig.accessType).to.equal(domain.accessType ?? 0n); - expect(distrConfig.paymentType).to.equal(domain.paymentType ?? 0n); - expect(distrConfig.pricerContract.toLowerCase()).to.equal(domain.pricerContract?.toLowerCase() ?? ZeroAddress); + assert.equal(distrConfig.accessType, domain.accessType ?? 0n); + assert.equal(distrConfig.paymentType, domain.paymentType ?? 0n); + assert.equal(distrConfig.pricerContract.toLowerCase(), domain.pricerContract?.toLowerCase() ?? ZeroAddress); } if (domain.isWorld) { - expect(domain.parentHash).to.equal(ZeroHash); - expect(!!domain.parent).to.be.false; - expect(domain.depth === 0); + assert.equal(domain.parentHash, ZeroHash); + assert.ok(!!domain.parent === false) + assert.ok(domain.depth === 0); } else { - // Because we no longer delete from the subgraph store on revoke, the domain is always present + // Because we do not delete from the subgraph store on revoke, the domain is always present // even if `isRevoked` is true - expect(!!domain.parent).to.be.true; - expect(domain.parentHash).to.not.equal(ZeroHash); - expect(domain.depth > 0); + assert.ok(!!domain.parent); + assert.notEqual(domain.parentHash, ZeroHash); + assert.ok(domain.depth > 0); } } \ No newline at end of file diff --git a/src/utils/migration/zns-contract-data.ts b/src/utils/migration/zns-contract-data.ts index 84671bc4d..2521f68b3 100644 --- a/src/utils/migration/zns-contract-data.ts +++ b/src/utils/migration/zns-contract-data.ts @@ -1,5 +1,5 @@ import { znsNames } from "../../deploy/missions/contracts/names"; -import { IZNSContracts } from "../../deploy/campaign/types"; +import { IZNSContracts } from "../../../test/helpers/types"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { getZNSFromDB } from "./database"; import { diff --git a/test/helpers/types.ts b/test/helpers/types.ts index 1d7dfd997..948c8f042 100644 --- a/test/helpers/types.ts +++ b/test/helpers/types.ts @@ -98,7 +98,7 @@ export interface IZNSContracts { rootRegistrar : ZNSRootRegistrar; fixedPricer : ZNSFixedPricer; subRegistrar : ZNSSubRegistrar; - zeroVaultAddress : string; + zeroVaultAddress ?: string; } export interface DeployZNSParams { From 321b8f36f603512733ee87b2dfdd8202287f308f Mon Sep 17 00:00:00 2001 From: MichaelKorchagin Date: Wed, 16 Apr 2025 19:10:32 -0700 Subject: [PATCH 061/114] Added withdrawal function for the Treasury contract. Added `onlyGovernor` modifier for the AC. Tested with pauses. --- contracts/access/AAccessControlled.sol | 8 ++ .../treasury/ZNSTreasuryPausable.sol | 23 +++++- hardhat.config.ts | 2 +- test/Pausable.upgrade.test.ts | 78 +++++++++++++++++++ 4 files changed, 108 insertions(+), 3 deletions(-) diff --git a/contracts/access/AAccessControlled.sol b/contracts/access/AAccessControlled.sol index 19de2287c..e510cecdf 100644 --- a/contracts/access/AAccessControlled.sol +++ b/contracts/access/AAccessControlled.sol @@ -31,6 +31,14 @@ abstract contract AAccessControlled { _; } + /** + * @notice Modifier to make a function callable only when caller is an Governor. + */ + modifier onlyGovernor() { + accessController.checkGovernor(msg.sender); + _; + } + /** * @notice Revert if `msg.sender` is not the `ZNSRootRegistrar.sol` contract * or an address holding REGISTRAR_ROLE. diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol index fb16165bc..73b24ab39 100644 --- a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -293,10 +293,29 @@ contract ZNSTreasuryPausable is AAccessControlled, ARegistryWiredPausable, UUPSU _setRegistry(registry_); } + /** + * @notice Withdraws all staked tokens from the contract to the specified address. + * Can only be called by the GOVERNOR_ROLE. + * @param token The address of the token to withdraw (ERC20). + * @param to The address to withdraw the tokens to. + */ + function withdrawStaked ( + address token, + address to + ) external onlyGovernor { + require(token != address(0), "ZNSTreasury: token passed as 0x0 address"); + require(to != address(0), "ZNSTreasury: to passed as 0x0 address"); + + IERC20(token).safeTransfer( + to, + IERC20(token).balanceOf(address(this)) + ); + } + /** * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. */ - function pause() external whenNotPaused onlyAdmin { + function pause() external override whenNotPaused onlyAdmin { _paused = true; emit Paused(msg.sender); } @@ -304,7 +323,7 @@ contract ZNSTreasuryPausable is AAccessControlled, ARegistryWiredPausable, UUPSU /** * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. */ - function unpause() external whenPaused onlyAdmin { + function unpause() external override whenPaused onlyAdmin { _paused = false; emit Unpaused(msg.sender); } diff --git a/hardhat.config.ts b/hardhat.config.ts index e251d8658..0b1911c42 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -37,7 +37,7 @@ subtask(TASK_TEST_RUN_MOCHA_TESTS) // keep it commented out and uncomment when using DevNet // locally. // !!! Uncomment this when using Tenderly !!! -tenderly.setup({ automaticVerifications: false }); +// tenderly.setup({ automaticVerifications: false }); const config : HardhatUserConfig = { solidity: { diff --git a/test/Pausable.upgrade.test.ts b/test/Pausable.upgrade.test.ts index 24e4c3caf..f006f7759 100644 --- a/test/Pausable.upgrade.test.ts +++ b/test/Pausable.upgrade.test.ts @@ -10,11 +10,15 @@ import { znsNames } from "../src/deploy/missions/contracts/names"; import { expect } from "chai"; import { AccessType, + ADMIN_ROLE, curvePriceConfigEmpty, DEFAULT_PRICE_CONFIG, distrConfigEmpty, + getAccessRevertMsg, + GOVERNOR_ROLE, paymentConfigEmpty, PaymentType, + REGISTRAR_ROLE, } from "./helpers"; import { registerDomainPath } from "./helpers/flows/registration"; import { IDomainConfigForTest, IFixedPriceConfig, ZNSContract } from "./helpers/types"; @@ -78,6 +82,9 @@ describe("ZNS Upgrade and Pause Test", () => { lvl6SubOwner, ] = await hre.ethers.getSigners(); + // to make sure the test runs on any machine + process.env.MOCK_MEOW_TOKEN = "true"; + const config : IDeployCampaignConfig = await getConfig({ deployer, zeroVaultAddress: hre.network.name !== "hardhat" @@ -750,4 +757,75 @@ describe("ZNS Upgrade and Pause Test", () => { } ); }); + + // TODO: pause the system and withdraw funds from treasury (znsUpgraded.treasury) + describe("#withdrawStaked()", () => { + before(async () => { + await znsUpgraded.treasury.connect(deployer).unpause(); + }); + + after(async () => { + await znsUpgraded.treasury.connect(deployer).pause(); + }); + + it("Should withdraw the correct amount", async () => { + await zns.accessController.connect(deployer).grantRole( + REGISTRAR_ROLE, + deployer.address + ); + const stakeAmt = ethers.parseEther("1"); + const protocolFee = ethers.parseEther("3"); + + const contractBalanceBeforeStake = await zns.meowToken.balanceOf(zns.treasury.target); + + await znsUpgraded.treasury.connect(deployer).stakeForDomain( + ethers.ZeroHash, + domainHashes[0], + lvl6SubOwner.address, + stakeAmt, + BigInt(0), + protocolFee + ); + + const { + token, + } = await znsUpgraded.treasury.stakedForDomain(domainHashes[0]); + + const balanceBeforeWithdraw = await zns.meowToken.balanceOf(lvl6SubOwner.address); + + await znsUpgraded.treasury.connect(deployer).withdrawStaked( + token, + lvl6SubOwner.address + ); + + const balanceAfterWithdraw = await zns.meowToken.balanceOf(lvl6SubOwner.address); + + expect( + balanceAfterWithdraw - balanceBeforeWithdraw + ).to.eq( + contractBalanceBeforeStake + stakeAmt + ); + + expect( + token + ).to.eq( + await zns.meowToken.getAddress() + ); + }); + + it("Should revert when called by NON Governor", async () => { + const { + paymentConfig, + } = domainConfigs[5].fullConfig; + + await expect( + znsUpgraded.treasury.connect(lvl5SubOwner).withdrawStaked( + paymentConfig.token, + lvl5SubOwner.address + ) + ).to.be.revertedWith( + getAccessRevertMsg(lvl5SubOwner.address, GOVERNOR_ROLE) + ); + }); + }); }); From 5828a6bb2c55f396b4b97976b26fd570158c26d4 Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 17 Apr 2025 12:34:27 -0400 Subject: [PATCH 062/114] run script against all users and additional validation to identify split ownership in domains --- src/utils/migration/01_validation.ts | 91 ++---------------- src/utils/migration/subgraph/client.ts | 4 - src/utils/migration/subgraph/index.ts | 1 - src/utils/migration/subgraph/queries.ts | 7 -- src/utils/migration/types.ts | 13 +-- src/utils/migration/validate.ts | 119 +++++++++++++----------- 6 files changed, 79 insertions(+), 156 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 31af4836d..91a84020d 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,10 +1,9 @@ import * as hre from "hardhat"; import { getUsersAndDomains } from "./subgraph"; -import { Domain } from "./types"; +import { Domain, InvalidDomain, User, ValidatedUser } from "./types"; import { getDBAdapter } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate" -import assert from "assert"; import { ZeroAddress } from "ethers"; @@ -12,10 +11,6 @@ import { ZeroAddress } from "ethers"; const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); - type User = { id: string, domains: Domain[] }; - type InvalidDomain = { message: string, domain: Domain }; - type ValidatedUser = { address: string, validDomains: Domain[], invalidDomains: InvalidDomain[] }; - const users = await getUsersAndDomains() as Array; console.log(`Found ${users.length} users`); @@ -24,23 +19,13 @@ const main = async () => { const validatedUsers : Array = []; // for each user, iterate list of domains - for(let i = 0; i < users.slice(0,5).length; i++) { - const user = users[i]; - + for(let [index, user] of users.entries()) { const validDomains : Array = [] const invalidDomains : Array = []; - if (user.id != ZeroAddress) { - // As extra validation, be sure the on-chain domain token balance of a user - // matches the number of domains given by the subgraph - const domainBalance = await zns.domainToken.balanceOf(user.id); - assert.equal(domainBalance, user.domains.length); - } - - for (let j = 0; j < user.domains.length; j++) { - const domain = user.domains[j]; + for (const domain of user.domains) { try { - await validateDomain(domain, zns, false); + await validateDomain(domain, zns); validDomains.push(domain); } catch (e) { // For debugging we keep invalid domains rather than throw @@ -48,23 +33,16 @@ const main = async () => { } } - if (invalidDomains.length === 0 && validDomains.length > 0) { + // Skip 0x0 address + if (user.id != ZeroAddress) { validatedUsers.push({ address: user.id, validDomains, invalidDomains }); - } else { - console.log(`Empty or Invalid Domains found for user ${user.id}`); - console.log(invalidDomains.length); - console.log(validDomains.length); } - // } else { - // // Shouldnt reach this - // // should skip if empty - // } - console.log(`Users Processed: ${i + 1}`); + console.log(`Users Processed: ${index + 1}`); } const dbName = "zns-domain-migration"; @@ -82,61 +60,6 @@ const main = async () => { process.exit(0); }; -// export const validateDomains = async ( -// admin : SignerWithAddress, -// users : Array // make user type -// ) : Promise>> => { -// const start = Date.now(); - -// // Get ZNS contracts from the MongoDB instance to validate against -// const zns = await getZNS(admin); - -// const invalidDomains : Array = []; - -// // array instead? -// const validatedUsers : Map> = new Map(); - -// const validatedUsers2 = []; -// // TODO remove subset when testedd -// const subsetUsers = users.slice(0,3); - -// let counter = 0; -// for (let user of subsetUsers) { -// const userDomains = users[counter] - -// console.log(`USERDOMAINS_OBJ: ${userDomains}`); -// const validDomains : Array = [] - - -// if (!userDomains) continue; - -// for (let domain of userDomains) { -// try { -// await validateDomain(domain, zns, false); -// validDomains.push(domain); -// } catch (e) { -// console.log((e as Error).message); -// invalidDomains.push(domain); -// } -// } - -// validatedUsers.set(user, validDomains); -// console.log(`Processed: ${++counter}`); -// } - -// if (invalidDomains.length > 0) { -// fs.writeFileSync("output/invalid-domains.json", JSON.stringify(invalidDomains, undefined, 2)); -// } - -// // There should be no invalid domains for full run -// assert.equal(invalidDomains.length, 0); - -// const end = Date.now(); -// console.log(`Validated all domains in ${end - start}ms`); - -// return validatedUsers; -// } - main().catch(error => { console.error(error); process.exitCode = 1; diff --git a/src/utils/migration/subgraph/client.ts b/src/utils/migration/subgraph/client.ts index 6e6a12f19..2a4020d06 100644 --- a/src/utils/migration/subgraph/client.ts +++ b/src/utils/migration/subgraph/client.ts @@ -1,6 +1,3 @@ -import("@apollo/client"); - -// CommonJS error import { ApolloClient, HttpLink, @@ -8,7 +5,6 @@ import { NormalizedCacheObject } from "@apollo/client"; -// const apollo = import("@apollo/client"); export const createClient = (subgraphUri ?: string) : ApolloClient => { const uri = subgraphUri ? subgraphUri : process.env.SUBGRAPH_URL_DEV; diff --git a/src/utils/migration/subgraph/index.ts b/src/utils/migration/subgraph/index.ts index 6cc345afa..5b09f17bb 100644 --- a/src/utils/migration/subgraph/index.ts +++ b/src/utils/migration/subgraph/index.ts @@ -3,7 +3,6 @@ import * as q from "./queries"; // Grab domain data from the subgraph and validate against what's actually on mainnet - export const getUsersAndDomains = async () => { const first = 1000; let skip = 0; diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index 4734a04a0..3e79e8ccf 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -17,7 +17,6 @@ export const getUsersAndDomains = gql` id } } - isRevoked depth label isWorld @@ -25,16 +24,10 @@ export const getUsersAndDomains = gql` parentHash parent { id - isRevoked label } accessType pricerContract - paymentToken { - id - name - symbol - } paymentType curvePriceConfig { id diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts index a51c4b289..5a2553272 100644 --- a/src/utils/migration/types.ts +++ b/src/utils/migration/types.ts @@ -8,7 +8,6 @@ export interface Domain { domainToken : DomainToken; depth : number; label : string; - isRevoked : boolean; isReclaimable : boolean; reclaimableAddress : string; isWorld : boolean; @@ -18,7 +17,6 @@ export interface Domain { accessType : string; paymentType : string; pricerContract : string; - paymentToken : PaymentToken; curvePriceConfig : CurvePriceConfig; fixedPriceConfig : FixedPriceConfig; subdomainCount : number; @@ -28,11 +26,6 @@ export interface Domain { creationBlock : number; } -interface User { - id : string; - domains : Array; -} - interface CurvePriceConfig { id : string; baseLength : string; @@ -93,4 +86,8 @@ export interface DomainData { export interface RegisteredDomains { domainHashes : Array, txHash : string -} \ No newline at end of file +} + +export type User = { id: string, domains: Domain[] }; +export type InvalidDomain = { message: string, domain: Domain }; +export type ValidatedUser = { address: string, validDomains: Domain[], invalidDomains: InvalidDomain[] }; diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index d8fe2eb9b..6485b3404 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -2,77 +2,92 @@ import { ZeroAddress, ZeroHash } from "ethers"; import { Domain } from "./types"; import { IZNSContracts } from "../../../test/helpers/types"; -import { - AccessType, - PaymentType, -} from "../../../test/helpers"; import assert from "assert"; export const validateDomain = async ( domain : Domain, zns : IZNSContracts, - postMigration : boolean = false ) => { - // Because we no longer delete from the store in the subgraph when a domain is revoked - // we have to first check `isRevoked` before checking the registry - if (!domain.isRevoked) { + // For speed in processing we group promises together + const promises = [ + zns.registry.exists(domain.id), + zns.registry.getDomainOwner(domain.id), + zns.domainToken.ownerOf(domain.tokenId), + zns.addressResolver.resolveDomainAddress(domain.id) + ] - // For speed in processing we group promises together - const promises = [ - zns.registry.exists(domain.id), - zns.registry.getDomainOwner(domain.id), - zns.domainToken.ownerOf(domain.tokenId), - zns.addressResolver.resolveDomainAddress(domain.id) - ] + const [ + exists, + domainOwner, + domainTokenOwner, + domainAddress + ] = await Promise.all(promises) as unknown as [boolean, string, string, string]; - const [ - exists, - domainOwner, - domainTokenOwner, - domainAddress - ] = await Promise.all(promises) as unknown as [boolean, string, string, string]; + assert.ok(!!exists, `Domain ${domain.id} does not exist in the registry`); - assert.ok(!!exists); + // Domain is in reclaimable state + assert.equal( + domain.owner.id.toLowerCase(), + domain.domainToken.owner.id.toLowerCase(), + `Domain ${domain.id} has split ownership. + Token owner: ${domain.domainToken.owner.id.toLowerCase()}, + Domain owner: ${domain.owner.id.toLowerCase()}` + ); - assert.equal( - domainOwner.toLowerCase(), - domain.owner.id.toLowerCase() - ); + assert.equal( + domainOwner.toLowerCase(), + domain.owner.id.toLowerCase(), + `Owner for domain ${domain.id} does not match. + Contract: ${domainOwner.toLowerCase()}, + Subgraph: ${domain.owner.id.toLowerCase()}` + ); - assert.equal( - domainTokenOwner.toLowerCase(), - domain.domainToken.owner.id.toLowerCase() - ); + assert.equal( + domainTokenOwner.toLowerCase(), + domain.domainToken.owner.id.toLowerCase(), + `Owner of domainToken for domain ${domain.id} does not match. + Contract: ${domainTokenOwner.toLowerCase()}, + Subgraph: ${domain.domainToken.owner.id.toLowerCase()}` + ); - assert.equal( - domainAddress.toLowerCase(), - domain.address.toLowerCase() - ); - } + assert.equal( + domainAddress.toLowerCase(), + domain.address.toLowerCase(), + `Domain ${domain.id} has differing domain addresses: + Contract: ${domainAddress.toLowerCase()} + Subgraph: ${domain.address.toLowerCase()}` + ); const distrConfig = await zns.subRegistrar.distrConfigs(domain.id); - // Props not yet set in the subgraph return null, but in the contract it will - // be 0 value, so we must mediate here - if (postMigration) { - assert.equal(distrConfig.accessType, AccessType.OPEN); - assert.equal(distrConfig.paymentType, PaymentType.DIRECT); - assert.equal(distrConfig.pricerContract.toLowerCase(), ZeroAddress); - } else { - assert.equal(distrConfig.accessType, domain.accessType ?? 0n); - assert.equal(distrConfig.paymentType, domain.paymentType ?? 0n); - assert.equal(distrConfig.pricerContract.toLowerCase(), domain.pricerContract?.toLowerCase() ?? ZeroAddress); - } + assert.equal(distrConfig.accessType, domain.accessType ?? 0n, + `Domain ${domain.id} has different access types. + Contract: ${distrConfig.accessType} + Subgraph: ${domain.accessType ?? 0n} + ` + ); + assert.equal(distrConfig.paymentType, domain.paymentType ?? 0n, + `Domain ${domain.id} has different payment types. + Contract: ${distrConfig.paymentType} + Subgraph: ${domain.paymentType ?? 0n} + ` + ); + assert.equal(distrConfig.pricerContract.toLowerCase(), domain.pricerContract?.toLowerCase() ?? ZeroAddress, + `Domain ${domain.id} has different pricer contracts. + Contract: ${distrConfig.pricerContract.toLowerCase()} + Subgraph: ${domain.pricerContract?.toLowerCase() ?? ZeroAddress} + ` + ); if (domain.isWorld) { - assert.equal(domain.parentHash, ZeroHash); - assert.ok(!!domain.parent === false) - assert.ok(domain.depth === 0); + assert.equal(domain.parentHash, ZeroHash), `Domain ${domain.id} 'isWorld' is true, but has parent hash`; + assert.ok(!!domain.parent === false, `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`) + assert.ok(domain.depth === 0, `Domain ${domain.id} 'isWorld' is true, but 'depth' is not 0`); } else { // Because we do not delete from the subgraph store on revoke, the domain is always present // even if `isRevoked` is true - assert.ok(!!domain.parent); - assert.notEqual(domain.parentHash, ZeroHash); - assert.ok(domain.depth > 0); + assert.ok(!!domain.parent, `Domain ${domain.id} 'isWorld' is false, but 'parent' is undefined`); + assert.notEqual(domain.parentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'parentHash' is 0x0`); + assert.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); } } \ No newline at end of file From d64f0fc06536dd04c6e7c3ac299e6dab70801d47 Mon Sep 17 00:00:00 2001 From: MichaelKorchagin Date: Fri, 18 Apr 2025 20:45:38 -0700 Subject: [PATCH 063/114] Added function which use governorn address to withdraw funds from upgraded Treasury contract. Added #getUpgradedVersion() for Mongo-Adapter. Tested. --- src/deploy/db/mongo-adapter/mongo-adapter.ts | 10 ++++ src/scripts/witdrawStaked.ts | 62 ++++++++++++++++++++ test/Pausable.upgrade.test.ts | 41 +++++++++++-- 3 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 src/scripts/witdrawStaked.ts diff --git a/src/deploy/db/mongo-adapter/mongo-adapter.ts b/src/deploy/db/mongo-adapter/mongo-adapter.ts index 18877c11c..e76d7b790 100644 --- a/src/deploy/db/mongo-adapter/mongo-adapter.ts +++ b/src/deploy/db/mongo-adapter/mongo-adapter.ts @@ -242,6 +242,16 @@ export class MongoDBAdapter { return v; } + async getUpgradedVersion () : Promise { + const v = await this.versions.findOne({ + type: VERSION_TYPES.upgraded, + }); + + if (!v) return null; + + return v; + } + async getLatestVersion () : Promise { const v = await this.getTempVersion(); diff --git a/src/scripts/witdrawStaked.ts b/src/scripts/witdrawStaked.ts new file mode 100644 index 000000000..ce9670e43 --- /dev/null +++ b/src/scripts/witdrawStaked.ts @@ -0,0 +1,62 @@ +import { ethers } from "hardhat"; +import { IDBVersion } from "../deploy/db/mongo-adapter/types"; +import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; + + +export const withdrawStakedByGovernon = async ({ + token, + to, + version, +} : { + token : string; + to ?: string; + version ?: IDBVersion | string; +}) => { + const [ governor ] = await ethers.getSigners(); + + if (!token) { + throw new Error("Token address is undefined"); + } + + const dbAdapter = await getMongoAdapter(); + + if (!version) { + const upgradedVersion = await dbAdapter.getUpgradedVersion(); + version = upgradedVersion ?? undefined; + } + + // Проверяем, что version имеет тип IDBVersion + if (typeof version === "string") { + throw new Error("Invalid version type: expected IDBVersion, got string"); + } + + if (!version) { + throw new Error("Version is undefined"); + } + + const ts = await dbAdapter.getContract("ZNSTreasury", version.dbVersion); + if (!ts) + throw new Error("ZNSTreasury contract not found for the specified/upgraded version"); + const treasury = new ethers.Contract(ts.address, ts.abi, governor); + + const toAddress = () => { + const recipient = to || process.env.SAFE_ADDRESS; + + if (!recipient) + throw new Error("Recipient address is undefined"); + + return recipient; + }; + + const tx = await treasury.withdrawStaked( + token, + toAddress() + ); + + if ((await ethers.provider.getNetwork()).name !== "hardhat") + await tx.wait( + process.env.CONFIRMATIONS_N ? Number(process.env.CONFIRMATIONS_N) : 2 + ); + + return tx; +}; \ No newline at end of file diff --git a/test/Pausable.upgrade.test.ts b/test/Pausable.upgrade.test.ts index f006f7759..979527731 100644 --- a/test/Pausable.upgrade.test.ts +++ b/test/Pausable.upgrade.test.ts @@ -10,7 +10,6 @@ import { znsNames } from "../src/deploy/missions/contracts/names"; import { expect } from "chai"; import { AccessType, - ADMIN_ROLE, curvePriceConfigEmpty, DEFAULT_PRICE_CONFIG, distrConfigEmpty, @@ -32,6 +31,7 @@ import { getLogger } from "../src/deploy/logger/create-logger"; import { updateDbAndVerifyAll } from "../src/upgrade/db"; import { VERSION_TYPES } from "../src/deploy/db/mongo-adapter/constants"; import { getGitTag } from "../src/utils/git-tag/get-tag"; +import { withdrawStakedByGovernon } from "../src/scripts/witdrawStaked"; describe("ZNS Upgrade and Pause Test", () => { @@ -758,7 +758,6 @@ describe("ZNS Upgrade and Pause Test", () => { ); }); - // TODO: pause the system and withdraw funds from treasury (znsUpgraded.treasury) describe("#withdrawStaked()", () => { before(async () => { await znsUpgraded.treasury.connect(deployer).unpause(); @@ -768,7 +767,7 @@ describe("ZNS Upgrade and Pause Test", () => { await znsUpgraded.treasury.connect(deployer).pause(); }); - it("Should withdraw the correct amount", async () => { + it("should withdraw the correct amount", async () => { await zns.accessController.connect(deployer).grantRole( REGISTRAR_ROLE, deployer.address @@ -813,7 +812,7 @@ describe("ZNS Upgrade and Pause Test", () => { ); }); - it("Should revert when called by NON Governor", async () => { + it("should revert when called by NON Governor", async () => { const { paymentConfig, } = domainConfigs[5].fullConfig; @@ -827,5 +826,39 @@ describe("ZNS Upgrade and Pause Test", () => { getAccessRevertMsg(lvl5SubOwner.address, GOVERNOR_ROLE) ); }); + + it("should withdraw funds from upgraded treasury using #withdrawStakedByGovernon()", async () => { + const stakeAmt = ethers.parseEther("1000"); + + await zns.meowToken.connect(lvl5SubOwner).approve( + znsUpgraded.treasury.target, + stakeAmt + ); + + // the deployer already has the `REGISTRAR_ROLE` + await znsUpgraded.treasury.connect(deployer).stakeForDomain( + ethers.ZeroHash, + domainHashes[5], + lvl5SubOwner.address, + stakeAmt, + BigInt(0), + BigInt(0), + ); + + const balanceBeforeWithdraw = await zns.meowToken.balanceOf(lvl5SubOwner.address); + + await withdrawStakedByGovernon({ + token: zns.meowToken.target.toString(), + to: lvl5SubOwner.address, + }); + + const balanceAfterWithdraw = await zns.meowToken.balanceOf(lvl5SubOwner.address); + + expect( + balanceAfterWithdraw - balanceBeforeWithdraw + ).to.eq( + stakeAmt + ); + }); }); }); From 36bca1fb9866ec10aedc65ada03ed83d9a63a503 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 28 May 2025 15:03:04 -0700 Subject: [PATCH 064/114] remove redundant modifier from AAccessControlled and do a direct admin check in Treasury --- contracts/access/AAccessControlled.sol | 8 -------- contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol | 9 ++++++--- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/contracts/access/AAccessControlled.sol b/contracts/access/AAccessControlled.sol index e510cecdf..19de2287c 100644 --- a/contracts/access/AAccessControlled.sol +++ b/contracts/access/AAccessControlled.sol @@ -31,14 +31,6 @@ abstract contract AAccessControlled { _; } - /** - * @notice Modifier to make a function callable only when caller is an Governor. - */ - modifier onlyGovernor() { - accessController.checkGovernor(msg.sender); - _; - } - /** * @notice Revert if `msg.sender` is not the `ZNSRootRegistrar.sol` contract * or an address holding REGISTRAR_ROLE. diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol index 73b24ab39..17f63830f 100644 --- a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -295,14 +295,17 @@ contract ZNSTreasuryPausable is AAccessControlled, ARegistryWiredPausable, UUPSU /** * @notice Withdraws all staked tokens from the contract to the specified address. - * Can only be called by the GOVERNOR_ROLE. + * Can only be called by the GOVERNOR_ROLE. Made specifically for the migration to another chain + * to free the tokens after the system is locked. * @param token The address of the token to withdraw (ERC20). * @param to The address to withdraw the tokens to. */ - function withdrawStaked ( + function withdrawStaked( address token, address to - ) external onlyGovernor { + ) external { + accessController.checkGovernor(msg.sender); + require(token != address(0), "ZNSTreasury: token passed as 0x0 address"); require(to != address(0), "ZNSTreasury: to passed as 0x0 address"); From c65ee2612d1418199fa8a71a9d6e7f184fa1f2df Mon Sep 17 00:00:00 2001 From: James Earle Date: Wed, 28 May 2025 15:52:49 -0700 Subject: [PATCH 065/114] change to way to read and structure database calls in 01 script for validation, registration needs rework after fundamental other changes. --- .gitignore | 1 + src/utils/migration/01_validation.ts | 88 +++++++++------ src/utils/migration/02_registration.ts | 133 ++++++++--------------- src/utils/migration/registration.ts | 1 - src/utils/migration/subgraph/index.ts | 22 ++-- src/utils/migration/subgraph/queries.ts | 57 +++++++++- src/utils/migration/validate.ts | 6 +- src/utils/migration/zns-contract-data.ts | 14 ++- 8 files changed, 181 insertions(+), 141 deletions(-) diff --git a/.gitignore b/.gitignore index bda523d7d..427a08446 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ coverage.json typechain typechain-types .idea +dist # Hardhat files cache diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 91a84020d..50ce68fb3 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,66 +1,84 @@ import * as hre from "hardhat"; -import { getUsersAndDomains } from "./subgraph"; +import { getDomains } from "./subgraph"; import { Domain, InvalidDomain, User, ValidatedUser } from "./types"; import { getDBAdapter } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate" -import { ZeroAddress } from "ethers"; -// For pagination of data in subgraph we use 'first' and 'skip' const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); - const users = await getUsersAndDomains() as Array; - - console.log(`Found ${users.length} users`); + // Keeping as separate collections from the start will help downstream registration + const rootDomainObjects = await getDomains(true); + const subdomainObjects = await getDomains(false); - const zns = await getZNS(migrationAdmin); - const validatedUsers : Array = []; + console.log(`Found ${rootDomainObjects.length + subdomainObjects.length} domains`); + + const env = process.env.ENV_LEVEL; + + if (!env) throw Error("No ENV_LEVEL set in .env file"); - // for each user, iterate list of domains - for(let [index, user] of users.entries()) { - const validDomains : Array = [] - const invalidDomains : Array = []; + const zns = await getZNS(migrationAdmin, env); - for (const domain of user.domains) { + const validRoots : Array = []; + const validSubs : Array = []; + const invalidDomains : Array = []; + + // Doing this creates strong typing and extensibility that allows + // the below `insertMany` calls to add properties to the object for `_id` + const roots = rootDomainObjects.map((d) => { return d as Domain; }); + const subs = subdomainObjects.map((d) => { return d as Domain; }); + + // Can iterate all at once for simplicity + let index = 0; + for(let domain of [...roots, ...subs]) { try { await validateDomain(domain, zns); - validDomains.push(domain); + + if (domain.isWorld) { + validRoots.push({ ...domain } as Domain); + } else { + validSubs.push({ ...domain } as Domain); + } } catch (e) { - // For debugging we keep invalid domains rather than throw + // For debugging we keep invalid domains rather than throw errors invalidDomains.push({ message: (e as Error).message, domain: domain }); } - } - - // Skip 0x0 address - if (user.id != ZeroAddress) { - validatedUsers.push({ - address: user.id, - validDomains, - invalidDomains - }); - } - - console.log(`Users Processed: ${index + 1}`); + + console.log(`Processed ${++index} domains`); } - const dbName = "zns-domain-migration"; - const uri = process.env.MONGO_DB_URI_WRITE; + // Connect to database collection and write user domain data to DB + const dbName = process.env.MONGO_DB_NAME_WRITE; + if (!dbName) throw Error("No DB name given"); - if (!uri) throw Error("No connection string provided"); + const uri = process.env.MONGO_DB_URI_WRITE; + if (!uri) throw Error("No connection string given"); let client = (await getDBAdapter(uri)).db(dbName); + const rootCollName = process.env.MONGO_DB_ROOT_COLL_NAME || "root-domains"; + const subCollName = process.env.MONGO_DB_SUB_COLL_NAME || "subdomains"; + // To avoid duplicate data, we clear the DB before any inserts - await client.dropCollection("user-domains"); - await client.collection("user-domains").insertMany(validatedUsers); + await client.dropCollection(rootCollName); + await client.collection(rootCollName).insertMany(validRoots); + + await client.dropCollection(subCollName); + await client.collection(subCollName).insertMany(validSubs); - // HH not exiting process properly, exit manually - process.exit(0); + // Domains that have split ownership will be considered invalid domains + if (invalidDomains.length > 0) { + const invalidCollName = process.env.MONGO_DB_INVALID_COLL_NAME || "invalid-domains"; + await client.dropCollection(invalidCollName); + await client.collection(invalidCollName).insertMany(invalidDomains); + } }; -main().catch(error => { +main() + .then(() => process.exit(0)) + .catch(error => { console.error(error); process.exitCode = 1; }); \ No newline at end of file diff --git a/src/utils/migration/02_registration.ts b/src/utils/migration/02_registration.ts index 5c29375d0..fe15ae201 100644 --- a/src/utils/migration/02_registration.ts +++ b/src/utils/migration/02_registration.ts @@ -6,105 +6,66 @@ import { postMigrationValidation, registerDomainsBulk } from "./registration"; import { getZNS } from "./zns-contract-data"; import { ROOTS_FILENAME, SUBS_FILENAME } from "./constants"; import { IZNSContracts } from "../../deploy/campaign/types"; +import { getDBAdapter } from "./database"; // Script #2 to be run AFTER validation of the domains with subgraph const main = async () => { const [ migrationAdmin, governor, admin ] = await hre.ethers.getSigners(); - // Read domain data from file output of 01_validate.ts - const rootDomains = JSON.parse(fs.readFileSync(ROOTS_FILENAME, { - encoding: "utf8" - })) as Array; - - const subdomains = JSON.parse(fs.readFileSync(SUBS_FILENAME, { - encoding: "utf8" - })) as Array; + // Overall flow will be: + // connect to DB + // read all roots from mongodb + // while there are unregistered root domains: + // register a batch + + // read all subs with depth 1 from mongodb + // while there are unregistered subdomains: + // register a batch + // read all subs with depth 2 from mongodb + // while there are unregistered subdomains: + // register a batch + // read all subs with depth 3 from mongodb + // while there are unregistered subdomains: + // register a batch + + // During above we will pack transactions with to always have 50 domains + // so if only 45 root domains remain at the end, we will also send the first 5 depth 1 subdomains + + // Steps to register a batch will mean using the Safe REST API to create a transaction + // for the owning safe that calls `registerRootDomainBulk` or `registerSubdomainBulk` + // Then we will wait for the transaction to be executed + // Technically we could also sign each tx and execute this way let zns : IZNSContracts; - if (hre.network.name === "hardhat") { - // Reset the network to be sure we aren't forking - await hre.network.provider.request({ - method: "hardhat_reset", - params: [], - }); - - const params = { - deployer: migrationAdmin, - governorAddresses: [migrationAdmin.address, governor.address], - adminAddresses: [migrationAdmin.address, admin.address], - }; - - // Recreate the domain tree with local ZNS - zns = await deployZNS(params); - } else if (hre.network.name === "sepolia") { - // Get instance of ZNS from DB - zns = await getZNS(migrationAdmin); - } else { - // TODO setup when zchain is deployed - throw new Error(`Network ${hre.network.name} not supported`); - } - - await zns.meowToken.connect(migrationAdmin).approve(await zns.treasury.getAddress(), hre.ethers.MaxUint256); - await zns.meowToken.connect(migrationAdmin).mint(migrationAdmin.address, hre.ethers.parseEther("8000000")); - - console.log( - `Balance of admin before: ${await zns.meowToken.balanceOf(migrationAdmin.address)}` - ); - - // Give approval to the RootRegistrar and SubRegistrar to transfer on behalf of the migration admin - await zns.domainToken.connect(migrationAdmin).setApprovalForAll(await zns.rootRegistrar.getAddress(), true); - await zns.domainToken.connect(migrationAdmin).setApprovalForAll(await zns.subRegistrar.getAddress(), true); + const env = process.env.ENV_LEVEL; + + if (!env) throw Error("No ENV_LEVEL set in .env file"); + + // Get instance of ZNS from DB + zns = await getZNS(migrationAdmin, env); + + // Connect to database collection and write user domain data to DB + const dbName = process.env.MONGO_DB_NAME_WRITE; + if (!dbName) throw Error("No DB name given"); + + const uri = process.env.MONGO_DB_URI_WRITE; + if (!uri) throw Error("No connection string given"); + + let client = (await getDBAdapter(uri)).db(dbName); + + const rootCollName = process.env.MONGO_DB_ROOT_COLL_NAME || "root-domains"; + + // Get all documents from collection + const domains = await client.collection(rootCollName).find().toArray(); + + console.log(domains.length); const startTime = Date.now(); // How many domains we will register in a single transaction const sliceSize = 50; - - // TODO because we no longer use the treasury at all we might be able to switch back to - // having owners just be the registrant for the domain, not the user then do a transfer - // would make it cheaper and faster, but on zchain gas probably won't matter - const start = 0; - - console.log(`Registering ${rootDomains.length - start} root domains with slice size ${sliceSize}`); - const registeredDomains = await registerDomainsBulk( - migrationAdmin, - rootDomains, - zns, - sliceSize, - start - ); - - console.log(`Registering ${subdomains.length} subdomains with slice size ${sliceSize}`); - const registeredSubdomains = await registerDomainsBulk( - migrationAdmin, - subdomains, - zns, - sliceSize, - 0 - ); - - // // ms -> s -> min - const totalTime = (Date.now() - startTime) / 1000 / 60; - console.log(`Registered ${rootDomains.length + subdomains.length} groups of domains in ${totalTime} minutes`); - console.log("Done") - - console.log(`txhash: ${registeredDomains[0].txHash}`); - console.log(`exists: ${await zns.registry.exists(registeredDomains[0].domainHashes[0])}`); - - console.log( - `Balance of admin after: ${await zns.meowToken.balanceOf(migrationAdmin.address)}` - ); - - // console.log("TEMP just doing postmigration validation for now to test") - console.log("Confirming with post-migration validation..."); - const fullDomains = rootDomains.concat(subdomains); - // await postMigrationValidation( - // zns, - // fullDomains - // ) - - // Manually exit here, HH runner doesn't exit properly + process.exit(0); }; diff --git a/src/utils/migration/registration.ts b/src/utils/migration/registration.ts index 78dab6d8e..12be279e1 100644 --- a/src/utils/migration/registration.ts +++ b/src/utils/migration/registration.ts @@ -3,7 +3,6 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { RegisteredDomains, Domain } from "./types"; import { IZNSContracts } from "../../deploy/campaign/types"; import { paymentConfigEmpty } from "../../../test/helpers"; -import { IZNSContractsLocal } from "../../../test/helpers/types"; import { DOMAIN_REGISTERED_TOPIC_SEPOLIA } from "./constants"; import { expect } from "chai"; import { validateDomain } from "./validate"; diff --git a/src/utils/migration/subgraph/index.ts b/src/utils/migration/subgraph/index.ts index 5b09f17bb..7f388842d 100644 --- a/src/utils/migration/subgraph/index.ts +++ b/src/utils/migration/subgraph/index.ts @@ -1,9 +1,8 @@ import { createClient } from "./client"; import * as q from "./queries"; - // Grab domain data from the subgraph and validate against what's actually on mainnet -export const getUsersAndDomains = async () => { +export const getDomains = async (isWorld : boolean) => { const first = 1000; let skip = 0; @@ -11,43 +10,44 @@ export const getUsersAndDomains = async () => { // First get all worlds let result = await client.query({ - query: q.getUsersAndDomains, + query: q.getDomains, variables: { first, skip, + isWorld }, }); if (result.error) throw Error(`Error in graph query: ${result.error}`); - const users = []; + const domains = []; // We do this to collect ALL domains in a single array - while (result.data.users.length > 0) { - + while (result.data.domains.length > 0) { // For each user, get every domain - for (const user of result.data.users) { + for (const domain of result.data.domains) { // user data from subgraph already has user and all domains // so just return this - users.push(user); + domains.push(domain); } // Get next batch of domains skip += 1000; - // Refresh client + // Refresh client each iteration client = await createClient(); result = await client.query({ - query: q.getUsersAndDomains, + query: q.getDomains, variables: { first, skip, + isWorld }, }); } - return users; + return domains; }; diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index 3e79e8ccf..64f4a8bcd 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -1,7 +1,7 @@ import { gql } from "@apollo/client/core"; export const getUsersAndDomains = gql` - query Domains($first: Int!, $skip: Int!) { + query UserDomains($first: Int!, $skip: Int!) { users(first: $first, skip: $skip) { id domains { @@ -48,3 +48,58 @@ export const getUsersAndDomains = gql` } } `; + +export const getDomains = gql` + query Domains($first: Int!, $skip: Int!, $isWorld: Boolean!) { + domains( + first: $first, + skip: $skip + where: { isWorld: $isWorld } + ) { + id + minter { + id + } + owner { + id + } + domainToken { + owner { + id + } + } + depth + label + isWorld + address + parentHash + amountPaidStake + amountPaidDirect + parent { + id + label + depth + isWorld + } + accessType + pricerContract + paymentType + curvePriceConfig { + id + } + fixedPriceConfig { + id + } + subdomainCount + address + tokenId + tokenURI + treasury { + id + beneficiaryAddress + } + creationBlock + creationTimestamp + } + } +`; diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index 6485b3404..5e57db921 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -10,20 +10,16 @@ export const validateDomain = async ( ) => { // For speed in processing we group promises together const promises = [ - zns.registry.exists(domain.id), zns.registry.getDomainOwner(domain.id), zns.domainToken.ownerOf(domain.tokenId), zns.addressResolver.resolveDomainAddress(domain.id) ] const [ - exists, domainOwner, domainTokenOwner, domainAddress - ] = await Promise.all(promises) as unknown as [boolean, string, string, string]; - - assert.ok(!!exists, `Domain ${domain.id} does not exist in the registry`); + ] = await Promise.all(promises) as unknown as [string, string, string]; // Domain is in reclaimable state assert.equal( diff --git a/src/utils/migration/zns-contract-data.ts b/src/utils/migration/zns-contract-data.ts index 2521f68b3..3c4fe5cde 100644 --- a/src/utils/migration/zns-contract-data.ts +++ b/src/utils/migration/zns-contract-data.ts @@ -17,16 +17,26 @@ import { let znsCache : IZNSContracts | null = null; -export const getZNS = async (signer : SignerWithAddress) => { +export const getZNS = async ( + signer : SignerWithAddress, + env : string +) => { if (!znsCache || Object.values(znsCache).length < 10) { const zns = await getZNSFromDB(); + const meowTokenAddress = zns.find((contract) => { + if (env === "prod") { + return contract.name === znsNames.meowToken.contract; + } else { + contract.name === znsNames.meowToken.contractMock + } + }); + // Get each contract and manually connect to a factory. // Using `getContractFactory()` returns an incorrect type of factory here const acAddress = zns.find((contract) => contract.name === znsNames.accessController.contract); const regAddress = zns.find((contract) => contract.name === znsNames.registry.contract); const domainTokenAddress = zns.find((contract) => contract.name === znsNames.domainToken.contract); - const meowTokenAddress = zns.find((contract) => contract.name === znsNames.meowToken.contractMock); // contract on prod, contractMock on testnet const addressResolverAddress = zns.find((contract) => contract.name === znsNames.addressResolver.contract); const curvePricerAddress = zns.find((contract) => contract.name === znsNames.curvePricer.contract); const treasuryAddress = zns.find((contract) => contract.name === znsNames.treasury.contract); From b19b75ca854984ebecfa44e413db817454e3a129 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 28 May 2025 16:46:23 -0700 Subject: [PATCH 066/114] rename and improve the withdrawal script --- src/scripts/witdrawStaked.ts | 62 --------------------------------- src/scripts/withdraw-staked.ts | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 62 deletions(-) delete mode 100644 src/scripts/witdrawStaked.ts create mode 100644 src/scripts/withdraw-staked.ts diff --git a/src/scripts/witdrawStaked.ts b/src/scripts/witdrawStaked.ts deleted file mode 100644 index ce9670e43..000000000 --- a/src/scripts/witdrawStaked.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ethers } from "hardhat"; -import { IDBVersion } from "../deploy/db/mongo-adapter/types"; -import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; - - -export const withdrawStakedByGovernon = async ({ - token, - to, - version, -} : { - token : string; - to ?: string; - version ?: IDBVersion | string; -}) => { - const [ governor ] = await ethers.getSigners(); - - if (!token) { - throw new Error("Token address is undefined"); - } - - const dbAdapter = await getMongoAdapter(); - - if (!version) { - const upgradedVersion = await dbAdapter.getUpgradedVersion(); - version = upgradedVersion ?? undefined; - } - - // Проверяем, что version имеет тип IDBVersion - if (typeof version === "string") { - throw new Error("Invalid version type: expected IDBVersion, got string"); - } - - if (!version) { - throw new Error("Version is undefined"); - } - - const ts = await dbAdapter.getContract("ZNSTreasury", version.dbVersion); - if (!ts) - throw new Error("ZNSTreasury contract not found for the specified/upgraded version"); - const treasury = new ethers.Contract(ts.address, ts.abi, governor); - - const toAddress = () => { - const recipient = to || process.env.SAFE_ADDRESS; - - if (!recipient) - throw new Error("Recipient address is undefined"); - - return recipient; - }; - - const tx = await treasury.withdrawStaked( - token, - toAddress() - ); - - if ((await ethers.provider.getNetwork()).name !== "hardhat") - await tx.wait( - process.env.CONFIRMATIONS_N ? Number(process.env.CONFIRMATIONS_N) : 2 - ); - - return tx; -}; \ No newline at end of file diff --git a/src/scripts/withdraw-staked.ts b/src/scripts/withdraw-staked.ts new file mode 100644 index 000000000..99673e362 --- /dev/null +++ b/src/scripts/withdraw-staked.ts @@ -0,0 +1,63 @@ +import * as hre from "hardhat"; +import { IDBVersion } from "../deploy/db/mongo-adapter/types"; +import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; +import { znsNames } from "../deploy/missions/contracts/names"; +import { ZNSTreasuryPausable__factory } from "../../typechain/factories/contracts/zns-pausable/treasury"; +import { ZNSTreasuryPausable } from "../../typechain/contracts/zns-pausable/treasury"; + + +export const withdrawStakedByGovernor = async ({ + token, + to, + version, +} : { + token : string; + to ?: string; + version ?: IDBVersion | null; +}) => { + const [ governor ] = await hre.ethers.getSigners(); + + if (!token) { + throw new Error("Token address is undefined"); + } + + const dbAdapter = await getMongoAdapter(); + + if (!version) { + version = await dbAdapter.getUpgradedVersion(); + } + + if (!version) { + throw new Error("Version is undefined"); + } + + const contractName = znsNames.treasury.contract; + + const dbContr = await dbAdapter.getContract( + contractName, + // version.dbVersion, + ); + + if (!dbContr) + throw new Error(`${contractName} contract not found for the specified/upgraded version`); + + const treasury = new ZNSTreasuryPausable__factory(governor) + .attach(dbContr.address) as ZNSTreasuryPausable; + + const recipient = to || process.env.TREASURY_WITHDRAW_RECIPIENT; + + if (!recipient) + throw new Error("Recipient address is undefined"); + + const tx = await treasury.withdrawStaked( + token, + recipient, + ); + + if (hre.network.name !== "hardhat") + await tx.wait( + process.env.CONFIRMATIONS_N ? Number(process.env.CONFIRMATIONS_N) : 2 + ); + + return tx; +}; From 8d5efca3196c05896719a1ecfa6eeca5095d9793 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 28 May 2025 16:46:33 -0700 Subject: [PATCH 067/114] extend tests --- test/Pausable.upgrade.test.ts | 39 ++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/test/Pausable.upgrade.test.ts b/test/Pausable.upgrade.test.ts index 979527731..1dfedb833 100644 --- a/test/Pausable.upgrade.test.ts +++ b/test/Pausable.upgrade.test.ts @@ -31,7 +31,7 @@ import { getLogger } from "../src/deploy/logger/create-logger"; import { updateDbAndVerifyAll } from "../src/upgrade/db"; import { VERSION_TYPES } from "../src/deploy/db/mongo-adapter/constants"; import { getGitTag } from "../src/utils/git-tag/get-tag"; -import { withdrawStakedByGovernon } from "../src/scripts/witdrawStaked"; +import { withdrawStakedByGovernor } from "../src/scripts/withdraw-staked"; describe("ZNS Upgrade and Pause Test", () => { @@ -128,11 +128,12 @@ describe("ZNS Upgrade and Pause Test", () => { lvl4SubOwner, lvl5SubOwner, lvl6SubOwner, - ].reduce(async (acc, { address }) => { - await acc; - const tx = await zns.meowToken.mint(address, ethers.parseEther("1000000")); - if (isRealNetwork) await tx.wait(2); - }, Promise.resolve() + ].reduce( + async (acc, { address }) => { + await acc; + const tx = await zns.meowToken.mint(address, ethers.parseEther("1000000")); + if (isRealNetwork) await tx.wait(2); + }, Promise.resolve() ); const tx = await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); if (isRealNetwork) await tx.wait(2); @@ -548,7 +549,7 @@ describe("ZNS Upgrade and Pause Test", () => { expect(contractsVersion).to.equal(getGitTag()); }); - it("should update docs for each upgraded contract properly", async () => { + it("should update db docs for each upgraded contract properly", async () => { await Object.values(contractNames).reduce( async (acc, { contract, instance }) => { await acc; @@ -772,8 +773,13 @@ describe("ZNS Upgrade and Pause Test", () => { REGISTRAR_ROLE, deployer.address ); - const stakeAmt = ethers.parseEther("1"); + const stakeAmt = ethers.parseEther("1132"); const protocolFee = ethers.parseEther("3"); + // approve + await zns.meowToken.connect(lvl6SubOwner).approve( + znsUpgraded.treasury.target, + stakeAmt + protocolFee + ); const contractBalanceBeforeStake = await zns.meowToken.balanceOf(zns.treasury.target); @@ -798,18 +804,16 @@ describe("ZNS Upgrade and Pause Test", () => { ); const balanceAfterWithdraw = await zns.meowToken.balanceOf(lvl6SubOwner.address); + const contractBalanceAfterWithdraw = await zns.meowToken.balanceOf(zns.treasury.target); expect( balanceAfterWithdraw - balanceBeforeWithdraw ).to.eq( contractBalanceBeforeStake + stakeAmt ); + expect(contractBalanceAfterWithdraw).to.eq(0n); - expect( - token - ).to.eq( - await zns.meowToken.getAddress() - ); + expect(token).to.eq(await zns.meowToken.getAddress()); }); it("should revert when called by NON Governor", async () => { @@ -846,19 +850,26 @@ describe("ZNS Upgrade and Pause Test", () => { ); const balanceBeforeWithdraw = await zns.meowToken.balanceOf(lvl5SubOwner.address); + const contractBalanceBeforeWithdraw = await zns.meowToken.balanceOf(zns.treasury.target); - await withdrawStakedByGovernon({ + await withdrawStakedByGovernor({ token: zns.meowToken.target.toString(), to: lvl5SubOwner.address, }); const balanceAfterWithdraw = await zns.meowToken.balanceOf(lvl5SubOwner.address); + const contractBalanceAfterWithdraw = await zns.meowToken.balanceOf(zns.treasury.target); expect( balanceAfterWithdraw - balanceBeforeWithdraw ).to.eq( stakeAmt ); + expect( + contractBalanceAfterWithdraw + ).to.eq( + contractBalanceBeforeWithdraw - stakeAmt + ); }); }); }); From 08315e1103ce8200448d8600b840388081d13180 Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 29 May 2025 11:44:54 -0700 Subject: [PATCH 068/114] remove invalid marker for split ownership domains, general flow for registration is in comments but will be implemented after larger merge of multiple PRs. Run linter --- src/utils/migration/01_validation.ts | 55 +++++++------ src/utils/migration/02_registration.ts | 6 +- src/utils/migration/constants.ts | 3 + src/utils/migration/database.ts | 10 +-- src/utils/migration/registration.ts | 98 ++++++++++++------------ src/utils/migration/subgraph/client.ts | 10 +-- src/utils/migration/subgraph/index.ts | 4 +- src/utils/migration/types.ts | 10 +-- src/utils/migration/validate.ts | 31 +++----- src/utils/migration/zns-contract-data.ts | 26 +++---- 10 files changed, 121 insertions(+), 132 deletions(-) create mode 100644 src/utils/migration/constants.ts diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 50ce68fb3..f3bbdfd9d 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -3,8 +3,8 @@ import { getDomains } from "./subgraph"; import { Domain, InvalidDomain, User, ValidatedUser } from "./types"; import { getDBAdapter } from "./database"; import { getZNS } from "./zns-contract-data"; -import { validateDomain } from "./validate" - +import { validateDomain } from "./validate"; +import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); @@ -12,7 +12,7 @@ const main = async () => { // Keeping as separate collections from the start will help downstream registration const rootDomainObjects = await getDomains(true); const subdomainObjects = await getDomains(false); - + console.log(`Found ${rootDomainObjects.length + subdomainObjects.length} domains`); const env = process.env.ENV_LEVEL; @@ -27,24 +27,24 @@ const main = async () => { // Doing this creates strong typing and extensibility that allows // the below `insertMany` calls to add properties to the object for `_id` - const roots = rootDomainObjects.map((d) => { return d as Domain; }); - const subs = subdomainObjects.map((d) => { return d as Domain; }); - + const roots = rootDomainObjects.map(d => d as Domain); + const subs = subdomainObjects.map(d => d as Domain); + // Can iterate all at once for simplicity let index = 0; - for(let domain of [...roots, ...subs]) { - try { - await validateDomain(domain, zns); - - if (domain.isWorld) { - validRoots.push({ ...domain } as Domain); - } else { - validSubs.push({ ...domain } as Domain); - } - } catch (e) { - // For debugging we keep invalid domains rather than throw errors - invalidDomains.push({ message: (e as Error).message, domain: domain }); + for(const domain of [...roots, ...subs]) { + try { + await validateDomain(domain, zns); + + if (domain.isWorld) { + validRoots.push({ ...domain } as Domain); + } else { + validSubs.push({ ...domain } as Domain); } + } catch (e) { + // For debugging we keep invalid domains rather than throw errors + invalidDomains.push({ message: (e as Error).message, domain }); + } console.log(`Processed ${++index} domains`); } @@ -56,17 +56,14 @@ const main = async () => { const uri = process.env.MONGO_DB_URI_WRITE; if (!uri) throw Error("No connection string given"); - let client = (await getDBAdapter(uri)).db(dbName); - - const rootCollName = process.env.MONGO_DB_ROOT_COLL_NAME || "root-domains"; - const subCollName = process.env.MONGO_DB_SUB_COLL_NAME || "subdomains"; + const client = (await getDBAdapter(uri)).db(dbName); // To avoid duplicate data, we clear the DB before any inserts - await client.dropCollection(rootCollName); - await client.collection(rootCollName).insertMany(validRoots); + await client.dropCollection(ROOT_COLL_NAME); + await client.collection(ROOT_COLL_NAME).insertMany(validRoots); - await client.dropCollection(subCollName); - await client.collection(subCollName).insertMany(validSubs); + await client.dropCollection(SUB_COLL_NAME); + await client.collection(SUB_COLL_NAME).insertMany(validSubs); // Domains that have split ownership will be considered invalid domains if (invalidDomains.length > 0) { @@ -79,6 +76,6 @@ const main = async () => { main() .then(() => process.exit(0)) .catch(error => { - console.error(error); - process.exitCode = 1; -}); \ No newline at end of file + console.error(error); + process.exitCode = 1; + }); \ No newline at end of file diff --git a/src/utils/migration/02_registration.ts b/src/utils/migration/02_registration.ts index fe15ae201..8f99714d3 100644 --- a/src/utils/migration/02_registration.ts +++ b/src/utils/migration/02_registration.ts @@ -17,7 +17,7 @@ const main = async () => { // read all roots from mongodb // while there are unregistered root domains: // register a batch - + // read all subs with depth 1 from mongodb // while there are unregistered subdomains: // register a batch @@ -52,7 +52,7 @@ const main = async () => { const uri = process.env.MONGO_DB_URI_WRITE; if (!uri) throw Error("No connection string given"); - let client = (await getDBAdapter(uri)).db(dbName); + const client = (await getDBAdapter(uri)).db(dbName); const rootCollName = process.env.MONGO_DB_ROOT_COLL_NAME || "root-domains"; @@ -65,7 +65,7 @@ const main = async () => { // How many domains we will register in a single transaction const sliceSize = 50; - + process.exit(0); }; diff --git a/src/utils/migration/constants.ts b/src/utils/migration/constants.ts new file mode 100644 index 000000000..853aa6962 --- /dev/null +++ b/src/utils/migration/constants.ts @@ -0,0 +1,3 @@ +export const ROOT_COLL_NAME = process.env.MONGO_DB_ROOT_COLL_NAME || "root-domains"; +export const SUB_COLL_NAME = process.env.MONGO_DB_SUB_COLL_NAME || "subdomains"; +export const INVALID_COLL_NAME = process.env.MONGO_DB_INVALID_COLL_NAME || "invalid-domains"; \ No newline at end of file diff --git a/src/utils/migration/database.ts b/src/utils/migration/database.ts index 7e0b2f87d..72cb64903 100644 --- a/src/utils/migration/database.ts +++ b/src/utils/migration/database.ts @@ -4,7 +4,7 @@ export let dbVersion : string; export const getDBAdapter = async ( connectionString : string -): Promise => { +) : Promise => { const mongoClient = new MongoClient( connectionString, { @@ -12,12 +12,12 @@ export const getDBAdapter = async ( version: ServerApiVersion.v1, strict: true, deprecationErrors: true, - } + }, } ); return await mongoClient.connect(); -} +}; export const getZNSFromDB = async () => { let version; @@ -32,7 +32,7 @@ export const getZNSFromDB = async () => { throw new Error("Failed to connect: missing MongoDB URI or version"); } - let dbAdapter = await getDBAdapter(uri); + const dbAdapter = await getDBAdapter(uri); if(!dbName) { throw new Error(`Failed to connect: database "${dbName}" not found`); @@ -40,7 +40,7 @@ export const getZNSFromDB = async () => { const db = await dbAdapter.db(dbName); - let zns = await db.collection("contracts").find( + const zns = await db.collection("contracts").find( { version } ).toArray(); diff --git a/src/utils/migration/registration.ts b/src/utils/migration/registration.ts index 12be279e1..821b28915 100644 --- a/src/utils/migration/registration.ts +++ b/src/utils/migration/registration.ts @@ -37,31 +37,31 @@ export const registerDomainsBulk = async ( const { domainHashes, txHash, retryData } = await registerBase({ regAdmin, zns, - domains: domains.slice(i, i + sliceSize) - }) - + domains: domains.slice(i, i + sliceSize), + }); + if (retryData) { - throw new Error("Error in registering domains") + throw new Error("Error in registering domains"); } - + registeredDomains.push({ domainHashes, txHash }); - + console.log("Registered domains: ", i + sliceSize); } else { console.log(`Skipping already registered domains: ${i} to ${i + sliceSize}}`); } - }; + } - // In the likely case that the list of domains is not divisble by slice size, we + // In the likely case that the list of domains is not divisble by slice size, we // want to make sure we do the last set of domains as well const { domainHashes, txHash, retryData } = await registerBase({ regAdmin, zns, - domains: domains.slice(terminator) // terminator -> end of array - }) + domains: domains.slice(terminator), // terminator -> end of array + }); if (retryData) { - throw new Error("Error in registering domains") + throw new Error("Error in registering domains"); } console.log("Registered additional domains: ", start + domainHashes.length); @@ -74,14 +74,14 @@ export const registerDomainsBulk = async ( export const registerBase = async ({ zns, regAdmin, - domains + domains, } : { zns : IZNSContractsLocal | IZNSContracts; regAdmin : SignerWithAddress; domains : Array; }) => { - const tokenOwners = domains.map((domain) => { + const tokenOwners = domains.map(domain => { if (domain.domainToken.owner.id === hre.ethers.ZeroAddress) { // The ERC721 token has been burned, must mint with the record owner instead // to recreate the tree fully @@ -91,11 +91,11 @@ export const registerBase = async ({ } }); - const distConfigs = domains.map((domain) => { - let config = { + const distConfigs = domains.map(domain => { + const config = { pricerContract: "", paymentType: 0n, - accessType: 1n // Always use `open` access type for migration + accessType: 1n, // Always use `open` access type for migration }; // Get pricer contract @@ -119,20 +119,18 @@ export const registerBase = async ({ // when passing args to the function downstream. This is a workaround. const tokenAddress = await zns.meowToken.getAddress(); - const paymentConfigs = domains.map((domain) => { - return { - beneficiary: !domain.treasury.beneficiaryAddress - ? ZeroAddress - : domain.treasury.beneficiaryAddress, - token: tokenAddress - } - }); + const paymentConfigs = domains.map(domain => ({ + beneficiary: !domain.treasury.beneficiaryAddress + ? ZeroAddress + : domain.treasury.beneficiaryAddress, + token: tokenAddress, + })); - const recordOwners = domains.map((domain) => { return domain.owner.id }); - const parentHashes = domains.map((domain) => { return domain.parentHash }); - const labels = domains.map((domain) => { return domain.label }); - const domainAddresses = domains.map((domain) => {return domain.address }); - const tokenURIs = domains.map((domain) => { return domain.tokenURI }); + const recordOwners = domains.map(domain => domain.owner.id); + const parentHashes = domains.map(domain => domain.parentHash); + const labels = domains.map(domain => domain.label); + const domainAddresses = domains.map(domain => domain.address); + const tokenURIs = domains.map(domain => domain.tokenURI); let tx; @@ -142,14 +140,14 @@ export const registerBase = async ({ if (parentHashes[0] === hre.ethers.ZeroHash) { const bulkMigrationArgs = { - tokenOwners: tokenOwners, - recordOwners: recordOwners, + tokenOwners, + recordOwners, names: labels, - domainAddresses: domainAddresses, - tokenURIs: tokenURIs, + domainAddresses, + tokenURIs, distributionConfigs: distConfigs, - paymentConfigs: paymentConfigs, - } + paymentConfigs, + }; // It is by intention that we aren't recreating user configs // We are just focusing on recreating the domain tree @@ -162,14 +160,14 @@ export const registerBase = async ({ } else { const bulkMigrationArgs = { domainToken: await zns.domainToken.getAddress(), - tokenOwners: tokenOwners, - recordOwners: recordOwners, - parentHashes: parentHashes, - labels: labels, - domainAddresses: domainAddresses, - tokenURIs: tokenURIs, + tokenOwners, + recordOwners, + parentHashes, + labels, + domainAddresses, + tokenURIs, distributionConfigs: distConfigs, - paymentConfigs: paymentConfigs, + paymentConfigs, }; tx = await zns.subRegistrar.connect(regAdmin).registerSubdomainBulk( @@ -183,13 +181,13 @@ export const registerBase = async ({ return { domainHash: undefined, txHash: undefined, - retryData: domains - } + retryData: domains, + }; } // Providing a number on hardhat will cause it to hang const blocks = hre.network.name === "hardhat" ? 0 : 5; - const txReceipt = await tx!.wait(blocks); + const txReceipt = await tx.wait(blocks); if (!txReceipt) { // Could this ever happen? Need this so downstream return states are never undefined @@ -197,16 +195,16 @@ export const registerBase = async ({ } // Collected the registered domains - let domainHashes = Array(); + const domainHashes = Array(); - const drEvents = txReceipt.logs.filter((log) => { + const drEvents = txReceipt.logs.filter(log => { if (log.topics[0] === DOMAIN_REGISTERED_TOPIC_SEPOLIA) { return log.topics[1]; // domainHash is always index 1 in this log } - }) + }); // console.log(`DREVENTS: ${drEvents.length}`); - drEvents.forEach((log) => { + drEvents.forEach(log => { domainHashes.push(log.topics[1]); }); @@ -231,4 +229,4 @@ export const postMigrationValidation = async ( console.log("Error validating domain: ", error); } } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/migration/subgraph/client.ts b/src/utils/migration/subgraph/client.ts index 2a4020d06..5ac3c66b7 100644 --- a/src/utils/migration/subgraph/client.ts +++ b/src/utils/migration/subgraph/client.ts @@ -1,8 +1,8 @@ import { - ApolloClient, - HttpLink, - InMemoryCache, - NormalizedCacheObject + ApolloClient, + HttpLink, + InMemoryCache, + NormalizedCacheObject, } from "@apollo/client"; @@ -12,7 +12,7 @@ export const createClient = (subgraphUri ?: string) : ApolloClient { variables: { first, skip, - isWorld + isWorld, }, }); @@ -42,7 +42,7 @@ export const getDomains = async (isWorld : boolean) => { variables: { first, skip, - isWorld + isWorld, }, }); } diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts index 5a2553272..14bb9996b 100644 --- a/src/utils/migration/types.ts +++ b/src/utils/migration/types.ts @@ -84,10 +84,10 @@ export interface DomainData { } export interface RegisteredDomains { - domainHashes : Array, - txHash : string + domainHashes : Array; + txHash : string; } -export type User = { id: string, domains: Domain[] }; -export type InvalidDomain = { message: string, domain: Domain }; -export type ValidatedUser = { address: string, validDomains: Domain[], invalidDomains: InvalidDomain[] }; +export interface User { id : string; domains : Array; } +export interface InvalidDomain { message : string; domain : Domain; } +export interface ValidatedUser { address : string; validDomains : Array; invalidDomains : Array; } diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index 5e57db921..9d6f271f0 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -1,34 +1,27 @@ import { ZeroAddress, ZeroHash } from "ethers"; import { Domain } from "./types"; -import { IZNSContracts } from "../../../test/helpers/types"; +import { IDistributionConfig, IZNSContracts } from "../../../test/helpers/types"; import assert from "assert"; export const validateDomain = async ( - domain : Domain, + domain : Domain, zns : IZNSContracts, ) => { // For speed in processing we group promises together const promises = [ zns.registry.getDomainOwner(domain.id), zns.domainToken.ownerOf(domain.tokenId), - zns.addressResolver.resolveDomainAddress(domain.id) - ] + zns.addressResolver.resolveDomainAddress(domain.id), + zns.subRegistrar.distrConfigs(domain.id) + ]; const [ domainOwner, domainTokenOwner, - domainAddress - ] = await Promise.all(promises) as unknown as [string, string, string]; - - // Domain is in reclaimable state - assert.equal( - domain.owner.id.toLowerCase(), - domain.domainToken.owner.id.toLowerCase(), - `Domain ${domain.id} has split ownership. - Token owner: ${domain.domainToken.owner.id.toLowerCase()}, - Domain owner: ${domain.owner.id.toLowerCase()}` - ); + domainAddress, + distrConfig + ] = await Promise.all(promises) as unknown as [string, string, string, IDistributionConfig]; assert.equal( domainOwner.toLowerCase(), @@ -54,8 +47,6 @@ export const validateDomain = async ( Subgraph: ${domain.address.toLowerCase()}` ); - const distrConfig = await zns.subRegistrar.distrConfigs(domain.id); - assert.equal(distrConfig.accessType, domain.accessType ?? 0n, `Domain ${domain.id} has different access types. Contract: ${distrConfig.accessType} @@ -73,11 +64,11 @@ export const validateDomain = async ( Contract: ${distrConfig.pricerContract.toLowerCase()} Subgraph: ${domain.pricerContract?.toLowerCase() ?? ZeroAddress} ` - ); + ); if (domain.isWorld) { assert.equal(domain.parentHash, ZeroHash), `Domain ${domain.id} 'isWorld' is true, but has parent hash`; - assert.ok(!!domain.parent === false, `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`) + assert.ok(!(!!domain.parent), `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`); assert.ok(domain.depth === 0, `Domain ${domain.id} 'isWorld' is true, but 'depth' is not 0`); } else { // Because we do not delete from the subgraph store on revoke, the domain is always present @@ -86,4 +77,4 @@ export const validateDomain = async ( assert.notEqual(domain.parentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'parentHash' is 0x0`); assert.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); } -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/utils/migration/zns-contract-data.ts b/src/utils/migration/zns-contract-data.ts index 3c4fe5cde..f133b1a46 100644 --- a/src/utils/migration/zns-contract-data.ts +++ b/src/utils/migration/zns-contract-data.ts @@ -12,7 +12,7 @@ import { ZNSFixedPricer__factory, ZNSRootRegistrar__factory, ZNSSubRegistrar__factory, - ZNSTreasury__factory + ZNSTreasury__factory, } from "../../../typechain/index"; let znsCache : IZNSContracts | null = null; @@ -24,25 +24,25 @@ export const getZNS = async ( if (!znsCache || Object.values(znsCache).length < 10) { const zns = await getZNSFromDB(); - const meowTokenAddress = zns.find((contract) => { + const meowTokenAddress = zns.find(contract => { if (env === "prod") { return contract.name === znsNames.meowToken.contract; } else { - contract.name === znsNames.meowToken.contractMock + contract.name === znsNames.meowToken.contractMock; } }); // Get each contract and manually connect to a factory. // Using `getContractFactory()` returns an incorrect type of factory here - const acAddress = zns.find((contract) => contract.name === znsNames.accessController.contract); - const regAddress = zns.find((contract) => contract.name === znsNames.registry.contract); - const domainTokenAddress = zns.find((contract) => contract.name === znsNames.domainToken.contract); - const addressResolverAddress = zns.find((contract) => contract.name === znsNames.addressResolver.contract); - const curvePricerAddress = zns.find((contract) => contract.name === znsNames.curvePricer.contract); - const treasuryAddress = zns.find((contract) => contract.name === znsNames.treasury.contract); - const rootRegistrarAddress = zns.find((contract) => contract.name === znsNames.rootRegistrar.contract); - const fixedPricerAddress = zns.find((contract) => contract.name === znsNames.fixedPricer.contract); - const subRegistrarAddress = zns.find((contract) => contract.name === znsNames.subRegistrar.contract); + const acAddress = zns.find(contract => contract.name === znsNames.accessController.contract); + const regAddress = zns.find(contract => contract.name === znsNames.registry.contract); + const domainTokenAddress = zns.find(contract => contract.name === znsNames.domainToken.contract); + const addressResolverAddress = zns.find(contract => contract.name === znsNames.addressResolver.contract); + const curvePricerAddress = zns.find(contract => contract.name === znsNames.curvePricer.contract); + const treasuryAddress = zns.find(contract => contract.name === znsNames.treasury.contract); + const rootRegistrarAddress = zns.find(contract => contract.name === znsNames.rootRegistrar.contract); + const fixedPricerAddress = zns.find(contract => contract.name === znsNames.fixedPricer.contract); + const subRegistrarAddress = zns.find(contract => contract.name === znsNames.subRegistrar.contract); znsCache = { accessController: ZNSAccessController__factory.connect(acAddress!.address, signer), @@ -55,7 +55,7 @@ export const getZNS = async ( rootRegistrar: ZNSRootRegistrar__factory.connect(rootRegistrarAddress!.address, signer), fixedPricer: ZNSFixedPricer__factory.connect(fixedPricerAddress!.address, signer), subRegistrar: ZNSSubRegistrar__factory.connect(subRegistrarAddress!.address, signer), - } + }; } return znsCache; From bd68f192a3546a5982c60440414f4566192aa95f Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 29 May 2025 11:47:37 -0700 Subject: [PATCH 069/114] add reference to constants --- src/utils/migration/01_validation.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index f3bbdfd9d..1658228b4 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -67,9 +67,8 @@ const main = async () => { // Domains that have split ownership will be considered invalid domains if (invalidDomains.length > 0) { - const invalidCollName = process.env.MONGO_DB_INVALID_COLL_NAME || "invalid-domains"; - await client.dropCollection(invalidCollName); - await client.collection(invalidCollName).insertMany(invalidDomains); + await client.dropCollection(INVALID_COLL_NAME); + await client.collection(INVALID_COLL_NAME).insertMany(invalidDomains); } }; From 0eef1230e36a6fc17516c605a040d35071ba9b0d Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 29 May 2025 11:47:55 -0700 Subject: [PATCH 070/114] delete unused imports --- src/utils/migration/01_validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 1658228b4..7b4d2db0f 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,6 +1,6 @@ import * as hre from "hardhat"; import { getDomains } from "./subgraph"; -import { Domain, InvalidDomain, User, ValidatedUser } from "./types"; +import { Domain, InvalidDomain } from "./types"; import { getDBAdapter } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; From 209f2a0b30d5013f71af9eea0feffa1cbd4085c8 Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 29 May 2025 12:45:16 -0700 Subject: [PATCH 071/114] comment out private key used in HH config mainnet --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index ce5786fc7..e93eb1356 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -103,7 +103,7 @@ const config : HardhatUserConfig = { url: `${process.env.MAINNET_RPC_URL}`, accounts: [ // Read only - `${process.env.TESTNET_PRIVATE_KEY_A}`, + // `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet ], gasPrice: 80000000000, }, From 14ccd0b31a66382911ceea8a0716a4ea4105ad5a Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 29 May 2025 12:53:47 -0700 Subject: [PATCH 072/114] lint in CI fixes --- src/utils/migration/02_registration.ts | 4 ++++ src/utils/migration/database.ts | 12 ++++-------- src/utils/migration/registration.ts | 3 +++ src/utils/migration/types.ts | 14 +++++--------- src/utils/migration/validate.ts | 4 ++-- src/utils/migration/zns-contract-data.ts | 20 ++++++++++---------- 6 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/utils/migration/02_registration.ts b/src/utils/migration/02_registration.ts index 8f99714d3..5138bb7f1 100644 --- a/src/utils/migration/02_registration.ts +++ b/src/utils/migration/02_registration.ts @@ -8,6 +8,10 @@ import { ROOTS_FILENAME, SUBS_FILENAME } from "./constants"; import { IZNSContracts } from "../../deploy/campaign/types"; import { getDBAdapter } from "./database"; +// We will need to adjust this file in the future no matter what after merging happens +// ignore this file for now +/* eslint-disable */ + // Script #2 to be run AFTER validation of the domains with subgraph const main = async () => { const [ migrationAdmin, governor, admin ] = await hre.ethers.getSigners(); diff --git a/src/utils/migration/database.ts b/src/utils/migration/database.ts index 72cb64903..2597eb2b2 100644 --- a/src/utils/migration/database.ts +++ b/src/utils/migration/database.ts @@ -16,17 +16,13 @@ export const getDBAdapter = async ( } ); - return await mongoClient.connect(); + return mongoClient.connect(); }; export const getZNSFromDB = async () => { - let version; - let uri; - let dbName; - - version = process.env.MONGO_DB_VERSION; - uri = process.env.MONGO_DB_URI; - dbName = process.env.MONGO_DB_NAME; + const version = process.env.MONGO_DB_VERSION; + const uri = process.env.MONGO_DB_URI; + const dbName = process.env.MONGO_DB_NAME; if (!uri) { throw new Error("Failed to connect: missing MongoDB URI or version"); diff --git a/src/utils/migration/registration.ts b/src/utils/migration/registration.ts index 821b28915..5c0feaa8a 100644 --- a/src/utils/migration/registration.ts +++ b/src/utils/migration/registration.ts @@ -8,6 +8,9 @@ import { expect } from "chai"; import { validateDomain } from "./validate"; import { ZeroAddress } from "ethers"; +// We will need to adjust this file in the future no matter what after merging happens +// ignore this file for now +/* eslint-disable */ export const registerDomainsBulk = async ( regAdmin : SignerWithAddress, diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts index 14bb9996b..2242c5f55 100644 --- a/src/utils/migration/types.ts +++ b/src/utils/migration/types.ts @@ -1,4 +1,3 @@ -import { ContractTransactionReceipt } from "ethers"; import { IDistributionConfig, IPaymentConfig } from "../../../test/helpers/types"; export interface Domain { @@ -42,13 +41,6 @@ interface FixedPriceConfig { price : string; } -interface PaymentToken { - id : string; - name : string; - symbol : string; - decimals : string; -} - interface Treasury { id : string; beneficiaryAddress : string; @@ -90,4 +82,8 @@ export interface RegisteredDomains { export interface User { id : string; domains : Array; } export interface InvalidDomain { message : string; domain : Domain; } -export interface ValidatedUser { address : string; validDomains : Array; invalidDomains : Array; } +export interface ValidatedUser { + address : string; + validDomains : Array; + invalidDomains : Array; +} diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index 9d6f271f0..61debd486 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -13,14 +13,14 @@ export const validateDomain = async ( zns.registry.getDomainOwner(domain.id), zns.domainToken.ownerOf(domain.tokenId), zns.addressResolver.resolveDomainAddress(domain.id), - zns.subRegistrar.distrConfigs(domain.id) + zns.subRegistrar.distrConfigs(domain.id), ]; const [ domainOwner, domainTokenOwner, domainAddress, - distrConfig + distrConfig, ] = await Promise.all(promises) as unknown as [string, string, string, IDistributionConfig]; assert.equal( diff --git a/src/utils/migration/zns-contract-data.ts b/src/utils/migration/zns-contract-data.ts index f133b1a46..7e10f4150 100644 --- a/src/utils/migration/zns-contract-data.ts +++ b/src/utils/migration/zns-contract-data.ts @@ -45,16 +45,16 @@ export const getZNS = async ( const subRegistrarAddress = zns.find(contract => contract.name === znsNames.subRegistrar.contract); znsCache = { - accessController: ZNSAccessController__factory.connect(acAddress!.address, signer), - registry: ZNSRegistry__factory.connect(regAddress!.address, signer), - domainToken: ZNSDomainToken__factory.connect(domainTokenAddress!.address, signer), - meowToken: MeowTokenMock__factory.connect(meowTokenAddress!.address, signer), - addressResolver: ZNSAddressResolver__factory.connect(addressResolverAddress!.address, signer), - curvePricer: ZNSCurvePricer__factory.connect(curvePricerAddress!.address, signer), - treasury: ZNSTreasury__factory.connect(treasuryAddress!.address, signer), - rootRegistrar: ZNSRootRegistrar__factory.connect(rootRegistrarAddress!.address, signer), - fixedPricer: ZNSFixedPricer__factory.connect(fixedPricerAddress!.address, signer), - subRegistrar: ZNSSubRegistrar__factory.connect(subRegistrarAddress!.address, signer), + accessController: ZNSAccessController__factory.connect(acAddress?.address, signer), + registry: ZNSRegistry__factory.connect(regAddress?.address, signer), + domainToken: ZNSDomainToken__factory.connect(domainTokenAddress?.address, signer), + meowToken: MeowTokenMock__factory.connect(meowTokenAddress?.address, signer), + addressResolver: ZNSAddressResolver__factory.connect(addressResolverAddress?.address, signer), + curvePricer: ZNSCurvePricer__factory.connect(curvePricerAddress?.address, signer), + treasury: ZNSTreasury__factory.connect(treasuryAddress?.address, signer), + rootRegistrar: ZNSRootRegistrar__factory.connect(rootRegistrarAddress?.address, signer), + fixedPricer: ZNSFixedPricer__factory.connect(fixedPricerAddress?.address, signer), + subRegistrar: ZNSSubRegistrar__factory.connect(subRegistrarAddress?.address, signer), }; } From c5c96a73b2ec07800bb277f0ffc36e04028bc0dc Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 29 May 2025 13:04:44 -0700 Subject: [PATCH 073/114] add ts eslint disable as well --- src/utils/migration/02_registration.ts | 2 ++ src/utils/migration/registration.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/src/utils/migration/02_registration.ts b/src/utils/migration/02_registration.ts index 5138bb7f1..24ee19f36 100644 --- a/src/utils/migration/02_registration.ts +++ b/src/utils/migration/02_registration.ts @@ -11,6 +11,8 @@ import { getDBAdapter } from "./database"; // We will need to adjust this file in the future no matter what after merging happens // ignore this file for now /* eslint-disable */ +/* @typescript-eslint-disable */ + // Script #2 to be run AFTER validation of the domains with subgraph const main = async () => { diff --git a/src/utils/migration/registration.ts b/src/utils/migration/registration.ts index 5c0feaa8a..ee18562a7 100644 --- a/src/utils/migration/registration.ts +++ b/src/utils/migration/registration.ts @@ -11,6 +11,7 @@ import { ZeroAddress } from "ethers"; // We will need to adjust this file in the future no matter what after merging happens // ignore this file for now /* eslint-disable */ +/* @typescript-eslint-disable */ export const registerDomainsBulk = async ( regAdmin : SignerWithAddress, From 4d45eb39f41084f2527a04d817a76d4d619ed742 Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 29 May 2025 13:07:57 -0700 Subject: [PATCH 074/114] change in validate script for parens --- src/utils/migration/validate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index 61debd486..78e450747 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -67,7 +67,7 @@ export const validateDomain = async ( ); if (domain.isWorld) { - assert.equal(domain.parentHash, ZeroHash), `Domain ${domain.id} 'isWorld' is true, but has parent hash`; + assert.equal(domain.parentHash, ZeroHash, `Domain ${domain.id} 'isWorld' is true, but has parent hash`); assert.ok(!(!!domain.parent), `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`); assert.ok(domain.depth === 0, `Domain ${domain.id} 'isWorld' is true, but 'depth' is not 0`); } else { From f50abbbb8afecebaec4ab5cd2c0d343c158e2945 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 4 Jun 2025 13:23:22 -0700 Subject: [PATCH 075/114] linter fixes --- .eslintrc | 5 +++-- .solhint.json | 4 ++-- src/upgrade/upgrade.ts | 2 +- src/utils/migration/01_validation.ts | 3 ++- src/utils/migration/02_registration.ts | 9 ++------- src/utils/migration/registration.ts | 11 ++++------- 6 files changed, 14 insertions(+), 20 deletions(-) diff --git a/.eslintrc b/.eslintrc index 67dd7deb3..0fba55420 100644 --- a/.eslintrc +++ b/.eslintrc @@ -9,9 +9,10 @@ "no-console": "off", "no-shadow": "warn", "@typescript-eslint/no-shadow": "warn", - "no-invalid-this": "off" + "no-invalid-this": "off", + "jsdoc/newline-after-description": "off" // "@typescript-eslint/no-unused-vars": "off" // For debugging } } ] -} \ No newline at end of file +} diff --git a/.solhint.json b/.solhint.json index 4a5d360ce..40a659a39 100644 --- a/.solhint.json +++ b/.solhint.json @@ -3,7 +3,7 @@ "rules": { "compiler-version": [ "error", - "^0.8.18" + "0.8.18" ], "not-rely-on-time": "off", "no-inline-assembly": "off", @@ -24,4 +24,4 @@ "max-line-length": ["error", 120], "custom-errors": "off" } -} \ No newline at end of file +} diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index 7c2e393ab..a20257716 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -36,7 +36,7 @@ export const upgradeZNS = async ({ upgradedContracts[instanceName] = await upgradeZNSContract({ contractName, contractAddress: address, - governor: governor as SignerWithAddress, + governor, logger, }); diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 7b4d2db0f..9175f7126 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -6,6 +6,7 @@ import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; + const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); @@ -77,4 +78,4 @@ main() .catch(error => { console.error(error); process.exitCode = 1; - }); \ No newline at end of file + }); diff --git a/src/utils/migration/02_registration.ts b/src/utils/migration/02_registration.ts index 24ee19f36..41a7f4e22 100644 --- a/src/utils/migration/02_registration.ts +++ b/src/utils/migration/02_registration.ts @@ -1,12 +1,7 @@ import * as hre from "hardhat"; -import { Domain } from "./types"; -import * as fs from "fs"; -import { deployZNS } from "../../../test/helpers"; -import { postMigrationValidation, registerDomainsBulk } from "./registration"; import { getZNS } from "./zns-contract-data"; -import { ROOTS_FILENAME, SUBS_FILENAME } from "./constants"; -import { IZNSContracts } from "../../deploy/campaign/types"; import { getDBAdapter } from "./database"; +import { IZNSContracts } from "../../../test/helpers/types"; // We will need to adjust this file in the future no matter what after merging happens // ignore this file for now @@ -78,4 +73,4 @@ const main = async () => { main().catch(error => { console.error(error); process.exitCode = 1; -}); \ No newline at end of file +}); diff --git a/src/utils/migration/registration.ts b/src/utils/migration/registration.ts index ee18562a7..c24e2ef77 100644 --- a/src/utils/migration/registration.ts +++ b/src/utils/migration/registration.ts @@ -1,12 +1,9 @@ import * as hre from "hardhat"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { RegisteredDomains, Domain } from "./types"; -import { IZNSContracts } from "../../deploy/campaign/types"; -import { paymentConfigEmpty } from "../../../test/helpers"; -import { DOMAIN_REGISTERED_TOPIC_SEPOLIA } from "./constants"; -import { expect } from "chai"; import { validateDomain } from "./validate"; import { ZeroAddress } from "ethers"; +import { IZNSContracts } from "../../../test/helpers/types"; // We will need to adjust this file in the future no matter what after merging happens // ignore this file for now @@ -16,7 +13,7 @@ import { ZeroAddress } from "ethers"; export const registerDomainsBulk = async ( regAdmin : SignerWithAddress, domains : Array, // imagine this is ALL domains - zns : IZNSContracts, + zns : IZNSContracts, sliceSize : number, start : number, ) => { @@ -80,7 +77,7 @@ export const registerBase = async ({ regAdmin, domains, } : { - zns : IZNSContractsLocal | IZNSContracts; + zns : IZNSContracts; regAdmin : SignerWithAddress; domains : Array; }) => { @@ -233,4 +230,4 @@ export const postMigrationValidation = async ( console.log("Error validating domain: ", error); } } -}; \ No newline at end of file +}; From 1a53cac21141ed869c6f19cdeb933df126df0558 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 4 Jun 2025 13:30:15 -0700 Subject: [PATCH 076/114] update axios to safe version --- package.json | 2 +- yarn.lock | 45 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 97f19bebb..651db4b80 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "typescript": "^5.0.2" }, "dependencies": { - "axios": "^1.4.0", + "axios": "^1.8.2", "dotenv": "16.0.3", "mongodb": "^6.1.0", "winston": "^3.11.0" diff --git a/yarn.lock b/yarn.lock index e70420249..1545f4b38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2594,6 +2594,15 @@ axios@^1.4.0, axios@^1.5.1: form-data "^4.0.0" proxy-from-env "^1.1.0" +axios@^1.8.2: + version "1.9.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901" + integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -4436,6 +4445,11 @@ follow-redirects@^1.12.1, follow-redirects@^1.14.0, follow-redirects@^1.14.9, fo resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -8634,7 +8648,7 @@ string-format@^2.0.0: resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8652,6 +8666,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -8710,7 +8733,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8731,6 +8754,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9593,7 +9623,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9610,6 +9640,15 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 0c213ed9b0a7f9b52fb9505f46be40a756d8fa02 Mon Sep 17 00:00:00 2001 From: James Earle Date: Mon, 9 Jun 2025 14:07:58 -0700 Subject: [PATCH 077/114] update query and move env checks to be ahead of long running validation process --- .env.sample | 14 ++++++++++++-- hardhat.config.ts | 2 +- src/utils/migration/01_validation.ts | 12 ++++++------ src/utils/migration/subgraph/queries.ts | 4 ++++ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.env.sample b/.env.sample index 085892c89..c52bf6a8e 100644 --- a/.env.sample +++ b/.env.sample @@ -12,20 +12,24 @@ SEPOLIA_RPC_URL= # ENV vars for MongoDB, for local development you can use below MONGO_DB_URI="mongodb://localhost:27018" MONGO_DB_NAME="zns-campaign" + # Optional params for Mongo Client class specifically MONGO_DB_CLIENT_OPTS= + # This is crucial based on the DB behaviour you want. MongoAdapter will create a new DB version if this is NOT passed # and it will use existing version if specified. If you want to deploy from scratch, do not supply and the previous # versioned data will be wiped out. # If you wish to save the previous data and write a new version on top, look at the next ENV var. MONGO_DB_VERSION= + # This is crucial to saving the data written in previous DB (DEPLOYED) version. # If this is not passed or passed as "false", previous DB data will be wiped out. # If you want to save the previous contract data in DB, set this to "true"! ARCHIVE_PREVIOUS_DB_VERSION="true" | "false" # ENV vars for Logger -LOG_LEVEL="debug" | "info" | "warn" | "error +LOG_LEVEL="debug" | "info" | "warn" | "error" + # Removes logger output and does not write to file as well SILENT_LOGGER="false" | "true" @@ -33,6 +37,7 @@ SILENT_LOGGER="false" | "true" # true = we deploy the mock # false = we use a hard coded address and pull data from chain MOCK_MEOW_TOKEN= + # Address of the MEOW Token deployed to the network PRIOR to running Campaign or any other EXISTING token # This is only used if MOCK_MEOW_TOKEN is set to false (`test` and `prod` environments) STAKING_TOKEN_ADDRESS= @@ -42,7 +47,6 @@ MAX_PRICE= MIN_PRICE= MAX_LENGTH= BASE_LENGTH= - DECIMALS= PRECISION= PROTOCOL_FEE_PERC= @@ -65,6 +69,7 @@ ADMIN_ADDRESSES= MONITOR_CONTRACTS="false" VERIFY_CONTRACTS="false" +# For using OpenZeppelin Defender for deployment DEFENDER_KEY= DEFENDER_SECRET= RELAYER_KEY= @@ -81,3 +86,8 @@ TENDERLY_ACCOUNT_ID="zer0-os" # Below are used only for the `deploy:devnet` script (for testing new logic) NOT RELATED TO THE DEPLOY CAMPAIGN ! TENDERLY_DEVNET_TEMPLATE="zns-devnet" DEVNET_RPC_URL= + +# If executing the migration scripts, you must also specify +# the below to specify a different database to write data too +MONGO_DB_URI_WRITE= +MONGO_DB_NAME_WRITE= diff --git a/hardhat.config.ts b/hardhat.config.ts index e93eb1356..94070d91b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -103,7 +103,7 @@ const config : HardhatUserConfig = { url: `${process.env.MAINNET_RPC_URL}`, accounts: [ // Read only - // `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet + `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet ], gasPrice: 80000000000, }, diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 9175f7126..206fc617c 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -31,6 +31,12 @@ const main = async () => { const roots = rootDomainObjects.map(d => d as Domain); const subs = subdomainObjects.map(d => d as Domain); + const dbName = process.env.MONGO_DB_NAME_WRITE; + if (!dbName) throw Error("No DB name given"); + + const uri = process.env.MONGO_DB_URI_WRITE; + if (!uri) throw Error("No connection string given"); + // Can iterate all at once for simplicity let index = 0; for(const domain of [...roots, ...subs]) { @@ -51,12 +57,6 @@ const main = async () => { } // Connect to database collection and write user domain data to DB - const dbName = process.env.MONGO_DB_NAME_WRITE; - if (!dbName) throw Error("No DB name given"); - - const uri = process.env.MONGO_DB_URI_WRITE; - if (!uri) throw Error("No connection string given"); - const client = (await getDBAdapter(uri)).db(dbName); // To avoid duplicate data, we clear the DB before any inserts diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index 64f4a8bcd..924fa1ae2 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -97,6 +97,10 @@ export const getDomains = gql` treasury { id beneficiaryAddress + paymentToken { + name + symbol + } } creationBlock creationTimestamp From 71f3df3200b2e77d2c6862611a7045f7b5cd0269 Mon Sep 17 00:00:00 2001 From: James Earle Date: Mon, 9 Jun 2025 14:10:29 -0700 Subject: [PATCH 078/114] comment out mainnet key for CI --- hardhat.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 94070d91b..e93eb1356 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -103,7 +103,7 @@ const config : HardhatUserConfig = { url: `${process.env.MAINNET_RPC_URL}`, accounts: [ // Read only - `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet + // `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet ], gasPrice: 80000000000, }, From e80aad2f5a948fbe86445ab798c6b4667560b4ff Mon Sep 17 00:00:00 2001 From: James Earle Date: Mon, 9 Jun 2025 14:13:02 -0700 Subject: [PATCH 079/114] add id to query as well --- src/utils/migration/subgraph/queries.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index 924fa1ae2..e5818a226 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -98,6 +98,7 @@ export const getDomains = gql` id beneficiaryAddress paymentToken { + id name symbol } From 266d11a015299ade430058c9b9f5d423df0edb7a Mon Sep 17 00:00:00 2001 From: Kirill Date: Mon, 23 Jun 2025 13:24:32 -0700 Subject: [PATCH 080/114] make DB name error clearer Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/utils/migration/01_validation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 206fc617c..9afe1f963 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -32,7 +32,7 @@ const main = async () => { const subs = subdomainObjects.map(d => d as Domain); const dbName = process.env.MONGO_DB_NAME_WRITE; - if (!dbName) throw Error("No DB name given"); + if (!dbName) throw Error("Missing MONGO_DB_NAME_WRITE environment variable"); const uri = process.env.MONGO_DB_URI_WRITE; if (!uri) throw Error("No connection string given"); From dce09e23d628af7e4cada92b71500dd2f97d8bb8 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 23 Jun 2025 14:13:02 -0700 Subject: [PATCH 081/114] remove redundant registration scripts that should go to a different branch --- src/utils/migration/02_registration.ts | 76 -------- src/utils/migration/registration.ts | 233 ------------------------- 2 files changed, 309 deletions(-) delete mode 100644 src/utils/migration/02_registration.ts delete mode 100644 src/utils/migration/registration.ts diff --git a/src/utils/migration/02_registration.ts b/src/utils/migration/02_registration.ts deleted file mode 100644 index 41a7f4e22..000000000 --- a/src/utils/migration/02_registration.ts +++ /dev/null @@ -1,76 +0,0 @@ -import * as hre from "hardhat"; -import { getZNS } from "./zns-contract-data"; -import { getDBAdapter } from "./database"; -import { IZNSContracts } from "../../../test/helpers/types"; - -// We will need to adjust this file in the future no matter what after merging happens -// ignore this file for now -/* eslint-disable */ -/* @typescript-eslint-disable */ - - -// Script #2 to be run AFTER validation of the domains with subgraph -const main = async () => { - const [ migrationAdmin, governor, admin ] = await hre.ethers.getSigners(); - - // Overall flow will be: - // connect to DB - // read all roots from mongodb - // while there are unregistered root domains: - // register a batch - - // read all subs with depth 1 from mongodb - // while there are unregistered subdomains: - // register a batch - // read all subs with depth 2 from mongodb - // while there are unregistered subdomains: - // register a batch - // read all subs with depth 3 from mongodb - // while there are unregistered subdomains: - // register a batch - - // During above we will pack transactions with to always have 50 domains - // so if only 45 root domains remain at the end, we will also send the first 5 depth 1 subdomains - - // Steps to register a batch will mean using the Safe REST API to create a transaction - // for the owning safe that calls `registerRootDomainBulk` or `registerSubdomainBulk` - // Then we will wait for the transaction to be executed - // Technically we could also sign each tx and execute this way - - let zns : IZNSContracts; - - const env = process.env.ENV_LEVEL; - - if (!env) throw Error("No ENV_LEVEL set in .env file"); - - // Get instance of ZNS from DB - zns = await getZNS(migrationAdmin, env); - - // Connect to database collection and write user domain data to DB - const dbName = process.env.MONGO_DB_NAME_WRITE; - if (!dbName) throw Error("No DB name given"); - - const uri = process.env.MONGO_DB_URI_WRITE; - if (!uri) throw Error("No connection string given"); - - const client = (await getDBAdapter(uri)).db(dbName); - - const rootCollName = process.env.MONGO_DB_ROOT_COLL_NAME || "root-domains"; - - // Get all documents from collection - const domains = await client.collection(rootCollName).find().toArray(); - - console.log(domains.length); - - const startTime = Date.now(); - - // How many domains we will register in a single transaction - const sliceSize = 50; - - process.exit(0); -}; - -main().catch(error => { - console.error(error); - process.exitCode = 1; -}); diff --git a/src/utils/migration/registration.ts b/src/utils/migration/registration.ts deleted file mode 100644 index c24e2ef77..000000000 --- a/src/utils/migration/registration.ts +++ /dev/null @@ -1,233 +0,0 @@ -import * as hre from "hardhat"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { RegisteredDomains, Domain } from "./types"; -import { validateDomain } from "./validate"; -import { ZeroAddress } from "ethers"; -import { IZNSContracts } from "../../../test/helpers/types"; - -// We will need to adjust this file in the future no matter what after merging happens -// ignore this file for now -/* eslint-disable */ -/* @typescript-eslint-disable */ - -export const registerDomainsBulk = async ( - regAdmin : SignerWithAddress, - domains : Array, // imagine this is ALL domains - zns : IZNSContracts, - sliceSize : number, - start : number, -) => { - const registeredDomains = Array(); - - // 'start' is used for retry logic and represents the number of domains - // we have already minted - - // The number of iterations to do based on the size of the incoming domain array - const numIters = Math.floor((domains.length - start) / sliceSize); - - // Because the terminator represents the *total* number of domains to register, - // we add `isStart` back in - const terminator = start + (sliceSize * numIters); - - for (let i = start; i < terminator; i += sliceSize) { - const domainsForTx = domains.slice(i, i + sliceSize); - - // If the domain hash is already in the registry we have already registered this - // batch of domains, so we skip it - if (!await zns.registry.exists(domainsForTx[0].id)) { - const { domainHashes, txHash, retryData } = await registerBase({ - regAdmin, - zns, - domains: domains.slice(i, i + sliceSize), - }); - - if (retryData) { - throw new Error("Error in registering domains"); - } - - registeredDomains.push({ domainHashes, txHash }); - - console.log("Registered domains: ", i + sliceSize); - } else { - console.log(`Skipping already registered domains: ${i} to ${i + sliceSize}}`); - } - } - - // In the likely case that the list of domains is not divisble by slice size, we - // want to make sure we do the last set of domains as well - const { domainHashes, txHash, retryData } = await registerBase({ - regAdmin, - zns, - domains: domains.slice(terminator), // terminator -> end of array - }); - - if (retryData) { - throw new Error("Error in registering domains"); - } - - console.log("Registered additional domains: ", start + domainHashes.length); - - registeredDomains.push({ domainHashes, txHash }); - - return registeredDomains; -}; - -export const registerBase = async ({ - zns, - regAdmin, - domains, -} : { - zns : IZNSContracts; - regAdmin : SignerWithAddress; - domains : Array; -}) => { - - const tokenOwners = domains.map(domain => { - if (domain.domainToken.owner.id === hre.ethers.ZeroAddress) { - // The ERC721 token has been burned, must mint with the record owner instead - // to recreate the tree fully - return domain.owner.id; - } else { - return domain.domainToken.owner.id; - } - }); - - const distConfigs = domains.map(domain => { - const config = { - pricerContract: "", - paymentType: 0n, - accessType: 1n, // Always use `open` access type for migration - }; - - // Get pricer contract - if (!domain.pricerContract) { - config.pricerContract = ZeroAddress; - } else { - config.pricerContract = domain.pricerContract; - } - - // Get payment type - if (!domain.paymentType || domain.paymentType === "0") { - config.paymentType = 0n; - } else { - config.paymentType = 1n; - } - - return config; - }); - - // Awaiting this promise within the `map` function below causes type problems - // when passing args to the function downstream. This is a workaround. - const tokenAddress = await zns.meowToken.getAddress(); - - const paymentConfigs = domains.map(domain => ({ - beneficiary: !domain.treasury.beneficiaryAddress - ? ZeroAddress - : domain.treasury.beneficiaryAddress, - token: tokenAddress, - })); - - const recordOwners = domains.map(domain => domain.owner.id); - const parentHashes = domains.map(domain => domain.parentHash); - const labels = domains.map(domain => domain.label); - const domainAddresses = domains.map(domain => domain.address); - const tokenURIs = domains.map(domain => domain.tokenURI); - - let tx; - - try { - // Because we pre-filter using the query into sets of just root domains and just subdomains - // (ordered by depth) we know with certainty that if one parent hash is zero, they all are - if (parentHashes[0] === hre.ethers.ZeroHash) { - - const bulkMigrationArgs = { - tokenOwners, - recordOwners, - names: labels, - domainAddresses, - tokenURIs, - distributionConfigs: distConfigs, - paymentConfigs, - }; - - // It is by intention that we aren't recreating user configs - // We are just focusing on recreating the domain tree - tx = await zns.rootRegistrar.connect(regAdmin).registerRootDomainBulk( - bulkMigrationArgs, - // { - // gasLimit: 5000000 // TODO for debugging - // } - ); - } else { - const bulkMigrationArgs = { - domainToken: await zns.domainToken.getAddress(), - tokenOwners, - recordOwners, - parentHashes, - labels, - domainAddresses, - tokenURIs, - distributionConfigs: distConfigs, - paymentConfigs, - }; - - tx = await zns.subRegistrar.connect(regAdmin).registerSubdomainBulk( - bulkMigrationArgs - ); - } - } catch (e) { - console.log("Error registering domains: ", e); - // Return the domainData if something failed so we can log it - // for debugging purposes - return { - domainHash: undefined, - txHash: undefined, - retryData: domains, - }; - } - - // Providing a number on hardhat will cause it to hang - const blocks = hre.network.name === "hardhat" ? 0 : 5; - const txReceipt = await tx.wait(blocks); - - if (!txReceipt) { - // Could this ever happen? Need this so downstream return states are never undefined - throw new Error("Transaction succeeded without receipt"); - } - - // Collected the registered domains - const domainHashes = Array(); - - const drEvents = txReceipt.logs.filter(log => { - if (log.topics[0] === DOMAIN_REGISTERED_TOPIC_SEPOLIA) { - return log.topics[1]; // domainHash is always index 1 in this log - } - }); - // console.log(`DREVENTS: ${drEvents.length}`); - - drEvents.forEach(log => { - domainHashes.push(log.topics[1]); - }); - - // console.log(`DOMAINHASHES: ${domainHashes.length}`); - - // console.log(txReceipt.hash); - - return { domainHashes, txHash: txReceipt.hash, retryData: undefined }; -}; - -export const postMigrationValidation = async ( - zns : IZNSContractsLocal | IZNSContracts, - domains : Array, -) => { - - // TODO figure out error with users who have not called to reclaim there domain - // after a transfer - for (const domain of domains) { - const error = await validateDomain(domain, zns, true); - - if (error) { - console.log("Error validating domain: ", error); - } - } -}; From e47f5235ead197eac6803b44ae1a903ad1e41b9b Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 24 Jun 2025 17:17:28 -0700 Subject: [PATCH 082/114] fix integration test --- test/DeployCampaign.integration.test.ts | 49 +++++++------------------ test/helpers/deploy-helpers.ts | 12 ++++-- 2 files changed, 22 insertions(+), 39 deletions(-) diff --git a/test/DeployCampaign.integration.test.ts b/test/DeployCampaign.integration.test.ts index 1ff978a2d..9fb2a93c6 100644 --- a/test/DeployCampaign.integration.test.ts +++ b/test/DeployCampaign.integration.test.ts @@ -39,20 +39,20 @@ describe("DeployCampaign - Integration", () => { const logger = getLogger(); // Default baselength is 4, maxLength is 50 - const shortDomain = "mazz"; // Length 4 - const mediumDomain = "mesder"; // Length 6 - const longDomain = "mesderwilderwilderwilderwilderwilderwilderwilderwil"; // Length 51 + const shortDomain = "mazzz"; // Length 4 + const mediumDomain = "mesderz"; // Length 6 + const longDomain = "mesderwilderwilderwilderwilderwilderwilderwilderwilz"; // Length 51 const shortHash = hashDomainLabel(shortDomain); const mediumHash = hashDomainLabel(mediumDomain); const longHash = hashDomainLabel(longDomain); - const freeShortSubdomain = "pubj"; // Length 4 - const freeMediumSubdomain = "pubjer"; // Length 6 - const freeLongSubdomain = "pubjerwilderwilderwilderwilderwilderwilderwilderwil"; // Length 51 + const freeShortSubdomain = "pubjj"; // Length 4 + const freeMediumSubdomain = "pubjjer"; // Length 6 + const freeLongSubdomain = "pubjerwilderwilderwilderwilderwilderwilderwilderwilj"; // Length 51 - const paidShortSubdomain = "purf"; // Length 4 - const paidMediumSubdomain = "purfer"; // Length 6 - const paidLongSubdomain = "purferwilderwilderwilderwilderwilderwilderwilderwil"; // Length 51 + const paidShortSubdomain = "purfj"; // Length 4 + const paidMediumSubdomain = "purferj"; // Length 6 + const paidLongSubdomain = "purferwilderwilderwilderwilderwilderwilderwilderwilj"; // Length 51 // Resolve subdomain hashes through async call `hashWithParent` in `before` hook let freeShortSubHash : string; @@ -62,42 +62,22 @@ describe("DeployCampaign - Integration", () => { let paidMediumSubHash : string; let paidLongSubHash : string; - const mintAmount = ethers.parseEther("10000000"); + const mintAmount = ethers.parseEther("1000000000"); const domains = [shortDomain, mediumDomain, longDomain]; before(async () => { - [ deployAdmin, zeroVault, userA, userB, userC, userD, userE, userF ] = await hre.ethers.getSigners(); + [ deployAdmin, userA, userB, userC, userD, userE, userF, zeroVault ] = await hre.ethers.getSigners(); // Reads `ENV_LEVEL` environment variable to determine rules to be enforced - let deployer; - let provider; - - if (hre.network.name === "hardhat") { - deployer = deployAdmin; - provider = new hre.ethers.JsonRpcProvider(process.env.SEPOLIA_RPC_URL); - } else { - const credentials = { - apiKey: process.env.DEFENDER_KEY, - apiSecret: process.env.DEFENDER_SECRET, - relayerApiKey: process.env.RELAYER_KEY, - relayerApiSecret: process.env.RELAYER_SECRET, - }; - - const client = new Defender(credentials); - provider = client.relaySigner.getProvider(); - deployer = client.relaySigner.getSigner(provider, { speed: "fast" }); - } - + const deployer = deployAdmin; config = await getConfig({ deployer, zeroVaultAddress: zeroVault.address, }); - config.mockMeowToken = hre.network.name === "hardhat"; - // First run the `run-campaign` script, then modify the `MONGO_DB_VERSION` environment variable // Then run this test. The campaign won't be run, but those addresses will be picked up from the DB const campaign = await runZnsCampaign({ config }); @@ -137,7 +117,7 @@ describe("DeployCampaign - Integration", () => { await approveBulk(users, zns); // Give the user funds - if (hre.network.name === "hardhat" && config.mockMeowToken) { + if (config.mockMeowToken) { await mintBulk( users, mintAmount, @@ -315,11 +295,10 @@ describe("DeployCampaign - Integration", () => { it("Reclaims then revokes correctly", async () => { // 5. Reclaim and revoke domain const tx = await zns.registry.connect(userC).updateDomainOwner(freeLongSubHash, userA.address); + if (hre.network.name !== "hardhat") await tx.wait(1); await expect(tx).to.emit(zns.registry, "DomainOwnerSet").withArgs(freeLongSubHash, userA.address); logger.info(`Subdomain ${freeLongSubHash} ownership given to user ${userA.address} from user ${userC.address}`); - if (hre.network.name !== "hardhat") await tx.wait(1); - const tx1 = await zns.rootRegistrar.connect(userC).reclaimDomain(freeLongSubHash); await expect(tx1).to.emit(zns.rootRegistrar, "DomainReclaimed").withArgs(freeLongSubHash, userC.address); diff --git a/test/helpers/deploy-helpers.ts b/test/helpers/deploy-helpers.ts index c962aa1f4..22d745841 100644 --- a/test/helpers/deploy-helpers.ts +++ b/test/helpers/deploy-helpers.ts @@ -35,10 +35,14 @@ export const mintBulk = async ( zns : TZNSContractState, ) => { for (const signer of signers) { - await zns.meowToken.connect(signer).mint( + const tx = await zns.meowToken.connect(signer).mint( signer.address, amount ); + + if (hre.network.name !== "hardhat") { + await tx.wait(2); + } } }; @@ -112,7 +116,7 @@ export const registerRootDomainBulk = async ( const balanceAfter = await zns.meowToken.balanceOf(signers[index].address); const [price, protocolFee] = await zns.curvePricer.getPriceAndFee(ethers.ZeroHash, domain, true); - expect(balanceAfter).to.be.eq(balanceBefore - price - protocolFee); + expect(balanceBefore - balanceAfter).to.be.eq(price + protocolFee); const domainHash = hashDomainLabel(domain); expect(await zns.registry.exists(domainHash)).to.be.true; @@ -165,7 +169,7 @@ export const registerSubdomainBulk = async ( const [price, stakeFee] = await zns.curvePricer.getPriceAndFee(parents[index], subdomain, true); const protocolFee = await zns.curvePricer.getFeeForPrice(ethers.ZeroHash, price + stakeFee); - expect(balanceAfter).to.be.eq(balanceBefore - price - stakeFee - protocolFee); + expect(balanceBefore - balanceAfter).to.be.eq(price + stakeFee + protocolFee); } @@ -173,4 +177,4 @@ export const registerSubdomainBulk = async ( index++; } -}; \ No newline at end of file +}; From c757485f11924dd4fe3bf069fcd1c18456b63207 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 24 Jun 2025 17:17:55 -0700 Subject: [PATCH 083/114] fix upgrade scripts --- src/upgrade/db.ts | 2 +- src/upgrade/scripts/get-implementations.ts | 4 ++-- src/upgrade/upgrade.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/upgrade/db.ts b/src/upgrade/db.ts index e2588775d..f3f89a03e 100644 --- a/src/upgrade/db.ts +++ b/src/upgrade/db.ts @@ -43,7 +43,7 @@ export const updateDbAndVerifyAll = async ( newDbVersion, }); - if (hre.network.name !== "hardhat") { + if (hre.network.name !== "hardhat" && contractName !== znsNames.accessController.contract) { await hre.run("verify:verify", { address: implAddress, }); diff --git a/src/upgrade/scripts/get-implementations.ts b/src/upgrade/scripts/get-implementations.ts index fd2da91ab..96f4e66ba 100644 --- a/src/upgrade/scripts/get-implementations.ts +++ b/src/upgrade/scripts/get-implementations.ts @@ -1,11 +1,11 @@ -import { getContractDataForUpgrade } from "../upgrade"; +import { getContractDataForUpgrade, getContractNamesToUpgrade } from "../upgrade"; import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; import * as hre from "hardhat"; export const getProxyImplementations = async () => { const dbAdapter = await getMongoAdapter(); - const contractData = await getContractDataForUpgrade(dbAdapter); + const contractData = await getContractDataForUpgrade(dbAdapter, getContractNamesToUpgrade()); await Object.values(contractData).reduce( async (acc, { contractName, address }) => { diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index a20257716..7c2e393ab 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -36,7 +36,7 @@ export const upgradeZNS = async ({ upgradedContracts[instanceName] = await upgradeZNSContract({ contractName, contractAddress: address, - governor, + governor: governor as SignerWithAddress, logger, }); From a3d0293b8d55edb95b8a20e8b013537b0ff2e6c5 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 24 Jun 2025 17:27:46 -0700 Subject: [PATCH 084/114] add scripts for pausing and checking pause --- src/upgrade/scripts/check-paused.ts | 39 +++++++++++++++++++++++++ src/upgrade/scripts/pause-all.ts | 45 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/upgrade/scripts/check-paused.ts create mode 100644 src/upgrade/scripts/pause-all.ts diff --git a/src/upgrade/scripts/check-paused.ts b/src/upgrade/scripts/check-paused.ts new file mode 100644 index 000000000..19f105ad5 --- /dev/null +++ b/src/upgrade/scripts/check-paused.ts @@ -0,0 +1,39 @@ +import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; +import { getContractDataForUpgrade, getContractNamesToUpgrade } from "../upgrade"; +import * as hre from "hardhat"; +import { getLogger } from "../../deploy/logger/create-logger"; +import { IZNSPausable } from "../../../typechain"; + + +const checkPaused = async () => { + const logger = getLogger(); + const dbAdapter = await getMongoAdapter(logger); + const contractData = await getContractDataForUpgrade(dbAdapter, getContractNamesToUpgrade()); + + for (const { contractName, address } of contractData) { + const factory = await hre.ethers.getContractFactory(`${contractName}Pausable`); + const contract = factory.attach(address) as IZNSPausable; + + if (typeof contract.paused === "function") { + const isPaused = await contract.paused(); + console.log(`${contractName} at ${address} is ${isPaused ? "paused" : "not paused"}`); + } else { + console.warn(`${contractName} does not have a paused() function`); + } + } +}; + +checkPaused() + .then(() => { + const logger = getLogger(); + logger.info("Paused status check completed successfully."); + process.exit(0); + }) + .catch(error => { + const logger = getLogger(); + logger.error(` + Error checking paused status: ${error.message} + Stack: ${error.stack} + `); + process.exit(1); + }); diff --git a/src/upgrade/scripts/pause-all.ts b/src/upgrade/scripts/pause-all.ts new file mode 100644 index 000000000..c26102c05 --- /dev/null +++ b/src/upgrade/scripts/pause-all.ts @@ -0,0 +1,45 @@ +import * as hre from "hardhat"; +import { getLogger } from "../../deploy/logger/create-logger"; +import { getContractDataForUpgrade, getContractNamesToUpgrade } from "../upgrade"; +import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; +import { IZNSPausable } from "../../../typechain"; + + +const pauseAllContracts = async () => { + const [governor] = await hre.ethers.getSigners(); + const logger = getLogger(); + + logger.info(`Governor acquired as ${governor.address}`); + + const dbAdapter = await getMongoAdapter(logger); + const contractData = await getContractDataForUpgrade(dbAdapter, getContractNamesToUpgrade()); + + for (const { contractName, address } of contractData) { + const factory = await hre.ethers.getContractFactory(`${contractName}Pausable`); + const contract = factory.attach(address) as IZNSPausable; + + if (typeof contract.pause === "function") { + logger.info(`Pausing ${contractName} at ${address}`); + const tx = await contract.connect(governor).pause(); + await tx.wait(2); + logger.info(`${contractName} paused successfully`); + } else { + logger.warn(`${contractName} does not have a pause function`); + } + } +}; + +pauseAllContracts() + .then(() => { + const logger = getLogger(); + logger.info("All contracts paused successfully."); + process.exit(0); + }) + .catch(error => { + const logger = getLogger(); + logger.error(` + Error pausing contracts: ${error.message} + Stack: ${error.stack} + `); + process.exit(1); + }); From 0288a6ddb8e02d4068af9dbbe2fe171cc6da4a04 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 24 Jun 2025 17:28:07 -0700 Subject: [PATCH 085/114] remove redundant import --- test/DeployCampaign.integration.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/DeployCampaign.integration.test.ts b/test/DeployCampaign.integration.test.ts index 9fb2a93c6..65bedf5cb 100644 --- a/test/DeployCampaign.integration.test.ts +++ b/test/DeployCampaign.integration.test.ts @@ -15,7 +15,7 @@ import { registerRootDomainBulk, registerSubdomainBulk, } from "./helpers/deploy-helpers"; -import { Defender } from "@openzeppelin/defender-sdk"; + describe("DeployCampaign - Integration", () => { // Minters From 9afbeea9d612f8eccc3e29886614a846ab17bd90 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Wed, 25 Jun 2025 12:41:55 -0700 Subject: [PATCH 086/114] add scripts to renounce roles for deployer acc after the upgrade --- src/upgrade/scripts/renounce-roles.ts | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/upgrade/scripts/renounce-roles.ts diff --git a/src/upgrade/scripts/renounce-roles.ts b/src/upgrade/scripts/renounce-roles.ts new file mode 100644 index 000000000..c5eb205cd --- /dev/null +++ b/src/upgrade/scripts/renounce-roles.ts @@ -0,0 +1,52 @@ +import * as hre from "hardhat"; +import { getLogger } from "../../deploy/logger/create-logger"; +import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; +import { znsNames } from "../../deploy/missions/contracts/names"; +import { IContractDbData } from "../../deploy/db/types"; +import { ZNSAccessController } from "../../../typechain"; + + +const renounceRoles = async () => { + const [ deployer ] = await hre.ethers.getSigners(); + + const logger = getLogger(); + const dbAdapter = await getMongoAdapter(logger); + + const { + address: accessControllerAddress, + } = await dbAdapter.getContract(znsNames.accessController.contract) as IContractDbData; + + const accessController = await hre.ethers.getContractAt( + znsNames.accessController.contract, + accessControllerAddress + ) as unknown as ZNSAccessController; + + const adminRole = await accessController.GOVERNOR_ROLE(); + const governorRole = await accessController.ADMIN_ROLE(); + + logger.info(`Renouncing ADMIN_ROLE for ${deployer.address}`); + const tx1 = await accessController.renounceRole(adminRole, deployer.address); + await tx1.wait(2); + + logger.info(`Renouncing GOVERNOR_ROLE for ${deployer.address}`); + const tx2 = await accessController.renounceRole(governorRole, deployer.address); + await tx2.wait(2); + + const isAdmin = await accessController.isAdmin(deployer.address); + const isGovernor = await accessController.isGovernor(deployer.address); + + if (isAdmin || isGovernor) { + throw new Error(`Failed to renounce roles. isAdmin: ${isAdmin}, isGovernor: ${isGovernor}`); + } +}; + + +renounceRoles() + .then(() => { + getLogger().info("Roles renounced successfully."); + process.exit(0); + }) + .catch(e => { + getLogger().error(`Error renouncing roles: ${e.message}, stack: ${e.stack}`); + process.exit(1); + }); From d26819cc24985e112461a79365107e26496a07d0 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Thu, 26 Jun 2025 12:45:09 -0700 Subject: [PATCH 087/114] small fixes for domain data scripts --- src/utils/migration/01_validation.ts | 2 +- src/utils/migration/zns-contract-data.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 9afe1f963..4db573f88 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -77,5 +77,5 @@ main() .then(() => process.exit(0)) .catch(error => { console.error(error); - process.exitCode = 1; + process.exit(1); }); diff --git a/src/utils/migration/zns-contract-data.ts b/src/utils/migration/zns-contract-data.ts index 7e10f4150..8b0c0cc16 100644 --- a/src/utils/migration/zns-contract-data.ts +++ b/src/utils/migration/zns-contract-data.ts @@ -28,7 +28,7 @@ export const getZNS = async ( if (env === "prod") { return contract.name === znsNames.meowToken.contract; } else { - contract.name === znsNames.meowToken.contractMock; + return contract.name === znsNames.meowToken.contractMock; } }); @@ -59,4 +59,4 @@ export const getZNS = async ( } return znsCache; -}; \ No newline at end of file +}; From 01cd56ccf041e98e89a5c8bbb6f3c89621068f82 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Thu, 26 Jun 2025 12:45:46 -0700 Subject: [PATCH 088/114] avoid redundant checks --- src/utils/migration/validate.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index 78e450747..e06e5aa58 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -1,9 +1,9 @@ - import { ZeroAddress, ZeroHash } from "ethers"; import { Domain } from "./types"; import { IDistributionConfig, IZNSContracts } from "../../../test/helpers/types"; import assert from "assert"; + export const validateDomain = async ( domain : Domain, zns : IZNSContracts, @@ -59,12 +59,14 @@ export const validateDomain = async ( Subgraph: ${domain.paymentType ?? 0n} ` ); - assert.equal(distrConfig.pricerContract.toLowerCase(), domain.pricerContract?.toLowerCase() ?? ZeroAddress, - `Domain ${domain.id} has different pricer contracts. - Contract: ${distrConfig.pricerContract.toLowerCase()} - Subgraph: ${domain.pricerContract?.toLowerCase() ?? ZeroAddress} - ` - ); + + // Not important. Could be a bug in the subgraph + // assert.equal(distrConfig.pricerContract.toLowerCase(), domain.pricerContract?.toLowerCase() ?? ZeroAddress, + // `Domain ${domain.id} has different pricer contracts. + // Contract: ${distrConfig.pricerContract.toLowerCase()} + // Subgraph: ${domain.pricerContract?.toLowerCase() ?? ZeroAddress} + // ` + // ); if (domain.isWorld) { assert.equal(domain.parentHash, ZeroHash, `Domain ${domain.id} 'isWorld' is true, but has parent hash`); @@ -73,8 +75,9 @@ export const validateDomain = async ( } else { // Because we do not delete from the subgraph store on revoke, the domain is always present // even if `isRevoked` is true - assert.ok(!!domain.parent, `Domain ${domain.id} 'isWorld' is false, but 'parent' is undefined`); + // Not important. Could be a bug in the subgraph + // assert.ok(!!domain.parent, `Domain ${domain.id} 'isWorld' is false, but 'parent' is undefined`); assert.notEqual(domain.parentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'parentHash' is 0x0`); assert.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); } -}; \ No newline at end of file +}; From 7fa6140078d5b889f4e8ddedc2c1dd1d69aacaec Mon Sep 17 00:00:00 2001 From: James Earle Date: Fri, 27 Jun 2025 16:00:23 -0700 Subject: [PATCH 089/114] better resolution on parentHash, check both props in case revoked domain --- debug.txt | 140 ++++++++++++++++++++++++ hardhat.config.ts | 2 +- src/utils/migration/01_validation.ts | 15 ++- src/utils/migration/subgraph/queries.ts | 1 + src/utils/migration/validate.ts | 27 ++++- 5 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 debug.txt diff --git a/debug.txt b/debug.txt new file mode 100644 index 000000000..2aa7db047 --- /dev/null +++ b/debug.txt @@ -0,0 +1,140 @@ +yarn run v1.22.19 +$ C:\Users\j_ear\Documents\Code\zNS\node_modules\.bin\hardhat run src/utils/migration/01_validation.ts --network mainnet +Found 3601 domains +Processed 1 domains +Processed 2 domains +Processed 3 domains +Processed 4 domains +Processed 5 domains +Processed 6 domains +Processed 7 domains +Processed 8 domains +Processed 9 domains +Processed 10 domains +Processed 11 domains +Processed 12 domains +Processed 13 domains +Processed 14 domains +Processed 15 domains +Processed 16 domains +Processed 17 domains +Processed 18 domains +Processed 19 domains +Processed 20 domains +Processed 21 domains +Processed 22 domains +Processed 23 domains +Processed 24 domains +Processed 25 domains +Processed 26 domains +Processed 27 domains +Processed 28 domains +Processed 29 domains +Processed 30 domains +Processed 31 domains +Processed 32 domains +Processed 33 domains +Processed 34 domains +Processed 35 domains +Processed 36 domains +Processed 37 domains +Processed 38 domains +Processed 39 domains +Processed 40 domains +Processed 41 domains +Processed 42 domains +Processed 43 domains +Processed 44 domains +Processed 45 domains +Processed 46 domains +Processed 47 domains +Processed 48 domains +Processed 49 domains +Processed 50 domains +Processed 51 domains +Processed 52 domains +Processed 53 domains +Processed 54 domains +Processed 55 domains +Processed 56 domains +Processed 57 domains +Processed 58 domains +Processed 59 domains +Processed 60 domains +Processed 61 domains +Processed 62 domains +Processed 63 domains +Processed 64 domains +Processed 65 domains +Processed 66 domains +Processed 67 domains +Processed 68 domains +Processed 69 domains +Processed 70 domains +Processed 71 domains +Processed 72 domains +Processed 73 domains +Processed 74 domains +Processed 75 domains +Processed 76 domains +Processed 77 domains +Processed 78 domains +Processed 79 domains +Processed 80 domains +Processed 81 domains +Processed 82 domains +Processed 83 domains +Processed 84 domains +Processed 85 domains +Processed 86 domains +Processed 87 domains +Processed 88 domains +Processed 89 domains +Processed 90 domains +Processed 91 domains +Processed 92 domains +Processed 93 domains +Processed 94 domains +Processed 95 domains +Processed 96 domains +Processed 97 domains +Processed 98 domains +Processed 99 domains +Processed 100 domains +Processed 101 domains +Processed 102 domains +Processed 103 domains +Processed 104 domains +Processed 105 domains +Processed 106 domains +Processed 107 domains +Processed 108 domains +Processed 109 domains +Processed 110 domains +Processed 111 domains +Processed 112 domains +Processed 113 domains +Processed 114 domains +Processed 115 domains +Processed 116 domains +Processed 117 domains +Processed 118 domains +Processed 119 domains +Processed 120 domains +Processed 121 domains +Processed 122 domains +Processed 123 domains +Processed 124 domains +Processed 125 domains +Processed 126 domains +Processed 127 domains +Processed 128 domains +Processed 129 domains +Processed 130 domains +Processed 131 domains +Processed 132 domains +Processed 133 domains +Processed 134 domains +Processed 135 domains +Processed 136 domains +Processed 137 domains diff --git a/hardhat.config.ts b/hardhat.config.ts index e93eb1356..94070d91b 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -103,7 +103,7 @@ const config : HardhatUserConfig = { url: `${process.env.MAINNET_RPC_URL}`, accounts: [ // Read only - // `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet + `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet ], gasPrice: 80000000000, }, diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 4db573f88..c4da77453 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -6,7 +6,18 @@ import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; - +/** + * Reqiuired .env vars + * - MAINNET_RPC_URL + * - MONGO_DB_URI - For read only access to mainnet contracts + * - MONGO_DB_NAME + * - MONGO_DB_VERSION + * - MONGO_DB_URI_WRITE - For writing valid collections to a separate database + * - MONGO_DB_NAME_WRITE + * + * // env-level? + * + */ const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); @@ -41,8 +52,6 @@ const main = async () => { let index = 0; for(const domain of [...roots, ...subs]) { try { - await validateDomain(domain, zns); - if (domain.isWorld) { validRoots.push({ ...domain } as Domain); } else { diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index e5818a226..a7cf78249 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -80,6 +80,7 @@ export const getDomains = gql` label depth isWorld + parentHash } accessType pricerContract diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index e06e5aa58..a28491ae2 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -9,6 +9,29 @@ export const validateDomain = async ( zns : IZNSContracts, ) => { // For speed in processing we group promises together + // need to know factually it was a revoked parent for certain subdomains + // check phash for existence, if domain was revoked, add to appropriate coll with 0x0 owner + // in transfer script check if owner is 0 then dont transfer + let resolvedParentHash; + + if (domain.parent && domain.parent.id) { + resolvedParentHash = domain.parent.id; + } else if (domain.parentHash) { + resolvedParentHash = domain.parentHash; + } + + assert.ok( + resolvedParentHash || domain.depth === 0, + `Subdomain with no parent information found + Label: ${domain.label}, + DomainHash: ${domain.id}, + TokenId: ${domain.tokenId}, + Owner: ${domain.owner.id}` + ) + + // this check gives type safety downstream + if (!resolvedParentHash) throw Error("shouldnt ever hit this error") + const promises = [ zns.registry.getDomainOwner(domain.id), zns.domainToken.ownerOf(domain.tokenId), @@ -69,7 +92,7 @@ export const validateDomain = async ( // ); if (domain.isWorld) { - assert.equal(domain.parentHash, ZeroHash, `Domain ${domain.id} 'isWorld' is true, but has parent hash`); + assert.equal(resolvedParentHash, ZeroHash, `Domain ${domain.id} 'isWorld' is true, but has parent hash ${resolvedParentHash}`); assert.ok(!(!!domain.parent), `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`); assert.ok(domain.depth === 0, `Domain ${domain.id} 'isWorld' is true, but 'depth' is not 0`); } else { @@ -77,7 +100,7 @@ export const validateDomain = async ( // even if `isRevoked` is true // Not important. Could be a bug in the subgraph // assert.ok(!!domain.parent, `Domain ${domain.id} 'isWorld' is false, but 'parent' is undefined`); - assert.notEqual(domain.parentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'parentHash' is 0x0`); + assert.notEqual(resolvedParentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'resolvedParentHash' is 0x0`); assert.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); } }; From 98143494a5c04b2792218763bb94876a2a0eadbd Mon Sep 17 00:00:00 2001 From: James Earle Date: Fri, 27 Jun 2025 16:01:59 -0700 Subject: [PATCH 090/114] remove debug script for testing --- debug.txt | 140 ------------------------------------------------------ 1 file changed, 140 deletions(-) delete mode 100644 debug.txt diff --git a/debug.txt b/debug.txt deleted file mode 100644 index 2aa7db047..000000000 --- a/debug.txt +++ /dev/null @@ -1,140 +0,0 @@ -yarn run v1.22.19 -$ C:\Users\j_ear\Documents\Code\zNS\node_modules\.bin\hardhat run src/utils/migration/01_validation.ts --network mainnet -Found 3601 domains -Processed 1 domains -Processed 2 domains -Processed 3 domains -Processed 4 domains -Processed 5 domains -Processed 6 domains -Processed 7 domains -Processed 8 domains -Processed 9 domains -Processed 10 domains -Processed 11 domains -Processed 12 domains -Processed 13 domains -Processed 14 domains -Processed 15 domains -Processed 16 domains -Processed 17 domains -Processed 18 domains -Processed 19 domains -Processed 20 domains -Processed 21 domains -Processed 22 domains -Processed 23 domains -Processed 24 domains -Processed 25 domains -Processed 26 domains -Processed 27 domains -Processed 28 domains -Processed 29 domains -Processed 30 domains -Processed 31 domains -Processed 32 domains -Processed 33 domains -Processed 34 domains -Processed 35 domains -Processed 36 domains -Processed 37 domains -Processed 38 domains -Processed 39 domains -Processed 40 domains -Processed 41 domains -Processed 42 domains -Processed 43 domains -Processed 44 domains -Processed 45 domains -Processed 46 domains -Processed 47 domains -Processed 48 domains -Processed 49 domains -Processed 50 domains -Processed 51 domains -Processed 52 domains -Processed 53 domains -Processed 54 domains -Processed 55 domains -Processed 56 domains -Processed 57 domains -Processed 58 domains -Processed 59 domains -Processed 60 domains -Processed 61 domains -Processed 62 domains -Processed 63 domains -Processed 64 domains -Processed 65 domains -Processed 66 domains -Processed 67 domains -Processed 68 domains -Processed 69 domains -Processed 70 domains -Processed 71 domains -Processed 72 domains -Processed 73 domains -Processed 74 domains -Processed 75 domains -Processed 76 domains -Processed 77 domains -Processed 78 domains -Processed 79 domains -Processed 80 domains -Processed 81 domains -Processed 82 domains -Processed 83 domains -Processed 84 domains -Processed 85 domains -Processed 86 domains -Processed 87 domains -Processed 88 domains -Processed 89 domains -Processed 90 domains -Processed 91 domains -Processed 92 domains -Processed 93 domains -Processed 94 domains -Processed 95 domains -Processed 96 domains -Processed 97 domains -Processed 98 domains -Processed 99 domains -Processed 100 domains -Processed 101 domains -Processed 102 domains -Processed 103 domains -Processed 104 domains -Processed 105 domains -Processed 106 domains -Processed 107 domains -Processed 108 domains -Processed 109 domains -Processed 110 domains -Processed 111 domains -Processed 112 domains -Processed 113 domains -Processed 114 domains -Processed 115 domains -Processed 116 domains -Processed 117 domains -Processed 118 domains -Processed 119 domains -Processed 120 domains -Processed 121 domains -Processed 122 domains -Processed 123 domains -Processed 124 domains -Processed 125 domains -Processed 126 domains -Processed 127 domains -Processed 128 domains -Processed 129 domains -Processed 130 domains -Processed 131 domains -Processed 132 domains -Processed 133 domains -Processed 134 domains -Processed 135 domains -Processed 136 domains -Processed 137 domains From 4c87e75ac257f46d057149e61a94cfe4f1024552 Mon Sep 17 00:00:00 2001 From: James Earle Date: Sat, 28 Jun 2025 16:33:26 -0700 Subject: [PATCH 091/114] update query for additional parent information for domains at multiple depths --- src/utils/migration/01_validation.ts | 8 +++++--- src/utils/migration/subgraph/queries.ts | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index c4da77453..6d41a8445 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -14,9 +14,6 @@ import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; * - MONGO_DB_VERSION * - MONGO_DB_URI_WRITE - For writing valid collections to a separate database * - MONGO_DB_NAME_WRITE - * - * // env-level? - * */ const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); @@ -40,7 +37,11 @@ const main = async () => { // Doing this creates strong typing and extensibility that allows // the below `insertMany` calls to add properties to the object for `_id` const roots = rootDomainObjects.map(d => d as Domain); + console.log(`Found ${roots.length} root domains`); + const subs = subdomainObjects.map(d => d as Domain); + console.log(`Found ${subs.length} subdomains`); + const dbName = process.env.MONGO_DB_NAME_WRITE; if (!dbName) throw Error("Missing MONGO_DB_NAME_WRITE environment variable"); @@ -52,6 +53,7 @@ const main = async () => { let index = 0; for(const domain of [...roots, ...subs]) { try { + await validateDomain(domain, zns); if (domain.isWorld) { validRoots.push({ ...domain } as Domain); } else { diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index a7cf78249..da7ea5f70 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -25,6 +25,20 @@ export const getUsersAndDomains = gql` parent { id label + tokenId + parentHash + parent { + id + label + tokenId + parentHash + parent { + id + label + tokenId + parentHash + } + } } accessType pricerContract From cca073a8e75bb998df37cbc3d609cfe68611c356 Mon Sep 17 00:00:00 2001 From: James Earle Date: Tue, 1 Jul 2025 11:59:58 -0700 Subject: [PATCH 092/114] update query and rerun script, additional comment on needed vars --- src/utils/migration/01_validation.ts | 20 +++--- src/utils/migration/subgraph/queries.ts | 96 +++++++------------------ src/utils/migration/types.ts | 3 +- src/utils/migration/validate.ts | 9 --- 4 files changed, 39 insertions(+), 89 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 6d41a8445..630ee480f 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,7 +1,7 @@ import * as hre from "hardhat"; import { getDomains } from "./subgraph"; import { Domain, InvalidDomain } from "./types"; -import { getDBAdapter } from "./database"; +import { getDBAdapter, getZNSFromDB } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; @@ -14,6 +14,7 @@ import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; * - MONGO_DB_VERSION * - MONGO_DB_URI_WRITE - For writing valid collections to a separate database * - MONGO_DB_NAME_WRITE + * - ENV_LEVEL - Should be set to `prod` with **READ ONLY** private key in hardhat to read mainnet contracts */ const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); @@ -35,14 +36,13 @@ const main = async () => { const invalidDomains : Array = []; // Doing this creates strong typing and extensibility that allows - // the below `insertMany` calls to add properties to the object for `_id` + // the below `insertMany` calls to add properties to the object for `_id` properly const roots = rootDomainObjects.map(d => d as Domain); console.log(`Found ${roots.length} root domains`); const subs = subdomainObjects.map(d => d as Domain); console.log(`Found ${subs.length} subdomains`); - const dbName = process.env.MONGO_DB_NAME_WRITE; if (!dbName) throw Error("Missing MONGO_DB_NAME_WRITE environment variable"); @@ -53,11 +53,15 @@ const main = async () => { let index = 0; for(const domain of [...roots, ...subs]) { try { - await validateDomain(domain, zns); - if (domain.isWorld) { - validRoots.push({ ...domain } as Domain); - } else { - validSubs.push({ ...domain } as Domain); + // Revoked domains are kept in the subgraph for data integrity + // but will not match any onchain data, so we can skip + if (!domain.isRevoked) { + await Promise.all([validateDomain(domain, zns)]); + if (domain.isWorld) { + validRoots.push({ ...domain } as Domain); + } else { + validSubs.push({ ...domain } as Domain); + } } } catch (e) { // For debugging we keep invalid domains rather than throw errors diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index da7ea5f70..d53306788 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -1,68 +1,5 @@ import { gql } from "@apollo/client/core"; -export const getUsersAndDomains = gql` - query UserDomains($first: Int!, $skip: Int!) { - users(first: $first, skip: $skip) { - id - domains { - id - minter { - id - } - owner { - id - } - domainToken { - owner { - id - } - } - depth - label - isWorld - address - parentHash - parent { - id - label - tokenId - parentHash - parent { - id - label - tokenId - parentHash - parent { - id - label - tokenId - parentHash - } - } - } - accessType - pricerContract - paymentType - curvePriceConfig { - id - } - fixedPriceConfig { - id - } - subdomainCount - address - tokenId - tokenURI - treasury { - id - beneficiaryAddress - } - creationBlock - } - } - } -`; - export const getDomains = gql` query Domains($first: Int!, $skip: Int!, $isWorld: Boolean!) { domains( @@ -84,17 +21,39 @@ export const getDomains = gql` } depth label - isWorld address + isWorld + isRevoked parentHash - amountPaidStake - amountPaidDirect parent { id label depth isWorld + isRevoked + tokenId + tokenURI parentHash + parent { + id + label + depth + isWorld + isRevoked + tokenId + tokenURI + parentHash + parent { + id + label + depth + isWorld + isRevoked + tokenId + tokenURI + parentHash + } + } } accessType pricerContract @@ -112,11 +71,6 @@ export const getDomains = gql` treasury { id beneficiaryAddress - paymentToken { - id - name - symbol - } } creationBlock creationTimestamp diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts index 2242c5f55..9e5e9693a 100644 --- a/src/utils/migration/types.ts +++ b/src/utils/migration/types.ts @@ -9,9 +9,10 @@ export interface Domain { label : string; isReclaimable : boolean; reclaimableAddress : string; - isWorld : boolean; address : string; parentHash : string; + isWorld : boolean; + isRevoked: boolean; parent : Domain | null; accessType : string; paymentType : string; diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index a28491ae2..3dce28702 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -83,14 +83,6 @@ export const validateDomain = async ( ` ); - // Not important. Could be a bug in the subgraph - // assert.equal(distrConfig.pricerContract.toLowerCase(), domain.pricerContract?.toLowerCase() ?? ZeroAddress, - // `Domain ${domain.id} has different pricer contracts. - // Contract: ${distrConfig.pricerContract.toLowerCase()} - // Subgraph: ${domain.pricerContract?.toLowerCase() ?? ZeroAddress} - // ` - // ); - if (domain.isWorld) { assert.equal(resolvedParentHash, ZeroHash, `Domain ${domain.id} 'isWorld' is true, but has parent hash ${resolvedParentHash}`); assert.ok(!(!!domain.parent), `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`); @@ -99,7 +91,6 @@ export const validateDomain = async ( // Because we do not delete from the subgraph store on revoke, the domain is always present // even if `isRevoked` is true // Not important. Could be a bug in the subgraph - // assert.ok(!!domain.parent, `Domain ${domain.id} 'isWorld' is false, but 'parent' is undefined`); assert.notEqual(resolvedParentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'resolvedParentHash' is 0x0`); assert.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); } From cbd19d860b69910ba409e0328b68ac13ddeef67e Mon Sep 17 00:00:00 2001 From: James Earle Date: Tue, 1 Jul 2025 12:03:21 -0700 Subject: [PATCH 093/114] rerun script, update subgraph query, add additional env var comment --- src/utils/migration/01_validation.ts | 20 +++--- src/utils/migration/subgraph/queries.ts | 96 +++++++------------------ src/utils/migration/types.ts | 3 +- src/utils/migration/validate.ts | 9 --- 4 files changed, 39 insertions(+), 89 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 6d41a8445..630ee480f 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,7 +1,7 @@ import * as hre from "hardhat"; import { getDomains } from "./subgraph"; import { Domain, InvalidDomain } from "./types"; -import { getDBAdapter } from "./database"; +import { getDBAdapter, getZNSFromDB } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; @@ -14,6 +14,7 @@ import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; * - MONGO_DB_VERSION * - MONGO_DB_URI_WRITE - For writing valid collections to a separate database * - MONGO_DB_NAME_WRITE + * - ENV_LEVEL - Should be set to `prod` with **READ ONLY** private key in hardhat to read mainnet contracts */ const main = async () => { const [ migrationAdmin ] = await hre.ethers.getSigners(); @@ -35,14 +36,13 @@ const main = async () => { const invalidDomains : Array = []; // Doing this creates strong typing and extensibility that allows - // the below `insertMany` calls to add properties to the object for `_id` + // the below `insertMany` calls to add properties to the object for `_id` properly const roots = rootDomainObjects.map(d => d as Domain); console.log(`Found ${roots.length} root domains`); const subs = subdomainObjects.map(d => d as Domain); console.log(`Found ${subs.length} subdomains`); - const dbName = process.env.MONGO_DB_NAME_WRITE; if (!dbName) throw Error("Missing MONGO_DB_NAME_WRITE environment variable"); @@ -53,11 +53,15 @@ const main = async () => { let index = 0; for(const domain of [...roots, ...subs]) { try { - await validateDomain(domain, zns); - if (domain.isWorld) { - validRoots.push({ ...domain } as Domain); - } else { - validSubs.push({ ...domain } as Domain); + // Revoked domains are kept in the subgraph for data integrity + // but will not match any onchain data, so we can skip + if (!domain.isRevoked) { + await Promise.all([validateDomain(domain, zns)]); + if (domain.isWorld) { + validRoots.push({ ...domain } as Domain); + } else { + validSubs.push({ ...domain } as Domain); + } } } catch (e) { // For debugging we keep invalid domains rather than throw errors diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index da7ea5f70..d53306788 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -1,68 +1,5 @@ import { gql } from "@apollo/client/core"; -export const getUsersAndDomains = gql` - query UserDomains($first: Int!, $skip: Int!) { - users(first: $first, skip: $skip) { - id - domains { - id - minter { - id - } - owner { - id - } - domainToken { - owner { - id - } - } - depth - label - isWorld - address - parentHash - parent { - id - label - tokenId - parentHash - parent { - id - label - tokenId - parentHash - parent { - id - label - tokenId - parentHash - } - } - } - accessType - pricerContract - paymentType - curvePriceConfig { - id - } - fixedPriceConfig { - id - } - subdomainCount - address - tokenId - tokenURI - treasury { - id - beneficiaryAddress - } - creationBlock - } - } - } -`; - export const getDomains = gql` query Domains($first: Int!, $skip: Int!, $isWorld: Boolean!) { domains( @@ -84,17 +21,39 @@ export const getDomains = gql` } depth label - isWorld address + isWorld + isRevoked parentHash - amountPaidStake - amountPaidDirect parent { id label depth isWorld + isRevoked + tokenId + tokenURI parentHash + parent { + id + label + depth + isWorld + isRevoked + tokenId + tokenURI + parentHash + parent { + id + label + depth + isWorld + isRevoked + tokenId + tokenURI + parentHash + } + } } accessType pricerContract @@ -112,11 +71,6 @@ export const getDomains = gql` treasury { id beneficiaryAddress - paymentToken { - id - name - symbol - } } creationBlock creationTimestamp diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts index 2242c5f55..9e5e9693a 100644 --- a/src/utils/migration/types.ts +++ b/src/utils/migration/types.ts @@ -9,9 +9,10 @@ export interface Domain { label : string; isReclaimable : boolean; reclaimableAddress : string; - isWorld : boolean; address : string; parentHash : string; + isWorld : boolean; + isRevoked: boolean; parent : Domain | null; accessType : string; paymentType : string; diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index a28491ae2..3dce28702 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -83,14 +83,6 @@ export const validateDomain = async ( ` ); - // Not important. Could be a bug in the subgraph - // assert.equal(distrConfig.pricerContract.toLowerCase(), domain.pricerContract?.toLowerCase() ?? ZeroAddress, - // `Domain ${domain.id} has different pricer contracts. - // Contract: ${distrConfig.pricerContract.toLowerCase()} - // Subgraph: ${domain.pricerContract?.toLowerCase() ?? ZeroAddress} - // ` - // ); - if (domain.isWorld) { assert.equal(resolvedParentHash, ZeroHash, `Domain ${domain.id} 'isWorld' is true, but has parent hash ${resolvedParentHash}`); assert.ok(!(!!domain.parent), `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`); @@ -99,7 +91,6 @@ export const validateDomain = async ( // Because we do not delete from the subgraph store on revoke, the domain is always present // even if `isRevoked` is true // Not important. Could be a bug in the subgraph - // assert.ok(!!domain.parent, `Domain ${domain.id} 'isWorld' is false, but 'parent' is undefined`); assert.notEqual(resolvedParentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'resolvedParentHash' is 0x0`); assert.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); } From a523fb75dfcb97cf52aa3eb3fdf53797c3cefce2 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Jul 2025 14:20:36 -0700 Subject: [PATCH 094/114] small fixes for the validation --- hardhat.config.ts | 3 +-- package.json | 3 ++- src/utils/migration/01_validation.ts | 13 +++++++------ src/utils/migration/validate.ts | 16 ++++++++-------- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 94070d91b..56a5c1794 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -98,12 +98,11 @@ const config : HardhatUserConfig = { enabled: false, }, networks: { - // TODO upg: add forking, but make an env var to turn it on/off in the upgrade test mainnet: { url: `${process.env.MAINNET_RPC_URL}`, accounts: [ // Read only - `${process.env.TESTNET_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet + `${process.env.MAINNET_TEST_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet ], gasPrice: 80000000000, }, diff --git a/package.json b/package.json index 651db4b80..c772253e9 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "mongo:stop": "docker-compose stop", "mongo:down": "docker-compose down", "mongo:drop": "ts-node src/utils/drop-db.ts", - "run-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia" + "run-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia", + "get-domain-data": "hardhat run src/utils/migration/01_validation.ts --network mainnet" }, "pre-commit": [ "lint" diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 630ee480f..b541bc032 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -57,11 +57,12 @@ const main = async () => { // but will not match any onchain data, so we can skip if (!domain.isRevoked) { await Promise.all([validateDomain(domain, zns)]); - if (domain.isWorld) { - validRoots.push({ ...domain } as Domain); - } else { - validSubs.push({ ...domain } as Domain); - } + } + + if (domain.isWorld) { + validRoots.push({ ...domain } as Domain); + } else { + validSubs.push({ ...domain } as Domain); } } catch (e) { // For debugging we keep invalid domains rather than throw errors @@ -81,7 +82,7 @@ const main = async () => { await client.dropCollection(SUB_COLL_NAME); await client.collection(SUB_COLL_NAME).insertMany(validSubs); - // Domains that have split ownership will be considered invalid domains + // Domains that have data inconsistencies if (invalidDomains.length > 0) { await client.dropCollection(INVALID_COLL_NAME); await client.collection(INVALID_COLL_NAME).insertMany(invalidDomains); diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index 3dce28702..ef6a337af 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -27,10 +27,10 @@ export const validateDomain = async ( DomainHash: ${domain.id}, TokenId: ${domain.tokenId}, Owner: ${domain.owner.id}` - ) + ); // this check gives type safety downstream - if (!resolvedParentHash) throw Error("shouldnt ever hit this error") + if (!resolvedParentHash) throw Error("shouldnt ever hit this error"); const promises = [ zns.registry.getDomainOwner(domain.id), @@ -70,12 +70,12 @@ export const validateDomain = async ( Subgraph: ${domain.address.toLowerCase()}` ); - assert.equal(distrConfig.accessType, domain.accessType ?? 0n, - `Domain ${domain.id} has different access types. - Contract: ${distrConfig.accessType} - Subgraph: ${domain.accessType ?? 0n} - ` - ); + // assert.equal(distrConfig.accessType, domain.accessType ?? 0n, + // `Domain ${domain.id} has different access types. + // Contract: ${distrConfig.accessType} + // Subgraph: ${domain.accessType ?? 0n} + // ` + // ); assert.equal(distrConfig.paymentType, domain.paymentType ?? 0n, `Domain ${domain.id} has different payment types. Contract: ${distrConfig.paymentType} From d7637dc3173c8c235535a5bab58daa0412e863b1 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Jul 2025 17:00:36 -0700 Subject: [PATCH 095/114] fix for hh config --- hardhat.config.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 56a5c1794..dbd68e50e 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -98,14 +98,14 @@ const config : HardhatUserConfig = { enabled: false, }, networks: { - mainnet: { - url: `${process.env.MAINNET_RPC_URL}`, - accounts: [ - // Read only - `${process.env.MAINNET_TEST_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet - ], - gasPrice: 80000000000, - }, + // mainnet: { + // url: `${process.env.MAINNET_RPC_URL}`, + // accounts: [ + // // Read only + // `${process.env.MAINNET_TEST_PRIVATE_KEY}`, // Commented for CI, uncomment this when using Mainnet + // ], + // gasPrice: 80000000000, + // }, sepolia: { url: `${process.env.SEPOLIA_RPC_URL}`, timeout: 10000000, From ebb701cb0f13d805384711595b659b9e1eaf4827 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 1 Jul 2025 17:05:31 -0700 Subject: [PATCH 096/114] linter fixes --- src/utils/migration/01_validation.ts | 3 ++- src/utils/migration/types.ts | 2 +- src/utils/migration/validate.ts | 14 +++++++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index b541bc032..2fb546b12 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,11 +1,12 @@ import * as hre from "hardhat"; import { getDomains } from "./subgraph"; import { Domain, InvalidDomain } from "./types"; -import { getDBAdapter, getZNSFromDB } from "./database"; +import { getDBAdapter } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; + /** * Reqiuired .env vars * - MAINNET_RPC_URL diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts index 9e5e9693a..69c1a072a 100644 --- a/src/utils/migration/types.ts +++ b/src/utils/migration/types.ts @@ -12,7 +12,7 @@ export interface Domain { address : string; parentHash : string; isWorld : boolean; - isRevoked: boolean; + isRevoked : boolean; parent : Domain | null; accessType : string; paymentType : string; diff --git a/src/utils/migration/validate.ts b/src/utils/migration/validate.ts index ef6a337af..9bd7b841e 100644 --- a/src/utils/migration/validate.ts +++ b/src/utils/migration/validate.ts @@ -1,4 +1,4 @@ -import { ZeroAddress, ZeroHash } from "ethers"; +import { ZeroHash } from "ethers"; import { Domain } from "./types"; import { IDistributionConfig, IZNSContracts } from "../../../test/helpers/types"; import assert from "assert"; @@ -84,14 +84,22 @@ export const validateDomain = async ( ); if (domain.isWorld) { - assert.equal(resolvedParentHash, ZeroHash, `Domain ${domain.id} 'isWorld' is true, but has parent hash ${resolvedParentHash}`); + assert.equal( + resolvedParentHash, + ZeroHash, + `Domain ${domain.id} 'isWorld' is true, but has parent hash ${resolvedParentHash}` + ); assert.ok(!(!!domain.parent), `Domain ${domain.id} 'isWorld' is true, but 'hasParent' is true`); assert.ok(domain.depth === 0, `Domain ${domain.id} 'isWorld' is true, but 'depth' is not 0`); } else { // Because we do not delete from the subgraph store on revoke, the domain is always present // even if `isRevoked` is true // Not important. Could be a bug in the subgraph - assert.notEqual(resolvedParentHash, ZeroHash,`Domain ${domain.id} 'isWorld' is false, but 'resolvedParentHash' is 0x0`); + assert.notEqual( + resolvedParentHash, + ZeroHash, + `Domain ${domain.id} 'isWorld' is false, but 'resolvedParentHash' is 0x0` + ); assert.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); } }; From e5acb457ec2e62effb55952a6c517c5c70140025 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Tue, 22 Jul 2025 16:36:31 -0700 Subject: [PATCH 097/114] compareStorageData does not support appended state variables in upgraded contracts --- src/upgrade/storage-data.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index d625078dc..68fe214ac 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -32,7 +32,7 @@ export const readContractStorage = async ( ) : Promise => { const newAcc = await acc; - if (!type.includes("mapping") && !type.includes("array")) { + if (!type.includes("mapping") && !type.includes("array") && label.slice(0, 1) !== "_") { try { const value = await contractObj[(label as keyof ZNSContract)](); @@ -57,6 +57,8 @@ export const compareStorageData = ( (acc : ContractStorageDiff | undefined, stateVar, idx) => { const [key, value] = Object.entries(stateVar)[0]; + if (!dataBefore[idx]) return acc; + if (value !== dataBefore[idx][key]) { console.error( `Mismatch on state var ${key} at idx ${idx}! Prev value: ${dataBefore[idx][key]}, new value: ${value}` From 05aaae9c24019e49de11d314caa7bcb0c2a49894 Mon Sep 17 00:00:00 2001 From: James Earle Date: Wed, 23 Jul 2025 11:48:18 -0700 Subject: [PATCH 098/114] Fix - GraphQL response Errors from Subgraph are not handled --- .circleci/config.yml | 2 +- package.json | 2 +- src/utils/migration/subgraph/index.ts | 4 +- yarn.lock | 175 +++++++++++++++++--------- 4 files changed, 119 insertions(+), 64 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a3e13bdae..2110ed4d8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ orbs: defaults: &defaults working_directory: ~/repo docker: - - image: cimg/node:18.15.0 + - image: cimg/node:18.18.0 - image: mongo:7.0.0-rc5-jammy jobs: diff --git a/package.json b/package.json index c772253e9..01aeae767 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@types/graphql": "^14.5.0", "@types/mocha": "^9.1.0", "@types/node": "^18.15.11", - "@zero-tech/eslint-config-cpt": "0.2.7", + "@zero-tech/eslint-config-cpt": "0.2.8", "@zero-tech/ztoken": "2.0.0", "chai": "^4.3.10", "eslint": "^8.37.0", diff --git a/src/utils/migration/subgraph/index.ts b/src/utils/migration/subgraph/index.ts index 1438e835c..fd7d66466 100644 --- a/src/utils/migration/subgraph/index.ts +++ b/src/utils/migration/subgraph/index.ts @@ -18,7 +18,7 @@ export const getDomains = async (isWorld : boolean) => { }, }); - if (result.error) throw Error(`Error in graph query: ${result.error}`); + if (result.errors) throw Error(`Error in graph query: ${result.errors}`); const domains = []; @@ -45,6 +45,8 @@ export const getDomains = async (isWorld : boolean) => { isWorld, }, }); + + if (result.errors) throw Error(`Error in graph query: ${result.errors}`); } return domains; diff --git a/yarn.lock b/yarn.lock index 1545f4b38..dd4f93e12 100644 --- a/yarn.lock +++ b/yarn.lock @@ -198,14 +198,16 @@ resolved "https://registry.yarnpkg.com/@ensdomains/resolver/-/resolver-0.2.4.tgz#c10fe28bf5efbf49bff4666d909aed0265efbc89" integrity sha512-bvaTH34PMCbv6anRa9I/0zjLJgY4EuznbEMgbV77JBCQ9KNC46rzi0avuxpOfu+xDjPEtSFGqVEOr5GlUSGudA== -"@es-joy/jsdoccomment@~0.37.0": - version "0.37.1" - resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.37.1.tgz#fa32a41ba12097452693343e09ad4d26d157aedd" - integrity sha512-5vxWJ1gEkEF0yRd0O+uK6dHJf7adrxwQSX8PuRiPfFSAbNLnY0ZJfXaZucoz14Jj2N11xn2DnlEPwWRpYpvRjg== +"@es-joy/jsdoccomment@~0.50.2": + version "0.50.2" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.50.2.tgz#707768f0cb62abe0703d51aa9086986d230a5d5c" + integrity sha512-YAdE/IJSpwbOTiaURNCKECdAwqrJuFiZhylmesBcIRawtYKnBR2wxPhoIewMg+Yu+QuYvHfJNReWpoxGBKOChA== dependencies: - comment-parser "1.3.1" - esquery "^1.5.0" - jsdoc-type-pratt-parser "~4.0.0" + "@types/estree" "^1.0.6" + "@typescript-eslint/types" "^8.11.0" + comment-parser "1.4.1" + esquery "^1.6.0" + jsdoc-type-pratt-parser "~4.1.0" "@eslint-community/eslint-utils@^4.2.0": version "4.4.0" @@ -1839,6 +1841,11 @@ dependencies: "@types/node" "*" +"@types/estree@^1.0.6": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e" + integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w== + "@types/form-data@0.0.33": version "0.0.33" resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-0.0.33.tgz#c9ac85b2a5fd18435b8c85d9ecb50e6d6c893ff8" @@ -2033,6 +2040,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== +"@typescript-eslint/types@^8.11.0": + version "8.38.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.38.0.tgz#297351c994976b93c82ac0f0e206c8143aa82529" + integrity sha512-wzkUfX3plUqij4YwWaJyqhiPE5UCRVlFpKn1oCRn2O1bJ592XxWJj8ROQ3JD5MYXLORW84063z3tZTb/cs4Tyw== + "@typescript-eslint/typescript-estree@5.62.0": version "5.62.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" @@ -2101,10 +2113,10 @@ dependencies: tslib "^2.3.0" -"@zero-tech/eslint-config-cpt@0.2.7": - version "0.2.7" - resolved "https://registry.yarnpkg.com/@zero-tech/eslint-config-cpt/-/eslint-config-cpt-0.2.7.tgz#ec1d94848737863a8c9b9e226fd586b7f8a97346" - integrity sha512-reFmMkcPBjkQgq2hD5FDWfuGIl4dWvKoIdigq3zS375QNLAqQwuY2EPK2RRLQw7Qcw8nTvPFfv4Gwu2fm8yVfQ== +"@zero-tech/eslint-config-cpt@0.2.8": + version "0.2.8" + resolved "https://registry.yarnpkg.com/@zero-tech/eslint-config-cpt/-/eslint-config-cpt-0.2.8.tgz#f4b69187e65f61d519c77755f5ae0963efeb5c9d" + integrity sha512-i5v/tl6Nv23gM8HGXJiiYh5NaL1guARDtka2cx7T6K7g41zd9NZPynHQeGHHtv3zvcFG/hP5J8uS7O3k4DpplA== dependencies: "@typescript-eslint/eslint-plugin" "^5.57.1" "@typescript-eslint/parser" "^5.57.1" @@ -2112,7 +2124,7 @@ eslint-config-airbnb "^19.0.4" eslint-config-airbnb-base "^15.0.0" eslint-plugin-import "^2.27.5" - eslint-plugin-jsdoc "^40.1.1" + eslint-plugin-jsdoc "^50.3.0" eslint-plugin-prefer-arrow "^1.2.3" typescript "^5.0.2" @@ -2174,6 +2186,11 @@ acorn-walk@^8.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.1.tgz#2f10f5b69329d90ae18c58bf1fa8fccd8b959a43" integrity sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw== +acorn@^8.15.0: + version "8.15.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" + integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== + acorn@^8.4.1, acorn@^8.9.0: version "8.11.2" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" @@ -2376,6 +2393,11 @@ archy@~1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== +are-docs-informative@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963" + integrity sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig== + are-we-there-yet@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" @@ -3269,10 +3291,10 @@ commander@^9.4.0: resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== -comment-parser@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b" - integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA== +comment-parser@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc" + integrity sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg== common-ancestor-path@^1.0.1: version "1.0.1" @@ -3493,6 +3515,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.1.tgz#e5a8bc6cbc4c6cd3e64308b0693a3d4fa550189b" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -3898,18 +3927,21 @@ eslint-plugin-import@^2.27.5: semver "^6.3.1" tsconfig-paths "^3.15.0" -eslint-plugin-jsdoc@^40.1.1: - version "40.3.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-40.3.0.tgz#75a91ab71c41bb797db05a32d9528ce3ab613e90" - integrity sha512-EhCqpzRkxoT2DUB4AnrU0ggBYvTh3bWrLZzQTupq6vSVE6XzNwJVKsOHa41GCoevnsWMBNmoDVjXWGqckjuG1g== +eslint-plugin-jsdoc@^50.3.0: + version "50.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-50.8.0.tgz#a8d192ccca26df368a2fbaff17c9dddefacd773f" + integrity sha512-UyGb5755LMFWPrZTEqqvTJ3urLz1iqj+bYOHFNag+sw3NvaMWP9K2z+uIn37XfNALmQLQyrBlJ5mkiVPL7ADEg== dependencies: - "@es-joy/jsdoccomment" "~0.37.0" - comment-parser "1.3.1" - debug "^4.3.4" + "@es-joy/jsdoccomment" "~0.50.2" + are-docs-informative "^0.0.2" + comment-parser "1.4.1" + debug "^4.4.1" escape-string-regexp "^4.0.0" - esquery "^1.5.0" - semver "^7.3.8" - spdx-expression-parse "^3.0.1" + espree "^10.3.0" + esquery "^1.6.0" + parse-imports-exports "^0.2.4" + semver "^7.7.2" + spdx-expression-parse "^4.0.0" eslint-plugin-prefer-arrow@^1.2.3: version "1.2.3" @@ -3937,6 +3969,11 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + eslint@^8.37.0: version "8.56.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" @@ -3981,6 +4018,15 @@ eslint@^8.37.0: strip-ansi "^6.0.1" text-table "^0.2.0" +espree@^10.3.0: + version "10.4.0" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.4.0.tgz#d54f4949d4629005a1fa168d937c3ff1f7e2a837" + integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== + dependencies: + acorn "^8.15.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.2.1" + espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" @@ -4000,13 +4046,20 @@ esprima@^4.0.0, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.2, esquery@^1.5.0: +esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== dependencies: estraverse "^5.1.0" +esquery@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.6.0.tgz#91419234f804d852a82dceec3e16cdc22cf9dae7" + integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg== + dependencies: + estraverse "^5.1.0" + esrecurse@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" @@ -5745,10 +5798,10 @@ js-yaml@4.1.0, js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsdoc-type-pratt-parser@~4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114" - integrity sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ== +jsdoc-type-pratt-parser@~4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz#ff6b4a3f339c34a6c188cbf50a16087858d22113" + integrity sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg== json-buffer@3.0.1: version "3.0.1" @@ -6680,7 +6733,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.2: +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.2, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7423,6 +7476,13 @@ parse-conflict-json@^3.0.0, parse-conflict-json@^3.0.1: just-diff "^6.0.0" just-diff-apply "^5.2.0" +parse-imports-exports@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz#e3fb3b5e264cfb55c25b5dfcbe7f410f8dc4e7af" + integrity sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ== + dependencies: + parse-statements "1.0.11" + parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" @@ -7459,6 +7519,11 @@ parse-json@^7.0.0: lines-and-columns "^2.0.3" type-fest "^3.8.0" +parse-statements@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/parse-statements/-/parse-statements-1.0.11.tgz#8787c5d383ae5746568571614be72b0689584344" + integrity sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA== + path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" @@ -8245,13 +8310,18 @@ semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: +semver@^7.0.0, semver@^7.1.1, semver@^7.1.2, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" +semver@^7.7.2: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + serialize-javascript@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" @@ -8575,6 +8645,14 @@ spdx-expression-parse@^3.0.0, spdx-expression-parse@^3.0.1: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" +spdx-expression-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794" + integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + spdx-license-ids@^3.0.0: version "3.0.16" resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz#a14f64e0954f6e25cc6587bd4f392522db0d998f" @@ -8648,7 +8726,7 @@ string-format@^2.0.0: resolved "https://registry.yarnpkg.com/string-format/-/string-format-2.0.0.tgz#f2df2e7097440d3b65de31b6d40d54c96eaffb9b" integrity sha512-bbEs3scLeYNXLecRRuk6uJxdXUSj6le/8rNPHChIJTn2V79aXVTR1EH2OH5zLKKoz0V02fOUKZZcw01pLUShZA== -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -8666,15 +8744,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" @@ -8733,7 +8802,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -8754,13 +8823,6 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -9623,7 +9685,7 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -9640,15 +9702,6 @@ wrap-ansi@^2.0.0: string-width "^1.0.1" strip-ansi "^3.0.1" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From fbd93d5d00e57d9ff8231ec5ce0c428c5abe4150 Mon Sep 17 00:00:00 2001 From: James Earle Date: Wed, 23 Jul 2025 13:41:00 -0700 Subject: [PATCH 099/114] Fix - Improve error handling and validation for insertMany on MongoDB collections --- src/utils/migration/01_validation.ts | 27 ++++++++++++++++++--------- src/utils/migration/database.ts | 18 +++++++++++++++++- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 2fb546b12..857e9c8c7 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,10 +1,11 @@ import * as hre from "hardhat"; import { getDomains } from "./subgraph"; import { Domain, InvalidDomain } from "./types"; -import { getDBAdapter } from "./database"; +import { getDBAdapter, updateCollection } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; +import { Db } from "mongodb"; /** @@ -74,19 +75,27 @@ const main = async () => { } // Connect to database collection and write user domain data to DB - const client = (await getDBAdapter(uri)).db(dbName); + const client : Db = (await getDBAdapter(uri)).db(dbName); - // To avoid duplicate data, we clear the DB before any inserts - await client.dropCollection(ROOT_COLL_NAME); - await client.collection(ROOT_COLL_NAME).insertMany(validRoots); + await updateCollection( + client, + ROOT_COLL_NAME, + validRoots + ); - await client.dropCollection(SUB_COLL_NAME); - await client.collection(SUB_COLL_NAME).insertMany(validSubs); + await updateCollection( + client, + SUB_COLL_NAME, + validSubs + ); // Domains that have data inconsistencies if (invalidDomains.length > 0) { - await client.dropCollection(INVALID_COLL_NAME); - await client.collection(INVALID_COLL_NAME).insertMany(invalidDomains); + await updateCollection( + client, + INVALID_COLL_NAME, + invalidDomains + ); } }; diff --git a/src/utils/migration/database.ts b/src/utils/migration/database.ts index 2597eb2b2..8f2d2578c 100644 --- a/src/utils/migration/database.ts +++ b/src/utils/migration/database.ts @@ -1,4 +1,4 @@ -import { MongoClient, ServerApiVersion } from "mongodb"; +import { Db, MongoClient, ServerApiVersion, Document } from "mongodb"; export let dbVersion : string; @@ -41,4 +41,20 @@ export const getZNSFromDB = async () => { ).toArray(); return zns; +}; + +export const updateCollection = async ( + client : Db, + collName : string, + documents : Array, +) => { + // To avoid duplicate data, we clear the DB before any inserts + await client.dropCollection(collName); + + const result = await client.collection(collName).insertMany(documents); + const diff = documents.length - result.insertedCount; + + if (diff > 0) { + throw new Error(`Error: Failed to insert ${diff} domains on call to \`insertMany\``); + } }; \ No newline at end of file From 82ae45c467a8baae1823a8ec552446abaf2c7139 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Mon, 28 Jul 2025 12:46:14 -0700 Subject: [PATCH 100/114] remove indentation in readContractStorage --- src/upgrade/storage-data.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts index 68fe214ac..d739d5394 100644 --- a/src/upgrade/storage-data.ts +++ b/src/upgrade/storage-data.ts @@ -32,14 +32,15 @@ export const readContractStorage = async ( ) : Promise => { const newAcc = await acc; - if (!type.includes("mapping") && !type.includes("array") && label.slice(0, 1) !== "_") { - try { - const value = await contractObj[(label as keyof ZNSContract)](); + if (type.includes("mapping") || type.includes("array") || label.slice(0, 1) === "_") + return newAcc; // Skip mappings, arrays and private variables - newAcc.push({ [label]: value }); - } catch (e : unknown) { - logger.debug(`Error on LABEL ${label}: ${(e as Error).message}`); - } + try { + const value = await contractObj[(label as keyof ZNSContract)](); + + newAcc.push({ [label]: value }); + } catch (e : unknown) { + logger.debug(`Error on LABEL ${label}: ${(e as Error).message}`); } return newAcc; From 2477ead8c9a6507c5861bed97125556f7f7ec262 Mon Sep 17 00:00:00 2001 From: James Earle Date: Tue, 5 Aug 2025 11:51:43 -0400 Subject: [PATCH 101/114] two extra env vars for clarity, change names in package.json of scripts, speed up woth less frequent logs when excuting --- .gitignore | 3 ++- package.json | 4 ++-- src/utils/migration/01_validation.ts | 8 +++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index a1e046c7d..7cd75c320 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ node_modules -.env +*.env* coverage coverage.json typechain @@ -7,6 +7,7 @@ typechain-types .idea .vscode dist +*.log # Hardhat files cache diff --git a/package.json b/package.json index 01aeae767..dc46ea1d9 100644 --- a/package.json +++ b/package.json @@ -34,8 +34,8 @@ "mongo:stop": "docker-compose stop", "mongo:down": "docker-compose down", "mongo:drop": "ts-node src/utils/drop-db.ts", - "run-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia", - "get-domain-data": "hardhat run src/utils/migration/01_validation.ts --network mainnet" + "deploy-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia", + "run-validation": "hardhat run src/utils/migration/01_validation.ts --network mainnet" }, "pre-commit": [ "lint" diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 857e9c8c7..0666b6e2a 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -10,6 +10,8 @@ import { Db } from "mongodb"; /** * Reqiuired .env vars + * - SUBGRAPH_URL_DEV - The URL to read from `zns-mainnet-dev` subgraph + * - MAINNET_PRIVATE_KEY - A **READ ONLY** private key to use in validation on chain * - MAINNET_RPC_URL * - MONGO_DB_URI - For read only access to mainnet contracts * - MONGO_DB_NAME @@ -71,7 +73,11 @@ const main = async () => { invalidDomains.push({ message: (e as Error).message, domain }); } - console.log(`Processed ${++index} domains`); + ++index; + + if (index % 50 === 0) { + console.log(`Processed ${index} domains`); + } } // Connect to database collection and write user domain data to DB From 7c9be93d00f0492456cebfd15474a551b976472b Mon Sep 17 00:00:00 2001 From: James Earle Date: Thu, 7 Aug 2025 15:54:45 -0400 Subject: [PATCH 102/114] issue updating db entry for versioning after verifying --- hardhat.config.ts | 22 +++++---- package.json | 3 +- src/upgrade/db.ts | 62 +++++++++++++++++++------- src/upgrade/scripts/execute-upgrade.ts | 2 +- 4 files changed, 64 insertions(+), 25 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index dbd68e50e..fde05a3e4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -98,6 +98,12 @@ const config : HardhatUserConfig = { enabled: false, }, networks: { + // zephyr: { + // url: `${process.env.ZEPHYR_RPC_URL}`, + // accounts: [ + // `${process.env.ZNS_DEPLOYER}` + // ] + // }, // mainnet: { // url: `${process.env.MAINNET_RPC_URL}`, // accounts: [ @@ -109,14 +115,14 @@ const config : HardhatUserConfig = { sepolia: { url: `${process.env.SEPOLIA_RPC_URL}`, timeout: 10000000, - // accounts: [ // Comment out for CI, uncomment this when using Sepolia - // `${process.env.TESTNET_PRIVATE_KEY_A}`, - // `${process.env.TESTNET_PRIVATE_KEY_B}`, - // `${process.env.TESTNET_PRIVATE_KEY_C}`, - // `${process.env.TESTNET_PRIVATE_KEY_D}`, - // `${process.env.TESTNET_PRIVATE_KEY_E}`, - // `${process.env.TESTNET_PRIVATE_KEY_F}`, - // ], + accounts: [ // Comment out for CI, uncomment this when using Sepolia + `${process.env.TESTNET_PRIVATE_KEY_A}`, + // `${process.env.TESTNET_PRIVATE_KEY_B}`, + // `${process.env.TESTNET_PRIVATE_KEY_C}`, + // `${process.env.TESTNET_PRIVATE_KEY_D}`, + `${process.env.TESTNET_PRIVATE_KEY_E}`, + // `${process.env.TESTNET_PRIVATE_KEY_F}`, + ], // // Must have to avoid instead failing as `invalid length for result data` error // throwOnCallFailures: false, // not sure if this even works }, diff --git a/package.json b/package.json index dc46ea1d9..634e287b1 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "mongo:down": "docker-compose down", "mongo:drop": "ts-node src/utils/drop-db.ts", "deploy-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia", - "run-validation": "hardhat run src/utils/migration/01_validation.ts --network mainnet" + "run-validation": "hardhat run src/utils/migration/01_validation.ts --network mainnet", + "upgrade-sepolia": "hardhat run src/upgrade/scripts/execute-upgrade.ts --network sepolia" }, "pre-commit": [ "lint" diff --git a/src/upgrade/db.ts b/src/upgrade/db.ts index f3f89a03e..04adcd384 100644 --- a/src/upgrade/db.ts +++ b/src/upgrade/db.ts @@ -11,6 +11,28 @@ export const updateDbAndVerifyAll = async ( ) => { const logger = getLogger(); + const version = process.env.MONGO_DB_VERSION + + let newDbVersion : string; + + if (version) { + newDbVersion = version; + } else { + newDbVersion = Date.now().toString(); + } + + const newContractsVersion = dbAdapter.getContractsVersionFromFile(); + logger.info( + `Updating DB "${dbAdapter.dbName}" with new version: ${newDbVersion} and contracts version: ${newContractsVersion}` + ); + + // Update the version in the DB to TEMP while processing + const insertResult = await dbAdapter.versions.insertOne({ + type: VERSION_TYPES.temp, + dbVersion: newDbVersion, + contractsVersion: newContractsVersion, + }); + const contractNames = JSON.parse(JSON.stringify(znsNames)); delete contractNames.erc1967Proxy; contractNames.meowToken.contract = process.env.MOCK_MEOW_TOKEN === "true" @@ -19,12 +41,6 @@ export const updateDbAndVerifyAll = async ( const contractData = await getContractDataForUpgrade(dbAdapter, contractNames); - const newDbVersion = Date.now().toString(); - const newContractsVersion = dbAdapter.getContractsVersionFromFile(); - logger.info( - `Updating DB "${dbAdapter.dbName}" with new version: ${newDbVersion} and contracts version: ${newContractsVersion}` - ); - for (const { contractName, address } of contractData) { let implAddress : string | null; if (contractName === znsNames.accessController.contract) { @@ -44,18 +60,34 @@ export const updateDbAndVerifyAll = async ( }); if (hre.network.name !== "hardhat" && contractName !== znsNames.accessController.contract) { - await hre.run("verify:verify", { - address: implAddress, - }); + try { + await hre.run("verify:verify", { + address: implAddress, + }); + } catch (e) { + logger.error(`Verification of ${address} failed with error ${e}`); + } } } - // Update the version in the DB - await dbAdapter.versions.insertOne({ - type: VERSION_TYPES.upgraded, - dbVersion: newDbVersion, - contractsVersion: newContractsVersion, - }); + // Update the version in the DB as UPGRADED + const replaceResult = await dbAdapter.versions.replaceOne( + { + _id: insertResult.insertedId, + }, + { + type: VERSION_TYPES.upgraded, + dbVersion: newDbVersion, + contractsVersion: newContractsVersion, + }, + { + upsert: true + } + ) + + if (replaceResult.matchedCount === 0) { + throw new Error(`Failed to update db data entry for version ${newDbVersion}`); + } logger.info("DB update finished successfully."); }; diff --git a/src/upgrade/scripts/execute-upgrade.ts b/src/upgrade/scripts/execute-upgrade.ts index 45426ca3b..a108e99c6 100644 --- a/src/upgrade/scripts/execute-upgrade.ts +++ b/src/upgrade/scripts/execute-upgrade.ts @@ -7,7 +7,7 @@ const execute = async () => { const logger = getLogger(); const dbAdapter = await getMongoAdapter(logger); - logger.info("Prepairing contract data for the upgrade..."); + logger.info("Preparing contract data for the upgrade..."); const contractData = await getContractDataForUpgrade( dbAdapter, From 7562b23004d2c33c31e82b197cadfd7310ac342b Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Thu, 7 Aug 2025 14:07:47 -0700 Subject: [PATCH 103/114] some bug fixes for db update --- hardhat.config.ts | 4 ++-- src/upgrade/db.ts | 14 +++----------- src/upgrade/scripts/pause-all.ts | 11 +++++++---- src/upgrade/upgrade.ts | 3 ++- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index fde05a3e4..0ed46db55 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -116,11 +116,11 @@ const config : HardhatUserConfig = { url: `${process.env.SEPOLIA_RPC_URL}`, timeout: 10000000, accounts: [ // Comment out for CI, uncomment this when using Sepolia - `${process.env.TESTNET_PRIVATE_KEY_A}`, + `${process.env.TEST_USER_A_KEY}`, // `${process.env.TESTNET_PRIVATE_KEY_B}`, // `${process.env.TESTNET_PRIVATE_KEY_C}`, // `${process.env.TESTNET_PRIVATE_KEY_D}`, - `${process.env.TESTNET_PRIVATE_KEY_E}`, + `${process.env.TEST_USER_E_KEY}`, // `${process.env.TESTNET_PRIVATE_KEY_F}`, ], // // Must have to avoid instead failing as `invalid length for result data` error diff --git a/src/upgrade/db.ts b/src/upgrade/db.ts index 04adcd384..93c038867 100644 --- a/src/upgrade/db.ts +++ b/src/upgrade/db.ts @@ -11,15 +11,7 @@ export const updateDbAndVerifyAll = async ( ) => { const logger = getLogger(); - const version = process.env.MONGO_DB_VERSION - - let newDbVersion : string; - - if (version) { - newDbVersion = version; - } else { - newDbVersion = Date.now().toString(); - } + const newDbVersion = Date.now().toString(); const newContractsVersion = dbAdapter.getContractsVersionFromFile(); logger.info( @@ -81,9 +73,9 @@ export const updateDbAndVerifyAll = async ( contractsVersion: newContractsVersion, }, { - upsert: true + upsert: true, } - ) + ); if (replaceResult.matchedCount === 0) { throw new Error(`Failed to update db data entry for version ${newDbVersion}`); diff --git a/src/upgrade/scripts/pause-all.ts b/src/upgrade/scripts/pause-all.ts index c26102c05..8d7fb8cc7 100644 --- a/src/upgrade/scripts/pause-all.ts +++ b/src/upgrade/scripts/pause-all.ts @@ -19,10 +19,13 @@ const pauseAllContracts = async () => { const contract = factory.attach(address) as IZNSPausable; if (typeof contract.pause === "function") { - logger.info(`Pausing ${contractName} at ${address}`); - const tx = await contract.connect(governor).pause(); - await tx.wait(2); - logger.info(`${contractName} paused successfully`); + const isPaused = await contract.paused(); + if (!isPaused) { + logger.info(`Pausing ${contractName} at ${address}`); + const tx = await contract.connect(governor).pause(); + await tx.wait(2); + logger.info(`${contractName} paused successfully`); + } } else { logger.warn(`${contractName} does not have a pause function`); } diff --git a/src/upgrade/upgrade.ts b/src/upgrade/upgrade.ts index 7c2e393ab..d3ec8c18a 100644 --- a/src/upgrade/upgrade.ts +++ b/src/upgrade/upgrade.ts @@ -112,7 +112,8 @@ export const getContractDataForUpgrade = async ( ) => { const contractData = await acc; - const { address } = await dbAdapter.getContract(contract) as IContractDbData; + const contractDoc = await dbAdapter.getContract(contract, dbAdapter.curVersion) as IContractDbData; + const { address } = contractDoc; contractData.push({ contractName: contract, From da769a01925c4216bfc906548edd2f54b8703c1b Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 8 Aug 2025 13:45:14 -0700 Subject: [PATCH 104/114] update withdrawStake script and make it executable --- src/scripts/withdraw-staked.ts | 45 +++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/scripts/withdraw-staked.ts b/src/scripts/withdraw-staked.ts index 99673e362..4caa3d588 100644 --- a/src/scripts/withdraw-staked.ts +++ b/src/scripts/withdraw-staked.ts @@ -9,11 +9,9 @@ import { ZNSTreasuryPausable } from "../../typechain/contracts/zns-pausable/trea export const withdrawStakedByGovernor = async ({ token, to, - version, } : { token : string; to ?: string; - version ?: IDBVersion | null; }) => { const [ governor ] = await hre.ethers.getSigners(); @@ -23,14 +21,6 @@ export const withdrawStakedByGovernor = async ({ const dbAdapter = await getMongoAdapter(); - if (!version) { - version = await dbAdapter.getUpgradedVersion(); - } - - if (!version) { - throw new Error("Version is undefined"); - } - const contractName = znsNames.treasury.contract; const dbContr = await dbAdapter.getContract( @@ -54,10 +44,37 @@ export const withdrawStakedByGovernor = async ({ recipient, ); - if (hre.network.name !== "hardhat") - await tx.wait( - process.env.CONFIRMATIONS_N ? Number(process.env.CONFIRMATIONS_N) : 2 - ); + await tx.wait( + process.env.CONFIRMATIONS_N ? Number(process.env.CONFIRMATIONS_N) : 2 + ); return tx; }; + +// call the above function with await properly below +void (async () => { + try { + const token = process.env.WITHDRAW_TOKEN_ADDRESS; + const to = process.env.TREASURY_WITHDRAW_RECIPIENT; + + if (!token || !to) { + throw new Error("TOKEN_ADDRESS environment variable is not set"); + } + + const tx = await withdrawStakedByGovernor({ + token, + to, + }); + + console.log(`Withdrawal transaction successful: ${tx.hash}`); + process.exit(0); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + } catch (error : Error) { + console.error( + `Error withdrawing staked tokens: ${error.message} + ${error.stack}` + ); + process.exit(1); + } +})(); From 8d3700a86d2318dcb58f737bd347e3cef04502fe Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 8 Aug 2025 13:46:20 -0700 Subject: [PATCH 105/114] comment out networks in HH config --- hardhat.config.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/hardhat.config.ts b/hardhat.config.ts index 0ed46db55..f6c505800 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -112,20 +112,20 @@ const config : HardhatUserConfig = { // ], // gasPrice: 80000000000, // }, - sepolia: { - url: `${process.env.SEPOLIA_RPC_URL}`, - timeout: 10000000, - accounts: [ // Comment out for CI, uncomment this when using Sepolia - `${process.env.TEST_USER_A_KEY}`, - // `${process.env.TESTNET_PRIVATE_KEY_B}`, - // `${process.env.TESTNET_PRIVATE_KEY_C}`, - // `${process.env.TESTNET_PRIVATE_KEY_D}`, - `${process.env.TEST_USER_E_KEY}`, - // `${process.env.TESTNET_PRIVATE_KEY_F}`, - ], - // // Must have to avoid instead failing as `invalid length for result data` error - // throwOnCallFailures: false, // not sure if this even works - }, + // sepolia: { + // url: `${process.env.SEPOLIA_RPC_URL}`, + // timeout: 10000000, + // accounts: [ // Comment out for CI, uncomment this when using Sepolia + // `${process.env.TEST_USER_A_KEY}`, + // // `${process.env.TESTNET_PRIVATE_KEY_B}`, + // // `${process.env.TESTNET_PRIVATE_KEY_C}`, + // // `${process.env.TESTNET_PRIVATE_KEY_D}`, + // `${process.env.TEST_USER_E_KEY}`, + // // `${process.env.TESTNET_PRIVATE_KEY_F}`, + // ], + // // // Must have to avoid instead failing as `invalid length for result data` error + // // throwOnCallFailures: false, // not sure if this even works + // }, devnet: { // Add current URL that you spawned if not using automated spawning url: `${process.env.DEVNET_RPC_URL}`, From 5c0f1d9eaa27e0411117ee5a69e7452dce9648e4 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 8 Aug 2025 13:55:48 -0700 Subject: [PATCH 106/114] split withdrawStaked function from the execution script --- src/scripts/withdraw-stake-run.ts | 29 +++++++++++++++++++++++ src/{scripts => utils}/withdraw-staked.ts | 29 ----------------------- test/Pausable.upgrade.test.ts | 2 +- 3 files changed, 30 insertions(+), 30 deletions(-) create mode 100644 src/scripts/withdraw-stake-run.ts rename src/{scripts => utils}/withdraw-staked.ts (63%) diff --git a/src/scripts/withdraw-stake-run.ts b/src/scripts/withdraw-stake-run.ts new file mode 100644 index 000000000..48ca59d18 --- /dev/null +++ b/src/scripts/withdraw-stake-run.ts @@ -0,0 +1,29 @@ +import { withdrawStakedByGovernor } from "../utils/withdraw-staked"; + + +void (async () => { + try { + const token = process.env.WITHDRAW_TOKEN_ADDRESS; + const to = process.env.TREASURY_WITHDRAW_RECIPIENT; + + if (!token || !to) { + throw new Error("TOKEN_ADDRESS environment variable is not set"); + } + + const tx = await withdrawStakedByGovernor({ + token, + to, + }); + + console.log(`Withdrawal transaction successful: ${tx.hash}`); + process.exit(0); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + } catch (error : Error) { + console.error( + `Error withdrawing staked tokens: ${error.message} + ${error.stack}` + ); + process.exit(1); + } +})(); diff --git a/src/scripts/withdraw-staked.ts b/src/utils/withdraw-staked.ts similarity index 63% rename from src/scripts/withdraw-staked.ts rename to src/utils/withdraw-staked.ts index 4caa3d588..4fbee6a0a 100644 --- a/src/scripts/withdraw-staked.ts +++ b/src/utils/withdraw-staked.ts @@ -1,5 +1,4 @@ import * as hre from "hardhat"; -import { IDBVersion } from "../deploy/db/mongo-adapter/types"; import { getMongoAdapter } from "../deploy/db/mongo-adapter/get-adapter"; import { znsNames } from "../deploy/missions/contracts/names"; import { ZNSTreasuryPausable__factory } from "../../typechain/factories/contracts/zns-pausable/treasury"; @@ -50,31 +49,3 @@ export const withdrawStakedByGovernor = async ({ return tx; }; - -// call the above function with await properly below -void (async () => { - try { - const token = process.env.WITHDRAW_TOKEN_ADDRESS; - const to = process.env.TREASURY_WITHDRAW_RECIPIENT; - - if (!token || !to) { - throw new Error("TOKEN_ADDRESS environment variable is not set"); - } - - const tx = await withdrawStakedByGovernor({ - token, - to, - }); - - console.log(`Withdrawal transaction successful: ${tx.hash}`); - process.exit(0); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - } catch (error : Error) { - console.error( - `Error withdrawing staked tokens: ${error.message} - ${error.stack}` - ); - process.exit(1); - } -})(); diff --git a/test/Pausable.upgrade.test.ts b/test/Pausable.upgrade.test.ts index 1dfedb833..a1ae8d564 100644 --- a/test/Pausable.upgrade.test.ts +++ b/test/Pausable.upgrade.test.ts @@ -31,7 +31,7 @@ import { getLogger } from "../src/deploy/logger/create-logger"; import { updateDbAndVerifyAll } from "../src/upgrade/db"; import { VERSION_TYPES } from "../src/deploy/db/mongo-adapter/constants"; import { getGitTag } from "../src/utils/git-tag/get-tag"; -import { withdrawStakedByGovernor } from "../src/scripts/withdraw-staked"; +import { withdrawStakedByGovernor } from "../src/utils/withdraw-staked"; describe("ZNS Upgrade and Pause Test", () => { From 7f0d8f47ffec1aa4661de33176fcf366cd4d2fe4 Mon Sep 17 00:00:00 2001 From: Whytecrowe Date: Fri, 8 Aug 2025 14:18:38 -0700 Subject: [PATCH 107/114] fix awaiter in withdrawStaked --- src/utils/withdraw-staked.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/utils/withdraw-staked.ts b/src/utils/withdraw-staked.ts index 4fbee6a0a..d8c533856 100644 --- a/src/utils/withdraw-staked.ts +++ b/src/utils/withdraw-staked.ts @@ -43,9 +43,11 @@ export const withdrawStakedByGovernor = async ({ recipient, ); - await tx.wait( - process.env.CONFIRMATIONS_N ? Number(process.env.CONFIRMATIONS_N) : 2 - ); + if (hre.network.name !== "hardhat") { + await tx.wait( + process.env.CONFIRMATIONS_N ? Number(process.env.CONFIRMATIONS_N) : 2 + ); + } return tx; }; From eeadb7f97b83ff71748003d1a60212463beaa532 Mon Sep 17 00:00:00 2001 From: James Earle Date: Fri, 15 Aug 2025 12:36:28 -0400 Subject: [PATCH 108/114] create script for token airdrop after migration, improve comments in validation script --- src/utils/migration/01_validation.ts | 33 +++++- src/utils/migration/03_airdrop.ts | 143 ++++++++++++++++++++++++ src/utils/migration/subgraph/queries.ts | 44 +++++--- src/utils/migration/types.ts | 11 +- 4 files changed, 212 insertions(+), 19 deletions(-) create mode 100644 src/utils/migration/03_airdrop.ts diff --git a/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts index 0666b6e2a..4baf3ecb4 100644 --- a/src/utils/migration/01_validation.ts +++ b/src/utils/migration/01_validation.ts @@ -1,14 +1,37 @@ import * as hre from "hardhat"; +import { Db } from "mongodb"; import { getDomains } from "./subgraph"; import { Domain, InvalidDomain } from "./types"; import { getDBAdapter, updateCollection } from "./database"; import { getZNS } from "./zns-contract-data"; import { validateDomain } from "./validate"; import { INVALID_COLL_NAME, ROOT_COLL_NAME, SUB_COLL_NAME } from "./constants"; -import { Db } from "mongodb"; +import { getLogger } from "../../deploy/logger/create-logger"; + +const logger = getLogger(); /** + * This is the first of 3 scripts required to run the full domain migration process for zNS v1.0 -> v2.0 + * + * 01_validation.ts - Collect Ethereum mainnet domain data for zNS from the subgraph and validate + * it's legitimacy against actual on-chain data. Once validated it is uploaded to database for + * access in downstream functions + * + * 02_registration.ts - Read domain data collected by step #1 and form batches of domain registration + * calls for the `bulk` functions present on zNS v2.0 and propose them to the Safe instance on Z Chain. + * We do this level by level so script requires multiple steps as the lack of parent domains existing will + * cause subdomains of that domain to fail gas estimation for the batch. We then also call to transfer to + * transfer each registered domain to the rightful owner. Domains that are revoked must be registered to + * successfully register any subdomains, so these domains are left out of the transfer. As a result the final + * owner for these domains is the Safe and the final execution will call to revoke all of these domains + * specifically so that the namespace is available for users in the future. + * **Note** This script is NOT present on this branch. Switch to branch `rc/zchain-native-main` + * + * 03_airdrop.ts - As the final step we seek to reimburse the original domain holders on L1. This script + * aggregates how much each user has paid in total and in what token and then writes that data to a .csv file + * We can upload this file directly to the L1 Safe using the `CSV Airdrop` app enabled by them directly. + * * Reqiuired .env vars * - SUBGRAPH_URL_DEV - The URL to read from `zns-mainnet-dev` subgraph * - MAINNET_PRIVATE_KEY - A **READ ONLY** private key to use in validation on chain @@ -27,7 +50,7 @@ const main = async () => { const rootDomainObjects = await getDomains(true); const subdomainObjects = await getDomains(false); - console.log(`Found ${rootDomainObjects.length + subdomainObjects.length} domains`); + logger.info(`Found ${rootDomainObjects.length + subdomainObjects.length} domains`); const env = process.env.ENV_LEVEL; @@ -42,10 +65,10 @@ const main = async () => { // Doing this creates strong typing and extensibility that allows // the below `insertMany` calls to add properties to the object for `_id` properly const roots = rootDomainObjects.map(d => d as Domain); - console.log(`Found ${roots.length} root domains`); + logger.info(`Found ${roots.length} root domains`); const subs = subdomainObjects.map(d => d as Domain); - console.log(`Found ${subs.length} subdomains`); + logger.info(`Found ${subs.length} subdomains`); const dbName = process.env.MONGO_DB_NAME_WRITE; if (!dbName) throw Error("Missing MONGO_DB_NAME_WRITE environment variable"); @@ -76,7 +99,7 @@ const main = async () => { ++index; if (index % 50 === 0) { - console.log(`Processed ${index} domains`); + logger.info(`Processed ${index} domains`); } } diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts new file mode 100644 index 000000000..f4c3db946 --- /dev/null +++ b/src/utils/migration/03_airdrop.ts @@ -0,0 +1,143 @@ +import { ethers } from "ethers"; +import { getDomains } from "./subgraph"; +import { Domain } from "./types"; +import * as fs from "fs"; +import { getLogger } from "../../deploy/logger/create-logger"; + +const logger = getLogger(); + +/** + * Reqiuired .env vars + * - SUBGRAPH_URL_DEV - The URL to read from `zns-mainnet-dev` subgraph + * - DEFAULT_PAYMENT_TOKEN - The symbol of the default payment token used + */ +const main = async () => { + // Keeping as separate collections from the start will help downstream registration + const rootDomainObjects = await getDomains(true); + const subdomainObjects = await getDomains(false); + + // Track totals owed to users for payments from domain registration + const userAmounts = new Map>(); + + // If payment token resolution fails for a domain we hold onto it for debugging + const errorDomains = []; + + // Doing this creates strong typing and extensibility that allows + // the below `insertMany` calls to add properties to the object for `_id` properly + const roots = rootDomainObjects.map(d => d as Domain); + logger.info(`Found ${roots.length} root domains`); + + const subs = subdomainObjects.map(d => d as Domain); + logger.info(`Found ${subs.length} subdomains`); + + // Keep tack of tokens as we iterate for later use + const tokenPairs : Map = new Map(); + + logger.info("Processing..."); + for (const [i,d] of [...roots, ...subs].entries()) { + // Both will be null if the domain was free (register by parent owner) + // In this case user isn't owed any refund, so we skip + if (!d.amountPaidDirect && !d.amountPaidStake) { + continue; + } else if (!d.isRevoked) { + // Because of how the contracts are structured, it isn't possible + // to get the contract address of the payment token at registration + // so we must specify a default here instead + let paymentToken = process.env.DEFAULT_PAYMENT_TOKEN; + + if (!paymentToken) { + throw new Error("Error: No default payment token specified."); + } + + if (!d.isWorld) { + // Subdomains may use other tokens, so resolve + if (d.parent && d.parent.treasury && d.parent.treasury.paymentToken) { + // Override the default name if it is specified + paymentToken = d.parent.treasury.paymentToken.symbol; + + if (!tokenPairs.has(paymentToken)) { + tokenPairs.set(paymentToken, d.parent.treasury.paymentToken.id); + } + } else { + errorDomains.push(d); + continue; + } + } + + const amountPaid = !d.amountPaidDirect ? d.amountPaidStake : d.amountPaidDirect; + const tokenAmounts = userAmounts.get(d.owner.id); + + if (tokenAmounts) { + // Get the amount of `parentPaymentToken` they have paid + const amount = tokenAmounts.get(paymentToken); + + // They may be paying with `parentPaymentToken` for the first time, get amount + const realAmount = !amount ? 0n : amount; + + tokenAmounts.set(paymentToken, realAmount + BigInt(amountPaid)); + userAmounts.set(d.owner.id, tokenAmounts); + } else { + const tokenAmount = new Map(); + tokenAmount.set(paymentToken, BigInt(amountPaid)); + userAmounts.set(d.owner.id, tokenAmount); + } + } + + // Track our progress + if (i % 50 === 0) { + logger.info(i); + } + } + + logger.info(`userAmounts.size: ${userAmounts.size}`); + logger.info(`errorDomains.length: ${errorDomains.length}`); + + // Now transform collected data into csv or needed transaction data per row + const rows = []; + const headers = ["token_type","token_address","receiver","amount"]; + + for (const userAmount of userAmounts.entries()) { + const user : string = userAmount[0]; + const amountsMap : Map = userAmount[1]; + + // If the user has more payment tokens than there are specified by the TOKEN_PAIRS .env var, + // then fail, we cannot calculate their refund and their is likely a mistake somewhere + if (amountsMap.size > tokenPairs.size) { + throw new Error("Error: User has used more tokens in payment than found in domain iteration"); + } + + // Row to build up as we read the specific token values + const row = ["erc20"]; + + for(const pair of tokenPairs) { + // 0 is token symbol + const amount = amountsMap.get(pair[0]); + + // Amount may be null for one or both + if (amount) { + // 1 is token contract address + row.push(pair[1], user, ethers.formatEther(amount).toString()); + } + } + + // It's possible that no payment tokens were setup and the domain was free. Reading the data above + // when this is true would cause an invalid row to get pushed to the array. So we check the length here + // to only push complete rows to the array instead + if(row.length === 4) { + rows.push(row); + } + } + + logger.info(`rows.length: ${rows.length}`); + + + fs.writeFileSync("03_errorDomains.json", JSON.stringify(errorDomains, null, 2)); + fs.writeFileSync("03_userAmounts.csv", `${headers}\n${rows.join("\n")}`); +}; + +main().catch(error => { + logger.error(error.message); + process.exit(1); +}).finally(() => { + process.exit(0); +}); diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts index d53306788..470838559 100644 --- a/src/utils/migration/subgraph/queries.ts +++ b/src/utils/migration/subgraph/queries.ts @@ -25,6 +25,16 @@ export const getDomains = gql` isWorld isRevoked parentHash + amountPaidStake + amountPaidDirect + accessType + pricerContract + paymentType + subdomainCount + tokenId + tokenURI + creationBlock + creationTimestamp parent { id label @@ -34,6 +44,13 @@ export const getDomains = gql` tokenId tokenURI parentHash + treasury { + paymentToken { + id + name + symbol + } + } parent { id label @@ -43,6 +60,13 @@ export const getDomains = gql` tokenId tokenURI parentHash + treasury { + paymentToken { + id + name + symbol + } + } parent { id label @@ -52,28 +76,22 @@ export const getDomains = gql` tokenId tokenURI parentHash + treasury { + paymentToken { + id + name + symbol + } + } } } } - accessType - pricerContract - paymentType curvePriceConfig { id } fixedPriceConfig { id } - subdomainCount - address - tokenId - tokenURI - treasury { - id - beneficiaryAddress - } - creationBlock - creationTimestamp } } `; diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts index 69c1a072a..58b08a538 100644 --- a/src/utils/migration/types.ts +++ b/src/utils/migration/types.ts @@ -13,6 +13,8 @@ export interface Domain { parentHash : string; isWorld : boolean; isRevoked : boolean; + amountPaidStake : bigint; + amountPaidDirect : bigint; parent : Domain | null; accessType : string; paymentType : string; @@ -45,7 +47,14 @@ interface FixedPriceConfig { interface Treasury { id : string; beneficiaryAddress : string; - domain : Domain; // cyclic? + paymentToken : PaymentToken; + domain : Domain; +} + +interface PaymentToken { + id : string; + name : string; + symbol : string; } interface DomainToken { From 0f7b0be3ab93431bb7750467baafe62b0129cd02 Mon Sep 17 00:00:00 2001 From: James Earle Date: Fri, 15 Aug 2025 15:08:47 -0400 Subject: [PATCH 109/114] minor changes for var naming, removing some unnecessary code --- src/utils/migration/03_airdrop.ts | 31 ++++++++----------------------- 1 file changed, 8 insertions(+), 23 deletions(-) diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts index f4c3db946..81eebb8d3 100644 --- a/src/utils/migration/03_airdrop.ts +++ b/src/utils/migration/03_airdrop.ts @@ -13,8 +13,8 @@ const logger = getLogger(); */ const main = async () => { // Keeping as separate collections from the start will help downstream registration - const rootDomainObjects = await getDomains(true); - const subdomainObjects = await getDomains(false); + const roots = await getDomains(true) as Domain[]; + const subs = await getDomains(false) as Domain[]; // Track totals owed to users for payments from domain registration const userAmounts = new Map>(); @@ -22,16 +22,8 @@ const main = async () => { // If payment token resolution fails for a domain we hold onto it for debugging const errorDomains = []; - // Doing this creates strong typing and extensibility that allows - // the below `insertMany` calls to add properties to the object for `_id` properly - const roots = rootDomainObjects.map(d => d as Domain); - logger.info(`Found ${roots.length} root domains`); - - const subs = subdomainObjects.map(d => d as Domain); - logger.info(`Found ${subs.length} subdomains`); - // Keep tack of tokens as we iterate for later use - const tokenPairs : Map = new Map(); + const tokensMap : Map = new Map(); logger.info("Processing..."); for (const [i,d] of [...roots, ...subs].entries()) { @@ -55,8 +47,8 @@ const main = async () => { // Override the default name if it is specified paymentToken = d.parent.treasury.paymentToken.symbol; - if (!tokenPairs.has(paymentToken)) { - tokenPairs.set(paymentToken, d.parent.treasury.paymentToken.id); + if (!tokensMap.has(paymentToken)) { + tokensMap.set(paymentToken, d.parent.treasury.paymentToken.id); } } else { errorDomains.push(d); @@ -100,23 +92,17 @@ const main = async () => { const user : string = userAmount[0]; const amountsMap : Map = userAmount[1]; - // If the user has more payment tokens than there are specified by the TOKEN_PAIRS .env var, - // then fail, we cannot calculate their refund and their is likely a mistake somewhere - if (amountsMap.size > tokenPairs.size) { - throw new Error("Error: User has used more tokens in payment than found in domain iteration"); - } - // Row to build up as we read the specific token values const row = ["erc20"]; - for(const pair of tokenPairs) { + for(const token of tokensMap) { // 0 is token symbol - const amount = amountsMap.get(pair[0]); + const amount = amountsMap.get(token[0]); // Amount may be null for one or both if (amount) { // 1 is token contract address - row.push(pair[1], user, ethers.formatEther(amount).toString()); + row.push(token[1], user, ethers.formatEther(amount).toString()); } } @@ -130,7 +116,6 @@ const main = async () => { logger.info(`rows.length: ${rows.length}`); - fs.writeFileSync("03_errorDomains.json", JSON.stringify(errorDomains, null, 2)); fs.writeFileSync("03_userAmounts.csv", `${headers}\n${rows.join("\n")}`); }; From 90e18b3d2f908208179422610818934754d08eef Mon Sep 17 00:00:00 2001 From: James Earle Date: Fri, 15 Aug 2025 15:30:24 -0400 Subject: [PATCH 110/114] fix linter and typos --- src/utils/migration/03_airdrop.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts index 81eebb8d3..27dbbab9c 100644 --- a/src/utils/migration/03_airdrop.ts +++ b/src/utils/migration/03_airdrop.ts @@ -7,14 +7,14 @@ import { getLogger } from "../../deploy/logger/create-logger"; const logger = getLogger(); /** - * Reqiuired .env vars + * Required .env vars * - SUBGRAPH_URL_DEV - The URL to read from `zns-mainnet-dev` subgraph * - DEFAULT_PAYMENT_TOKEN - The symbol of the default payment token used */ const main = async () => { // Keeping as separate collections from the start will help downstream registration - const roots = await getDomains(true) as Domain[]; - const subs = await getDomains(false) as Domain[]; + const roots = await getDomains(true) as Array; + const subs = await getDomains(false) as Array; // Track totals owed to users for payments from domain registration const userAmounts = new Map>(); @@ -22,7 +22,7 @@ const main = async () => { // If payment token resolution fails for a domain we hold onto it for debugging const errorDomains = []; - // Keep tack of tokens as we iterate for later use + // Keep track of tokens as we iterate for later use const tokensMap : Map = new Map(); logger.info("Processing..."); From 2e5394af86b30ed614c4db6f6076932cc0daa139 Mon Sep 17 00:00:00 2001 From: James Earle Date: Mon, 18 Aug 2025 15:54:17 -0700 Subject: [PATCH 111/114] modify to only refund users that paid with stakes --- src/utils/migration/03_airdrop.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts index 27dbbab9c..d338dafb9 100644 --- a/src/utils/migration/03_airdrop.ts +++ b/src/utils/migration/03_airdrop.ts @@ -27,11 +27,9 @@ const main = async () => { logger.info("Processing..."); for (const [i,d] of [...roots, ...subs].entries()) { - // Both will be null if the domain was free (register by parent owner) + // Will be null if the domain was free (registered by parent owner) // In this case user isn't owed any refund, so we skip - if (!d.amountPaidDirect && !d.amountPaidStake) { - continue; - } else if (!d.isRevoked) { + if (d.amountPaidStake && !d.isRevoked) { // Because of how the contracts are structured, it isn't possible // to get the contract address of the payment token at registration // so we must specify a default here instead @@ -42,7 +40,7 @@ const main = async () => { } if (!d.isWorld) { - // Subdomains may use other tokens, so resolve + // Subdomains may use other tokens, resolve here if (d.parent && d.parent.treasury && d.parent.treasury.paymentToken) { // Override the default name if it is specified paymentToken = d.parent.treasury.paymentToken.symbol; @@ -56,7 +54,6 @@ const main = async () => { } } - const amountPaid = !d.amountPaidDirect ? d.amountPaidStake : d.amountPaidDirect; const tokenAmounts = userAmounts.get(d.owner.id); if (tokenAmounts) { @@ -66,11 +63,11 @@ const main = async () => { // They may be paying with `parentPaymentToken` for the first time, get amount const realAmount = !amount ? 0n : amount; - tokenAmounts.set(paymentToken, realAmount + BigInt(amountPaid)); + tokenAmounts.set(paymentToken, realAmount + BigInt(d.amountPaidStake)); userAmounts.set(d.owner.id, tokenAmounts); } else { const tokenAmount = new Map(); - tokenAmount.set(paymentToken, BigInt(amountPaid)); + tokenAmount.set(paymentToken, BigInt(d.amountPaidStake)); userAmounts.set(d.owner.id, tokenAmount); } } From 4385b3168118bae4bd8f575b619808bc31a6dbbc Mon Sep 17 00:00:00 2001 From: James Earle Date: Mon, 18 Aug 2025 16:19:44 -0700 Subject: [PATCH 112/114] tracker for token totals --- src/utils/migration/03_airdrop.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts index d338dafb9..bd1ba97cc 100644 --- a/src/utils/migration/03_airdrop.ts +++ b/src/utils/migration/03_airdrop.ts @@ -19,6 +19,9 @@ const main = async () => { // Track totals owed to users for payments from domain registration const userAmounts = new Map>(); + // Track the total amount to be sent for double checking the contract balance later + const totals = new Map(); + // If payment token resolution fails for a domain we hold onto it for debugging const errorDomains = []; @@ -59,14 +62,21 @@ const main = async () => { if (tokenAmounts) { // Get the amount of `parentPaymentToken` they have paid const amount = tokenAmounts.get(paymentToken); + const total = totals.get(paymentToken); // They may be paying with `parentPaymentToken` for the first time, get amount const realAmount = !amount ? 0n : amount; + const realTotal = !total ? 0n : total; + + totals.set(paymentToken, realTotal + realAmount); tokenAmounts.set(paymentToken, realAmount + BigInt(d.amountPaidStake)); userAmounts.set(d.owner.id, tokenAmounts); } else { - const tokenAmount = new Map(); + const tokenAmount = new Map(); + + totals.set(paymentToken, BigInt(d.amountPaidStake)); + tokenAmount.set(paymentToken, BigInt(d.amountPaidStake)); userAmounts.set(d.owner.id, tokenAmount); } @@ -78,6 +88,10 @@ const main = async () => { } } + for (let token of totals.entries()) { + logger.info(`Total for token ${token[0]}: ${token[1]}`); + } + logger.info(`userAmounts.size: ${userAmounts.size}`); logger.info(`errorDomains.length: ${errorDomains.length}`); From 534b3ddcfb4e1d07e39d17ed1a6687741e3a9d1e Mon Sep 17 00:00:00 2001 From: James Earle Date: Mon, 18 Aug 2025 16:24:07 -0700 Subject: [PATCH 113/114] linter --- src/utils/migration/03_airdrop.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts index bd1ba97cc..9d1351527 100644 --- a/src/utils/migration/03_airdrop.ts +++ b/src/utils/migration/03_airdrop.ts @@ -88,7 +88,7 @@ const main = async () => { } } - for (let token of totals.entries()) { + for (const token of totals.entries()) { logger.info(`Total for token ${token[0]}: ${token[1]}`); } From f841effadb7bbb3780c2ec6f926965f7d9b2b8b6 Mon Sep 17 00:00:00 2001 From: James Earle Date: Mon, 18 Aug 2025 16:51:03 -0700 Subject: [PATCH 114/114] update to remove from user-specific checks --- src/utils/migration/03_airdrop.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts index 9d1351527..fd82633b2 100644 --- a/src/utils/migration/03_airdrop.ts +++ b/src/utils/migration/03_airdrop.ts @@ -62,24 +62,24 @@ const main = async () => { if (tokenAmounts) { // Get the amount of `parentPaymentToken` they have paid const amount = tokenAmounts.get(paymentToken); - const total = totals.get(paymentToken); // They may be paying with `parentPaymentToken` for the first time, get amount const realAmount = !amount ? 0n : amount; - const realTotal = !total ? 0n : total; - - totals.set(paymentToken, realTotal + realAmount); tokenAmounts.set(paymentToken, realAmount + BigInt(d.amountPaidStake)); userAmounts.set(d.owner.id, tokenAmounts); } else { const tokenAmount = new Map(); - totals.set(paymentToken, BigInt(d.amountPaidStake)); - tokenAmount.set(paymentToken, BigInt(d.amountPaidStake)); userAmounts.set(d.owner.id, tokenAmount); } + + // Update totals tracking + const totalForToken = totals.get(paymentToken); + const realTotalForToken = !totalForToken ? 0n : totalForToken; + + totals.set(paymentToken, realTotalForToken + BigInt(d.amountPaidStake)); } // Track our progress