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/.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/.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/.gitattributes b/.gitattributes new file mode 100644 index 000000000..176a458f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore index 2abca5351..7cd75c320 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ node_modules -.env +*.env* coverage coverage.json typechain typechain-types .idea +.vscode +dist +*.log # Hardhat files cache @@ -19,3 +22,5 @@ docker*.tgz # We don't ever use the generated manifests .openzeppelin + +output diff --git a/.solcover.js b/.solcover.js index d4924e9a2..9f03e96e9 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', + 'zns-pausable' ] }; 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/contracts/zns-pausable/IZNSPausable.sol b/contracts/zns-pausable/IZNSPausable.sol new file mode 100644 index 000000000..d9f8aa67f --- /dev/null +++ b/contracts/zns-pausable/IZNSPausable.sol @@ -0,0 +1,21 @@ +// 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; + + function paused() external view returns (bool); +} diff --git a/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol new file mode 100644 index 000000000..6404e087a --- /dev/null +++ b/contracts/zns-pausable/price/IZNSCurvePricerPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSCurvePricer } from "../../price/IZNSCurvePricer.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSCurvePricerPausable is IZNSCurvePricer, IZNSPausable {} diff --git a/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol b/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol new file mode 100644 index 000000000..23ac14c3d --- /dev/null +++ b/contracts/zns-pausable/price/IZNSFixedPricerPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSFixedPricer } from "../../price/IZNSFixedPricer.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSFixedPricerPausable is IZNSFixedPricer, IZNSPausable {} diff --git a/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol new file mode 100644 index 000000000..7b5637458 --- /dev/null +++ b/contracts/zns-pausable/price/ZNSCurvePricerPausable.sol @@ -0,0 +1,396 @@ +// 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 { IZNSCurvePricer } from "../../price/IZNSCurvePricer.sol"; +import { StringUtils } from "../../utils/StringUtils.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.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, ARegistryWiredPausable, 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 `ARegistryWiredPausable`. + */ + function setRegistry(address registry_) + external + override( + ARegistryWiredPausable, + IZNSCurvePricer + ) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override 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..a6eb09423 --- /dev/null +++ b/contracts/zns-pausable/price/ZNSFixedPricerPausable.sol @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: MIT +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"; + + +/** + * @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, ARegistryWiredPausable, 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 `ARegistryWiredPausable`. + */ + function setRegistry(address registry_) + public + override( + ARegistryWiredPausable, + IZNSFixedPricer + ) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override 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/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol new file mode 100644 index 000000000..c055cea02 --- /dev/null +++ b/contracts/zns-pausable/registrar/IZNSRootRegistrarPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSRootRegistrar } from "../../registrar/IZNSRootRegistrar.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSRootRegistrarPausable is IZNSRootRegistrar, IZNSPausable {} diff --git a/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol new file mode 100644 index 000000000..174247f64 --- /dev/null +++ b/contracts/zns-pausable/registrar/IZNSSubRegistrarPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSSubRegistrar } from "../../registrar/IZNSSubRegistrar.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSSubRegistrarPausable is IZNSSubRegistrar, IZNSPausable {} diff --git a/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol new file mode 100644 index 000000000..28462bed3 --- /dev/null +++ b/contracts/zns-pausable/registrar/ZNSRootRegistrarPausable.sol @@ -0,0 +1,453 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.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"; +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, + ARegistryWiredPausable, + IZNSRootRegistrarPausable { + using StringUtils for string; + + IZNSPricer public rootPricer; + IZNSTreasuryPausable public treasury; + IZNSDomainTokenPausable 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 whenNotPaused onlyRegistrar { + _coreRegister( + args + ); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override 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"); + + 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 + 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( + ARegistryWiredPausable, + IZNSRootRegistrar + ) 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 = IZNSTreasuryPausable(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 = IZNSDomainTokenPausable(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..40313acd8 --- /dev/null +++ b/contracts/zns-pausable/registrar/ZNSSubRegistrarPausable.sol @@ -0,0 +1,405 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSPricer } from "../../types/IZNSPricer.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/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, ARegistryWiredPausable, 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 `ARegistryWiredPausable`. + */ + function setRegistry(address registry_) + public + override( + ARegistryWiredPausable, + IZNSSubRegistrar + ) 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 override 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/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/registry/IZNSRegistryPausable.sol b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol new file mode 100644 index 000000000..b19d25f6f --- /dev/null +++ b/contracts/zns-pausable/registry/IZNSRegistryPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSRegistry } from "../../registry/IZNSRegistry.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSRegistryPausable is IZNSRegistry, IZNSPausable {} diff --git a/contracts/zns-pausable/registry/ZNSRegistryPausable.sol b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol new file mode 100644 index 000000000..ca5a20106 --- /dev/null +++ b/contracts/zns-pausable/registry/ZNSRegistryPausable.sol @@ -0,0 +1,356 @@ +// 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"; + + +/** + * @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, + 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; + + 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 + */ + 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" + ); + _; + } + + 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(); + } + + /** + * @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]; + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override 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 + * @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 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 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 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 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); + } +} diff --git a/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol new file mode 100644 index 000000000..130a2ede7 --- /dev/null +++ b/contracts/zns-pausable/resolver/IZNSAddressResolverPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSAddressResolver } from "../../resolver/IZNSAddressResolver.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSAddressResolverPausable is IZNSAddressResolver, IZNSPausable {} diff --git a/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol new file mode 100644 index 000000000..478c32f33 --- /dev/null +++ b/contracts/zns-pausable/resolver/ZNSAddressResolverPausable.sol @@ -0,0 +1,155 @@ +// 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 { IZNSAddressResolver } from "../../resolver/IZNSAddressResolver.sol"; +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.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, + ARegistryWiredPausable, + 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, IZNSAddressResolver) 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(ARegistryWiredPausable, IZNSAddressResolver) onlyAdmin { + _setRegistry(_registry); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override 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/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol new file mode 100644 index 000000000..c8b30297f --- /dev/null +++ b/contracts/zns-pausable/token/IZNSDomainTokenPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSDomainToken } from "../../token/IZNSDomainToken.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSDomainTokenPausable is IZNSDomainToken, IZNSPausable {} diff --git a/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol new file mode 100644 index 000000000..5f57dd4a1 --- /dev/null +++ b/contracts/zns-pausable/token/ZNSDomainTokenPausable.sol @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: MIT +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 +} 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"; + + +/** + * @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, + IZNSDomainToken + ) 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 + */ + 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); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override 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); + } + + 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 + * @param interfaceId The interface ID + */ + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override( + ERC721Upgradeable, + ERC721URIStorageUpgradeable, + ERC2981Upgradeable, + IZNSDomainToken + ) 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/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol new file mode 100644 index 000000000..0dfb9094b --- /dev/null +++ b/contracts/zns-pausable/treasury/IZNSTreasuryPausable.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSTreasury } from "../../treasury/IZNSTreasury.sol"; +import { IZNSPausable } from "../IZNSPausable.sol"; + + +interface IZNSTreasuryPausable is IZNSTreasury, IZNSPausable {} diff --git a/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol new file mode 100644 index 000000000..17f63830f --- /dev/null +++ b/contracts/zns-pausable/treasury/ZNSTreasuryPausable.sol @@ -0,0 +1,363 @@ +// SPDX-License-Identifier: MIT +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 "../../treasury/IZNSTreasury.sol"; +import { ARegistryWiredPausable } from "../registry/ARegistryWiredPausable.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, ARegistryWiredPausable, 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(), "ZNSTreasury: Contract is paused"); + _; + } + + modifier whenPaused() { + require(paused(), "ZNSTreasury: 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 `ARegistryWiredPausable`. + */ + function setRegistry( + address registry_ + ) external override(ARegistryWiredPausable, IZNSTreasury) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @notice Withdraws all staked tokens from the contract to the specified address. + * 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( + address token, + address to + ) 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"); + + IERC20(token).safeTransfer( + to, + IERC20(token).balanceOf(address(this)) + ); + } + + /** + * @notice Pauses the contract. Can only be called by the ADMIN_ROLE. + */ + function pause() external override whenNotPaused onlyAdmin { + _paused = true; + emit Paused(msg.sender); + } + + /** + * @notice Unpauses the contract. Can only be called by the ADMIN_ROLE. + */ + function unpause() external override whenPaused onlyAdmin { + _paused = false; + emit Unpaused(msg.sender); + } + + /** + * @dev Returns true if the contract is paused, and false otherwise. + */ + function paused() public view override 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/hardhat.config.ts b/hardhat.config.ts index 3259b20ae..f6c505800 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,155 +1,165 @@ -/* 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: { + // zephyr: { + // url: `${process.env.ZEPHYR_RPC_URL}`, + // accounts: [ + // `${process.env.ZNS_DEPLOYER}` + // ] + // }, + // 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, + // 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}`, + 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..634e287b1 100644 --- a/package.json +++ b/package.json @@ -1,83 +1,88 @@ -{ - "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", + "deploy-sepolia": "hardhat run src/deploy/run-campaign.ts --network sepolia", + "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" + ], + "devDependencies": { + "@apollo/client": "^3.5.6", + "@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/graphql": "^14.5.0", + "@types/mocha": "^9.1.0", + "@types/node": "^18.15.11", + "@zero-tech/eslint-config-cpt": "0.2.8", + "@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", + "react": "^19.1.0", + "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.8.2", + "dotenv": "16.0.3", + "mongodb": "^6.1.0", + "winston": "^3.11.0" + } +} 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/deploy/db/mongo-adapter/get-adapter.ts b/src/deploy/db/mongo-adapter/get-adapter.ts index ea571f105..63dba9072 100644 --- a/src/deploy/db/mongo-adapter/get-adapter.ts +++ b/src/deploy/db/mongo-adapter/get-adapter.ts @@ -1,66 +1,45 @@ -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 => { + 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, + 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", + }; + + if (mongoAdapter) { + logger.debug("Returning existing MongoDBAdapter instance"); + return mongoAdapter; + } else { + logger.debug(`Creating new MongoDBAdapter instance with version: ${params.version}`); + mongoAdapter = new MongoDBAdapter({ + ...params, + }); + await mongoAdapter.initialize(params.version); + } + + return mongoAdapter ; +}; 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/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 }), ); diff --git a/src/deploy/run-campaign.ts b/src/deploy/run-campaign.ts index 78ff2e36d..731839d01 100644 --- a/src/deploy/run-campaign.ts +++ b/src/deploy/run-campaign.ts @@ -1,37 +1,27 @@ -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 * as hre from "hardhat"; +import { getConfig } from "./campaign/environments"; +import { runZnsCampaign } from "./zns-campaign"; +import { getLogger } from "./logger/create-logger"; + + +const logger = getLogger(); + +const runCampaign = async () => { + const [ deployer ] = await hre.ethers.getSigners(); + + + const config = await getConfig({ + deployer, + }); + + await runZnsCampaign({ + config, + }); +}; + +runCampaign().catch(error => { + logger.error(error.stack); + process.exit(1); +}).finally(() => { + process.exit(0); +}); 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/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/upgrade/db.ts b/src/upgrade/db.ts new file mode 100644 index 000000000..93c038867 --- /dev/null +++ b/src/upgrade/db.ts @@ -0,0 +1,119 @@ +import * as hre from "hardhat"; +import { MongoDBAdapter } from "../deploy/db/mongo-adapter/mongo-adapter"; +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 updateDbAndVerifyAll = async ( + dbAdapter : MongoDBAdapter, +) => { + const logger = getLogger(); + + const 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" + ? contractNames.meowToken.contractMock + : contractNames.meowToken.contract; + + const contractData = await getContractDataForUpgrade(dbAdapter, contractNames); + + for (const { contractName, address } of contractData) { + 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, + }); + + if (hre.network.name !== "hardhat" && contractName !== znsNames.accessController.contract) { + 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 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."); +}; + +export const updateContractInDb = async ({ + dbAdapter, + contractName, + proxyAddress, + implAddress, + newDbVersion, +} : { + dbAdapter : MongoDBAdapter; + contractName : string; + proxyAddress : string; + implAddress : string | null; + newDbVersion : string; +}) => { + const artifactName = contractName === znsNames.accessController.contract + || contractName === znsNames.meowToken.contract + || contractName === znsNames.meowToken.contractMock + ? contractName + : `${contractName}Pausable`; + + const { abi, bytecode } = hre.artifacts.readArtifactSync(artifactName); + + await dbAdapter.writeContract( + contractName, + { + name: contractName, + address: proxyAddress, + implementation: implAddress, + abi: JSON.stringify(abi), + bytecode, + }, + newDbVersion, + ); +}; 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/execute-upgrade.ts b/src/upgrade/scripts/execute-upgrade.ts new file mode 100644 index 000000000..a108e99c6 --- /dev/null +++ b/src/upgrade/scripts/execute-upgrade.ts @@ -0,0 +1,37 @@ +import { getLogger } from "../../deploy/logger/create-logger"; +import { getContractDataForUpgrade, getContractNamesToUpgrade, 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("Preparing contract data for the upgrade..."); + + const contractData = await getContractDataForUpgrade( + dbAdapter, + getContractNamesToUpgrade() + ); + + logger.info("Contract data prepared. Starting the upgrade..."); + + const znsUpgraded = await upgradeZNS({ + contractData, + 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! Message: ${e.message}, Stack: ${e.stack}`); + process.exit(1); + }); diff --git a/src/upgrade/scripts/get-implementations.ts b/src/upgrade/scripts/get-implementations.ts new file mode 100644 index 000000000..96f4e66ba --- /dev/null +++ b/src/upgrade/scripts/get-implementations.ts @@ -0,0 +1,30 @@ +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, getContractNamesToUpgrade()); + + 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/pause-all.ts b/src/upgrade/scripts/pause-all.ts new file mode 100644 index 000000000..8d7fb8cc7 --- /dev/null +++ b/src/upgrade/scripts/pause-all.ts @@ -0,0 +1,48 @@ +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") { + 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`); + } + } +}; + +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); + }); 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); + }); diff --git a/src/upgrade/scripts/update-db.ts b/src/upgrade/scripts/update-db.ts new file mode 100644 index 000000000..ae7d51324 --- /dev/null +++ b/src/upgrade/scripts/update-db.ts @@ -0,0 +1,19 @@ +import { getLogger } from "../../deploy/logger/create-logger"; +import { getMongoAdapter } from "../../deploy/db/mongo-adapter/get-adapter"; +import { updateDbAndVerifyAll } from "../db"; + + +const executeDbUpdate = async () => { + const logger = getLogger(); + const dbAdapter = await getMongoAdapter(logger); + + await updateDbAndVerifyAll(dbAdapter); +}; + + +executeDbUpdate() + .then(() => process.exit(0)) + .catch(e => { + console.error(e); + process.exit(1); + }); diff --git a/src/upgrade/storage-data.ts b/src/upgrade/storage-data.ts new file mode 100644 index 000000000..d739d5394 --- /dev/null +++ b/src/upgrade/storage-data.ts @@ -0,0 +1,83 @@ +import * as hre from "hardhat"; +import { ContractFactory } from "ethers"; +import { getStorageLayout, getUnlinkedBytecode, getVersion, StorageLayout } from "@openzeppelin/upgrades-core"; +import { readValidations } from "@openzeppelin/hardhat-upgrades/dist/utils/validations"; +import { ContractStorageData, ContractStorageDiff } from "./types"; +import { ZNSContract } from "../../test/helpers/types"; +import { getLogger } from "../deploy/logger/create-logger"; + + +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 logger = getLogger(); + const layout = await getContractStorageLayout(contractFactory); + + return layout.storage.reduce( + async ( + acc : Promise, + { label, type } + ) : Promise => { + const newAcc = await acc; + + if (type.includes("mapping") || type.includes("array") || label.slice(0, 1) === "_") + return newAcc; // Skip mappings, arrays and private variables + + 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; + }, + Promise.resolve([]) + ); +}; + + +export const compareStorageData = ( + dataBefore : ContractStorageData, + dataAfter : ContractStorageData, +) => { + const storageDiff = dataAfter.reduce( + (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}` + ); + + 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 new file mode 100644 index 000000000..40a45d7ac --- /dev/null +++ b/src/upgrade/types.ts @@ -0,0 +1,48 @@ +import { + ZNSAddressResolverPausable, + ZNSCurvePricerPausable, + ZNSDomainTokenPausable, ZNSFixedPricerPausable, + ZNSRegistryPausable, ZNSRootRegistrarPausable, ZNSSubRegistrarPausable, ZNSTreasuryPausable, +} from "../../typechain"; +import { Addressable } from "ethers"; + + +export type ContractStorageElement = string | number | Array; + +export type ContractStorageData = Array<{ + [label : string] : ContractStorageElement; +}>; + +export type ContractStorageDiff = Array<{ + key : string; + valueBefore : ContractStorageElement; + valueAfter : ContractStorageElement; +}>; + +export interface IContractData { + contractName : string; + instanceName : keyof IZNSContractsUpgraded; + address : string | Addressable; +} + +export type ZNSContractUpgraded = + 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 new file mode 100644 index 000000000..d3ec8c18a --- /dev/null +++ b/src/upgrade/upgrade.ts @@ -0,0 +1,127 @@ +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 { 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 { znsNames } from "../deploy/missions/contracts/names"; +import { IContractDbData } from "../deploy/db/types"; + + +export const upgradeZNS = async ({ + governorExt, + contractData, + logger, +} : { + governorExt ?: SignerWithAddress; + contractData : Array; + logger : TLogger; +}) => { + let governor = governorExt; + if (!governor) { + [ governor ] = await hre.ethers.getSigners(); + } + + logger.info(`Governor acquired as ${governor.address}`); + + const znsUpgraded = await contractData.reduce( + async ( + acc : Promise, + { contractName, instanceName, address } + ) => { + const upgradedContracts = await acc; + + upgradedContracts[instanceName] = await upgradeZNSContract({ + contractName, + contractAddress: address, + governor: governor as SignerWithAddress, + logger, + }); + + 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); + + let upgradedContract = await hre.upgrades.upgradeProxy( + contractAddress, + upgradedFactory, + ); + + upgradedContract = await upgradedContract.waitForDeployment(); + + logger.info(`Upgraded ${contractName} to new implementation.`); + + const storageDataPostUpgrade = await readContractStorage( + upgradedFactory, + 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 as unknown as ZNSContractUpgraded; +}; + +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, + contractNames : Partial, +) : Promise> => Object.values(contractNames).reduce( + async ( + acc : Promise>, + { contract, instance } + ) => { + const contractData = await acc; + + const contractDoc = await dbAdapter.getContract(contract, dbAdapter.curVersion) as IContractDbData; + const { address } = contractDoc; + + contractData.push({ + contractName: contract, + instanceName: instance, + address, + }); + + return contractData; + }, + Promise.resolve([]) +); 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/src/utils/migration/01_validation.ts b/src/utils/migration/01_validation.ts new file mode 100644 index 000000000..4baf3ecb4 --- /dev/null +++ b/src/utils/migration/01_validation.ts @@ -0,0 +1,136 @@ +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 { 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 + * - 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 - 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(); + + // Keeping as separate collections from the start will help downstream registration + const rootDomainObjects = await getDomains(true); + const subdomainObjects = await getDomains(false); + + logger.info(`Found ${rootDomainObjects.length + subdomainObjects.length} domains`); + + const env = process.env.ENV_LEVEL; + + if (!env) throw Error("No ENV_LEVEL set in .env file"); + + const zns = await getZNS(migrationAdmin, env); + + 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` 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`); + + const dbName = process.env.MONGO_DB_NAME_WRITE; + 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"); + + // Can iterate all at once for simplicity + let index = 0; + for(const domain of [...roots, ...subs]) { + try { + // 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 + invalidDomains.push({ message: (e as Error).message, domain }); + } + + ++index; + + if (index % 50 === 0) { + logger.info(`Processed ${index} domains`); + } + } + + // Connect to database collection and write user domain data to DB + const client : Db = (await getDBAdapter(uri)).db(dbName); + + await updateCollection( + client, + ROOT_COLL_NAME, + validRoots + ); + + await updateCollection( + client, + SUB_COLL_NAME, + validSubs + ); + + // Domains that have data inconsistencies + if (invalidDomains.length > 0) { + await updateCollection( + client, + INVALID_COLL_NAME, + invalidDomains + ); + } +}; + +main() + .then(() => process.exit(0)) + .catch(error => { + console.error(error); + process.exit(1); + }); diff --git a/src/utils/migration/03_airdrop.ts b/src/utils/migration/03_airdrop.ts new file mode 100644 index 000000000..fd82633b2 --- /dev/null +++ b/src/utils/migration/03_airdrop.ts @@ -0,0 +1,139 @@ +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(); + +/** + * 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 Array; + const subs = await getDomains(false) as Array; + + // 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 = []; + + // Keep track of tokens as we iterate for later use + const tokensMap : Map = new Map(); + + logger.info("Processing..."); + for (const [i,d] of [...roots, ...subs].entries()) { + // 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.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 + 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, 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; + + if (!tokensMap.has(paymentToken)) { + tokensMap.set(paymentToken, d.parent.treasury.paymentToken.id); + } + } else { + errorDomains.push(d); + continue; + } + } + + 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(d.amountPaidStake)); + userAmounts.set(d.owner.id, tokenAmounts); + } else { + const tokenAmount = new Map(); + + 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 + if (i % 50 === 0) { + logger.info(i); + } + } + + for (const 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}`); + + // 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]; + + // Row to build up as we read the specific token values + const row = ["erc20"]; + + for(const token of tokensMap) { + // 0 is token symbol + const amount = amountsMap.get(token[0]); + + // Amount may be null for one or both + if (amount) { + // 1 is token contract address + row.push(token[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/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 new file mode 100644 index 000000000..8f2d2578c --- /dev/null +++ b/src/utils/migration/database.ts @@ -0,0 +1,60 @@ +import { Db, MongoClient, ServerApiVersion, Document } 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 mongoClient.connect(); +}; + +export const getZNSFromDB = async () => { + 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"); + } + + const dbAdapter = await getDBAdapter(uri); + + if(!dbName) { + throw new Error(`Failed to connect: database "${dbName}" not found`); + } + + const db = await dbAdapter.db(dbName); + + const zns = await db.collection("contracts").find( + { version } + ).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 diff --git a/src/utils/migration/subgraph/client.ts b/src/utils/migration/subgraph/client.ts new file mode 100644 index 000000000..5ac3c66b7 --- /dev/null +++ b/src/utils/migration/subgraph/client.ts @@ -0,0 +1,20 @@ +import { + ApolloClient, + HttpLink, + InMemoryCache, + NormalizedCacheObject, +} from "@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, fetch }), + cache: new InMemoryCache(), + }); + + return client; +}; diff --git a/src/utils/migration/subgraph/index.ts b/src/utils/migration/subgraph/index.ts new file mode 100644 index 000000000..fd7d66466 --- /dev/null +++ b/src/utils/migration/subgraph/index.ts @@ -0,0 +1,55 @@ +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 getDomains = async (isWorld : boolean) => { + const first = 1000; + let skip = 0; + + let client = await createClient(); + + // First get all worlds + let result = await client.query({ + query: q.getDomains, + variables: { + first, + skip, + isWorld, + }, + }); + + if (result.errors) throw Error(`Error in graph query: ${result.errors}`); + + const domains = []; + + // We do this to collect ALL domains in a single array + while (result.data.domains.length > 0) { + // For each user, get every domain + for (const domain of result.data.domains) { + // user data from subgraph already has user and all domains + // so just return this + domains.push(domain); + } + + // Get next batch of domains + skip += 1000; + + // Refresh client each iteration + client = await createClient(); + + result = await client.query({ + query: q.getDomains, + variables: { + first, + skip, + isWorld, + }, + }); + + if (result.errors) throw Error(`Error in graph query: ${result.errors}`); + } + + return domains; +}; + + diff --git a/src/utils/migration/subgraph/queries.ts b/src/utils/migration/subgraph/queries.ts new file mode 100644 index 000000000..470838559 --- /dev/null +++ b/src/utils/migration/subgraph/queries.ts @@ -0,0 +1,97 @@ +import { gql } from "@apollo/client/core"; + +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 + address + isWorld + isRevoked + parentHash + amountPaidStake + amountPaidDirect + accessType + pricerContract + paymentType + subdomainCount + tokenId + tokenURI + creationBlock + creationTimestamp + parent { + id + label + depth + isWorld + isRevoked + tokenId + tokenURI + parentHash + treasury { + paymentToken { + id + name + symbol + } + } + parent { + id + label + depth + isWorld + isRevoked + tokenId + tokenURI + parentHash + treasury { + paymentToken { + id + name + symbol + } + } + parent { + id + label + depth + isWorld + isRevoked + tokenId + tokenURI + parentHash + treasury { + paymentToken { + id + name + symbol + } + } + } + } + } + curvePriceConfig { + id + } + fixedPriceConfig { + id + } + } + } +`; diff --git a/src/utils/migration/types.ts b/src/utils/migration/types.ts new file mode 100644 index 000000000..58b08a538 --- /dev/null +++ b/src/utils/migration/types.ts @@ -0,0 +1,99 @@ +import { IDistributionConfig, IPaymentConfig } from "../../../test/helpers/types"; + +export interface Domain { + id : string; + minter : User; + owner : User; + domainToken : DomainToken; + depth : number; + label : string; + isReclaimable : boolean; + reclaimableAddress : string; + address : string; + parentHash : string; + isWorld : boolean; + isRevoked : boolean; + amountPaidStake : bigint; + amountPaidDirect : bigint; + parent : Domain | null; + accessType : string; + paymentType : string; + pricerContract : string; + curvePriceConfig : CurvePriceConfig; + fixedPriceConfig : FixedPriceConfig; + subdomainCount : number; + tokenId : string; + tokenURI : string; + treasury : Treasury; + creationBlock : number; +} + +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 Treasury { + id : string; + beneficiaryAddress : string; + paymentToken : PaymentToken; + domain : Domain; +} + +interface PaymentToken { + id : string; + name : string; + symbol : string; +} + +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; +} + +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 new file mode 100644 index 000000000..9bd7b841e --- /dev/null +++ b/src/utils/migration/validate.ts @@ -0,0 +1,105 @@ +import { 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, +) => { + // 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), + zns.addressResolver.resolveDomainAddress(domain.id), + zns.subRegistrar.distrConfigs(domain.id), + ]; + + const [ + domainOwner, + domainTokenOwner, + domainAddress, + distrConfig, + ] = await Promise.all(promises) as unknown as [string, string, string, IDistributionConfig]; + + 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(), + `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(), + `Domain ${domain.id} has differing domain addresses: + Contract: ${domainAddress.toLowerCase()} + 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.paymentType, domain.paymentType ?? 0n, + `Domain ${domain.id} has different payment types. + Contract: ${distrConfig.paymentType} + Subgraph: ${domain.paymentType ?? 0n} + ` + ); + + 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`); + 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.ok(domain.depth > 0,`Domain ${domain.id} 'isWorld' is false, but 'depth' is 0`); + } +}; diff --git a/src/utils/migration/zns-contract-data.ts b/src/utils/migration/zns-contract-data.ts new file mode 100644 index 000000000..8b0c0cc16 --- /dev/null +++ b/src/utils/migration/zns-contract-data.ts @@ -0,0 +1,62 @@ +import { znsNames } from "../../deploy/missions/contracts/names"; +import { IZNSContracts } from "../../../test/helpers/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, + 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 { + return 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); + + 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; +}; diff --git a/src/utils/withdraw-staked.ts b/src/utils/withdraw-staked.ts new file mode 100644 index 000000000..d8c533856 --- /dev/null +++ b/src/utils/withdraw-staked.ts @@ -0,0 +1,53 @@ +import * as hre from "hardhat"; +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, +} : { + token : string; + to ?: string; +}) => { + const [ governor ] = await hre.ethers.getSigners(); + + if (!token) { + throw new Error("Token address is undefined"); + } + + const dbAdapter = await getMongoAdapter(); + + 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; +}; diff --git a/test/DeployCampaign.integration.test.ts b/test/DeployCampaign.integration.test.ts index 1ff978a2d..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 @@ -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/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/Pausable.upgrade.test.ts b/test/Pausable.upgrade.test.ts new file mode 100644 index 000000000..a1ae8d564 --- /dev/null +++ b/test/Pausable.upgrade.test.ts @@ -0,0 +1,875 @@ +/* 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"; +import { runZnsCampaign } from "../src/deploy/zns-campaign"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +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"; +import { + AccessType, + 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"; +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"; +import { getMongoAdapter, resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; +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/utils/withdraw-staked"; + + +describe("ZNS Upgrade and Pause Test", () => { + let deployer : SignerWithAddress; + let rootOwner : SignerWithAddress; + let lvl2SubOwner : SignerWithAddress; + let lvl3SubOwner : SignerWithAddress; + let lvl4SubOwner : SignerWithAddress; + let lvl5SubOwner : SignerWithAddress; + let lvl6SubOwner : SignerWithAddress; + + let zns : TZNSContractState; + + let domainConfigs : Array; + let domainHashes : Array; + + const fixedPrice = ethers.parseEther("1375.612"); + const fixedFeePercentage = BigInt(200); + + const contractNames = getContractNamesToUpgrade(); + + let contractData : Array; + let znsUpgraded : IZNSContractsUpgraded; + + let preUpgradeZnsStorage : Array; + let preUpgradeImpls : Array; + + 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; }>; + }; + + let dbVersionDeploy : IDBVersion; + let dbAdapterUpgrade : MongoDBAdapter; + + before(async () => { + [ + deployer, + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + 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" + ? process.env.ZERO_VAULT_ADDRESS as string + : deployer.address, + }); + + resetMongoAdapter(); + + const campaign = await runZnsCampaign({ + config, + }); + + zns = campaign.state.contracts; + zns.zeroVaultAddress = hre.network.name !== "hardhat" + ? process.env.ZERO_VAULT_ADDRESS as string + : deployer.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] as ZNSContract; + + const storage = await readContractStorage(contractFactory, contractObj); + + return [...newAcc, storage]; + }, Promise.resolve([]) + ); + + logger.debug("Funding users..."); + // Give funds to users + 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() + ); + 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 = [ + { + 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) }, + }, + }, + { + 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, + }, + }, + { + 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, + }, + }, + { + 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, + }, + }, + { + 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 }, + }, + }, + { + 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, + }, + }, + ]; + + logger.debug("Registering a path of domains..."); + const regResults = await registerDomainPath({ + zns, + domainConfigs, + confirmations: isRealNetwork ? 2 : undefined, + }); + + domainHashes = regResults.map(({ domainHash }) => domainHash); + + // get contract data for the upgrade helper + contractData = await getContractDataForUpgrade(dbAdapterDeploy, getContractNamesToUpgrade()); + + 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(); + + // run the upgrade + znsUpgraded = await upgradeZNS({ + governorExt: deployer, + contractData, + logger, + }); + + // update database records to new implementations + await updateDbAndVerifyAll(dbAdapterUpgrade); + + // list of all the methods that are blocked with `whenNotPaused` modifier + // along with arguments for calls + methodCalls = { + [znsNames.registry.instance]: [ + { + method: "setOwnersOperator", + args: [deployer.address, true], + }, + { + method: "createDomainRecord", + args: [hre.ethers.ZeroHash, deployer.address, "address"], + }, + { + method: "updateDomainRecord", + args: [hre.ethers.ZeroHash, deployer.address, "address"], + }, + { + method: "updateDomainOwner", + args: [hre.ethers.ZeroHash, deployer.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, rootOwner.address, 1], + }, + { + // @ts-ignore + method: "safeTransferFrom(address,address,uint256)", + args: [deployer.address, rootOwner.address, "1"], + }, + { + // @ts-ignore + method: "safeTransferFrom(address,address,uint256,bytes)", + args: [deployer.address, rootOwner.address, "1", hre.ethers.ZeroHash], + }, + { + method: "approve", + args: [rootOwner.address, 1], + }, + { + method: "setApprovalForAll", + args: [rootOwner.address, true], + }, + { + method: "register", + args: [rootOwner.address, 123n, "dummyURI"], + }, + { + method: "revoke", + args: [123n], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.addressResolver.instance]: [ + { + method: "setAddress", + args: [hre.ethers.ZeroHash, rootOwner.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, rootOwner.address, 1n, 1n, 1n], + }, + { + method: "unstakeForDomain", + args: [hre.ethers.ZeroHash, rootOwner.address], + }, + { + method: "processDirectPayment", + args: [hre.ethers.ZeroHash, hre.ethers.ZeroHash, rootOwner.address, 1n, 1n], + }, + { + method: "setPaymentConfig", + args: [hre.ethers.ZeroHash, paymentConfigEmpty], + }, + { + method: "setBeneficiary", + args: [hre.ethers.ZeroHash, rootOwner.address], + }, + { + method: "setPaymentToken", + args: [hre.ethers.ZeroHash, rootOwner.address], + }, + { + method: "pause", + args: [], + }, + ], + [znsNames.rootRegistrar.instance]: [ + { + method: "registerRootDomain", + args: ["domain", rootOwner.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", rootOwner.address, "uri", distrConfigEmpty, paymentConfigEmpty], + }, + { + method: "setDistributionConfigForDomain", + args: [hre.ethers.ZeroHash, distrConfigEmpty], + }, + { + method: "setPricerContractForDomain", + args: [hre.ethers.ZeroHash, rootOwner.address], + }, + { + method: "setPaymentTypeForDomain", + args: [hre.ethers.ZeroHash, 0], + }, + { + method: "setAccessTypeForDomain", + args: [hre.ethers.ZeroHash, 0], + }, + { + method: "updateMintlistForDomain", + args: [hre.ethers.ZeroHash, [rootOwner.address], [true]], + }, + { + method: "clearMintlistForDomain", + args: [hre.ethers.ZeroHash], + }, + { + method: "clearMintlistAndLock", + args: [hre.ethers.ZeroHash], + }, + { + method: "pause", + args: [], + }, + ], + }; + }); + + after(async () => { + if (hre.network.name === "hardhat") { + await dbAdapterUpgrade.dropDB(); + resetMongoAdapter(); + 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); + 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); + }); + + 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", () => { + 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.not.equal(upgradedDbVersion); + expect(upgradedDbVersion).to.not.equal(process.env.MONGO_DB_VERSION); + + expect(contractsVersion).to.equal(getGitTag()); + }); + + it("should update db 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, upgradedDbVersion) 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.not.equal(dbVersionDeploy.dbVersion); + expect(versionPostUpgrade).to.equal(upgradedDbVersion); + }, 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( + 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 + let tx = await zns.subRegistrar.connect(user).setDistributionConfigForDomain( + domainHash, + { + pricerContract: newPricer, + paymentType: newPaymentType, + accessType: newAccessType, + } + ); + if (isRealNetwork) await tx.wait(2); + // 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 + tx = await zns.treasury.connect(user).setPaymentConfig( + domainHash, + { + 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(rootOwner.address); + expect(paymentConfig.beneficiary).to.equal(rootOwner.address); + + if ((fullConfig.priceConfig as IFixedPriceConfig).price) { + // check FixedPricer storage + const newPriceConfig = { + price: 111n, + feePercentage: 111n, + isSet: true, + }; + // set new values + 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); + 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 + 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); + 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 + 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(rootOwner.address); + + // check DomainToken storage + // set new values + 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(rootOwner.address); + + // check Registry storage + // set new values + 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(rootOwner.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((storagePre, idx) => { + const storagePost = postUpgradeStorageData[idx]; + + expect(storagePre.length).to.equal(storagePost.length); + + storagePre.forEach((pre, idx2) => { + const post = storagePost[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]; + + const tx = await contract.connect(deployer).pause(); + if (isRealNetwork) await tx.wait(2); + + 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`); + } + }); + } + ); + }); + + 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("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); + + 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); + 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()); + }); + + 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) + ); + }); + + 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); + const contractBalanceBeforeWithdraw = await zns.meowToken.balanceOf(zns.treasury.target); + + 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 + ); + }); + }); +}); 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..baa756ed6 100644 --- a/test/ZNSRootRegistrar.test.ts +++ b/test/ZNSRootRegistrar.test.ts @@ -1,1364 +1,1366 @@ -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"; +import { resetMongoAdapter } from "../src/deploy/db/mongo-adapter/get-adapter"; + +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(); + resetMongoAdapter(); + }); + + 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..22d745841 100644 --- a/test/helpers/deploy-helpers.ts +++ b/test/helpers/deploy-helpers.ts @@ -1,176 +1,180 @@ - -// 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 + +// 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) { + const tx = await zns.meowToken.connect(signer).mint( + signer.address, + amount + ); + + if (hre.network.name !== "hardhat") { + await tx.wait(2); + } + } +}; + +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(balanceBefore - balanceAfter).to.be.eq(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(balanceBefore - balanceAfter).to.be.eq(price + stakeFee + protocolFee); + } + + + expect(await zns.registry.exists(subdomainHashes[index])).to.be.true; + + index++; + } +}; 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..ab51ef10e 100644 --- a/test/helpers/register-setup.ts +++ b/test/helpers/register-setup.ts @@ -5,12 +5,13 @@ 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"; 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; 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 { diff --git a/yarn.lock b/yarn.lock index 9a1d19870..dd4f93e12 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" @@ -179,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" @@ -586,6 +607,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" @@ -1815,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" @@ -1830,6 +1861,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" @@ -2002,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" @@ -2042,10 +2085,38 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@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== +"@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.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" @@ -2053,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" @@ -2115,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" @@ -2317,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" @@ -2535,6 +2616,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" @@ -3201,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" @@ -3425,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" @@ -3830,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" @@ -3869,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" @@ -3913,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" @@ -3932,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" @@ -4377,6 +4498,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" @@ -4835,6 +4961,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 +5175,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 +5778,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== @@ -5653,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" @@ -6122,6 +6267,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" @@ -6581,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== @@ -6997,7 +7149,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 +7253,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" @@ -7314,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" @@ -7350,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" @@ -7563,6 +7737,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 +7829,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 +8026,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" @@ -8112,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" @@ -8442,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" @@ -8711,6 +8922,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 +9125,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 +9190,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 +9858,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"