diff --git a/contracts/price/IZNSCurvePricer.sol b/contracts/price/IZNSCurvePricer.sol index 7ef0d4830..1b029f435 100644 --- a/contracts/price/IZNSCurvePricer.sol +++ b/contracts/price/IZNSCurvePricer.sol @@ -69,7 +69,8 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer { function getPrice( bytes32 parentHash, - string calldata label + string calldata label, + bool skipValidityCheck ) external view returns (uint256); function getFeeForPrice( @@ -79,7 +80,8 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer { function getPriceAndFee( bytes32 parentHash, - string calldata label + string calldata label, + bool skipValidityCheck ) external view returns ( uint256 price, uint256 stakeFee diff --git a/contracts/price/IZNSFixedPricer.sol b/contracts/price/IZNSFixedPricer.sol index 2968ae72f..cf09360db 100644 --- a/contracts/price/IZNSFixedPricer.sol +++ b/contracts/price/IZNSFixedPricer.sol @@ -37,7 +37,11 @@ interface IZNSFixedPricer is IZNSPricer { function setPrice(bytes32 domainHash, uint256 _price) external; - function getPrice(bytes32 parentHash, string calldata label) external view returns (uint256); + function getPrice( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view returns (uint256); function setFeePercentage( bytes32 domainHash, @@ -51,7 +55,8 @@ interface IZNSFixedPricer is IZNSPricer { function getPriceAndFee( bytes32 parentHash, - string calldata label + string calldata label, + bool skipValidityCheck ) external view returns (uint256 price, uint256 fee); function setPriceConfig( diff --git a/contracts/price/ZNSCurvePricer.sol b/contracts/price/ZNSCurvePricer.sol index fe23b2042..f35d89867 100644 --- a/contracts/price/ZNSCurvePricer.sol +++ b/contracts/price/ZNSCurvePricer.sol @@ -1,339 +1,354 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { IZNSCurvePricer } from "./IZNSCurvePricer.sol"; -import { StringUtils } from "../utils/StringUtils.sol"; -import { AAccessControlled } from "../access/AAccessControlled.sol"; -import { ARegistryWired } from "../registry/ARegistryWired.sol"; - - -/** - * @title Implementation of the Curve Pricing, module that calculates the price of a domain - * based on its length and the rules set by Zero ADMIN. - * This module uses an asymptotic curve that starts from `maxPrice` for all domains <= `baseLength`. - * It then decreases in price, using the calculated price function below, until it reaches `minPrice` - * at `maxLength` length of the domain name. Price after `maxLength` is fixed and always equal to `minPrice`. - */ -contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSCurvePricer { - 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; - - /// @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 - * @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 - */ - function getPrice( - bytes32 parentHash, - string calldata label - ) public view override returns (uint256) { - require( - priceConfigs[parentHash].isSet, - "ZNSCurvePricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" - ); - - 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 - ) external view override returns (uint256 price, uint256 stakeFee) { - price = getPrice(parentHash, label); - 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 ADMIN 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 { - 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 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 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 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 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 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 - onlyOwnerOrOperator(domainHash) { - require( - feePercentage <= PERCENTAGE_BASIS, - "ZNSCurvePricer: feePercentage cannot be greater than PERCENTAGE_BASIS" - ); - - priceConfigs[domainHash].feePercentage = feePercentage; - emit FeePercentageSet(domainHash, feePercentage); - } - - /** - * @notice Sets the registry address in state. - * @dev This function is required for all contracts inheriting `ARegistryWired`. - */ - function setRegistry(address registry_) external override(ARegistryWired, IZNSCurvePricer) onlyAdmin { - _setRegistry(registry_); - } - - /** - * @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); - } -} +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IZNSCurvePricer } from "./IZNSCurvePricer.sol"; +import { StringUtils } from "../utils/StringUtils.sol"; +import { AAccessControlled } from "../access/AAccessControlled.sol"; +import { ARegistryWired } from "../registry/ARegistryWired.sol"; + + +/** + * @title Implementation of the Curve Pricing, module that calculates the price of a domain + * based on its length and the rules set by Zero ADMIN. + * This module uses an asymptotic curve that starts from `maxPrice` for all domains <= `baseLength`. + * It then decreases in price, using the calculated price function below, until it reaches `minPrice` + * at `maxLength` length of the domain name. Price after `maxLength` is fixed and always equal to `minPrice`. + */ +contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSCurvePricer { + 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; + + /// @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 ADMIN 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 { + 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 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 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 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 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 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 + onlyOwnerOrOperator(domainHash) { + require( + feePercentage <= PERCENTAGE_BASIS, + "ZNSCurvePricer: feePercentage cannot be greater than PERCENTAGE_BASIS" + ); + + priceConfigs[domainHash].feePercentage = feePercentage; + emit FeePercentageSet(domainHash, feePercentage); + } + + /** + * @notice Sets the registry address in state. + * @dev This function is required for all contracts inheriting `ARegistryWired`. + */ + function setRegistry(address registry_) external override(ARegistryWired, IZNSCurvePricer) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @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/price/ZNSFixedPricer.sol b/contracts/price/ZNSFixedPricer.sol index c93ff21f6..e79d3103d 100644 --- a/contracts/price/ZNSFixedPricer.sol +++ b/contracts/price/ZNSFixedPricer.sol @@ -1,159 +1,180 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -import { AAccessControlled } from "../access/AAccessControlled.sol"; -import { ARegistryWired } from "../registry/ARegistryWired.sol"; -import { IZNSFixedPricer } from "./IZNSFixedPricer.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - - -/** - * @notice Pricer contract that uses the most straightforward fixed pricing model - * that doesn't depend on the length of the label. -*/ -contract ZNSFixedPricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSFixedPricer { - - 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; - - /// @custom:oz-upgrades-unsafe-allow constructor - constructor() { - _disableInitializers(); - } - - function initialize(address _accessController, address _registry) external override initializer { - _setAccessController(_accessController); - setRegistry(_registry); - } - - // TODO audit question: should we add onlyProxy modifiers for every function in proxied contracts ?? - /** - * @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 onlyOwnerOrOperator(domainHash) { - _setPrice(domainHash, _price); - } - - /** - * @notice Gets the price for a subdomain candidate label under the parent domain. - * @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 - */ - // solhint-disable-next-line no-unused-vars - function getPrice(bytes32 parentHash, string calldata label) public override view returns (uint256) { - require( - priceConfigs[parentHash].isSet, - "ZNSFixedPricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" - ); - 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 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 { - 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 - */ - function getPriceAndFee( - bytes32 parentHash, - string calldata label - ) external view override returns (uint256 price, uint256 fee) { - price = getPrice(parentHash, label); - fee = getFeeForPrice(parentHash, price); - return (price, fee); - } - - /** - * @notice Sets the registry address in state. - * @dev This function is required for all contracts inheriting `ARegistryWired`. - */ - function setRegistry(address registry_) public override(ARegistryWired, IZNSFixedPricer) onlyAdmin { - _setRegistry(registry_); - } - - /** - * @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); - } -} +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { AAccessControlled } from "../access/AAccessControlled.sol"; +import { ARegistryWired } from "../registry/ARegistryWired.sol"; +import { IZNSFixedPricer } from "./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 ZNSFixedPricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSFixedPricer { + 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; + + /// @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 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 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 { + setPrice(domainHash, priceConfig.price); + setFeePercentage(domainHash, priceConfig.feePercentage); + priceConfigs[domainHash].isSet = true; + } + + /** + * @notice Part of the IZNSPricer interface - one of the functions required + * for any pricing contracts used with ZNS. It returns fee for a given price + * based on the value set by the owner of the parent domain. + * @param parentHash The hash of the parent domain under which fee is determined + * @param price The price to get the fee for + */ + function getFeeForPrice( + bytes32 parentHash, + uint256 price + ) public view override returns (uint256) { + return (price * priceConfigs[parentHash].feePercentage) / PERCENTAGE_BASIS; + } + + /** + * @notice Part of the IZNSPricer interface - one of the functions required + * for any pricing contracts used with ZNS. Returns both price and fee for a given label + * under the given parent. + * @param parentHash The hash of the parent domain under which price and fee are determined + * @param label The label of the subdomain candidate to get the price and fee for before/during registration + * @param skipValidityCheck If true, skips the validity check for the label + */ + function getPriceAndFee( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view override returns (uint256 price, uint256 fee) { + price = getPrice(parentHash, label, skipValidityCheck); + fee = getFeeForPrice(parentHash, price); + return (price, fee); + } + + /** + * @notice Sets the registry address in state. + * @dev This function is required for all contracts inheriting `ARegistryWired`. + */ + function setRegistry(address registry_) public override(ARegistryWired, IZNSFixedPricer) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @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/registrar/ZNSRootRegistrar.sol b/contracts/registrar/ZNSRootRegistrar.sol index 5b05a2ca8..c55025602 100644 --- a/contracts/registrar/ZNSRootRegistrar.sol +++ b/contracts/registrar/ZNSRootRegistrar.sol @@ -1,411 +1,411 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.18; - -import { IZNSRootRegistrar, CoreRegisterArgs } from "./IZNSRootRegistrar.sol"; -import { IZNSTreasury } from "../treasury/IZNSTreasury.sol"; -import { IZNSDomainToken } from "../token/IZNSDomainToken.sol"; -import { IZNSAddressResolver } from "../resolver/IZNSAddressResolver.sol"; -import { AAccessControlled } from "../access/AAccessControlled.sol"; -import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; -import { IZNSSubRegistrar } from "../registrar/IZNSSubRegistrar.sol"; -import { ARegistryWired } from "../registry/ARegistryWired.sol"; -import { IZNSPricer } from "../types/IZNSPricer.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 ZNSRootRegistrar is - UUPSUpgradeable, - AAccessControlled, - ARegistryWired, - IZNSRootRegistrar { - - IZNSPricer public rootPricer; - IZNSTreasury public treasury; - IZNSDomainToken public domainToken; - IZNSAddressResolver public addressResolver; - IZNSSubRegistrar public subRegistrar; - - /// @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 - * @param addressResolver_ Address of the ZNSAddressResolver contract - */ - function initialize( - address accessController_, - address registry_, - address rootPricer_, - address treasury_, - address domainToken_, - address addressResolver_ - ) external override initializer { - _setAccessController(accessController_); - setRegistry(registry_); - setRootPricer(rootPricer_); - setTreasury(treasury_); - setDomainToken(domainToken_); - setAddressResolver(addressResolver_); - } - - /** - * @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. - */ - function registerRootDomain( - string calldata name, - address domainAddress, - string calldata tokenURI, - DistributionConfig calldata distributionConfig - ) external override returns (bytes32) { - require( - bytes(name).length != 0, - "ZNSRootRegistrar: Domain Name not provided" - ); - - // 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); - - _coreRegister( - CoreRegisterArgs( - bytes32(0), - domainHash, - name, - msg.sender, - domainPrice, - 0, - domainAddress, - tokenURI, - true - ) - ); - - if (address(distributionConfig.pricerContract) != address(0)) { - // this adds additional gas to the register tx if passed - subRegistrar.setDistributionConfigForDomain(domainHash, distributionConfig); - } - - return domainHash; - } - - /** - * @notice External function used by `ZNSSubRegistrar` for the final stage of registering subdomains. - * @param args `CoreRegisterArgs`: Struct containing all the arguments required to register a domain - * with ZNSRootRegistrar.coreRegister(): - * + `parentHash`: The hash of the parent domain (0x0 for root domains) - * + `domainHash`: The hash of the domain to be registered - * + `label`: The label of the domain to be registered - * + `registrant`: The address of the user who is registering the domain - * + `price`: The determined price for the domain to be registered based on parent rules - * + `stakeFee`: The determined stake fee for the domain to be registered (only for PaymentType.STAKE!) - * + `domainAddress`: The address to which the domain will be resolved to - * + `tokenURI`: The tokenURI for the domain to be registered - * + `isStakePayment`: A flag for whether the payment is a stake payment or not - */ - function coreRegister( - CoreRegisterArgs memory args - ) external override onlyRegistrar { - _coreRegister( - args - ); - } - - /** - * @dev 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(addressResolver)); - addressResolver.setAddress(args.domainHash, args.domainAddress); - } else { - registry.createDomainRecord(args.domainHash, args.registrant, address(0)); - } - - emit DomainRegistered( - args.parentHash, - args.domainHash, - tokenId, - args.label, - 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 - { - 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 - { - require( - isOwnerOf(domainHash, msg.sender, OwnerOf.TOKEN), - "ZNSRootRegistrar: Not the owner of the Token" - ); - registry.updateDomainOwner(domainHash, msg.sender); - - emit DomainReclaimed(domainHash, msg.sender); - } - - /** - * @notice Function to validate that a given candidate is the owner of his Name, Token or both. - * @param domainHash Hash of the domain to check - * @param candidate Address of the candidate to check for ownership of the above domain's properties - * @param ownerOf Enum value to determine which ownership to check for: NAME, TOKEN, BOTH - */ - function isOwnerOf(bytes32 domainHash, address candidate, OwnerOf ownerOf) public view override returns (bool) { - if (ownerOf == OwnerOf.NAME) { - return candidate == registry.getDomainOwner(domainHash); - } else if (ownerOf == OwnerOf.TOKEN) { - return candidate == domainToken.ownerOf(uint256(domainHash)); - } else if (ownerOf == OwnerOf.BOTH) { - return candidate == registry.getDomainOwner(domainHash) - && candidate == domainToken.ownerOf(uint256(domainHash)); - } - - revert("Wrong enum value for `ownerOf`"); - } - - /** - * @notice Setter function for the `ZNSRegistry` address in state. - * Only ADMIN in `ZNSAccessController` can call this function. - * @param registry_ Address of the `ZNSRegistry` contract - */ - function setRegistry(address registry_) public override(ARegistryWired, 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 = IZNSTreasury(treasury_); - - emit TreasurySet(treasury_); - } - - /** - * @notice Setter function for the `ZNSDomainToken` address in state. - * Only ADMIN in `ZNSAccessController` can call this function. - * @param domainToken_ Address of the `ZNSDomainToken` contract - */ - function setDomainToken(address domainToken_) public override onlyAdmin { - require( - domainToken_ != address(0), - "ZNSRootRegistrar: domainToken_ is 0x0 address" - ); - domainToken = IZNSDomainToken(domainToken_); - - emit DomainTokenSet(domainToken_); - } - - /** - * @notice Setter for `ZNSSubRegistrar` contract. Only ADMIN in `ZNSAccessController` can call this function. - * @param subRegistrar_ Address of the `ZNSSubRegistrar` contract - */ - function setSubRegistrar(address subRegistrar_) external override onlyAdmin { - require(subRegistrar_ != address(0), "ZNSRootRegistrar: subRegistrar_ is 0x0 address"); - - subRegistrar = IZNSSubRegistrar(subRegistrar_); - emit SubRegistrarSet(subRegistrar_); - } - - /** - * @notice Setter function for the `ZNSAddressResolver` address in state. - * Only ADMIN in `ZNSAccessController` can call this function. - * @param addressResolver_ Address of the `ZNSAddressResolver` contract - */ - function setAddressResolver(address addressResolver_) public override onlyAdmin { - require( - addressResolver_ != address(0), - "ZNSRootRegistrar: addressResolver_ is 0x0 address" - ); - addressResolver = IZNSAddressResolver(addressResolver_); - - emit AddressResolverSet(addressResolver_); - } - - /** - * @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); - } -} +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IZNSRootRegistrar, CoreRegisterArgs } from "./IZNSRootRegistrar.sol"; +import { IZNSTreasury } from "../treasury/IZNSTreasury.sol"; +import { IZNSDomainToken } from "../token/IZNSDomainToken.sol"; +import { IZNSAddressResolver } from "../resolver/IZNSAddressResolver.sol"; +import { AAccessControlled } from "../access/AAccessControlled.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IZNSSubRegistrar } from "../registrar/IZNSSubRegistrar.sol"; +import { ARegistryWired } from "../registry/ARegistryWired.sol"; +import { IZNSPricer } from "../types/IZNSPricer.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 ZNSRootRegistrar is + UUPSUpgradeable, + AAccessControlled, + ARegistryWired, + IZNSRootRegistrar { + using StringUtils for string; + + IZNSPricer public rootPricer; + IZNSTreasury public treasury; + IZNSDomainToken public domainToken; + IZNSAddressResolver public addressResolver; + IZNSSubRegistrar public subRegistrar; + + /// @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 + * @param addressResolver_ Address of the ZNSAddressResolver contract + */ + function initialize( + address accessController_, + address registry_, + address rootPricer_, + address treasury_, + address domainToken_, + address addressResolver_ + ) external override initializer { + _setAccessController(accessController_); + setRegistry(registry_); + setRootPricer(rootPricer_); + setTreasury(treasury_); + setDomainToken(domainToken_); + setAddressResolver(addressResolver_); + } + + /** + * @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. + */ + function registerRootDomain( + string calldata name, + address domainAddress, + string calldata tokenURI, + DistributionConfig calldata distributionConfig + ) external override 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, + name, + msg.sender, + domainPrice, + 0, + domainAddress, + tokenURI, + true + ) + ); + + if (address(distributionConfig.pricerContract) != address(0)) { + // this adds additional gas to the register tx if passed + subRegistrar.setDistributionConfigForDomain(domainHash, distributionConfig); + } + + return domainHash; + } + + /** + * @notice External function used by `ZNSSubRegistrar` for the final stage of registering subdomains. + * @param args `CoreRegisterArgs`: Struct containing all the arguments required to register a domain + * with ZNSRootRegistrar.coreRegister(): + * + `parentHash`: The hash of the parent domain (0x0 for root domains) + * + `domainHash`: The hash of the domain to be registered + * + `label`: The label of the domain to be registered + * + `registrant`: The address of the user who is registering the domain + * + `price`: The determined price for the domain to be registered based on parent rules + * + `stakeFee`: The determined stake fee for the domain to be registered (only for PaymentType.STAKE!) + * + `domainAddress`: The address to which the domain will be resolved to + * + `tokenURI`: The tokenURI for the domain to be registered + * + `isStakePayment`: A flag for whether the payment is a stake payment or not + */ + function coreRegister( + CoreRegisterArgs memory args + ) external override onlyRegistrar { + _coreRegister( + args + ); + } + + /** + * @dev 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(addressResolver)); + addressResolver.setAddress(args.domainHash, args.domainAddress); + } else { + registry.createDomainRecord(args.domainHash, args.registrant, address(0)); + } + + emit DomainRegistered( + args.parentHash, + args.domainHash, + tokenId, + args.label, + 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 + { + 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 + { + require( + isOwnerOf(domainHash, msg.sender, OwnerOf.TOKEN), + "ZNSRootRegistrar: Not the owner of the Token" + ); + registry.updateDomainOwner(domainHash, msg.sender); + + emit DomainReclaimed(domainHash, msg.sender); + } + + /** + * @notice Function to validate that a given candidate is the owner of his Name, Token or both. + * @param domainHash Hash of the domain to check + * @param candidate Address of the candidate to check for ownership of the above domain's properties + * @param ownerOf Enum value to determine which ownership to check for: NAME, TOKEN, BOTH + */ + function isOwnerOf(bytes32 domainHash, address candidate, OwnerOf ownerOf) public view override returns (bool) { + if (ownerOf == OwnerOf.NAME) { + return candidate == registry.getDomainOwner(domainHash); + } else if (ownerOf == OwnerOf.TOKEN) { + return candidate == domainToken.ownerOf(uint256(domainHash)); + } else if (ownerOf == OwnerOf.BOTH) { + return candidate == registry.getDomainOwner(domainHash) + && candidate == domainToken.ownerOf(uint256(domainHash)); + } + + revert("Wrong enum value for `ownerOf`"); + } + + /** + * @notice Setter function for the `ZNSRegistry` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param registry_ Address of the `ZNSRegistry` contract + */ + function setRegistry(address registry_) public override(ARegistryWired, 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 = IZNSTreasury(treasury_); + + emit TreasurySet(treasury_); + } + + /** + * @notice Setter function for the `ZNSDomainToken` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param domainToken_ Address of the `ZNSDomainToken` contract + */ + function setDomainToken(address domainToken_) public override onlyAdmin { + require( + domainToken_ != address(0), + "ZNSRootRegistrar: domainToken_ is 0x0 address" + ); + domainToken = IZNSDomainToken(domainToken_); + + emit DomainTokenSet(domainToken_); + } + + /** + * @notice Setter for `ZNSSubRegistrar` contract. Only ADMIN in `ZNSAccessController` can call this function. + * @param subRegistrar_ Address of the `ZNSSubRegistrar` contract + */ + function setSubRegistrar(address subRegistrar_) external override onlyAdmin { + require(subRegistrar_ != address(0), "ZNSRootRegistrar: subRegistrar_ is 0x0 address"); + + subRegistrar = IZNSSubRegistrar(subRegistrar_); + emit SubRegistrarSet(subRegistrar_); + } + + /** + * @notice Setter function for the `ZNSAddressResolver` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param addressResolver_ Address of the `ZNSAddressResolver` contract + */ + function setAddressResolver(address addressResolver_) public override onlyAdmin { + require( + addressResolver_ != address(0), + "ZNSRootRegistrar: addressResolver_ is 0x0 address" + ); + addressResolver = IZNSAddressResolver(addressResolver_); + + emit AddressResolverSet(addressResolver_); + } + + /** + * @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/registrar/ZNSSubRegistrar.sol b/contracts/registrar/ZNSSubRegistrar.sol index 51f928b43..bf0a2b906 100644 --- a/contracts/registrar/ZNSSubRegistrar.sol +++ b/contracts/registrar/ZNSSubRegistrar.sol @@ -6,6 +6,7 @@ import { IZNSRootRegistrar, CoreRegisterArgs } from "./IZNSRootRegistrar.sol"; import { IZNSSubRegistrar } from "./IZNSSubRegistrar.sol"; import { AAccessControlled } from "../access/AAccessControlled.sol"; import { ARegistryWired } from "../registry/ARegistryWired.sol"; +import { StringUtils } from "../utils/StringUtils.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; @@ -16,6 +17,7 @@ import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils * of any level is in the `ZNSRootRegistrar.coreRegister()`. */ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSSubRegistrar { + using StringUtils for string; /** * @notice State var for the ZNSRootRegistrar contract that finalizes registration of subdomains. @@ -84,6 +86,9 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, string calldata tokenURI, DistributionConfig calldata distrConfig ) external override returns (bytes32) { + // Confirms string values are only [a-z0-9-] + label.validate(); + bytes32 domainHash = hashWithParent(parentHash, label); require( !registry.exists(domainHash), @@ -125,13 +130,15 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, (coreRegisterArgs.price, coreRegisterArgs.stakeFee) = IZNSPricer(address(parentConfig.pricerContract)) .getPriceAndFee( parentHash, - label + label, + true ); } else { coreRegisterArgs.price = IZNSPricer(address(parentConfig.pricerContract)) .getPrice( parentHash, - label + label, + true ); } } @@ -200,9 +207,6 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, */ function setPricerContractForDomain( bytes32 domainHash, - // TODO audit question: is this a problem that we expect the simplest interface - // but are able set any of the derived ones ?? - // Can someone by setting their own contract here introduce a vulnerability ?? IZNSPricer pricerContract ) public override { require( diff --git a/contracts/types/IZNSPricer.sol b/contracts/types/IZNSPricer.sol index 32f00c6c3..c10900c7b 100644 --- a/contracts/types/IZNSPricer.sol +++ b/contracts/types/IZNSPricer.sol @@ -10,10 +10,18 @@ interface IZNSPricer { /** * @dev `parentHash` param is here to allow pricer contracts * to have different price configs for different subdomains + * `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. */ function getPrice( bytes32 parentHash, - string calldata label + string calldata label, + bool skipValidityCheck ) external view returns (uint256); /** @@ -23,7 +31,8 @@ interface IZNSPricer { */ function getPriceAndFee( bytes32 parentHash, - string calldata label + string calldata label, + bool skipValidityCheck ) external view returns (uint256 price, uint256 fee); /** diff --git a/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol b/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol index a3061f5a3..ead994245 100644 --- a/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol +++ b/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol @@ -9,6 +9,8 @@ import { IZNSRootRegistrar, CoreRegisterArgs } from "../../registrar/IZNSRootReg import { AAccessControlled } from "../../access/AAccessControlled.sol"; import { ARegistryWired } from "../../registry/ARegistryWired.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { StringUtils } from "../../utils/StringUtils.sol"; + enum AccessType { LOCKED, @@ -46,6 +48,8 @@ contract ZNSSubRegistrarUpgradeMock is ZNSSubRegistrarMainState, UpgradeMock { + using StringUtils for string; + modifier onlyOwnerOperatorOrRegistrar(bytes32 domainHash) { require( registry.isOwnerOrOperator(domainHash, msg.sender) @@ -72,6 +76,8 @@ contract ZNSSubRegistrarUpgradeMock is string memory tokenURI, DistributionConfig calldata distrConfig ) external returns (bytes32) { + label.validate(); + DistributionConfig memory parentConfig = distrConfigs[parentHash]; bool isOwnerOrOperator = registry.isOwnerOrOperator(parentHash, msg.sender); @@ -109,13 +115,15 @@ contract ZNSSubRegistrarUpgradeMock is (coreRegisterArgs.price, coreRegisterArgs.stakeFee) = IZNSPricer(address(parentConfig.pricerContract)) .getPriceAndFee( parentHash, - label + label, + true ); } else { coreRegisterArgs.price = IZNSPricer(address(parentConfig.pricerContract)) .getPrice( parentHash, - label + label, + true ); } } diff --git a/contracts/utils/StringUtils.sol b/contracts/utils/StringUtils.sol index c2e1bab51..764084d12 100644 --- a/contracts/utils/StringUtils.sol +++ b/contracts/utils/StringUtils.sol @@ -33,4 +33,31 @@ library StringUtils { } return len; } + + /** + * @dev Confirm that a given string has only alphanumeric characters [a-z0-9-] + * @param s The string to validate + */ + function validate(string memory s) internal pure { + bytes memory nameBytes = bytes(s); + uint256 length = nameBytes.length; + + uint256 MAX_INT = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + require( + length > 0 && length < MAX_INT, + "StringUtils: Domain label too long or nonexistent" + ); + + for (uint256 i; i < length;) { + bytes1 b = nameBytes[i]; + // Valid strings are lower case a-z, 0-9, or a hyphen + require( + (b > 0x60 && b < 0x7B) || (b > 0x2F && b < 0x3A) || b == 0x2D, + "StringUtils: Invalid domain label" + ); + unchecked { + ++i; + } + } + } } \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 92e766842..cadebbf8d 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -1,112 +1,112 @@ -/* eslint-disable @typescript-eslint/no-var-requires, @typescript-eslint/no-unused-vars */ -require("dotenv").config(); - -import { HardhatUserConfig } from "hardhat/config"; -import * as tenderly from "@tenderly/hardhat-tenderly"; -import "@nomicfoundation/hardhat-toolbox"; -import "@nomiclabs/hardhat-ethers"; -import "@nomicfoundation/hardhat-network-helpers"; -import "@nomicfoundation/hardhat-chai-matchers"; -import "@openzeppelin/hardhat-upgrades"; -import "solidity-coverage"; -import "solidity-docgen"; -import "hardhat-gas-reporter"; - -// 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 DevNet !!! -// tenderly.setup({ automaticVerifications: false }); - -const config : HardhatUserConfig = { - solidity: { - compilers: [ - { - version: "0.8.18", - 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: "https://mainnet.infura.io/v3/97e75e0bbc6a4419a5dd7fe4a518b917", - gasPrice: 80000000000, - }, - goerli: { - url: "https://goerli.infura.io/v3/77c3d733140f4c12a77699e24cb30c27", - timeout: 10000000, - }, - devnet: { - // Add current URL that you spawned if not using automated spawning - url: `${process.env.DEVNET_RPC_URL}`, - chainId: 1, - }, - }, - etherscan: { - apiKey: `${process.env.ETHERSCAN_API_KEY}`, - }, - 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 */ +require("dotenv").config(); + +import { HardhatUserConfig } from "hardhat/config"; +import * as tenderly from "@tenderly/hardhat-tenderly"; +import "@nomicfoundation/hardhat-toolbox"; +import "@nomiclabs/hardhat-ethers"; +import "@nomicfoundation/hardhat-network-helpers"; +import "@nomicfoundation/hardhat-chai-matchers"; +import "@openzeppelin/hardhat-upgrades"; +import "solidity-coverage"; +import "solidity-docgen"; +import "hardhat-gas-reporter"; + +// 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 DevNet !!! +// tenderly.setup({ automaticVerifications: false }); + +const config : HardhatUserConfig = { + solidity: { + compilers: [ + { + version: "0.8.18", + 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: "https://mainnet.infura.io/v3/97e75e0bbc6a4419a5dd7fe4a518b917", + gasPrice: 80000000000, + }, + goerli: { + url: "https://goerli.infura.io/v3/77c3d733140f4c12a77699e24cb30c27", + timeout: 10000000, + }, + devnet: { + // Add current URL that you spawned if not using automated spawning + url: `${process.env.DEVNET_RPC_URL}`, + chainId: 1, + }, + }, + etherscan: { + apiKey: `${process.env.ETHERSCAN_API_KEY}`, + }, + 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/test/ZNSCurvePricer.test.ts b/test/ZNSCurvePricer.test.ts index e7c9876f6..9ef1a37da 100644 --- a/test/ZNSCurvePricer.test.ts +++ b/test/ZNSCurvePricer.test.ts @@ -1,1001 +1,1012 @@ -import * as hre from "hardhat"; -import { expect } from "chai"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { BigNumber, ethers } from "ethers"; -import { parseEther } from "ethers/lib/utils"; -import { IZNSContracts } from "./helpers/types"; -import { - deployZNS, - calcCurvePrice, - precisionMultiDefault, - CURVE_PRICE_CONFIG_ERR, - validateUpgrade, - PaymentType, - NOT_AUTHORIZED_REG_WIRED_ERR, - CURVE_NO_ZERO_PRECISION_MULTIPLIER_ERR, INITIALIZED_ERR, -} from "./helpers"; -import { decimalsDefault, priceConfigDefault, registrationFeePercDefault } from "./helpers/constants"; -import { - getAccessRevertMsg, -} from "./helpers/errors"; -import { ADMIN_ROLE, GOVERNOR_ROLE } from "./helpers/access"; -import { ZNSCurvePricerUpgradeMock__factory, ZNSCurvePricer__factory } from "../typechain"; -import { registrationWithSetup } from "./helpers/register-setup"; -import { getProxyImplAddress } from "./helpers/utils"; - -require("@nomicfoundation/hardhat-chai-matchers"); - -const { HashZero } = ethers.constants; - -describe("ZNSCurvePricer", () => { - let deployer : SignerWithAddress; - let user : SignerWithAddress; - let admin : SignerWithAddress; - let randomAcc : SignerWithAddress; - - let zns : IZNSContracts; - let domainHash : string; - - const defaultDomain = "wilder"; - - beforeEach(async () => { - [ - deployer, - user, - admin, - randomAcc, - ] = await hre.ethers.getSigners(); - - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address], - adminAddresses: [admin.address], - }); - - await zns.zeroToken.connect(user).approve(zns.treasury.address, ethers.constants.MaxUint256); - await zns.zeroToken.mint(user.address, priceConfigDefault.maxPrice); - - const fullConfig = { - distrConfig: { - paymentType: PaymentType.DIRECT, - pricerContract: zns.curvePricer.address, - accessType: 1, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: user.address, - }, - priceConfig: priceConfigDefault, - }; - - domainHash = await registrationWithSetup({ - zns, - user, - domainLabel: "testdomain", - fullConfig, - }); - }); - - it("Should NOT let initialize the implementation contract", async () => { - const factory = new ZNSCurvePricer__factory(deployer); - const impl = await getProxyImplAddress(zns.curvePricer.address); - const implContract = factory.attach(impl); - - await expect( - implContract.initialize( - zns.accessController.address, - zns.registry.address, - priceConfigDefault - ) - ).to.be.revertedWith(INITIALIZED_ERR); - }); - - it("Confirms values were initially set correctly", async () => { - const valueCalls = [ - zns.curvePricer.priceConfigs(domainHash), - ]; - - const [ - priceConfigFromSC, - ] = await Promise.all(valueCalls); - - const priceConfigArr = Object.values(priceConfigDefault); - - priceConfigArr.forEach( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (val, idx) => expect(val).to.eq(priceConfigFromSC[idx]) - ); - - const regFromSC = await zns.curvePricer.registry(); - const acFromSC = await zns.curvePricer.getAccessController(); - - expect(regFromSC).to.eq(zns.registry.address); - expect(acFromSC).to.eq(zns.accessController.address); - }); - - describe("#getPrice", async () => { - it("Returns 0 price for a root name with no length", async () => { - const { - price, - stakeFee, - } = await zns.curvePricer.getPriceAndFee(domainHash, ""); - expect(price).to.eq(0); - expect(stakeFee).to.eq(0); - }); - - it("Returns the base price for domains that are equal to the base length", async () => { - // Using the default length of 3 - const domain = "eth"; - const params = await zns.curvePricer.priceConfigs(domainHash); - - const domainPrice = await zns.curvePricer.getPrice(domainHash, domain); - expect(domainPrice).to.eq(params.maxPrice); - }); - - it("Returns the base price for domains that are less than the base length", async () => { - const domainA = "et"; - const domainB = "e"; - const params = await zns.curvePricer.priceConfigs(domainHash); - - let domainPrice = await zns.curvePricer.getPrice(domainHash, domainA); - expect(domainPrice).to.eq(params.maxPrice); - - (domainPrice = await zns.curvePricer.getPrice(domainHash, domainB)); - expect(domainPrice).to.eq(params.maxPrice); - }); - - it("Returns expected prices for a domain greater than the base length", async () => { - // create a constant string with 22 letters - const domainOne = "abcdefghijklmnopqrstuv"; - const domainTwo = "akkasddaasdas"; - - // these values have been calced separately to validate - // that both forumlas: SC + helper are correct - // this value has been calces with the default priceConfig - const domainOneRefValue = BigNumber.from("4545450000000000000000"); - const domainTwoRefValue = BigNumber.from("7692300000000000000000"); - - const domainOneExpPrice = await calcCurvePrice(domainOne, priceConfigDefault); - const domainTwoExpPrice = await calcCurvePrice(domainTwo, priceConfigDefault); - - const domainOnePriceSC = await zns.curvePricer.getPrice(domainHash, domainOne); - const domainTwoPriceSC = await zns.curvePricer.getPrice(domainHash, domainTwo); - - expect(domainOnePriceSC).to.eq(domainOneRefValue); - expect(domainOnePriceSC).to.eq(domainOneExpPrice); - - expect(domainTwoPriceSC).to.eq(domainTwoRefValue); - expect(domainTwoPriceSC).to.eq(domainTwoExpPrice); - }); - - it("Returns a price even if the domain name is very long", async () => { - // 255 length - const domain = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu"; - - const expectedPrice = await calcCurvePrice(domain, priceConfigDefault); - const domainPrice = await zns.curvePricer.getPrice(domainHash, domain); - - expect(domainPrice).to.eq(expectedPrice); - }); - - it("Returns a price for multiple lengths", async () => { - // Any value less than base length is always base price, so we only check - // domains that are greater than base length + 1 - const short = "wild"; - const medium = "wilderworld"; - const long = "wilderworld.beasts.pets.nfts.cats.calico.steve"; - - const expectedShortPrice = await calcCurvePrice(short, priceConfigDefault); - const shortPrice = await zns.curvePricer.getPrice(domainHash, short); - expect(expectedShortPrice).to.eq(shortPrice); - - const expectedMediumPrice = await calcCurvePrice(medium, priceConfigDefault); - const mediumPrice = await zns.curvePricer.getPrice(domainHash, medium); - expect(expectedMediumPrice).to.eq(mediumPrice); - - const expectedLongPrice = await calcCurvePrice(long, priceConfigDefault); - const longPrice = await zns.curvePricer.getPrice(domainHash, long); - expect(expectedLongPrice).to.eq(longPrice); - }); - - it("Prices Special Characters Accurately", async () => { - const domainSpecialCharacterSet1 = "±ƒc¢Ãv"; - const domainSpecialCharacterSet2 = "œ柸þ€§ᆰ"; - const domainWithoutSpecials = "abcdef"; - const expectedPrice = calcCurvePrice(domainWithoutSpecials, priceConfigDefault); - let domainPrice = await zns.curvePricer.getPrice(domainHash, domainSpecialCharacterSet1); - expect(domainPrice).to.eq(expectedPrice); - - (domainPrice = await zns.curvePricer.getPrice(domainHash, domainSpecialCharacterSet2)); - expect(domainPrice).to.eq(expectedPrice); - }); - - it("Can Price Names Longer Than 255 Characters", async () => { - // 261 length - const domain = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + - "a"; - const expectedPrice = calcCurvePrice(domain, priceConfigDefault); - const domainPrice = await zns.curvePricer.getPrice(domainHash, domain); - expect(domainPrice).to.eq(expectedPrice); - }); - - // eslint-disable-next-line max-len - it.skip("Doesn't create price spikes with any valid combination of values (SLOW TEST, ONLY RUN LOCALLY)", async () => { - // Start by expanding the search space to allow for domains that are up to 1000 characters - await zns.curvePricer.connect(user).setMaxLength(domainHash, BigNumber.from("1000")); - - const promises = []; - let config = await zns.curvePricer.priceConfigs(domainHash); - let domain = "a"; - - // baseLength = 0 is a special case - await zns.curvePricer.connect(user).setBaseLength(domainHash, 0); - const domainPrice = await zns.curvePricer.getPrice(domainHash, domain); - expect(domainPrice).to.eq(config.maxPrice); - - let outer = 1; - let inner = outer; - // Long-running loops here to iterate all the variations for baseLength and - while(config.maxLength.gt(outer)) { - // Reset "domain" to a single character each outer loop - domain = "a"; - - await zns.curvePricer.connect(user).setBaseLength(domainHash, outer); - config = await zns.curvePricer.priceConfigs(domainHash); - - while (config.maxLength.gt(inner)) { - const priceTx = zns.curvePricer.getPrice(domainHash, domain); - promises.push(priceTx); - - domain += "a"; - inner++; - } - outer++; - } - - const prices = await Promise.all(promises); - let k = 0; - while (k < prices.length) { - expect(prices[k]).to.be.lte(config.maxPrice); - k++; - } - }); - }); - - describe("#setPriceConfig", () => { - it("Should set the config for any existing domain hash, including 0x0", async () => { - const newConfig = { - baseLength: BigNumber.from("6"), - maxLength: BigNumber.from("35"), - maxPrice: parseEther("150"), - minPrice: parseEther("10"), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - isSet: true, - }; - - // as a user of "domainHash" that's not 0x0 - await zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); - - // as a ZNS deployer who owns the 0x0 hash - await zns.curvePricer.connect(deployer).setPriceConfig(HashZero, newConfig); - - const configUser = await zns.curvePricer.priceConfigs(domainHash); - - expect(configUser.baseLength).to.eq(newConfig.baseLength); - expect(configUser.maxLength).to.eq(newConfig.maxLength); - expect(configUser.maxPrice).to.eq(newConfig.maxPrice); - expect(configUser.minPrice).to.eq(newConfig.minPrice); - expect(configUser.precisionMultiplier).to.eq(newConfig.precisionMultiplier); - expect(configUser.feePercentage).to.eq(newConfig.feePercentage); - - const configDeployer = await zns.curvePricer.priceConfigs(HashZero); - - expect(configDeployer.baseLength).to.eq(newConfig.baseLength); - expect(configDeployer.maxLength).to.eq(newConfig.maxLength); - expect(configDeployer.maxPrice).to.eq(newConfig.maxPrice); - expect(configDeployer.minPrice).to.eq(newConfig.minPrice); - expect(configDeployer.precisionMultiplier).to.eq(newConfig.precisionMultiplier); - expect(configDeployer.feePercentage).to.eq(newConfig.feePercentage); - }); - - it("Should revert if setting a price config where spike is created at maxLength", async () => { - const newConfig = { - baseLength: BigNumber.from("6"), - maxLength: BigNumber.from("20"), - maxPrice: parseEther("10"), - minPrice: parseEther("6"), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - isSet: true, - }; - - await expect( - zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig) - ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); - }); - - it("Cannot go below the set minPrice", async () => { - // Using config numbers from audit - const newConfig = { - baseLength: BigNumber.from("5"), - maxLength: BigNumber.from("10"), - maxPrice: parseEther("10"), - minPrice: parseEther("5.5"), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - }; - - await expect( - zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig) - ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); - }); - - it("Should revert if called by anyone other than owner or operator", async () => { - const newConfig = { - baseLength: BigNumber.from("6"), - maxLength: BigNumber.from("20"), - maxPrice: parseEther("10"), - minPrice: parseEther("6"), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - isSet: true, - }; - - await expect( - zns.curvePricer.connect(randomAcc).setPriceConfig(domainHash, newConfig) - ).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - - await expect( - zns.curvePricer.connect(randomAcc).setPriceConfig(HashZero, newConfig) - ).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - }); - - it("Should emit PriceConfigSet event with correct parameters", async () => { - const newConfig = { - baseLength: BigNumber.from("6"), - maxLength: BigNumber.from("35"), - maxPrice: parseEther("150"), - minPrice: parseEther("10"), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - isSet: true, - }; - - const tx = zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); - - await expect(tx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( - domainHash, - newConfig.maxPrice, - newConfig.minPrice, - newConfig.maxLength, - newConfig.baseLength, - newConfig.precisionMultiplier, - newConfig.feePercentage, - ); - }); - - it("Fails validation when maxPrice < minPrice", async () => { - const newConfig = { - baseLength: BigNumber.from("3"), - maxLength: BigNumber.from("35"), - maxPrice: parseEther("1"), - minPrice: parseEther("2"), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - isSet: true, - }; - - const tx = zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); - - await expect(tx).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); - }); - }); - - describe("#setMaxPrice", () => { - it("Allows an authorized user to set the max price", async () => { - const newMaxPrice = priceConfigDefault.maxPrice.add(parseEther("10")); - - await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); - - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.maxPrice).to.eq(newMaxPrice); - }); - - it("Disallows an unauthorized user to set the max price", async () => { - const newMaxPrice = parseEther("0.7"); - - const tx = zns.curvePricer.connect(admin).setMaxPrice(domainHash, newMaxPrice); - await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - }); - - it("Allows setting the max price to zero", async () => { - const newMaxPrice = BigNumber.from("0"); - - await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); - const params = await zns.curvePricer.priceConfigs(domainHash); - - expect(params.maxPrice).to.eq(newMaxPrice); - }); - - it("Correctly sets max price", async () => { - const newMaxPrice = priceConfigDefault.maxPrice.add(parseEther("553")); - await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); - - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.maxPrice).to.eq(newMaxPrice); - }); - - it("Should revert when setting maxPrice that causes a spike at maxLength", async () => { - const newMaxPrice = parseEther("500"); - await expect( - zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice) - ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); - }); - - it("Causes any length domain to have a price of 0 if the maxPrice is 0", async () => { - const newMaxPrice = BigNumber.from("0"); - - await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); - - const shortDomain = "a"; - const longDomain = "abcdefghijklmnopqrstuvwxyz"; - - const shortPrice = await zns.curvePricer.getPrice(domainHash, shortDomain); - const longPrice = await zns.curvePricer.getPrice(domainHash, longDomain); - - expect(shortPrice).to.eq(BigNumber.from("0")); - expect(longPrice).to.eq(BigNumber.from("0")); - }); - - it("The price of a domain is modified relatively when the basePrice is changed", async () => { - const newMaxPrice = priceConfigDefault.maxPrice.add(parseEther("9")); - - const expectedPriceBefore = await calcCurvePrice(defaultDomain, priceConfigDefault); - const priceBefore= await zns.curvePricer.getPrice(domainHash, defaultDomain); - - expect(expectedPriceBefore).to.eq(priceBefore); - - await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); - - const newConfig = { - ...priceConfigDefault, - maxPrice: newMaxPrice, - }; - - const expectedPriceAfter = await calcCurvePrice(defaultDomain, newConfig); - const priceAfter = await zns.curvePricer.getPrice(domainHash, defaultDomain); - - expect(expectedPriceAfter).to.eq(priceAfter); - expect(expectedPriceAfter).to.be.gt(expectedPriceBefore); - expect(priceAfter).to.be.gt(priceBefore); - }); - }); - - describe("#setMinPrice", async () => { - it("Allows an authorized user to set the min price", async () => { - const newMinPrice = parseEther("0.1"); - - await zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice); - - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.minPrice).to.eq(newMinPrice); - }); - - it("Disallows an unauthorized user from setting the min price", async () => { - const newMinPrice = parseEther("0.1"); - - const tx = zns.curvePricer.connect(admin).setMinPrice(domainHash, newMinPrice); - await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - }); - - it("Allows setting to zero", async () => { - const zeroPrice = BigNumber.from("0"); - - await zns.curvePricer.connect(user).setMinPrice(domainHash, zeroPrice); - const params = await zns.curvePricer.priceConfigs(domainHash); - - expect(params.minPrice).to.eq(zeroPrice); - }); - - it("Successfully sets the min price correctly", async () => { - const newMinPrice = parseEther("0.1"); - await zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice); - - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.minPrice).to.eq(newMinPrice); - }); - - it("Causes any domain beyond the `maxLength` to always return `minPrice`", async () => { - // All domains longer than 15 characters are the same price - await zns.curvePricer.connect(user).setMaxLength(domainHash, "15"); - - const minPrice = parseEther("50"); - await zns.curvePricer.connect(user).setMinPrice(domainHash, minPrice); - - // 16 characters - const short = "abcdefghijklmnop"; - // 30 characters - const medium = "abcdefghijklmnoabcdefghijklmno"; - // 60 characters - const long = "abcdefghijklmnoabcdefghijklmnoabcdefghijklmnoabcdefghijklmno"; - - const priceCalls = [ - zns.curvePricer.getPrice(domainHash, short), - zns.curvePricer.getPrice(domainHash, medium), - zns.curvePricer.getPrice(domainHash, long), - ]; - - const [ - shortPrice, - mediumPrice, - longPrice, - ] = await Promise.all(priceCalls); - - expect(shortPrice).to.eq(minPrice); - expect(mediumPrice).to.eq(minPrice); - expect(longPrice).to.eq(minPrice); - }); - - it("Should revert when setting minPrice that causes a spike at maxLength", async () => { - const newMinPrice = priceConfigDefault.minPrice.add(parseEther("231")); - await expect( - zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice) - ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); - }); - }); - - describe("#setPrecisionMultiplier", () => { - it("Allows an authorized user to set the precision multiplier", async () => { - const newMultiplier = BigNumber.from("1"); - - await zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newMultiplier); - - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.precisionMultiplier).to.eq(newMultiplier); - }); - - it("Disallows an unauthorized user from setting the precision multiplier", async () => { - const newMultiplier = BigNumber.from("1"); - - - const tx = zns.curvePricer.connect(admin).setMinPrice(domainHash, newMultiplier); - await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - }); - - it("Fails when setting to zero", async () => { - const zeroMultiplier = BigNumber.from("0"); - - const tx = zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, zeroMultiplier); - await expect(tx).to.be.revertedWith(CURVE_NO_ZERO_PRECISION_MULTIPLIER_ERR); - }); - - it("Successfuly sets the precision multiplier when above 0", async () => { - const newMultiplier = BigNumber.from("3"); - await zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newMultiplier); - - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.precisionMultiplier).to.eq(newMultiplier); - }); - - it("Verifies new prices are affected after changing the precision multiplier", async () => { - const atIndex = 7; - - const before = await zns.curvePricer.getPrice(domainHash, defaultDomain); - const beforePriceString = before.toString(); - - expect(beforePriceString.charAt(atIndex)).to.eq("0"); - - // Default precision is 2 decimals, so increasing this value should represent in prices - // as a non-zero nect decimal place - const newPrecision = BigNumber.from(3); - const newPrecisionMultiplier = BigNumber.from(10).pow(decimalsDefault.sub(newPrecision)); - - await zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newPrecisionMultiplier); - - const after = await zns.curvePricer.getPrice(domainHash, defaultDomain); - const afterPriceString = after.toString(); - - expect(afterPriceString.charAt(atIndex)).to.not.eq("0"); - - }); - - it("Should revert when setting precisionMultiplier higher than 10^18", async () => { - const newMultiplier = parseEther("100"); - await expect( - zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newMultiplier) - ).to.be.revertedWith( - "ZNSCurvePricer: precisionMultiplier cannot be greater than 10^18" - ); - }); - }); - - describe("#setBaseLength", () => { - it("Allows an authorized user to set the base length", async () => { - const newLength = 5; - - await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); - const params = await zns.curvePricer.priceConfigs(domainHash); - - expect(params.baseLength).to.eq(newLength); - }); - - it("Disallows an unauthorized user to set the base length", async () => { - const newLength = 5; - - const tx = zns.curvePricer.connect(admin).setBaseLength(domainHash, newLength); - await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - }); - - it("Allows setting the base length to zero", async () => { - const newLength = 0; - - await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); - const params = await zns.curvePricer.priceConfigs(domainHash); - - expect(params.baseLength).to.eq(newLength); - }); - - it("Always returns the minPrice if both baseLength and maxLength are their min values", async () => { - const newConfig = { - baseLength: BigNumber.from(1), - maxLength: BigNumber.from(1), - maxPrice: BigNumber.from(100), - minPrice: BigNumber.from(10), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - isSet: true, - }; - - // We use `baseLength == 0` to indicate a special event like a promo or discount and always - // return `maxPrice` which can be set to whatever we need at the time. - await zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); - - const short = "abc"; - const medium = "abcdefghijklmnop"; - const long = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; - - const priceCalls = [ - zns.curvePricer.getPrice(domainHash, short), - zns.curvePricer.getPrice(domainHash, medium), - zns.curvePricer.getPrice(domainHash, long), - ]; - - const [shortPrice, mediumPrice, longPrice] = await Promise.all(priceCalls); - - expect(shortPrice).to.eq(newConfig.minPrice); - expect(mediumPrice).to.eq(newConfig.minPrice); - expect(longPrice).to.eq(newConfig.minPrice); - }); - - it("Causes any length domain to cost the base fee when set to max length of 255", async () => { - const newLength = 255; - await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); - const params = await zns.curvePricer.priceConfigs(domainHash); - - const shortDomain = "a"; - const longDomain = "abcdefghijklmnopqrstuvwxyz"; - - const shortPrice = await zns.curvePricer.getPrice(domainHash, shortDomain); - const longPrice = await zns.curvePricer.getPrice(domainHash, longDomain); - - expect(shortPrice).to.eq(params.maxPrice); - expect(longPrice).to.eq(params.maxPrice); - }); - - it("Causes prices to adjust correctly when length is increased", async () => { - const newLength = 8; - const paramsBefore = await zns.curvePricer.priceConfigs(domainHash); - - const expectedPriceBefore = await calcCurvePrice(defaultDomain, priceConfigDefault); - const priceBefore = await zns.curvePricer.getPrice(domainHash, defaultDomain); - expect(priceBefore).to.eq(expectedPriceBefore); - expect(priceBefore).to.not.eq(paramsBefore.maxPrice); - - await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); - - const paramsAfter = await zns.curvePricer.priceConfigs(domainHash); - - const newConfig = { - ...priceConfigDefault, - baseLength: BigNumber.from(newLength), - }; - - const expectedPriceAfter = await calcCurvePrice(defaultDomain, newConfig); - const priceAfter = await zns.curvePricer.getPrice(domainHash, defaultDomain); - expect(priceAfter).to.eq(expectedPriceAfter); - expect(priceAfter).to.eq(paramsAfter.maxPrice); - }); - - it("Causes prices to adjust correctly when length is decreased", async () => { - const length = 8; - await zns.curvePricer.connect(user).setBaseLength(domainHash, length); - - const newConfig1 = { - ...priceConfigDefault, - baseLength: BigNumber.from(length), - }; - - const paramsBefore = await zns.curvePricer.priceConfigs(domainHash); - - const expectedPriceBefore = await calcCurvePrice(defaultDomain, newConfig1); - const priceBefore = await zns.curvePricer.getPrice(domainHash, defaultDomain); - expect(priceBefore).to.eq(expectedPriceBefore); - expect(priceBefore).to.eq(paramsBefore.maxPrice); - - const newLength = 5; - await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); - - const newConfig2 = { - ...priceConfigDefault, - baseLength: BigNumber.from(newLength), - }; - - const paramsAfter = await zns.curvePricer.priceConfigs(domainHash); - - const expectedPriceAfter = await calcCurvePrice(defaultDomain, newConfig2); - const priceAfter = await zns.curvePricer.getPrice(domainHash, defaultDomain); - expect(priceAfter).to.eq(expectedPriceAfter); - expect(priceAfter).to.not.eq(paramsAfter.maxPrice); - }); - - it("Returns the maxPrice whenever the baseLength is 0", async () => { - const newRootLength = 0; - await zns.curvePricer.connect(user).setBaseLength(domainHash, newRootLength); - - let config = await zns.curvePricer.priceConfigs(domainHash); - let price = await zns.curvePricer.getPrice(domainHash, defaultDomain); - - expect(config.maxPrice).to.eq(price); - - // Modify the max price - await zns.curvePricer.connect(user).setMaxPrice( - domainHash, - priceConfigDefault.maxPrice.add(15) - ); - - config = await zns.curvePricer.priceConfigs(domainHash); - price = await zns.curvePricer.getPrice(domainHash, defaultDomain); - - expect(config.maxPrice).to.eq(price); - }); - - it("Adjusts prices correctly when setting base lengths to different values", async () => { - const newRootLength = 0; - await zns.curvePricer.connect(user).setBaseLength(domainHash, newRootLength); - const newConfig = { - ...priceConfigDefault, - baseLength: BigNumber.from(newRootLength), - }; - - const expectedRootPrice = await calcCurvePrice(defaultDomain, newConfig); - const rootPrice = await zns.curvePricer.getPrice(domainHash, defaultDomain); - - expect(rootPrice).to.eq(expectedRootPrice); - }); - - it("Should revert when setting baseLength that causes a spike at maxLength", async () => { - const newBaseLength = priceConfigDefault.baseLength.sub(1); - await expect( - zns.curvePricer.connect(user).setBaseLength(domainHash, newBaseLength) - ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); - }); - }); - - describe("#setMaxLength", () => { - it("Allows an authorized user to set the max length", async () => { - const newLength = 5; - - await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); - const params = await zns.curvePricer.priceConfigs(domainHash); - - expect(params.maxLength).to.eq(newLength); - }); - - it("Disallows an unauthorized user to set the max length", async () => { - const newLength = 5; - - const tx = zns.curvePricer.connect(admin).setMaxLength(domainHash, newLength); - await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - }); - - it("Allows setting the max length to zero", async () => { - const newLength = 0; - - await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); - const params = await zns.curvePricer.priceConfigs(domainHash); - - expect(params.maxLength).to.eq(newLength); - }); - - it("Still returns prices for domains within baseLength if the maxLength is zero", async () => { - const newLength = 0; - - await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); - - // Default price config sets baseLength to 4 - const short = "a"; - const long = "abcd"; - const beyondBaseLength = "abcde"; - - const priceCalls = [ - zns.curvePricer.getPrice(domainHash, short), - zns.curvePricer.getPrice(domainHash, long), - zns.curvePricer.getPrice(domainHash, beyondBaseLength), - ]; - - const [shortPrice, longPrice, beyondPrice] = await Promise.all(priceCalls); - - expect(shortPrice).to.eq(priceConfigDefault.maxPrice); - expect(longPrice).to.eq(priceConfigDefault.maxPrice); - expect(beyondPrice).to.eq(priceConfigDefault.minPrice); - }); - - it("Should revert when setting maxLength that causes a spike at maxLength", async () => { - const newMaxLength = priceConfigDefault.maxLength.add(10); - await expect( - zns.curvePricer.connect(user).setMaxLength(domainHash, newMaxLength) - ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); - }); - }); - - describe("#setFeePercentage", () => { - it("Successfully sets the fee percentage", async () => { - const newFeePerc = BigNumber.from(222); - await zns.curvePricer.connect(user).setFeePercentage(domainHash, newFeePerc); - const { feePercentage: feeFromSC } = await zns.curvePricer.priceConfigs(domainHash); - - expect(feeFromSC).to.eq(newFeePerc); - }); - - it("Disallows an unauthorized user to set the fee percentage", async () => { - const newFeePerc = BigNumber.from(222); - const tx = zns.curvePricer.connect(admin) - .setFeePercentage(domainHash, newFeePerc); - await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); - }); - - it("should revert when trying to set feePercentage higher than PERCENTAGE_BASIS", async () => { - const newFeePerc = BigNumber.from(10001); - await expect( - zns.curvePricer.connect(user).setFeePercentage(domainHash, newFeePerc) - ).to.be.revertedWith("ZNSCurvePricer: feePercentage cannot be greater than PERCENTAGE_BASIS"); - }); - }); - - describe("#getRegistrationFee", () => { - it("Successfully gets the fee for a price", async () => { - const stake = ethers.utils.parseEther("0.2"); - const fee = await zns.curvePricer.getFeeForPrice(domainHash, stake); - const expectedFee = stake.mul("222").div("10000"); - - expect(fee).to.eq(expectedFee); - }); - }); - - describe("#setAccessController", () => { - it("Successfully sets the access controller", async () => { - const currentAccessController = await zns.curvePricer.getAccessController(); - expect(currentAccessController).to.not.eq(randomAcc.address); - - const tx = await zns.curvePricer.setAccessController(randomAcc.address); - - const newAccessController = await zns.curvePricer.getAccessController(); - expect(newAccessController).to.eq(randomAcc.address); - - await expect(tx).to.emit(zns.curvePricer, "AccessControllerSet").withArgs(randomAcc.address); - }); - - it("Disallows an unauthorized user to set the access controller", async () => { - const tx = zns.curvePricer.connect(user).setAccessController(randomAcc.address); - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(user.address, ADMIN_ROLE) - ); - }); - - it("Disallows setting the access controller to the zero address", async () => { - const tx = zns.curvePricer.connect(admin).setAccessController(ethers.constants.AddressZero); - await expect(tx).to.be.revertedWith( - "AC: _accessController is 0x0 address" - ); - }); - }); - - describe("#setRegistry", () => { - it("Should successfully set the registry", async () => { - const currentRegistry = await zns.curvePricer.registry(); - expect(currentRegistry).to.not.eq(randomAcc.address); - - const tx = await zns.curvePricer.connect(admin).setRegistry(randomAcc.address); - - const newRegistry = await zns.curvePricer.registry(); - expect(newRegistry).to.eq(randomAcc.address); - - await expect(tx).to.emit(zns.curvePricer, "RegistrySet").withArgs(randomAcc.address); - }); - - it("Should NOT set the registry if called by anyone other than ADMIN_ROLE", async () => { - const tx = zns.curvePricer.connect(user).setRegistry(randomAcc.address); - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(user.address, ADMIN_ROLE) - ); - }); - }); - - describe("Events", () => { - it("Emits MaxPriceSet", async () => { - const newMaxPrice = priceConfigDefault.maxPrice.add(1); - - const tx = zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); - await expect(tx).to.emit(zns.curvePricer, "MaxPriceSet").withArgs(domainHash, newMaxPrice); - }); - - it("Emits BaseLengthSet", async () => { - const newLength = 5; - - const tx = zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); - await expect(tx).to.emit(zns.curvePricer, "BaseLengthSet").withArgs(domainHash, newLength); - }); - }); - - describe("UUPS", () => { - it("Allows an authorized user to upgrade the contract", async () => { - // CurvePricer to upgrade to - const factory = new ZNSCurvePricer__factory(deployer); - const newCurvePricer = await factory.deploy(); - await newCurvePricer.deployed(); - - // Confirm the deployer is a governor, as set in `deployZNS` helper - await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; - - const tx = zns.curvePricer.connect(deployer).upgradeTo(newCurvePricer.address); - await expect(tx).to.not.be.reverted; - }); - - it("Fails to upgrade if the caller is not authorized", async () => { - // CurvePricer to upgrade to - const factory = new ZNSCurvePricerUpgradeMock__factory(deployer); - const newCurvePricer = await factory.deploy(); - await newCurvePricer.deployed(); - - // Confirm the account is not a governor - await expect(zns.accessController.checkGovernor(randomAcc.address)).to.be.reverted; - - const tx = zns.curvePricer.connect(randomAcc).upgradeTo(newCurvePricer.address); - - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(randomAcc.address, GOVERNOR_ROLE) - ); - }); - - it("Verifies that variable values are not changed in the upgrade process", async () => { - const factory = new ZNSCurvePricerUpgradeMock__factory(deployer); - const newCurvePricer = await factory.deploy(); - await newCurvePricer.deployed(); - - await zns.curvePricer.connect(user).setBaseLength(domainHash, "7"); - await zns.curvePricer.connect(user).setMaxPrice( - domainHash, - priceConfigDefault.maxPrice.add(15) - ); - - const contractCalls = [ - zns.curvePricer.registry(), - zns.curvePricer.getAccessController(), - zns.curvePricer.priceConfigs(domainHash), - zns.curvePricer.getPrice(domainHash, "wilder"), - ]; - - await validateUpgrade(deployer, zns.curvePricer, newCurvePricer, factory, contractCalls); - }); - }); -}); +import * as hre from "hardhat"; +import { expect } from "chai"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { BigNumber, ethers } from "ethers"; +import { parseEther } from "ethers/lib/utils"; +import { IZNSContracts } from "./helpers/types"; +import { + deployZNS, + calcCurvePrice, + precisionMultiDefault, + CURVE_PRICE_CONFIG_ERR, + validateUpgrade, + PaymentType, + NOT_AUTHORIZED_REG_WIRED_ERR, + CURVE_NO_ZERO_PRECISION_MULTIPLIER_ERR, + INVALID_LENGTH_ERR, + INITIALIZED_ERR, INVALID_NAME_ERR, +} from "./helpers"; +import { decimalsDefault, priceConfigDefault, registrationFeePercDefault } from "./helpers/constants"; +import { + getAccessRevertMsg, +} from "./helpers/errors"; +import { ADMIN_ROLE, GOVERNOR_ROLE } from "./helpers/access"; +import { ZNSCurvePricerUpgradeMock__factory, ZNSCurvePricer__factory } from "../typechain"; +import { registrationWithSetup } from "./helpers/register-setup"; +import { getProxyImplAddress } from "./helpers/utils"; + +require("@nomicfoundation/hardhat-chai-matchers"); + +const { HashZero } = ethers.constants; + +describe("ZNSCurvePricer", () => { + let deployer : SignerWithAddress; + let user : SignerWithAddress; + let admin : SignerWithAddress; + let randomAcc : SignerWithAddress; + + let zns : IZNSContracts; + let domainHash : string; + + const defaultDomain = "wilder"; + + beforeEach(async () => { + [ + deployer, + user, + admin, + randomAcc, + ] = await hre.ethers.getSigners(); + + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address], + adminAddresses: [admin.address], + }); + + await zns.zeroToken.connect(user).approve(zns.treasury.address, ethers.constants.MaxUint256); + await zns.zeroToken.mint(user.address, priceConfigDefault.maxPrice); + + const fullConfig = { + distrConfig: { + paymentType: PaymentType.DIRECT, + pricerContract: zns.curvePricer.address, + accessType: 1, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: user.address, + }, + priceConfig: priceConfigDefault, + }; + + domainHash = await registrationWithSetup({ + zns, + user, + domainLabel: "testdomain", + fullConfig, + }); + }); + + it("Should NOT let initialize the implementation contract", async () => { + const factory = new ZNSCurvePricer__factory(deployer); + const impl = await getProxyImplAddress(zns.curvePricer.address); + const implContract = factory.attach(impl); + + await expect( + implContract.initialize( + zns.accessController.address, + zns.registry.address, + priceConfigDefault + ) + ).to.be.revertedWith(INITIALIZED_ERR); + }); + + it("Confirms values were initially set correctly", async () => { + const valueCalls = [ + zns.curvePricer.priceConfigs(domainHash), + ]; + + const [ + priceConfigFromSC, + ] = await Promise.all(valueCalls); + + const priceConfigArr = Object.values(priceConfigDefault); + + priceConfigArr.forEach( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (val, idx) => expect(val).to.eq(priceConfigFromSC[idx]) + ); + + const regFromSC = await zns.curvePricer.registry(); + const acFromSC = await zns.curvePricer.getAccessController(); + + expect(regFromSC).to.eq(zns.registry.address); + expect(acFromSC).to.eq(zns.accessController.address); + }); + + describe("#getPrice", async () => { + it("Returns 0 price for a label with no length if label validation is skipped", async () => { + const { + price, + stakeFee, + } = await zns.curvePricer.getPriceAndFee(domainHash, "", true); + expect(price).to.eq(0); + expect(stakeFee).to.eq(0); + }); + + it("Reverts for a label with no length if label validation is not skipped", async () => { + await expect(zns.curvePricer.getPrice(domainHash, "", false)).to.be.revertedWith(INVALID_LENGTH_ERR); + }); + + it("Reverts for invalid label if label validation is not skipped", async () => { + await expect(zns.curvePricer.getPrice(domainHash, "wilder!", false)).to.be.revertedWith(INVALID_NAME_ERR); + }); + + it("Returns the base price for domains that are equal to the base length", async () => { + // Using the default length of 3 + const domain = "eth"; + const params = await zns.curvePricer.priceConfigs(domainHash); + + const domainPrice = await zns.curvePricer.getPrice(domainHash, domain, true); + expect(domainPrice).to.eq(params.maxPrice); + }); + + it("Returns the base price for domains that are less than the base length", async () => { + const domainA = "et"; + const domainB = "e"; + const params = await zns.curvePricer.priceConfigs(domainHash); + + let domainPrice = await zns.curvePricer.getPrice(domainHash, domainA, true); + expect(domainPrice).to.eq(params.maxPrice); + + (domainPrice = await zns.curvePricer.getPrice(domainHash, domainB, true)); + expect(domainPrice).to.eq(params.maxPrice); + }); + + it("Returns expected prices for a domain greater than the base length", async () => { + // create a constant string with 22 letters + const domainOne = "abcdefghijklmnopqrstuv"; + const domainTwo = "akkasddaasdas"; + + // these values have been calced separately to validate + // that both forumlas: SC + helper are correct + // this value has been calces with the default priceConfig + const domainOneRefValue = BigNumber.from("4545450000000000000000"); + const domainTwoRefValue = BigNumber.from("7692300000000000000000"); + + const domainOneExpPrice = await calcCurvePrice(domainOne, priceConfigDefault); + const domainTwoExpPrice = await calcCurvePrice(domainTwo, priceConfigDefault); + + const domainOnePriceSC = await zns.curvePricer.getPrice(domainHash, domainOne, true); + const domainTwoPriceSC = await zns.curvePricer.getPrice(domainHash, domainTwo, true); + + expect(domainOnePriceSC).to.eq(domainOneRefValue); + expect(domainOnePriceSC).to.eq(domainOneExpPrice); + + expect(domainTwoPriceSC).to.eq(domainTwoRefValue); + expect(domainTwoPriceSC).to.eq(domainTwoExpPrice); + }); + + it("Returns a price even if the domain name is very long", async () => { + // 255 length + const domain = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstu"; + + const expectedPrice = await calcCurvePrice(domain, priceConfigDefault); + const domainPrice = await zns.curvePricer.getPrice(domainHash, domain, true); + + expect(domainPrice).to.eq(expectedPrice); + }); + + it("Returns a price for multiple lengths", async () => { + // Any value less than base length is always base price, so we only check + // domains that are greater than base length + 1 + const short = "wild"; + const medium = "wilderworld"; + const long = "wilderworldbeastspetsnftscatscalicosteve"; + + const expectedShortPrice = await calcCurvePrice(short, priceConfigDefault); + const shortPrice = await zns.curvePricer.getPrice(domainHash, short, true); + expect(expectedShortPrice).to.eq(shortPrice); + + const expectedMediumPrice = await calcCurvePrice(medium, priceConfigDefault); + const mediumPrice = await zns.curvePricer.getPrice(domainHash, medium, true); + expect(expectedMediumPrice).to.eq(mediumPrice); + + const expectedLongPrice = await calcCurvePrice(long, priceConfigDefault); + const longPrice = await zns.curvePricer.getPrice(domainHash, long, true); + expect(expectedLongPrice).to.eq(longPrice); + }); + + it("Can Price Names Longer Than 255 Characters", async () => { + // 261 length + const domain = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz" + + "a"; + const expectedPrice = calcCurvePrice(domain, priceConfigDefault); + const domainPrice = await zns.curvePricer.getPrice(domainHash, domain, true); + expect(domainPrice).to.eq(expectedPrice); + }); + + // eslint-disable-next-line max-len + it.skip("Doesn't create price spikes with any valid combination of values (SLOW TEST, ONLY RUN LOCALLY)", async () => { + // Start by expanding the search space to allow for domains that are up to 1000 characters + await zns.curvePricer.connect(user).setMaxLength(domainHash, BigNumber.from("1000")); + + const promises = []; + let config = await zns.curvePricer.priceConfigs(domainHash); + let domain = "a"; + + // baseLength = 0 is a special case + await zns.curvePricer.connect(user).setBaseLength(domainHash, 0); + const domainPrice = await zns.curvePricer.getPrice(domainHash, domain, true); + expect(domainPrice).to.eq(config.maxPrice); + + let outer = 1; + let inner = outer; + // Long-running loops here to iterate all the variations for baseLength and + while(config.maxLength.gt(outer)) { + // Reset "domain" to a single character each outer loop + domain = "a"; + + await zns.curvePricer.connect(user).setBaseLength(domainHash, outer); + config = await zns.curvePricer.priceConfigs(domainHash); + + while (config.maxLength.gt(inner)) { + const priceTx = zns.curvePricer.getPrice(domainHash, domain, true); + promises.push(priceTx); + + domain += "a"; + inner++; + } + outer++; + } + + const prices = await Promise.all(promises); + let k = 0; + while (k < prices.length) { + expect(prices[k]).to.be.lte(config.maxPrice); + k++; + } + }); + }); + + describe("#setPriceConfig", () => { + it("Can't price a name that has invalid characters", async () => { + // Valid names must match the pattern [a-z0-9] + const labelA = "WILDER"; + const labelB = "!?w1Id3r!?"; + const labelC = "!%$#^*?!#👍3^29"; + const labelD = "wo.rld"; + + await expect(zns.curvePricer.getPrice(domainHash, labelA, false)).to.be.revertedWith(INVALID_NAME_ERR); + await expect(zns.curvePricer.getPrice(domainHash, labelB, false)).to.be.revertedWith(INVALID_NAME_ERR); + await expect(zns.curvePricer.getPrice(domainHash, labelC, false)).to.be.revertedWith(INVALID_NAME_ERR); + await expect(zns.curvePricer.getPrice(domainHash, labelD, false)).to.be.revertedWith(INVALID_NAME_ERR); + }); + + it("Should set the config for any existing domain hash, including 0x0", async () => { + const newConfig = { + baseLength: BigNumber.from("6"), + maxLength: BigNumber.from("35"), + maxPrice: parseEther("150"), + minPrice: parseEther("10"), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + isSet: true, + }; + + // as a user of "domainHash" that's not 0x0 + await zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); + + // as a ZNS deployer who owns the 0x0 hash + await zns.curvePricer.connect(deployer).setPriceConfig(HashZero, newConfig); + + const configUser = await zns.curvePricer.priceConfigs(domainHash); + + expect(configUser.baseLength).to.eq(newConfig.baseLength); + expect(configUser.maxLength).to.eq(newConfig.maxLength); + expect(configUser.maxPrice).to.eq(newConfig.maxPrice); + expect(configUser.minPrice).to.eq(newConfig.minPrice); + expect(configUser.precisionMultiplier).to.eq(newConfig.precisionMultiplier); + expect(configUser.feePercentage).to.eq(newConfig.feePercentage); + + const configDeployer = await zns.curvePricer.priceConfigs(HashZero); + + expect(configDeployer.baseLength).to.eq(newConfig.baseLength); + expect(configDeployer.maxLength).to.eq(newConfig.maxLength); + expect(configDeployer.maxPrice).to.eq(newConfig.maxPrice); + expect(configDeployer.minPrice).to.eq(newConfig.minPrice); + expect(configDeployer.precisionMultiplier).to.eq(newConfig.precisionMultiplier); + expect(configDeployer.feePercentage).to.eq(newConfig.feePercentage); + }); + + it("Should revert if setting a price config where spike is created at maxLength", async () => { + const newConfig = { + baseLength: BigNumber.from("6"), + maxLength: BigNumber.from("20"), + maxPrice: parseEther("10"), + minPrice: parseEther("6"), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + isSet: true, + }; + + await expect( + zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig) + ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); + }); + + it("Cannot go below the set minPrice", async () => { + // Using config numbers from audit + const newConfig = { + baseLength: BigNumber.from("5"), + maxLength: BigNumber.from("10"), + maxPrice: parseEther("10"), + minPrice: parseEther("5.5"), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + }; + + await expect( + zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig) + ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); + }); + + it("Should revert if called by anyone other than owner or operator", async () => { + const newConfig = { + baseLength: BigNumber.from("6"), + maxLength: BigNumber.from("20"), + maxPrice: parseEther("10"), + minPrice: parseEther("6"), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + isSet: true, + }; + + await expect( + zns.curvePricer.connect(randomAcc).setPriceConfig(domainHash, newConfig) + ).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + + await expect( + zns.curvePricer.connect(randomAcc).setPriceConfig(HashZero, newConfig) + ).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + }); + + it("Should emit PriceConfigSet event with correct parameters", async () => { + const newConfig = { + baseLength: BigNumber.from("6"), + maxLength: BigNumber.from("35"), + maxPrice: parseEther("150"), + minPrice: parseEther("10"), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + isSet: true, + }; + + const tx = zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); + + await expect(tx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( + domainHash, + newConfig.maxPrice, + newConfig.minPrice, + newConfig.maxLength, + newConfig.baseLength, + newConfig.precisionMultiplier, + newConfig.feePercentage, + ); + }); + + it("Fails validation when maxPrice < minPrice", async () => { + const newConfig = { + baseLength: BigNumber.from("3"), + maxLength: BigNumber.from("35"), + maxPrice: parseEther("1"), + minPrice: parseEther("2"), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + isSet: true, + }; + + const tx = zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); + + await expect(tx).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); + }); + }); + + describe("#setMaxPrice", () => { + it("Allows an authorized user to set the max price", async () => { + const newMaxPrice = priceConfigDefault.maxPrice.add(parseEther("10")); + + await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); + + const params = await zns.curvePricer.priceConfigs(domainHash); + expect(params.maxPrice).to.eq(newMaxPrice); + }); + + it("Disallows an unauthorized user to set the max price", async () => { + const newMaxPrice = parseEther("0.7"); + + const tx = zns.curvePricer.connect(admin).setMaxPrice(domainHash, newMaxPrice); + await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + }); + + it("Allows setting the max price to zero", async () => { + const newMaxPrice = BigNumber.from("0"); + + await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); + const params = await zns.curvePricer.priceConfigs(domainHash); + + expect(params.maxPrice).to.eq(newMaxPrice); + }); + + it("Correctly sets max price", async () => { + const newMaxPrice = priceConfigDefault.maxPrice.add(parseEther("553")); + await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); + + const params = await zns.curvePricer.priceConfigs(domainHash); + expect(params.maxPrice).to.eq(newMaxPrice); + }); + + it("Should revert when setting maxPrice that causes a spike at maxLength", async () => { + const newMaxPrice = parseEther("500"); + await expect( + zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice) + ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); + }); + + it("Causes any length domain to have a price of 0 if the maxPrice is 0", async () => { + const newMaxPrice = BigNumber.from("0"); + + await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); + + const shortDomain = "a"; + const longDomain = "abcdefghijklmnopqrstuvwxyz"; + + const shortPrice = await zns.curvePricer.getPrice(domainHash, shortDomain, true); + const longPrice = await zns.curvePricer.getPrice(domainHash, longDomain, true); + + expect(shortPrice).to.eq(BigNumber.from("0")); + expect(longPrice).to.eq(BigNumber.from("0")); + }); + + it("The price of a domain is modified relatively when the basePrice is changed", async () => { + const newMaxPrice = priceConfigDefault.maxPrice.add(parseEther("9")); + + const expectedPriceBefore = await calcCurvePrice(defaultDomain, priceConfigDefault); + const priceBefore= await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + + expect(expectedPriceBefore).to.eq(priceBefore); + + await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); + + const newConfig = { + ...priceConfigDefault, + maxPrice: newMaxPrice, + }; + + const expectedPriceAfter = await calcCurvePrice(defaultDomain, newConfig); + const priceAfter = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + + expect(expectedPriceAfter).to.eq(priceAfter); + expect(expectedPriceAfter).to.be.gt(expectedPriceBefore); + expect(priceAfter).to.be.gt(priceBefore); + }); + }); + + describe("#setMinPrice", async () => { + it("Allows an authorized user to set the min price", async () => { + const newMinPrice = parseEther("0.1"); + + await zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice); + + const params = await zns.curvePricer.priceConfigs(domainHash); + expect(params.minPrice).to.eq(newMinPrice); + }); + + it("Disallows an unauthorized user from setting the min price", async () => { + const newMinPrice = parseEther("0.1"); + + const tx = zns.curvePricer.connect(admin).setMinPrice(domainHash, newMinPrice); + await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + }); + + it("Allows setting to zero", async () => { + const zeroPrice = BigNumber.from("0"); + + await zns.curvePricer.connect(user).setMinPrice(domainHash, zeroPrice); + const params = await zns.curvePricer.priceConfigs(domainHash); + + expect(params.minPrice).to.eq(zeroPrice); + }); + + it("Successfully sets the min price correctly", async () => { + const newMinPrice = parseEther("0.1"); + await zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice); + + const params = await zns.curvePricer.priceConfigs(domainHash); + expect(params.minPrice).to.eq(newMinPrice); + }); + + it("Causes any domain beyond the `maxLength` to always return `minPrice`", async () => { + // All domains longer than 15 characters are the same price + await zns.curvePricer.connect(user).setMaxLength(domainHash, "15"); + + const minPrice = parseEther("50"); + await zns.curvePricer.connect(user).setMinPrice(domainHash, minPrice); + + // 16 characters + const short = "abcdefghijklmnop"; + // 30 characters + const medium = "abcdefghijklmnoabcdefghijklmno"; + // 60 characters + const long = "abcdefghijklmnoabcdefghijklmnoabcdefghijklmnoabcdefghijklmno"; + + const priceCalls = [ + zns.curvePricer.getPrice(domainHash, short, true), + zns.curvePricer.getPrice(domainHash, medium, true), + zns.curvePricer.getPrice(domainHash, long, true), + ]; + + const [ + shortPrice, + mediumPrice, + longPrice, + ] = await Promise.all(priceCalls); + + expect(shortPrice).to.eq(minPrice); + expect(mediumPrice).to.eq(minPrice); + expect(longPrice).to.eq(minPrice); + }); + + it("Should revert when setting minPrice that causes a spike at maxLength", async () => { + const newMinPrice = priceConfigDefault.minPrice.add(parseEther("231")); + await expect( + zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice) + ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); + }); + }); + + describe("#setPrecisionMultiplier", () => { + it("Allows an authorized user to set the precision multiplier", async () => { + const newMultiplier = BigNumber.from("1"); + + await zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newMultiplier); + + const params = await zns.curvePricer.priceConfigs(domainHash); + expect(params.precisionMultiplier).to.eq(newMultiplier); + }); + + it("Disallows an unauthorized user from setting the precision multiplier", async () => { + const newMultiplier = BigNumber.from("1"); + + + const tx = zns.curvePricer.connect(admin).setMinPrice(domainHash, newMultiplier); + await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + }); + + it("Fails when setting to zero", async () => { + const zeroMultiplier = BigNumber.from("0"); + + const tx = zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, zeroMultiplier); + await expect(tx).to.be.revertedWith(CURVE_NO_ZERO_PRECISION_MULTIPLIER_ERR); + }); + + it("Successfuly sets the precision multiplier when above 0", async () => { + const newMultiplier = BigNumber.from("3"); + await zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newMultiplier); + + const params = await zns.curvePricer.priceConfigs(domainHash); + expect(params.precisionMultiplier).to.eq(newMultiplier); + }); + + it("Verifies new prices are affected after changing the precision multiplier", async () => { + const atIndex = 7; + + const before = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + const beforePriceString = before.toString(); + + expect(beforePriceString.charAt(atIndex)).to.eq("0"); + + // Default precision is 2 decimals, so increasing this value should represent in prices + // as a non-zero nect decimal place + const newPrecision = BigNumber.from(3); + const newPrecisionMultiplier = BigNumber.from(10).pow(decimalsDefault.sub(newPrecision)); + + await zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newPrecisionMultiplier); + + const after = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + const afterPriceString = after.toString(); + + expect(afterPriceString.charAt(atIndex)).to.not.eq("0"); + + }); + + it("Should revert when setting precisionMultiplier higher than 10^18", async () => { + const newMultiplier = parseEther("100"); + await expect( + zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newMultiplier) + ).to.be.revertedWith( + "ZNSCurvePricer: precisionMultiplier cannot be greater than 10^18" + ); + }); + }); + + describe("#setBaseLength", () => { + it("Allows an authorized user to set the base length", async () => { + const newLength = 5; + + await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); + const params = await zns.curvePricer.priceConfigs(domainHash); + + expect(params.baseLength).to.eq(newLength); + }); + + it("Disallows an unauthorized user to set the base length", async () => { + const newLength = 5; + + const tx = zns.curvePricer.connect(admin).setBaseLength(domainHash, newLength); + await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + }); + + it("Allows setting the base length to zero", async () => { + const newLength = 0; + + await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); + const params = await zns.curvePricer.priceConfigs(domainHash); + + expect(params.baseLength).to.eq(newLength); + }); + + it("Always returns the minPrice if both baseLength and maxLength are their min values", async () => { + const newConfig = { + baseLength: BigNumber.from(1), + maxLength: BigNumber.from(1), + maxPrice: BigNumber.from(100), + minPrice: BigNumber.from(10), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + isSet: true, + }; + + // We use `baseLength == 0` to indicate a special event like a promo or discount and always + // return `maxPrice` which can be set to whatever we need at the time. + await zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); + + const short = "abc"; + const medium = "abcdefghijklmnop"; + const long = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz"; + + const priceCalls = [ + zns.curvePricer.getPrice(domainHash, short, true), + zns.curvePricer.getPrice(domainHash, medium, true), + zns.curvePricer.getPrice(domainHash, long, true), + ]; + + const [shortPrice, mediumPrice, longPrice] = await Promise.all(priceCalls); + + expect(shortPrice).to.eq(newConfig.minPrice); + expect(mediumPrice).to.eq(newConfig.minPrice); + expect(longPrice).to.eq(newConfig.minPrice); + }); + + it("Causes any length domain to cost the base fee when set to max length of 255", async () => { + const newLength = 255; + await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); + const params = await zns.curvePricer.priceConfigs(domainHash); + + const shortDomain = "a"; + const longDomain = "abcdefghijklmnopqrstuvwxyz"; + + const shortPrice = await zns.curvePricer.getPrice(domainHash, shortDomain, true); + const longPrice = await zns.curvePricer.getPrice(domainHash, longDomain, true); + + expect(shortPrice).to.eq(params.maxPrice); + expect(longPrice).to.eq(params.maxPrice); + }); + + it("Causes prices to adjust correctly when length is increased", async () => { + const newLength = 8; + const paramsBefore = await zns.curvePricer.priceConfigs(domainHash); + + const expectedPriceBefore = await calcCurvePrice(defaultDomain, priceConfigDefault); + const priceBefore = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + expect(priceBefore).to.eq(expectedPriceBefore); + expect(priceBefore).to.not.eq(paramsBefore.maxPrice); + + await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); + + const paramsAfter = await zns.curvePricer.priceConfigs(domainHash); + + const newConfig = { + ...priceConfigDefault, + baseLength: BigNumber.from(newLength), + }; + + const expectedPriceAfter = await calcCurvePrice(defaultDomain, newConfig); + const priceAfter = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + expect(priceAfter).to.eq(expectedPriceAfter); + expect(priceAfter).to.eq(paramsAfter.maxPrice); + }); + + it("Causes prices to adjust correctly when length is decreased", async () => { + const length = 8; + await zns.curvePricer.connect(user).setBaseLength(domainHash, length); + + const newConfig1 = { + ...priceConfigDefault, + baseLength: BigNumber.from(length), + }; + + const paramsBefore = await zns.curvePricer.priceConfigs(domainHash); + + const expectedPriceBefore = await calcCurvePrice(defaultDomain, newConfig1); + const priceBefore = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + expect(priceBefore).to.eq(expectedPriceBefore); + expect(priceBefore).to.eq(paramsBefore.maxPrice); + + const newLength = 5; + await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); + + const newConfig2 = { + ...priceConfigDefault, + baseLength: BigNumber.from(newLength), + }; + + const paramsAfter = await zns.curvePricer.priceConfigs(domainHash); + + const expectedPriceAfter = await calcCurvePrice(defaultDomain, newConfig2); + const priceAfter = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + expect(priceAfter).to.eq(expectedPriceAfter); + expect(priceAfter).to.not.eq(paramsAfter.maxPrice); + }); + + it("Returns the maxPrice whenever the baseLength is 0", async () => { + const newRootLength = 0; + await zns.curvePricer.connect(user).setBaseLength(domainHash, newRootLength); + + let config = await zns.curvePricer.priceConfigs(domainHash); + let price = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + + expect(config.maxPrice).to.eq(price); + + // Modify the max price + await zns.curvePricer.connect(user).setMaxPrice( + domainHash, + priceConfigDefault.maxPrice.add(15) + ); + + config = await zns.curvePricer.priceConfigs(domainHash); + price = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + + expect(config.maxPrice).to.eq(price); + }); + + it("Adjusts prices correctly when setting base lengths to different values", async () => { + const newRootLength = 0; + await zns.curvePricer.connect(user).setBaseLength(domainHash, newRootLength); + const newConfig = { + ...priceConfigDefault, + baseLength: BigNumber.from(newRootLength), + }; + + const expectedRootPrice = await calcCurvePrice(defaultDomain, newConfig); + const rootPrice = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); + + expect(rootPrice).to.eq(expectedRootPrice); + }); + + it("Should revert when setting baseLength that causes a spike at maxLength", async () => { + const newBaseLength = priceConfigDefault.baseLength.sub(1); + await expect( + zns.curvePricer.connect(user).setBaseLength(domainHash, newBaseLength) + ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); + }); + }); + + describe("#setMaxLength", () => { + it("Allows an authorized user to set the max length", async () => { + const newLength = 5; + + await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); + const params = await zns.curvePricer.priceConfigs(domainHash); + + expect(params.maxLength).to.eq(newLength); + }); + + it("Disallows an unauthorized user to set the max length", async () => { + const newLength = 5; + + const tx = zns.curvePricer.connect(admin).setMaxLength(domainHash, newLength); + await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + }); + + it("Allows setting the max length to zero", async () => { + const newLength = 0; + + await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); + const params = await zns.curvePricer.priceConfigs(domainHash); + + expect(params.maxLength).to.eq(newLength); + }); + + it("Still returns prices for domains within baseLength if the maxLength is zero", async () => { + const newLength = 0; + + await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); + + // Default price config sets baseLength to 4 + const short = "a"; + const long = "abcd"; + const beyondBaseLength = "abcde"; + + const priceCalls = [ + zns.curvePricer.getPrice(domainHash, short, true), + zns.curvePricer.getPrice(domainHash, long, true), + zns.curvePricer.getPrice(domainHash, beyondBaseLength, true), + ]; + + const [shortPrice, longPrice, beyondPrice] = await Promise.all(priceCalls); + + expect(shortPrice).to.eq(priceConfigDefault.maxPrice); + expect(longPrice).to.eq(priceConfigDefault.maxPrice); + expect(beyondPrice).to.eq(priceConfigDefault.minPrice); + }); + + it("Should revert when setting maxLength that causes a spike at maxLength", async () => { + const newMaxLength = priceConfigDefault.maxLength.add(10); + await expect( + zns.curvePricer.connect(user).setMaxLength(domainHash, newMaxLength) + ).to.be.revertedWith(CURVE_PRICE_CONFIG_ERR); + }); + }); + + describe("#setFeePercentage", () => { + it("Successfully sets the fee percentage", async () => { + const newFeePerc = BigNumber.from(222); + await zns.curvePricer.connect(user).setFeePercentage(domainHash, newFeePerc); + const { feePercentage: feeFromSC } = await zns.curvePricer.priceConfigs(domainHash); + + expect(feeFromSC).to.eq(newFeePerc); + }); + + it("Disallows an unauthorized user to set the fee percentage", async () => { + const newFeePerc = BigNumber.from(222); + const tx = zns.curvePricer.connect(admin) + .setFeePercentage(domainHash, newFeePerc); + await expect(tx).to.be.revertedWith(NOT_AUTHORIZED_REG_WIRED_ERR); + }); + + it("should revert when trying to set feePercentage higher than PERCENTAGE_BASIS", async () => { + const newFeePerc = BigNumber.from(10001); + await expect( + zns.curvePricer.connect(user).setFeePercentage(domainHash, newFeePerc) + ).to.be.revertedWith("ZNSCurvePricer: feePercentage cannot be greater than PERCENTAGE_BASIS"); + }); + }); + + describe("#getRegistrationFee", () => { + it("Successfully gets the fee for a price", async () => { + const stake = ethers.utils.parseEther("0.2"); + const fee = await zns.curvePricer.getFeeForPrice(domainHash, stake); + const expectedFee = stake.mul("222").div("10000"); + + expect(fee).to.eq(expectedFee); + }); + }); + + describe("#setAccessController", () => { + it("Successfully sets the access controller", async () => { + const currentAccessController = await zns.curvePricer.getAccessController(); + expect(currentAccessController).to.not.eq(randomAcc.address); + + const tx = await zns.curvePricer.setAccessController(randomAcc.address); + + const newAccessController = await zns.curvePricer.getAccessController(); + expect(newAccessController).to.eq(randomAcc.address); + + await expect(tx).to.emit(zns.curvePricer, "AccessControllerSet").withArgs(randomAcc.address); + }); + + it("Disallows an unauthorized user to set the access controller", async () => { + const tx = zns.curvePricer.connect(user).setAccessController(randomAcc.address); + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(user.address, ADMIN_ROLE) + ); + }); + + it("Disallows setting the access controller to the zero address", async () => { + const tx = zns.curvePricer.connect(admin).setAccessController(ethers.constants.AddressZero); + await expect(tx).to.be.revertedWith( + "AC: _accessController is 0x0 address" + ); + }); + }); + + describe("#setRegistry", () => { + it("Should successfully set the registry", async () => { + const currentRegistry = await zns.curvePricer.registry(); + expect(currentRegistry).to.not.eq(randomAcc.address); + + const tx = await zns.curvePricer.connect(admin).setRegistry(randomAcc.address); + + const newRegistry = await zns.curvePricer.registry(); + expect(newRegistry).to.eq(randomAcc.address); + + await expect(tx).to.emit(zns.curvePricer, "RegistrySet").withArgs(randomAcc.address); + }); + + it("Should NOT set the registry if called by anyone other than ADMIN_ROLE", async () => { + const tx = zns.curvePricer.connect(user).setRegistry(randomAcc.address); + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(user.address, ADMIN_ROLE) + ); + }); + }); + + describe("Events", () => { + it("Emits MaxPriceSet", async () => { + const newMaxPrice = priceConfigDefault.maxPrice.add(1); + + const tx = zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); + await expect(tx).to.emit(zns.curvePricer, "MaxPriceSet").withArgs(domainHash, newMaxPrice); + }); + + it("Emits BaseLengthSet", async () => { + const newLength = 5; + + const tx = zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); + await expect(tx).to.emit(zns.curvePricer, "BaseLengthSet").withArgs(domainHash, newLength); + }); + }); + + describe("UUPS", () => { + it("Allows an authorized user to upgrade the contract", async () => { + // CurvePricer to upgrade to + const factory = new ZNSCurvePricer__factory(deployer); + const newCurvePricer = await factory.deploy(); + await newCurvePricer.deployed(); + + // Confirm the deployer is a governor, as set in `deployZNS` helper + await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; + + const tx = zns.curvePricer.connect(deployer).upgradeTo(newCurvePricer.address); + await expect(tx).to.not.be.reverted; + }); + + it("Fails to upgrade if the caller is not authorized", async () => { + // CurvePricer to upgrade to + const factory = new ZNSCurvePricerUpgradeMock__factory(deployer); + const newCurvePricer = await factory.deploy(); + await newCurvePricer.deployed(); + + // Confirm the account is not a governor + await expect(zns.accessController.checkGovernor(randomAcc.address)).to.be.reverted; + + const tx = zns.curvePricer.connect(randomAcc).upgradeTo(newCurvePricer.address); + + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(randomAcc.address, GOVERNOR_ROLE) + ); + }); + + it("Verifies that variable values are not changed in the upgrade process", async () => { + const factory = new ZNSCurvePricerUpgradeMock__factory(deployer); + const newCurvePricer = await factory.deploy(); + await newCurvePricer.deployed(); + + await zns.curvePricer.connect(user).setBaseLength(domainHash, "7"); + await zns.curvePricer.connect(user).setMaxPrice( + domainHash, + priceConfigDefault.maxPrice.add(15) + ); + + const contractCalls = [ + zns.curvePricer.registry(), + zns.curvePricer.getAccessController(), + zns.curvePricer.priceConfigs(domainHash), + zns.curvePricer.getPrice(domainHash, "wilder", true), + ]; + + await validateUpgrade(deployer, zns.curvePricer, newCurvePricer, factory, contractCalls); + }); + }); +}); diff --git a/test/ZNSFixedPricer.test.ts b/test/ZNSFixedPricer.test.ts index 7e31ab78a..10660d759 100644 --- a/test/ZNSFixedPricer.test.ts +++ b/test/ZNSFixedPricer.test.ts @@ -4,7 +4,7 @@ import { deployZNS, getAccessRevertMsg, GOVERNOR_ROLE, - INITIALIZED_ERR, + INITIALIZED_ERR, INVALID_NAME_ERR, NOT_AUTHORIZED_REG_WIRED_ERR, PaymentType, PERCENTAGE_BASIS, @@ -149,7 +149,7 @@ describe("ZNSFixedPricer", () => { await expect(tx).to.emit(zns.fixedPricer, "PriceSet").withArgs(domainHash, newPrice); expect( - await zns.fixedPricer.getPrice(domainHash, "testname") + await zns.fixedPricer.getPrice(domainHash, "testname", true) ).to.equal(newPrice); }); @@ -158,10 +158,16 @@ describe("ZNSFixedPricer", () => { await zns.fixedPricer.connect(user).setPrice(domainHash, newPrice); expect( - await zns.fixedPricer.getPrice(domainHash, "testname") + await zns.fixedPricer.getPrice(domainHash, "testname", false) ).to.equal(newPrice); }); + it("#getPrice() should revert for invalid label when not skipping the label validation", async () => { + await expect( + zns.fixedPricer.getPrice(domainHash, "tEstname", false) + ).to.be.revertedWith(INVALID_NAME_ERR); + }); + it("#getPriceAndFee() should return the correct price and fee", async () => { const newPrice = ethers.utils.parseEther("3213"); const newFee = BigNumber.from(1234); @@ -171,7 +177,7 @@ describe("ZNSFixedPricer", () => { const { price, fee, - } = await zns.fixedPricer.getPriceAndFee(domainHash, "testname"); + } = await zns.fixedPricer.getPriceAndFee(domainHash, "testname", false); expect(price).to.equal(newPrice); expect(fee).to.equal(newPrice.mul(newFee).div(PERCENTAGE_BASIS)); @@ -376,7 +382,7 @@ describe("ZNSFixedPricer", () => { zns.fixedPricer.registry(), zns.fixedPricer.getAccessController(), zns.fixedPricer.priceConfigs(domainHash), - zns.fixedPricer.getPrice(domainHash, "wilder"), + zns.fixedPricer.getPrice(domainHash, "wilder", false), ]; await validateUpgrade(deployer, zns.fixedPricer, newFixedPricer, factory, contractCalls); diff --git a/test/ZNSRootRegistrar.test.ts b/test/ZNSRootRegistrar.test.ts index 1f0557656..feff4f7e4 100644 --- a/test/ZNSRootRegistrar.test.ts +++ b/test/ZNSRootRegistrar.test.ts @@ -1,1139 +1,1214 @@ -import * as hre from "hardhat"; -import { expect } from "chai"; -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { - AccessType, defaultTokenURI, - deployZNS, - distrConfigEmpty, - hashDomainLabel, INITIALIZED_ERR, - INVALID_TOKENID_ERC_ERR, - normalizeName, - NOT_AUTHORIZED_REG_ERR, - NOT_BOTH_OWNER_RAR_ERR, - NOT_TOKEN_OWNER_RAR_ERR, - ONLY_NAME_OWNER_REG_ERR, - ONLY_OWNER_REGISTRAR_REG_ERR, OwnerOf, PaymentType, REGISTRAR_ROLE, - validateUpgrade, -} from "./helpers"; -import { IDistributionConfig, IZNSContracts } from "./helpers/types"; -import * as ethers from "ethers"; -import { BigNumber } from "ethers"; -import { defaultRootRegistration } from "./helpers/register-setup"; -import { checkBalance } from "./helpers/balances"; -import { precisionMultiDefault, priceConfigDefault, registrationFeePercDefault } from "./helpers/constants"; -import { calcCurvePrice, getPriceObject } from "./helpers/pricing"; -import { getDomainHashFromReceipt, getTokenIdFromReceipt } from "./helpers/events"; -import { getAccessRevertMsg } from "./helpers/errors"; -import { ADMIN_ROLE, GOVERNOR_ROLE } from "./helpers/access"; -import { ZNSRootRegistrar, ZNSRootRegistrar__factory, ZNSRootRegistrarUpgradeMock__factory } from "../typechain"; -import { PaymentConfigStruct } from "../typechain/contracts/treasury/IZNSTreasury"; -import { parseEther } from "ethers/lib/utils"; -import { getProxyImplAddress } from "./helpers/utils"; -import { upgrades } from "hardhat"; - -require("@nomicfoundation/hardhat-chai-matchers"); - - -describe("ZNSRootRegistrar", () => { - let deployer : SignerWithAddress; - let user : SignerWithAddress; - let governor : SignerWithAddress; - let admin : SignerWithAddress; - let randomUser : SignerWithAddress; - - let zns : IZNSContracts; - let zeroVault : SignerWithAddress; - let operator : SignerWithAddress; - const defaultDomain = normalizeName("wilder"); - let userBalanceInitial : BigNumber; - - beforeEach(async () => { - [deployer, zeroVault, user, operator, governor, admin, randomUser] = await hre.ethers.getSigners(); - // zeroVault address is used to hold the fee charged to the user when registering - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address, governor.address], - adminAddresses: [admin.address], - priceConfig: priceConfigDefault, - zeroVaultAddress: zeroVault.address, - }); - - userBalanceInitial = ethers.utils.parseEther("100000000000"); - // Give funds to user - await zns.zeroToken.connect(user).approve(zns.treasury.address, ethers.constants.MaxUint256); - await zns.zeroToken.mint(user.address, userBalanceInitial); - }); - - it("Gas tests", async () => { - const tokenURI = "https://example.com/817c64af"; - const distrConfig : IDistributionConfig = { - pricerContract: zns.curvePricer.address, - paymentType: 1, - accessType: 1, - }; - - const tx = await zns.rootRegistrar.connect(deployer).registerRootDomain( - defaultDomain, - deployer.address, - tokenURI, - distrConfig - ); - - const receipt = await tx.wait(); - - const domainHash = await getDomainHashFromReceipt(receipt); - - // Registering as deployer (owner of parent) and user is different gas values - await zns.subRegistrar.connect(deployer).registerSubdomain( - domainHash, - "subdomain", - deployer.address, - tokenURI, - distrConfigEmpty - ); - - 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 let initialize the implementation contract", async () => { - const factory = new ZNSRootRegistrar__factory(deployer); - const impl = await getProxyImplAddress(zns.rootRegistrar.address); - const implContract = factory.attach(impl); - - await expect( - implContract.initialize( - operator.address, - 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.constants.HashZero, user.address); - expect(await zns.registry.getDomainOwner(ethers.constants.HashZero)).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.constants.HashZero, 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.constants.HashZero, newTreasuryConfig); - - await expect(treasuryTx).to.emit( - zns.treasury, - "BeneficiarySet" - ).withArgs( - ethers.constants.HashZero, - user.address - ); - await expect(treasuryTx).to.emit( - zns.treasury, - "PaymentTokenSet" - ).withArgs( - ethers.constants.HashZero, - zeroVault.address - ); - - // Modify the curve pricer - const newPricerConfig = { - baseLength: BigNumber.from("6"), - maxLength: BigNumber.from("35"), - maxPrice: parseEther("150"), - minPrice: parseEther("10"), - precisionMultiplier: precisionMultiDefault, - feePercentage: registrationFeePercDefault, - isSet: true, - }; - - const pricerTx = await zns.curvePricer.connect(user).setPriceConfig( - ethers.constants.HashZero, - newPricerConfig, - ); - - await expect(pricerTx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( - ethers.constants.HashZero, - 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.zeroToken.balanceOf(user.address); - expect(balance).to.eq(userBalanceInitial); - - const allowance = await zns.zeroToken.allowance(user.address, zns.treasury.address); - expect(allowance).to.eq(ethers.constants.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, - [ - zns.accessController.address, - zns.registry.address, - zns.curvePricer.address, - zns.treasury.address, - zns.domainToken.address, - zns.addressResolver.address, - ], - { - 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( - zns.accessController.address, - randomUser.address, - 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.constants.HashZero, - domainHash: ethers.constants.HashZero, - label: "randomname", - registrant: ethers.constants.AddressZero, - price: "0", - stakeFee: "0", - domainAddress: ethers.constants.AddressZero, - tokenURI: "", - isStakePayment: false, - }) - ).to.be.revertedWith( - getAccessRevertMsg(randomUser.address, REGISTRAR_ROLE) - ); - }); - - it("#isOwnerOf() returns correct bools", async () => { - const topLevelTx = await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - const domainHash = await getDomainHashFromReceipt(topLevelTx); - const tokenId = BigNumber.from(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.constants.AddressZero) - ).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("ZNSRootRegistrar: Domain Name not provided"); - }); - - // 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.constants.AddressZero, - tokenURI, - distrConfigEmpty - ); - - const hashFromTS = hashDomainLabel(defaultDomain); - - await expect(tx).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( - ethers.constants.HashZero, - hashFromTS, - BigNumber.from(hashFromTS), - defaultDomain, - user.address, - ethers.constants.AddressZero, - ); - - 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: zns.fixedPricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }; - const tokenURI = "https://example.com/817c64af"; - - const tx = await zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - ethers.constants.AddressZero, - tokenURI, - distrConfig - ); - - const receipt = await tx.wait(0); - - const domainHash = await getDomainHashFromReceipt(receipt); - - 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.zeroToken.balanceOf(user.address); - const balanceBeforeVault = await zns.zeroToken.balanceOf(zeroVault.address); - - // Deploy "wilder" with default configuration - const tx = await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - const domainHash = await getDomainHashFromReceipt(tx); - const { - totalPrice, - expectedPrice, - stakeFee, - } = await getPriceObject(defaultDomain, priceConfigDefault); - - await checkBalance({ - token: zns.zeroToken, - balanceBefore: balanceBeforeUser, - userAddress: user.address, - target: totalPrice, - }); - - await checkBalance({ - token: zns.zeroToken, - 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(zns.zeroToken.address); - }); - - it("Sets the correct data in Registry", async () => { - const tx = await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - - const namehashRef = hashDomainLabel(defaultDomain); - const domainHash = await getDomainHashFromReceipt(tx); - 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(zns.addressResolver.address); - }); - - it("Fails when the user does not have enough funds", async () => { - const balance = await zns.zeroToken.balanceOf(user.address); - await zns.zeroToken.connect(user).transfer(randomUser.address, balance); - - const tx = defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - await expect(tx).to.be.revertedWith("ERC20: transfer amount exceeds balance"); - }); - - // eslint-disable-next-line max-len - it("Allows unicode characters in domain names and matches the hash of normalized string acquired from namehash library", async () => { - const unicodeDomainLabel = "œ柸þ€§ᆰ"; - - const normalizedDomainLabel = normalizeName(unicodeDomainLabel); - - const tx = await defaultRootRegistration({ - user, - zns, - domainName: normalizedDomainLabel, - }); - - const domainHash = await getDomainHashFromReceipt(tx); - // validate that namehash lib works the same way as our contract hashing - // TODO: a security issue with namehash lib is the usage of non-ASCII characters - // this should be handled at the SDK/dApp level! - const namehashRef = hashDomainLabel(unicodeDomainLabel); - expect(domainHash).to.eq(namehashRef); - expect(await zns.registry.exists(domainHash)).to.be.true; - - const expectedStaked = await calcCurvePrice(normalizedDomainLabel, priceConfigDefault); - const { amount: staked } = await zns.treasury.stakedForDomain(domainHash); - expect(expectedStaked).to.eq(staked); - }); - - 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.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ); - - await expect(tx).to.not.be.reverted; - }); - - it("Records the correct domain hash", async () => { - const tx = await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - - const domainHash = await getDomainHashFromReceipt(tx); - - 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 () => { - const tx = await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - }); - - const tokenId = await getTokenIdFromReceipt(tx); - const owner = await zns.domainToken.ownerOf(tokenId); - expect(owner).to.eq(user.address); - }); - - it("Resolves the correct address from the domain", async () => { - const tx = await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - domainContent: zns.rootRegistrar.address, - }); - const domainHash = await getDomainHashFromReceipt(tx); - - const resolvedAddress = await zns.addressResolver.getAddress(domainHash); - expect(resolvedAddress).to.eq(zns.rootRegistrar.address); - }); - - 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.constants.HashZero, "0"); - await zns.curvePricer.connect(deployer).setMinPrice(ethers.constants.HashZero, "0"); - - const userBalanceBefore = await zns.zeroToken.balanceOf(user.address); - const vaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); - - // register a domain - await zns.rootRegistrar.connect(user).registerRootDomain( - defaultDomain, - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ); - - const userBalanceAfter = await zns.zeroToken.balanceOf(user.address); - const vaultBalanceAfter = await zns.zeroToken.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.zeroToken.filters.Transfer( - user.address, - ); - const events = await zns.zeroToken.queryFilter(transferEventFilter); - expect(events.length).to.eq(0); - }); - }); - - describe("Reclaiming Domains", () => { - it("Can reclaim name/stake if Token is owned", async () => { - // Register Top level - const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromReceipt(topLevelTx); - const tokenId = await getTokenIdFromReceipt(topLevelTx); - 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(zns.zeroToken.address); - expect(token).to.equal(tokenAfterReclaim); - }); - - it("Reclaiming domain token emits DomainReclaimed event", async () => { - const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromReceipt(topLevelTx); - const tokenId = await getTokenIdFromReceipt(topLevelTx); - - // Transfer the domain token - await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); - // Reclaim the Domain - const tx = await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); - const receipt = await tx.wait(0); - - // Verify Transfer event is emitted - expect(receipt.events?.[1].event).to.eq("DomainReclaimed"); - expect(receipt.events?.[1].args?.domainHash).to.eq( - domainHash - ); - expect(receipt.events?.[1].args?.registrant).to.eq( - user.address - ); - }); - - it("Cannot reclaim name/stake if token is not owned", async () => { - const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromReceipt(topLevelTx); - // 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 - const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromReceipt(topLevelTx); - const tokenId = await getTokenIdFromReceipt(topLevelTx); - 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(zns.zeroToken.address); - expect(token).to.equal(tokenAfterReclaim); - }); - - it("Can revoke and unstake after reclaiming", async () => { - // Verify Balance - const balance = await zns.zeroToken.balanceOf(user.address); - expect(balance).to.eq(userBalanceInitial); - - // Register Top level - const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromReceipt(topLevelTx); - const tokenId = await getTokenIdFromReceipt(topLevelTx); - - // Validated staked values - const { - expectedPrice: expectedStaked, - } = await getPriceObject(defaultDomain, priceConfigDefault); - const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); - expect(staked).to.eq(expectedStaked); - expect(token).to.eq(zns.zeroToken.address); - - // 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(ethers.BigNumber.from("0")); - expect(finalToken).to.equal(ethers.constants.AddressZero); - - // Verify final balances - const computedFinalBalance = balance.add(staked); - const finalBalance = await zns.zeroToken.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 - const topLevelTx = await defaultRootRegistration({ - user, - zns, - domainName: defaultDomain, - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - }); - - const domainHash = await getDomainHashFromReceipt(topLevelTx); - - // add mintlist to check revocation - await zns.subRegistrar.connect(user).updateMintlistForDomain( - domainHash, - [user.address, zeroVault.address], - [true, true] - ); - - const ogPrice = BigNumber.from(135); - await zns.fixedPricer.connect(user).setPriceConfig( - domainHash, - { - price: ogPrice, - feePercentage: BigNumber.from(0), - isSet: true, - } - ); - expect(await zns.fixedPricer.getPrice(domainHash, defaultDomain)).to.eq(ogPrice); - - const tokenId = await getTokenIdFromReceipt(topLevelTx); - - // 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.zeroToken.balanceOf(user.address); - expect(balance).to.eq(userBalanceInitial); - - // Register Top level - const tx = await defaultRootRegistration({ user, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromReceipt(tx); - - // Validated staked values - const { - expectedPrice: expectedStaked, - stakeFee: expectedStakeFee, - } = await getPriceObject(defaultDomain, priceConfigDefault); - const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); - expect(staked).to.eq(expectedStaked); - expect(token).to.eq(zns.zeroToken.address); - - // Get balance after staking - const balanceAfterStaking = await zns.zeroToken.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(ethers.BigNumber.from("0")); - expect(finalToken).to.equal(ethers.constants.AddressZero); - - // Verify final balances - const computedBalanceAfterStaking = balanceAfterStaking.add(staked); - const balanceMinusFee = balance.sub(expectedStakeFee); - expect(computedBalanceAfterStaking).to.equal(balanceMinusFee); - const finalBalance = await zns.zeroToken.balanceOf(user.address); - expect(computedBalanceAfterStaking).to.equal(finalBalance); - }); - - it("Cannot revoke if Name is owned by another user", async () => { - // Register Top level - const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const parentDomainHash = await getDomainHashFromReceipt(topLevelTx); - 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 - const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); - const parentDomainHash = await getDomainHashFromReceipt(topLevelTx); - const owner = await zns.registry.connect(user).getDomainOwner(parentDomainHash); - expect(owner).to.not.equal(user.address); - - const tokenId = BigNumber.from(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 - const tx = await defaultRootRegistration({ user, zns, domainName: defaultDomain }); - const domainHash = await getDomainHashFromReceipt(tx); - - // 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.constants.AddressZero); - 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.constants.AddressZero); - 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.constants.AddressZero); - 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.constants.AddressZero); - await expect(tx).to.be.revertedWith("ZNSRootRegistrar: domainToken_ is 0x0 address"); - }); - }); - - describe("#setAddressResolver", () => { - it("Should set AddressResolver and fire AddressResolverSet event", async () => { - const currentResolver = await zns.rootRegistrar.addressResolver(); - const tx = await zns.rootRegistrar.connect(deployer).setAddressResolver(randomUser.address); - const newResolver = await zns.rootRegistrar.addressResolver(); - - await expect(tx).to.emit(zns.rootRegistrar, "AddressResolverSet").withArgs(randomUser.address); - - expect(newResolver).to.equal(randomUser.address); - expect(currentResolver).to.not.equal(newResolver); - }); - - it("Should revert if not called by ADMIN", async () => { - const tx = zns.rootRegistrar.connect(user).setAddressResolver(randomUser.address); - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(user.address, ADMIN_ROLE) - ); - }); - - it("Should revert if AddressResolver is address zero", async () => { - const tx = zns.rootRegistrar.connect(deployer).setAddressResolver(ethers.constants.AddressZero); - await expect(tx).to.be.revertedWith("ZNSRootRegistrar: addressResolver_ 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.deployed(); - - const upgradeTx = zns.rootRegistrar.connect(deployer).upgradeTo(registrar.address); - 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.deployed(); - - const tx = zns.rootRegistrar.connect(randomUser).upgradeTo(registrar.address); - - 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.deployed(); - - const domainName = "world"; - const domainHash = hashDomainLabel(domainName); - - await zns.zeroToken.connect(randomUser).approve(zns.treasury.address, ethers.constants.MaxUint256); - await zns.zeroToken.mint(randomUser.address, priceConfigDefault.maxPrice); - - await zns.rootRegistrar.connect(randomUser).registerRootDomain( - domainName, - randomUser.address, - defaultTokenURI, - distrConfigEmpty - ); - - await zns.rootRegistrar.setAddressResolver(randomUser.address); - - const contractCalls = [ - zns.rootRegistrar.getAccessController(), - zns.rootRegistrar.registry(), - zns.rootRegistrar.treasury(), - zns.rootRegistrar.domainToken(), - zns.rootRegistrar.addressResolver(), - zns.registry.exists(domainHash), - zns.treasury.stakedForDomain(domainHash), - zns.domainToken.name(), - zns.domainToken.symbol(), - zns.curvePricer.getPrice(ethers.constants.HashZero, domainName), - ]; - - await validateUpgrade(deployer, zns.rootRegistrar, registrar, registrarFactory, contractCalls); - }); - }); -}); +import * as hre from "hardhat"; +import { expect } from "chai"; +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { + AccessType, defaultTokenURI, + deployZNS, + distrConfigEmpty, + hashDomainLabel, INVALID_LENGTH_ERR, + INITIALIZED_ERR, + INVALID_TOKENID_ERC_ERR, + normalizeName, + NOT_AUTHORIZED_REG_ERR, + NOT_BOTH_OWNER_RAR_ERR, + NOT_TOKEN_OWNER_RAR_ERR, + ONLY_NAME_OWNER_REG_ERR, + ONLY_OWNER_REGISTRAR_REG_ERR, OwnerOf, PaymentType, REGISTRAR_ROLE, + validateUpgrade, +} from "./helpers"; +import { IDistributionConfig, IZNSContracts } from "./helpers/types"; +import * as ethers from "ethers"; +import { BigNumber } from "ethers"; +import { defaultRootRegistration } from "./helpers/register-setup"; +import { checkBalance } from "./helpers/balances"; +import { precisionMultiDefault, priceConfigDefault, registrationFeePercDefault } from "./helpers/constants"; +import { getPriceObject } from "./helpers/pricing"; +import { getDomainHashFromReceipt, getTokenIdFromReceipt } from "./helpers/events"; +import { getAccessRevertMsg, INVALID_NAME_ERR } from "./helpers/errors"; +import { ADMIN_ROLE, GOVERNOR_ROLE } from "./helpers/access"; +import { ZNSRootRegistrar, ZNSRootRegistrar__factory, ZNSRootRegistrarUpgradeMock__factory } from "../typechain"; +import { PaymentConfigStruct } from "../typechain/contracts/treasury/IZNSTreasury"; +import { parseEther } from "ethers/lib/utils"; +import { getProxyImplAddress } from "./helpers/utils"; +import { upgrades } from "hardhat"; + +require("@nomicfoundation/hardhat-chai-matchers"); + + +describe("ZNSRootRegistrar", () => { + let deployer : SignerWithAddress; + let user : SignerWithAddress; + let governor : SignerWithAddress; + let admin : SignerWithAddress; + let randomUser : SignerWithAddress; + + let zns : IZNSContracts; + let zeroVault : SignerWithAddress; + let operator : SignerWithAddress; + const defaultDomain = normalizeName("wilder"); + let userBalanceInitial : BigNumber; + + beforeEach(async () => { + [deployer, zeroVault, user, operator, governor, admin, randomUser] = await hre.ethers.getSigners(); + // zeroVault address is used to hold the fee charged to the user when registering + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address, governor.address], + adminAddresses: [admin.address], + priceConfig: priceConfigDefault, + zeroVaultAddress: zeroVault.address, + }); + + userBalanceInitial = ethers.utils.parseEther("100000000000"); + // Give funds to user + await zns.zeroToken.connect(user).approve(zns.treasury.address, ethers.constants.MaxUint256); + await zns.zeroToken.mint(user.address, userBalanceInitial); + }); + + it("Gas tests", async () => { + const tokenURI = "https://example.com/817c64af"; + const distrConfig : IDistributionConfig = { + pricerContract: zns.curvePricer.address, + paymentType: 1, + accessType: 1, + }; + + const tx = await zns.rootRegistrar.connect(deployer).registerRootDomain( + defaultDomain, + deployer.address, + tokenURI, + distrConfig + ); + + const receipt = await tx.wait(); + + const domainHash = await getDomainHashFromReceipt(receipt); + + // Registering as deployer (owner of parent) and user is different gas values + await zns.subRegistrar.connect(deployer).registerSubdomain( + domainHash, + "subdomain", + deployer.address, + tokenURI, + distrConfigEmpty + ); + + 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 let initialize the implementation contract", async () => { + const factory = new ZNSRootRegistrar__factory(deployer); + const impl = await getProxyImplAddress(zns.rootRegistrar.address); + const implContract = factory.attach(impl); + + await expect( + implContract.initialize( + operator.address, + 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.constants.HashZero, user.address); + expect(await zns.registry.getDomainOwner(ethers.constants.HashZero)).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.constants.HashZero, 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.constants.HashZero, newTreasuryConfig); + + await expect(treasuryTx).to.emit( + zns.treasury, + "BeneficiarySet" + ).withArgs( + ethers.constants.HashZero, + user.address + ); + await expect(treasuryTx).to.emit( + zns.treasury, + "PaymentTokenSet" + ).withArgs( + ethers.constants.HashZero, + zeroVault.address + ); + + // Modify the curve pricer + const newPricerConfig = { + baseLength: BigNumber.from("6"), + maxLength: BigNumber.from("35"), + maxPrice: parseEther("150"), + minPrice: parseEther("10"), + precisionMultiplier: precisionMultiDefault, + feePercentage: registrationFeePercDefault, + isSet: true, + }; + + const pricerTx = await zns.curvePricer.connect(user).setPriceConfig( + ethers.constants.HashZero, + newPricerConfig, + ); + + await expect(pricerTx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( + ethers.constants.HashZero, + 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.zeroToken.balanceOf(user.address); + expect(balance).to.eq(userBalanceInitial); + + const allowance = await zns.zeroToken.allowance(user.address, zns.treasury.address); + expect(allowance).to.eq(ethers.constants.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, + [ + zns.accessController.address, + zns.registry.address, + zns.curvePricer.address, + zns.treasury.address, + zns.domainToken.address, + zns.addressResolver.address, + ], + { + 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( + zns.accessController.address, + randomUser.address, + 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.constants.HashZero, + domainHash: ethers.constants.HashZero, + label: "randomname", + registrant: ethers.constants.AddressZero, + price: "0", + stakeFee: "0", + domainAddress: ethers.constants.AddressZero, + tokenURI: "", + isStakePayment: false, + }) + ).to.be.revertedWith( + getAccessRevertMsg(randomUser.address, REGISTRAR_ROLE) + ); + }); + + it("#isOwnerOf() returns correct bools", async () => { + const topLevelTx = await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + const domainHash = await getDomainHashFromReceipt(topLevelTx); + const tokenId = BigNumber.from(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.constants.AddressZero) + ).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.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ); + + await expect(tx1).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.constants.HashZero, + lettersHash, + BigNumber.from(lettersHash), + letters, + deployer.address, + ethers.constants.AddressZero, + ); + + const tx2 = zns.rootRegistrar.connect(deployer).registerRootDomain( + alphaNumeric, + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ); + + await expect(tx2).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.constants.HashZero, + alphaNumericHash, + BigNumber.from(alphaNumericHash), + alphaNumeric, + deployer.address, + ethers.constants.AddressZero, + ); + + const tx3 = zns.rootRegistrar.connect(deployer).registerRootDomain( + withHyphen, + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ); + + await expect(tx3).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.constants.HashZero, + withHyphenHash, + BigNumber.from(withHyphenHash), + withHyphen, + deployer.address, + ethers.constants.AddressZero, + ); + }); + + 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.constants.AddressZero, + tokenURI, + distrConfigEmpty + ); + + const hashFromTS = hashDomainLabel(defaultDomain); + + await expect(tx).to.emit(zns.rootRegistrar, "DomainRegistered").withArgs( + ethers.constants.HashZero, + hashFromTS, + BigNumber.from(hashFromTS), + defaultDomain, + user.address, + ethers.constants.AddressZero, + ); + + 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: zns.fixedPricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }; + const tokenURI = "https://example.com/817c64af"; + + const tx = await zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + ethers.constants.AddressZero, + tokenURI, + distrConfig + ); + + const receipt = await tx.wait(0); + + const domainHash = await getDomainHashFromReceipt(receipt); + + 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.zeroToken.balanceOf(user.address); + const balanceBeforeVault = await zns.zeroToken.balanceOf(zeroVault.address); + + // Deploy "wilder" with default configuration + const tx = await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + const domainHash = await getDomainHashFromReceipt(tx); + const { + totalPrice, + expectedPrice, + stakeFee, + } = await getPriceObject(defaultDomain, priceConfigDefault); + + await checkBalance({ + token: zns.zeroToken, + balanceBefore: balanceBeforeUser, + userAddress: user.address, + target: totalPrice, + }); + + await checkBalance({ + token: zns.zeroToken, + 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(zns.zeroToken.address); + }); + + it("Sets the correct data in Registry", async () => { + const tx = await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + + const namehashRef = hashDomainLabel(defaultDomain); + const domainHash = await getDomainHashFromReceipt(tx); + 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(zns.addressResolver.address); + }); + + it("Fails when the user does not have enough funds", async () => { + const balance = await zns.zeroToken.balanceOf(user.address); + await zns.zeroToken.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.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ); + + await expect(tx).to.not.be.reverted; + }); + + it("Records the correct domain hash", async () => { + const tx = await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + + const domainHash = await getDomainHashFromReceipt(tx); + + 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 () => { + const tx = await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + }); + + const tokenId = await getTokenIdFromReceipt(tx); + const owner = await zns.domainToken.ownerOf(tokenId); + expect(owner).to.eq(user.address); + }); + + it("Resolves the correct address from the domain", async () => { + const tx = await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + domainContent: zns.rootRegistrar.address, + }); + const domainHash = await getDomainHashFromReceipt(tx); + + const resolvedAddress = await zns.addressResolver.getAddress(domainHash); + expect(resolvedAddress).to.eq(zns.rootRegistrar.address); + }); + + 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.constants.HashZero, "0"); + await zns.curvePricer.connect(deployer).setMinPrice(ethers.constants.HashZero, "0"); + + const userBalanceBefore = await zns.zeroToken.balanceOf(user.address); + const vaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); + + // register a domain + await zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ); + + const userBalanceAfter = await zns.zeroToken.balanceOf(user.address); + const vaultBalanceAfter = await zns.zeroToken.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.zeroToken.filters.Transfer( + user.address, + ); + const events = await zns.zeroToken.queryFilter(transferEventFilter); + expect(events.length).to.eq(0); + }); + }); + + describe("Reclaiming Domains", () => { + it("Can reclaim name/stake if Token is owned", async () => { + // Register Top level + const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromReceipt(topLevelTx); + const tokenId = await getTokenIdFromReceipt(topLevelTx); + 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(zns.zeroToken.address); + expect(token).to.equal(tokenAfterReclaim); + }); + + it("Reclaiming domain token emits DomainReclaimed event", async () => { + const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromReceipt(topLevelTx); + const tokenId = await getTokenIdFromReceipt(topLevelTx); + + // Transfer the domain token + await zns.domainToken.connect(deployer).transferFrom(deployer.address, user.address, tokenId); + // Reclaim the Domain + const tx = await zns.rootRegistrar.connect(user).reclaimDomain(domainHash); + const receipt = await tx.wait(0); + + // Verify Transfer event is emitted + expect(receipt.events?.[1].event).to.eq("DomainReclaimed"); + expect(receipt.events?.[1].args?.domainHash).to.eq( + domainHash + ); + expect(receipt.events?.[1].args?.registrant).to.eq( + user.address + ); + }); + + it("Cannot reclaim name/stake if token is not owned", async () => { + const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromReceipt(topLevelTx); + // 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 + const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromReceipt(topLevelTx); + const tokenId = await getTokenIdFromReceipt(topLevelTx); + 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(zns.zeroToken.address); + expect(token).to.equal(tokenAfterReclaim); + }); + + it("Can revoke and unstake after reclaiming", async () => { + // Verify Balance + const balance = await zns.zeroToken.balanceOf(user.address); + expect(balance).to.eq(userBalanceInitial); + + // Register Top level + const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromReceipt(topLevelTx); + const tokenId = await getTokenIdFromReceipt(topLevelTx); + + // Validated staked values + const { + expectedPrice: expectedStaked, + } = await getPriceObject(defaultDomain, priceConfigDefault); + const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); + expect(staked).to.eq(expectedStaked); + expect(token).to.eq(zns.zeroToken.address); + + // 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(ethers.BigNumber.from("0")); + expect(finalToken).to.equal(ethers.constants.AddressZero); + + // Verify final balances + const computedFinalBalance = balance.add(staked); + const finalBalance = await zns.zeroToken.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 + const topLevelTx = await defaultRootRegistration({ + user, + zns, + domainName: defaultDomain, + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + }); + + const domainHash = await getDomainHashFromReceipt(topLevelTx); + + // add mintlist to check revocation + await zns.subRegistrar.connect(user).updateMintlistForDomain( + domainHash, + [user.address, zeroVault.address], + [true, true] + ); + + const ogPrice = BigNumber.from(135); + await zns.fixedPricer.connect(user).setPriceConfig( + domainHash, + { + price: ogPrice, + feePercentage: BigNumber.from(0), + isSet: true, + } + ); + expect(await zns.fixedPricer.getPrice(domainHash, defaultDomain, false)).to.eq(ogPrice); + + const tokenId = await getTokenIdFromReceipt(topLevelTx); + + // 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.zeroToken.balanceOf(user.address); + expect(balance).to.eq(userBalanceInitial); + + // Register Top level + const tx = await defaultRootRegistration({ user, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromReceipt(tx); + + // Validated staked values + const { + expectedPrice: expectedStaked, + stakeFee: expectedStakeFee, + } = await getPriceObject(defaultDomain, priceConfigDefault); + const { amount: staked, token } = await zns.treasury.stakedForDomain(domainHash); + expect(staked).to.eq(expectedStaked); + expect(token).to.eq(zns.zeroToken.address); + + // Get balance after staking + const balanceAfterStaking = await zns.zeroToken.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(ethers.BigNumber.from("0")); + expect(finalToken).to.equal(ethers.constants.AddressZero); + + // Verify final balances + const computedBalanceAfterStaking = balanceAfterStaking.add(staked); + const balanceMinusFee = balance.sub(expectedStakeFee); + expect(computedBalanceAfterStaking).to.equal(balanceMinusFee); + const finalBalance = await zns.zeroToken.balanceOf(user.address); + expect(computedBalanceAfterStaking).to.equal(finalBalance); + }); + + it("Cannot revoke if Name is owned by another user", async () => { + // Register Top level + const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const parentDomainHash = await getDomainHashFromReceipt(topLevelTx); + 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 + const topLevelTx = await defaultRootRegistration({ user: deployer, zns, domainName: defaultDomain }); + const parentDomainHash = await getDomainHashFromReceipt(topLevelTx); + const owner = await zns.registry.connect(user).getDomainOwner(parentDomainHash); + expect(owner).to.not.equal(user.address); + + const tokenId = BigNumber.from(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 + const tx = await defaultRootRegistration({ user, zns, domainName: defaultDomain }); + const domainHash = await getDomainHashFromReceipt(tx); + + // 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.constants.AddressZero); + 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.constants.AddressZero); + 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.constants.AddressZero); + 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.constants.AddressZero); + await expect(tx).to.be.revertedWith("ZNSRootRegistrar: domainToken_ is 0x0 address"); + }); + }); + + describe("#setAddressResolver", () => { + it("Should set AddressResolver and fire AddressResolverSet event", async () => { + const currentResolver = await zns.rootRegistrar.addressResolver(); + const tx = await zns.rootRegistrar.connect(deployer).setAddressResolver(randomUser.address); + const newResolver = await zns.rootRegistrar.addressResolver(); + + await expect(tx).to.emit(zns.rootRegistrar, "AddressResolverSet").withArgs(randomUser.address); + + expect(newResolver).to.equal(randomUser.address); + expect(currentResolver).to.not.equal(newResolver); + }); + + it("Should revert if not called by ADMIN", async () => { + const tx = zns.rootRegistrar.connect(user).setAddressResolver(randomUser.address); + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(user.address, ADMIN_ROLE) + ); + }); + + it("Should revert if AddressResolver is address zero", async () => { + const tx = zns.rootRegistrar.connect(deployer).setAddressResolver(ethers.constants.AddressZero); + await expect(tx).to.be.revertedWith("ZNSRootRegistrar: addressResolver_ 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.deployed(); + + const upgradeTx = zns.rootRegistrar.connect(deployer).upgradeTo(registrar.address); + 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.deployed(); + + const tx = zns.rootRegistrar.connect(randomUser).upgradeTo(registrar.address); + + 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.deployed(); + + const domainName = "world"; + const domainHash = hashDomainLabel(domainName); + + await zns.zeroToken.connect(randomUser).approve(zns.treasury.address, ethers.constants.MaxUint256); + await zns.zeroToken.mint(randomUser.address, priceConfigDefault.maxPrice); + + await zns.rootRegistrar.connect(randomUser).registerRootDomain( + domainName, + randomUser.address, + defaultTokenURI, + distrConfigEmpty + ); + + await zns.rootRegistrar.setAddressResolver(randomUser.address); + + const contractCalls = [ + zns.rootRegistrar.getAccessController(), + zns.rootRegistrar.registry(), + zns.rootRegistrar.treasury(), + zns.rootRegistrar.domainToken(), + zns.rootRegistrar.addressResolver(), + zns.registry.exists(domainHash), + zns.treasury.stakedForDomain(domainHash), + zns.domainToken.name(), + zns.domainToken.symbol(), + zns.curvePricer.getPrice(ethers.constants.HashZero, domainName, false), + ]; + + await validateUpgrade(deployer, zns.rootRegistrar, registrar, registrarFactory, contractCalls); + }); + }); +}); diff --git a/test/ZNSSubRegistrar.test.ts b/test/ZNSSubRegistrar.test.ts index 438f0fade..82490d3c7 100644 --- a/test/ZNSSubRegistrar.test.ts +++ b/test/ZNSSubRegistrar.test.ts @@ -1,3458 +1,3537 @@ -import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; -import { IDomainConfigForTest, IFixedPriceConfig, IPathRegResult, IZNSContracts } from "./helpers/types"; -import { - AccessType, - ADMIN_ROLE, - defaultTokenURI, - deployZNS, - distrConfigEmpty, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR, - fullDistrConfigEmpty, - getAccessRevertMsg, - getPriceObject, - getStakingOrProtocolFee, - GOVERNOR_ROLE, - INITIALIZED_ERR, - INVALID_TOKENID_ERC_ERR, NO_BENEFICIARY_ERR, - ONLY_NAME_OWNER_REG_ERR, paymentConfigEmpty, - PaymentType, - precisionDefault, - priceConfigDefault, curvePriceConfigEmpty, - validateUpgrade, -} from "./helpers"; -import * as hre from "hardhat"; -import * as ethers from "ethers"; -import { BigNumber } from "ethers"; -import { expect } from "chai"; -import { registerDomainPath, validatePathRegistration } from "./helpers/flows/registration"; -import assert from "assert"; -import { registrationWithSetup } from "./helpers/register-setup"; -import { getDomainHashFromEvent } from "./helpers/events"; -import { time } from "@nomicfoundation/hardhat-network-helpers"; -import { CustomDecimalTokenMock, ZNSSubRegistrar__factory, ZNSSubRegistrarUpgradeMock__factory } from "../typechain"; -import { parseEther, parseUnits } from "ethers/lib/utils"; -import { deployCustomDecToken } from "./helpers/deploy/mocks"; -import { getProxyImplAddress } from "./helpers/utils"; - - -describe("ZNSSubRegistrar", () => { - let deployer : SignerWithAddress; - let rootOwner : SignerWithAddress; - let governor : SignerWithAddress; - let admin : SignerWithAddress; - let lvl2SubOwner : SignerWithAddress; - let lvl3SubOwner : SignerWithAddress; - let lvl4SubOwner : SignerWithAddress; - let lvl5SubOwner : SignerWithAddress; - let lvl6SubOwner : SignerWithAddress; - let branchLvl1Owner : SignerWithAddress; - let branchLvl2Owner : SignerWithAddress; - let random : SignerWithAddress; - let operator : SignerWithAddress; - let multiOwner : SignerWithAddress; - - let zns : IZNSContracts; - let zeroVault : SignerWithAddress; - - describe("Single Subdomain Registration", () => { - let rootHash : string; - let rootPriceConfig : IFixedPriceConfig; - const subTokenURI = "https://token-uri.com/8756a4b6f"; - - before(async () => { - [ - deployer, - zeroVault, - governor, - admin, - rootOwner, - lvl2SubOwner, - ] = await hre.ethers.getSigners(); - // zeroVault address is used to hold the fee charged to the user when registering - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address, governor.address], - adminAddresses: [admin.address], - priceConfig: priceConfigDefault, - zeroVaultAddress: zeroVault.address, - }); - // Give funds to users - await Promise.all( - [ - rootOwner, - lvl2SubOwner, - ].map(async ({ address }) => - zns.zeroToken.mint(address, ethers.utils.parseEther("100000000000"))) - ); - await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - rootPriceConfig = { - price: ethers.utils.parseEther("1375.612"), - feePercentage: BigNumber.from(0), - }; - - // register root domain - rootHash = await registrationWithSetup({ - zns, - user: rootOwner, - domainLabel: "root", - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: rootPriceConfig, - }, - }); - }); - - // eslint-disable-next-line max-len - it("should revert when trying to register a subdomain before parent has set it's config with FixedPricer", async () => { - // register a new root domain - const newRootHash = await registrationWithSetup({ - zns, - user: rootOwner, - domainLabel: "rootunsetfixed", - setConfigs: false, - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: { - price: BigNumber.from(0), - feePercentage: BigNumber.from(0), - }, - }, - }); - - await expect( - zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - newRootHash, - "subunset", - lvl2SubOwner.address, - subTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - "ZNSFixedPricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" - ); - }); - - // eslint-disable-next-line max-len - it("should revert when trying to register a subdomain before parent has set it's config with CurvePricer", async () => { - // register a new root domain - const newRootHash = await registrationWithSetup({ - zns, - user: rootOwner, - domainLabel: "rootunsetcurve", - setConfigs: false, - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: curvePriceConfigEmpty, - }, - }); - - await expect( - zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - newRootHash, - "subunset", - lvl2SubOwner.address, - subTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - "ZNSCurvePricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" - ); - }); - - it("should register subdomain with the correct tokenURI assigned to the domain token minted", async () => { - const subHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "sub", - tokenURI: subTokenURI, - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig: { - price: ethers.utils.parseEther("777.325"), - feePercentage: BigNumber.from(0), - }, - }, - }); - - const tokenId = BigNumber.from(subHash).toString(); - const tokenURI = await zns.domainToken.tokenURI(tokenId); - expect(tokenURI).to.eq(subTokenURI); - }); - - it("should revert when trying to register a subdomain under a non-existent parent", async () => { - // check that 0x0 hash can NOT be passed as parentHash - await expect( - zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - ethers.constants.HashZero, - "sub", - lvl2SubOwner.address, - subTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - DISTRIBUTION_LOCKED_NOT_EXIST_ERR - ); - - // check that a random non-existent hash can NOT be passed as parentHash - const randomHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("random")); - await expect( - zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - randomHash, - "sub", - lvl2SubOwner.address, - subTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - DISTRIBUTION_LOCKED_NOT_EXIST_ERR - ); - }); - - it("should register subdomain with a single char label", async () => { - const subHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "a", - tokenURI: subTokenURI, - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig: { - price: ethers.utils.parseEther("777.325"), - feePercentage: BigNumber.from(0), - }, - }, - }); - - const tokenId = BigNumber.from(subHash).toString(); - const tokenURI = await zns.domainToken.tokenURI(tokenId); - expect(tokenURI).to.eq(subTokenURI); - - // check registry - const dataFromReg = await zns.registry.getDomainRecord(subHash); - expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); - expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); - }); - - // ! this value can change based on the block gas limit ! - it("should register subdomain with a label length of 100000 chars", async () => { - const subHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "a".repeat(100000), - tokenURI: subTokenURI, - fullConfig: fullDistrConfigEmpty, - }); - - const tokenId = BigNumber.from(subHash).toString(); - const tokenURI = await zns.domainToken.tokenURI(tokenId); - expect(tokenURI).to.eq(subTokenURI); - - // check registry - const dataFromReg = await zns.registry.getDomainRecord(subHash); - expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); - expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); - }); - - it("should revert when user has insufficient funds", async () => { - const label = "subinsufficientfunds"; - const { expectedPrice } = getPriceObject(label, rootPriceConfig); - const userBalanceBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const userBalanceAfter = userBalanceBefore.sub(expectedPrice); - await zns.zeroToken.connect(lvl2SubOwner).transfer(deployer.address, userBalanceAfter); - - // add allowance - await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - await expect( - zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - rootHash, - label, - lvl2SubOwner.address, - subTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - "ERC20: transfer amount exceeds balance" - ); - - // transfer back for other tests - await zns.zeroToken.connect(deployer).transfer(lvl2SubOwner.address, userBalanceAfter); - }); - - it("should revert when user has insufficient allowance", async () => { - const label = "subinsufficientallowance"; - const { expectedPrice } = getPriceObject(label, rootPriceConfig); - - // add allowance - await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, expectedPrice.sub(1)); - - await expect( - zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - rootHash, - label, - lvl2SubOwner.address, - subTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - "ERC20: transfer amount exceeds allowance" - ); - }); - - it("should revert on payment when parent's beneficiary has not yet been set and when stakeFee is > 0", async () => { - // register a new parent with direct payment and no payment config - const parentHash1 = await registrationWithSetup({ - zns, - user: rootOwner, - domainLabel: "parentnoconfigdirect", - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: paymentConfigEmpty, - priceConfig: rootPriceConfig, - }, - }); - - // set the token address - await zns.treasury.connect(rootOwner).setPaymentToken(parentHash1, zns.zeroToken.address); - - // register a new parent with stake payment and no payment config - const parentHash2 = await registrationWithSetup({ - zns, - user: rootOwner, - domainLabel: "parentnoconfigstake", - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.STAKE, - }, - paymentConfig: paymentConfigEmpty, - priceConfig: priceConfigDefault, - }, - }); - - // set the token address - await zns.treasury.connect(rootOwner).setPaymentToken(parentHash2, zns.zeroToken.address); - - // register subdomains under new parents - await expect( - registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: parentHash1, - domainLabel: "sub1", - }) - ).to.be.revertedWith(NO_BENEFICIARY_ERR); - - await expect( - registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: parentHash2, - domainLabel: "sub2", - }) - ).to.be.revertedWith(NO_BENEFICIARY_ERR); - - // change stakeFee to 0 - await zns.curvePricer.connect(rootOwner).setFeePercentage( - parentHash2, - BigNumber.from(0) - ); - - let subHash; - // try register a subdomain again - await expect( - subHash = registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: parentHash2, - domainLabel: "sub2", - }) - ).to.be.fulfilled; - - await zns.registry.exists(subHash); - }); - }); - - describe("Operations within domain paths", () => { - let domainConfigs : Array; - let regResults : Array; - - const fixedPrice = ethers.utils.parseEther("1375.612"); - const fixedFeePercentage = BigNumber.from(200); - - before(async () => { - [ - deployer, - zeroVault, - governor, - admin, - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - branchLvl1Owner, - branchLvl2Owner, - multiOwner, - ] = await hre.ethers.getSigners(); - // zeroVault address is used to hold the fee charged to the user when registering - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address, governor.address], - adminAddresses: [admin.address], - priceConfig: priceConfigDefault, - zeroVaultAddress: zeroVault.address, - }); - - // Give funds to users - await Promise.all( - [ - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - branchLvl1Owner, - branchLvl2Owner, - multiOwner, - ].map(async ({ address }) => - zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) - ); - await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - domainConfigs = [ - { - user: rootOwner, - domainLabel: "root", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, - }, - }, - { - user: lvl2SubOwner, - domainLabel: "lvltwo", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig: priceConfigDefault, - }, - }, - { - user: lvl3SubOwner, - domainLabel: "lvlthree", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl3SubOwner.address, - }, - priceConfig: priceConfigDefault, - }, - }, - { - user: lvl4SubOwner, - domainLabel: "lvlfour", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl4SubOwner.address, - }, - priceConfig: priceConfigDefault, - - }, - }, - { - user: lvl5SubOwner, - domainLabel: "lvlfive", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl5SubOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - - }, - }, - { - user: lvl6SubOwner, - domainLabel: "lvlsix", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl6SubOwner.address, - }, - priceConfig: priceConfigDefault, - }, - }, - ]; - - regResults = await registerDomainPath({ - zns, - domainConfigs, - }); - - assert.equal(regResults.length, domainConfigs.length); - }); - - it("should register a path of 6 domains with different configs", async () => { - await validatePathRegistration({ - zns, - domainConfigs, - regResults, - }); - }); - - it("should be able to register multiple domains under multiple levels for the same owner", async () => { - const configs = [ - { - user: multiOwner, - domainLabel: "multiownerdomone", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: multiOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, - }, - }, - { - user: multiOwner, - domainLabel: "multiownerdomtwo", - parentHash: regResults[0].domainHash, - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.LOCKED, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: zeroVault.address, - }, - priceConfig: priceConfigDefault, - }, - }, - { - user: multiOwner, - domainLabel: "multiownerdomthree", - parentHash: regResults[1].domainHash, - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.MINTLIST, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: multiOwner.address, - }, - priceConfig: priceConfigDefault, - }, - }, - { - user: multiOwner, - domainLabel: "multiownerdomfour", - parentHash: regResults[2].domainHash, - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: zeroVault.address, - }, - priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, - }, - }, - { - user: multiOwner, - domainLabel: "multiownerdomfive", - parentHash: regResults[3].domainHash, - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: multiOwner.address, - }, - priceConfig: priceConfigDefault, - }, - }, - { - user: multiOwner, - domainLabel: "multiownerdomsix", - parentHash: regResults[4].domainHash, - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: zeroVault.address, - }, - priceConfig: priceConfigDefault, - }, - }, - { - user: multiOwner, - domainLabel: "multiownerdomseven", - parentHash: regResults[5].domainHash, - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: multiOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }, - ]; - - // prep - await zns.zeroToken.connect(multiOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - // register - const domainHashes = await configs.reduce( - async ( - acc : Promise>, - { - user, - parentHash, - domainLabel, - fullConfig, - }) : Promise> => { - const newAcc = await acc; - - const newHash = await registrationWithSetup({ - zns, - user, - parentHash, - domainLabel, - fullConfig, - }); - - return [...newAcc, newHash]; - }, Promise.resolve([]) - ); - - // check - await domainHashes.reduce( - async (acc, domainHash, idx) => { - await acc; - const { owner, resolver } = await zns.registry.getDomainRecord(domainHash); - expect(owner).to.eq(multiOwner.address); - expect(resolver).to.eq(zns.addressResolver.address); - - const tokenId = BigNumber.from(domainHash).toString(); - const tokenOwner = await zns.domainToken.ownerOf(tokenId); - expect(tokenOwner).to.eq(multiOwner.address); - - const { - pricerContract, - accessType, - paymentType, - } = await zns.subRegistrar.distrConfigs(domainHash); - expect(pricerContract).to.eq(configs[idx].fullConfig.distrConfig.pricerContract); - expect(accessType).to.eq(configs[idx].fullConfig.distrConfig.accessType); - expect(paymentType).to.eq(configs[idx].fullConfig.distrConfig.paymentType); - - const { - token, - beneficiary, - } = await zns.treasury.paymentConfigs(domainHash); - expect(token).to.eq(configs[idx].fullConfig.paymentConfig.token); - expect(beneficiary).to.eq(configs[idx].fullConfig.paymentConfig.beneficiary); - - const domainAddress = await zns.addressResolver.getAddress(domainHash); - expect(domainAddress).to.eq(multiOwner.address); - }, Promise.resolve() - ); - }); - - it("should revoke lvl 6 domain without refund, lock registration and remove mintlist", async () => { - const domainHash = regResults[5].domainHash; - - // add to mintlist - await zns.subRegistrar.connect(lvl6SubOwner).updateMintlistForDomain( - domainHash, - [lvl6SubOwner.address, lvl2SubOwner.address], - [true, true] - ); - - const userBalBefore = await zns.zeroToken.balanceOf(lvl6SubOwner.address); - - await zns.rootRegistrar.connect(lvl6SubOwner).revokeDomain( - domainHash, - ); - - const userBalAfter = await zns.zeroToken.balanceOf(lvl6SubOwner.address); - - expect(userBalAfter.sub(userBalBefore)).to.eq(0); - - // make sure that accessType has been set to LOCKED - // and nobody can register a subdomain under this domain - const { accessType: accessTypeFromSC } = await zns.subRegistrar.distrConfigs(domainHash); - expect(accessTypeFromSC).to.eq(AccessType.LOCKED); - - // make sure that mintlist has been removed - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, lvl6SubOwner.address)).to.eq(false); - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, lvl2SubOwner.address)).to.eq(false); - - await expect( - zns.subRegistrar.connect(lvl6SubOwner).registerSubdomain( - domainHash, - "newsubdomain", - lvl6SubOwner.address, - defaultTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - DISTRIBUTION_LOCKED_NOT_EXIST_ERR - ); - - const dataFromReg = await zns.registry.getDomainRecord(domainHash); - expect(dataFromReg.owner).to.eq(ethers.constants.AddressZero); - expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); - - const tokenId = BigNumber.from(domainHash).toString(); - await expect( - zns.domainToken.ownerOf(tokenId) - ).to.be.revertedWith( - INVALID_TOKENID_ERC_ERR - ); - - await expect( - zns.registry.connect(lvl6SubOwner).updateDomainRecord(domainHash, rootOwner.address, lvl6SubOwner.address) - ).to.be.revertedWith(ONLY_NAME_OWNER_REG_ERR); - }); - - it("should revoke lvl 5 domain with refund", async () => { - const domainHash = regResults[4].domainHash; - - const userBalanceBefore = await zns.zeroToken.balanceOf(lvl5SubOwner.address); - const parentBalBefore = await zns.zeroToken.balanceOf(lvl4SubOwner.address); - const paymentContractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); - - await zns.rootRegistrar.connect(lvl5SubOwner).revokeDomain(domainHash); - - const userBalAfter = await zns.zeroToken.balanceOf(lvl5SubOwner.address); - const parentBalAfter = await zns.zeroToken.balanceOf(lvl4SubOwner.address); - const paymentContractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); - - const { expectedPrice } = getPriceObject(domainConfigs[4].domainLabel); - - expect( - userBalAfter.sub(userBalanceBefore) - ).to.eq( - expectedPrice - ); - expect( - parentBalBefore.sub(parentBalAfter) - ).to.eq( - BigNumber.from(0) - ); - expect( - paymentContractBalBefore.sub(paymentContractBalAfter) - ).to.eq( - expectedPrice - ); - - // make sure that accessType has been set to LOCKED - // and nobody can register a subdomain under this domain - const { accessType: accessTypeFromSC } = await zns.subRegistrar.distrConfigs(domainHash); - expect(accessTypeFromSC).to.eq(AccessType.LOCKED); - - await expect( - zns.subRegistrar.connect(lvl6SubOwner).registerSubdomain( - domainHash, - "newsubdomain", - lvl6SubOwner.address, - defaultTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith( - DISTRIBUTION_LOCKED_NOT_EXIST_ERR - ); - - const dataFromReg = await zns.registry.getDomainRecord(domainHash); - expect(dataFromReg.owner).to.eq(ethers.constants.AddressZero); - expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); - - const tokenId = BigNumber.from(domainHash).toString(); - await expect( - zns.domainToken.ownerOf(tokenId) - ).to.be.revertedWith( - INVALID_TOKENID_ERC_ERR - ); - - await expect( - zns.registry.connect(lvl5SubOwner).updateDomainRecord(domainHash, rootOwner.address, lvl6SubOwner.address) - ).to.be.revertedWith(ONLY_NAME_OWNER_REG_ERR); - }); - - it("should register a new 2 lvl path at lvl 3 of the existing path", async () => { - const newConfigs = [ - { - user: branchLvl1Owner, - domainLabel: "lvlthreenew", - parentHash: regResults[2].domainHash, - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: branchLvl1Owner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }, - { - user: branchLvl2Owner, - domainLabel: "lvlfournew", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: branchLvl2Owner.address, - }, - priceConfig: priceConfigDefault, - }, - }, - ]; - - const newRegResults = await registerDomainPath({ - zns, - domainConfigs: newConfigs, - }); - - await validatePathRegistration({ - zns, - domainConfigs: newConfigs, - regResults: newRegResults, - }); - }); - - it("should revoke lvl 3 domain (child) with refund after lvl 2 (parent) has been revoked", async () => { - const lvl2Hash = regResults[1].domainHash; - const lvl3Hash = regResults[2].domainHash; - - const childExists = await zns.registry.exists(lvl3Hash); - assert.ok(childExists); - - // revoke parent - await zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain( - lvl2Hash, - ); - - // make sure all parent's distribution configs still exist - const parentDistrConfig = await zns.subRegistrar.distrConfigs(lvl2Hash); - const parentPaymentConfig = await zns.treasury.paymentConfigs(lvl2Hash); - expect(parentDistrConfig.pricerContract).to.eq(domainConfigs[1].fullConfig.distrConfig.pricerContract); - expect( - parentDistrConfig.paymentType - ).to.eq( - domainConfigs[1].fullConfig.distrConfig.paymentType - ); - expect( - parentPaymentConfig.token - ).to.eq( - domainConfigs[1].fullConfig.paymentConfig.token - ); - expect( - parentPaymentConfig.beneficiary - ).to.eq( - domainConfigs[1].fullConfig.paymentConfig.beneficiary - ); - - expect(parentDistrConfig.pricerContract).to.eq(zns.curvePricer.address); - - // check a couple of fields from price config - const priceConfig = await zns.curvePricer.priceConfigs(lvl2Hash); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if ("maxPrice" in domainConfigs[1].fullConfig.priceConfig!) { - expect(priceConfig.maxPrice).to.eq(domainConfigs[1].fullConfig.priceConfig.maxPrice); - } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if ("minPrice" in domainConfigs[1].fullConfig.priceConfig!) { - expect(priceConfig.minPrice).to.eq(domainConfigs[1].fullConfig.priceConfig.minPrice); - } - - // make sure the child's stake is still there - const { amount: childStakedAmt } = await zns.treasury.stakedForDomain(lvl3Hash); - const { expectedPrice } = getPriceObject(domainConfigs[2].domainLabel); - - expect(childStakedAmt).to.eq(expectedPrice); - - const userBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - - // revoke child - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - lvl3Hash, - ); - - const userBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - - expect(userBalAfter.sub(userBalBefore)).to.eq(expectedPrice); - - const childExistsAfter = await zns.registry.exists(lvl3Hash); - assert.ok(!childExistsAfter); - - const { amount: stakedAfterRevoke } = await zns.treasury.stakedForDomain(lvl3Hash); - expect(stakedAfterRevoke).to.eq(0); - - const dataFromReg = await zns.registry.getDomainRecord(lvl3Hash); - expect(dataFromReg.owner).to.eq(ethers.constants.AddressZero); - expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); - - const tokenId = BigNumber.from(lvl3Hash).toString(); - await expect( - zns.domainToken.ownerOf(tokenId) - ).to.be.revertedWith( - INVALID_TOKENID_ERC_ERR - ); - - await expect( - zns.registry.connect(lvl3SubOwner).updateDomainRecord(lvl3Hash, rootOwner.address, lvl4SubOwner.address) - ).to.be.revertedWith(ONLY_NAME_OWNER_REG_ERR); - }); - - it("should let anyone register a previously revoked domain", async () => { - const lvl2Hash = regResults[1].domainHash; - const parentHash = regResults[0].domainHash; - - const exists = await zns.registry.exists(lvl2Hash); - if (!exists) { - const newHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash, - domainLabel: domainConfigs[1].domainLabel, - fullConfig: domainConfigs[1].fullConfig, - }); - - expect(newHash).to.eq(lvl2Hash); - } - - // revoke subdomain - await zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain( - lvl2Hash, - ); - - // someone else is taking it - const newConfig = [ - { - user: branchLvl1Owner, - domainLabel: "lvltwonew", - parentHash, - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: branchLvl1Owner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }, - ]; - - const newResult = await registerDomainPath({ - zns, - domainConfigs: newConfig, - }); - - await validatePathRegistration({ - zns, - domainConfigs: newConfig, - regResults: newResult, - }); - }); - - it("should NOT register a child (subdomain) under a parent (root domain) that has been revoked", async () => { - const lvl1Hash = regResults[0].domainHash; - - // revoke parent - await zns.rootRegistrar.connect(rootOwner).revokeDomain( - lvl1Hash - ); - - const exists = await zns.registry.exists(lvl1Hash); - assert.ok(!exists); - - await expect( - zns.subRegistrar.connect(branchLvl1Owner).registerSubdomain( - lvl1Hash, - "newsubdomain", - branchLvl1Owner.address, - defaultTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); - - // register root back for other tests - await registrationWithSetup({ - zns, - user: rootOwner, - parentHash: ethers.constants.HashZero, - domainLabel: domainConfigs[0].domainLabel, - fullConfig: domainConfigs[0].fullConfig, - }); - }); - - it("should NOT register a child (subdomain) under a parent (subdomain) that has been revoked", async () => { - const lvl4Hash = regResults[3].domainHash; - - // revoke parent - await zns.rootRegistrar.connect(lvl4SubOwner).revokeDomain( - lvl4Hash, - ); - - const exists = await zns.registry.exists(lvl4Hash); - assert.ok(!exists); - - await expect( - zns.subRegistrar.connect(branchLvl2Owner).registerSubdomain( - lvl4Hash, - "newsubdomain", - branchLvl2Owner.address, - defaultTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); - }); - - // eslint-disable-next-line max-len - it("should allow setting a new config and start distributing subdomain when registering a previously revoked parent", async () => { - if (!await zns.registry.exists(regResults[1].domainHash)) { - await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: regResults[0].domainHash, - domainLabel: domainConfigs[1].domainLabel, - fullConfig: domainConfigs[1].fullConfig, - }); - } - - // revoke parent - await zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain(regResults[1].domainHash); - - expect(await zns.registry.exists(regResults[1].domainHash)).to.eq(false); - - // register again with new owner and config - const newHash = await registrationWithSetup({ - zns, - user: branchLvl1Owner, - parentHash: regResults[0].domainHash, - domainLabel: domainConfigs[1].domainLabel, - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.MINTLIST, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: branchLvl1Owner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, - }, - }); - - expect(newHash).to.eq(regResults[1].domainHash); - - // add new child owner to mintlist - await zns.subRegistrar.connect(branchLvl1Owner).updateMintlistForDomain( - newHash, - [ branchLvl2Owner.address ], - [ true ], - ); - - const parentOwnerFromReg = await zns.registry.getDomainOwner(newHash); - expect(parentOwnerFromReg).to.eq(branchLvl1Owner.address); - - const childBalBefore = await zns.zeroToken.balanceOf(branchLvl2Owner.address); - - // try register a new child under the new parent - const newChildHash = await registrationWithSetup({ - zns, - user: branchLvl2Owner, - parentHash: newHash, - domainLabel: "newchildddd", - fullConfig: fullDistrConfigEmpty, - }); - - const childBalAfter = await zns.zeroToken.balanceOf(branchLvl2Owner.address); - - // check that the new child has been registered - const childOwnerFromReg = await zns.registry.getDomainOwner(newChildHash); - expect(childOwnerFromReg).to.eq(branchLvl2Owner.address); - - const protocolFee = getStakingOrProtocolFee(fixedPrice); - - // make sure child payed based on the new parent config - expect(childBalBefore.sub(childBalAfter)).to.eq(fixedPrice.add(protocolFee)); - }); - }); - - describe("Token movements with different distr setups", () => { - let rootHash : string; - let fixedPrice : BigNumber; - let feePercentage : BigNumber; - let token2 : CustomDecimalTokenMock; - let token5 : CustomDecimalTokenMock; - let token8 : CustomDecimalTokenMock; - let token13 : CustomDecimalTokenMock; - let token18 : CustomDecimalTokenMock; - - const decimalValues = { - two: BigNumber.from(2), - five: BigNumber.from(5), - eight: BigNumber.from(8), - thirteen: BigNumber.from(13), - eighteen: BigNumber.from(18), - }; - - before(async () => { - [ - deployer, - zeroVault, - governor, - admin, - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - branchLvl1Owner, - branchLvl2Owner, - ] = await hre.ethers.getSigners(); - // zeroVault address is used to hold the fee charged to the user when registering - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address, governor.address], - adminAddresses: [admin.address], - priceConfig: priceConfigDefault, - zeroVaultAddress: zeroVault.address, - }); - - ([ - token2, - token5, - token8, - token13, - token18, - ] = await Object.values(decimalValues).reduce( - async (acc : Promise>, decimals) => { - const newAcc = await acc; - - const token = await deployCustomDecToken(deployer, decimals); - - return [...newAcc, token]; - }, Promise.resolve([]) - )); - - // Give funds to users - await Promise.all( - [ - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - branchLvl1Owner, - branchLvl2Owner, - ].map(async ({ address }) => - zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) - ); - await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - // register root domain - rootHash = await registrationWithSetup({ - zns, - user: rootOwner, - domainLabel: "root", - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: { - price: ethers.utils.parseEther("1375.612"), - feePercentage: BigNumber.from(0), - }, - }, - }); - }); - - it("FixedPricer - StakePayment - stake fee - 5 decimals", async () => { - const decimals = await token5.decimals(); - expect(decimals).to.eq(decimalValues.five); - - fixedPrice = parseUnits("1375.17", decimalValues.five); - feePercentage = BigNumber.from(200); - - const priceConfig = { - price: fixedPrice, - feePercentage, - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "fixedstake", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: token5.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const label = "fixedstakechild"; - - const { - expectedPrice, - stakeFee: stakeFee, - } = getPriceObject(label, priceConfig); - const protocolFee = getStakingOrProtocolFee( - expectedPrice.add(stakeFee) - ); - - // send future child some tokens - await token5.connect(deployer).transfer(lvl3SubOwner.address, expectedPrice.add(stakeFee).add(protocolFee)); - - const contractBalBefore = await token5.balanceOf(zns.treasury.address); - const parentBalBefore = await token5.balanceOf(lvl2SubOwner.address); - const childBalBefore = await token5.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await token5.balanceOf(zeroVault.address); - - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - fullConfig: fullDistrConfigEmpty, - }); - - const parentBalAfter = await token5.balanceOf(lvl2SubOwner.address); - const childBalAfter = await token5.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await token5.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await token5.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(stakeFee); - expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(stakeFee).add(protocolFee)); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should offer refund ! - const contractBalAfterRevoke = await token5.balanceOf(zns.treasury.address); - const childBalAfterRevoke = await token5.balanceOf(lvl3SubOwner.address); - const parentBalAfterRevoke = await token5.balanceOf(lvl2SubOwner.address); - const zeroVaultBalanceAfterRevoke = await token5.balanceOf(zeroVault.address); - - expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("FixedPricer - StakePayment - no fee - 18 decimals", async () => { - const priceConfig = { - price: parseUnits("397.77", decimalValues.eighteen), - feePercentage: BigNumber.from(0), - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "fixedstakenofee", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - token: token18.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const label = "fixedstakenofeechild"; - - const { expectedPrice } = getPriceObject(label, priceConfig); - const protocolFee = getStakingOrProtocolFee(expectedPrice); - - // send future child some tokens - await token18.connect(deployer).transfer( - lvl3SubOwner.address, - expectedPrice.add(protocolFee) - ); - - const contractBalBefore = await token18.balanceOf(zns.treasury.address); - const parentBalBefore = await token18.balanceOf(lvl2SubOwner.address); - const childBalBefore = await token18.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await token18.balanceOf(zeroVault.address); - - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const parentBalAfter = await token18.balanceOf(lvl2SubOwner.address); - const childBalAfter = await token18.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await token18.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await token18.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); - expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should offer refund ! - const contractBalAfterRevoke = await token18.balanceOf(zns.treasury.address); - const childBalAfterRevoke = await token18.balanceOf(lvl3SubOwner.address); - const parentBalAfterRevoke = await token18.balanceOf(lvl2SubOwner.address); - const zeroVaultBalanceAfterRevoke = await token18.balanceOf(zeroVault.address); - - expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("FixedPricer - DirectPayment - no fee - 8 decimals", async () => { - const priceConfig = { - price: parseUnits("11.371", decimalValues.eight), - feePercentage: BigNumber.from(0), - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "fixeddirectnofee", - fullConfig: { - distrConfig: { - paymentType: PaymentType.DIRECT, - pricerContract: zns.fixedPricer.address, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: token8.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const label = "fixeddirectnofeechild"; - const { expectedPrice } = getPriceObject(label, priceConfig); - const protocolFee = getStakingOrProtocolFee(expectedPrice); - - // send future child some tokens - await token8.connect(deployer).transfer( - lvl3SubOwner.address, - expectedPrice.add(protocolFee) - ); - - const parentBalBefore = await token8.balanceOf(lvl2SubOwner.address); - const childBalBefore = await token8.balanceOf(lvl3SubOwner.address); - const contractBalBefore = await token8.balanceOf(zns.treasury.address); - const zeroVaultBalanceBefore = await token8.balanceOf(zeroVault.address); - - - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - fullConfig: fullDistrConfigEmpty, - }); - - const parentBalAfter = await token8.balanceOf(lvl2SubOwner.address); - const childBalAfter = await token8.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await token8.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await token8.balanceOf(zeroVault.address); - - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(expectedPrice); - expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should NOT offer refund ! - const parentBalAfterRevoke = await token8.balanceOf(lvl2SubOwner.address); - const childBalAfterRevoke = await token8.balanceOf(lvl3SubOwner.address); - const contractBalAfterRevoke = await token8.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfterRevoke = await token8.balanceOf(zeroVault.address); - - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); - expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("CurvePricer - StakePayment - stake fee - 13 decimals", async () => { - const priceConfig = { - maxPrice: parseUnits("30000.93", decimalValues.thirteen), - minPrice: parseUnits("2000.11", decimalValues.thirteen), - maxLength: BigNumber.from(50), - baseLength: BigNumber.from(4), - precisionMultiplier: BigNumber.from(10).pow( - decimalValues.thirteen - .sub(precisionDefault) - ), - feePercentage: BigNumber.from(185), - isSet: true, - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "asympstake", - fullConfig: { - distrConfig: { - paymentType: PaymentType.STAKE, - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: token13.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const label = "curvestakechild"; - - const { - expectedPrice, - stakeFee: stakeFee, - } = getPriceObject(label, priceConfig); - const protocolFee = getStakingOrProtocolFee( - expectedPrice.add(stakeFee) - ); - - // send future child some tokens - await token13.connect(deployer).transfer( - lvl3SubOwner.address, - expectedPrice.add(stakeFee).add(protocolFee) - ); - - const contractBalBefore = await token13.balanceOf(zns.treasury.address); - const parentBalBefore = await token13.balanceOf(lvl2SubOwner.address); - const childBalBefore = await token13.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await token13.balanceOf(zeroVault.address); - - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - fullConfig: fullDistrConfigEmpty, - }); - - const contractBalAfter = await token13.balanceOf(zns.treasury.address); - const parentBalAfter = await token13.balanceOf(lvl2SubOwner.address); - const childBalAfter = await token13.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceAfter = await token13.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(stakeFee); - expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee).add(stakeFee)); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should offer refund ! - const contractBalAfterRevoke = await token13.balanceOf(zns.treasury.address); - const childBalAfterRevoke = await token13.balanceOf(lvl3SubOwner.address); - const parentBalAfterRevoke = await token13.balanceOf(lvl2SubOwner.address); - const zeroVaultBalanceAfterRevoke = await token13.balanceOf(zeroVault.address); - - expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("CurvePricer - StakePayment - no fee - 2 decimals", async () => { - const priceConfig = { - maxPrice: parseUnits("234.46", decimalValues.two), - minPrice: parseUnits("3.37", decimalValues.two), - maxLength: BigNumber.from(20), - baseLength: BigNumber.from(2), - precisionMultiplier: BigNumber.from(1), - feePercentage: BigNumber.from(0), - isSet: true, - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "curvestakenofee", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - token: token2.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const label = "curvestakenofeechild"; - - const { expectedPrice } = getPriceObject(label, priceConfig); - const protocolFee = getStakingOrProtocolFee(expectedPrice); - - // send future child some tokens - await token2.connect(deployer).transfer( - lvl3SubOwner.address, - expectedPrice.add(protocolFee) - ); - - const contractBalBefore = await token2.balanceOf(zns.treasury.address); - const parentBalBefore = await token2.balanceOf(lvl2SubOwner.address); - const childBalBefore = await token2.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await token2.balanceOf(zeroVault.address); - - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const contractBalAfter = await token2.balanceOf(zns.treasury.address); - const parentBalAfter = await token2.balanceOf(lvl2SubOwner.address); - const childBalAfter = await token2.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceAfter = await token2.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); - expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should offer refund ! - const contractBalAfterRevoke = await token2.balanceOf(zns.treasury.address); - const childBalAfterRevoke = await token2.balanceOf(lvl3SubOwner.address); - const parentBalAfterRevoke = await token2.balanceOf(lvl2SubOwner.address); - const zeroVaultBalanceAfterRevoke = await token2.balanceOf(zeroVault.address); - - expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("CurvePricer - DirectPayment - no fee - 18 decimals", async () => { - const priceConfig = { - ...priceConfigDefault, - feePercentage: BigNumber.from(0), - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "curvedirectnofee", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - // zero has 18 decimals - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const label = "asdirectnofeechild"; - - const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); - const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); - - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); - - const { expectedPrice } = getPriceObject(label, priceConfig); - const protocolFee = getStakingOrProtocolFee(expectedPrice); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(expectedPrice); - expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should NOT offer refund ! - const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); - expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("FixedPricer + DirectPayment with price = 0 - should NOT perform any transfers", async () => { - const priceConfig = { - price: BigNumber.from(0), - feePercentage: BigNumber.from(0), - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "zeroprice", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); - const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); - - const label = "zeropricechild"; - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); - expect(childBalBefore.sub(childBalAfter)).to.eq(0); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); - - // validate transfer events are not happenning - const latestBlock = await time.latestBlock(); - const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); - const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); - const transfersToParent = await zns.zeroToken.queryFilter( - transferFilterToParent, - latestBlock - 3, - latestBlock - ); - const transfersToTreasury = await zns.zeroToken.queryFilter( - transferFilterToTreasury, - latestBlock - 3, - latestBlock - ); - expect(transfersToParent.length).to.eq(0); - expect(transfersToTreasury.length).to.eq(0); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should NOT offer refund ! - const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); - expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("CurvePricer + DirectPayment with price = 0 - should NOT perform any transfers", async () => { - const priceConfig = { - ...priceConfigDefault, - maxPrice: BigNumber.from(0), - minPrice: BigNumber.from(0), - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "zeropricead", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); - const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); - - const label = "zeropricechildad"; - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); - expect(childBalBefore.sub(childBalAfter)).to.eq(0); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); - - // validate transfer events are not happenning - const latestBlock = await time.latestBlock(); - const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); - const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); - const transfersToParent = await zns.zeroToken.queryFilter( - transferFilterToParent, - latestBlock - 3, - latestBlock - ); - const transfersToTreasury = await zns.zeroToken.queryFilter( - transferFilterToTreasury, - latestBlock - 3, - latestBlock - ); - expect(transfersToParent.length).to.eq(0); - expect(transfersToTreasury.length).to.eq(0); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should NOT offer refund ! - const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); - expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("CurvePricer + StakePayment with price = 0 - should NOT perform any transfers", async () => { - const priceConfig = { - ...priceConfigDefault, - maxPrice: BigNumber.from(0), - minPrice: BigNumber.from(0), - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "zeropriceas", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); - const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); - - const label = "zeropricechildas"; - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); - expect(childBalBefore.sub(childBalAfter)).to.eq(0); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); - - // validate transfer events are not happenning - const latestBlock = await time.latestBlock(); - const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); - const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); - const transfersToParent = await zns.zeroToken.queryFilter( - transferFilterToParent, - latestBlock - 3, - latestBlock - ); - const transfersToTreasury = await zns.zeroToken.queryFilter( - transferFilterToTreasury, - latestBlock - 3, - latestBlock - ); - expect(transfersToParent.length).to.eq(0); - expect(transfersToTreasury.length).to.eq(0); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should NOT offer refund ! - const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); - expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("FixedPricer + StakePayment with price = 0 - should NOT perform any transfers", async () => { - const priceConfig = { - price: BigNumber.from(0), - // we are trying to set a feePercentage, but that should still result to 0 fee - // since fee is based on price - feePercentage: BigNumber.from(5), - }; - - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "zeropricefs", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig, - }, - }); - - const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); - const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); - - const label = "zeropricechildfs"; - const childHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); - expect(childBalBefore.sub(childBalAfter)).to.eq(0); - expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); - expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); - - // validate transfer events are not happenning - const latestBlock = await time.latestBlock(); - const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); - const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); - const transfersToParent = await zns.zeroToken.queryFilter( - transferFilterToParent, - latestBlock - 3, - latestBlock - ); - const transfersToTreasury = await zns.zeroToken.queryFilter( - transferFilterToTreasury, - latestBlock - 3, - latestBlock - ); - expect(transfersToParent.length).to.eq(0); - expect(transfersToTreasury.length).to.eq(0); - - // revoke - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - childHash, - ); - - // should NOT offer refund ! - const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); - const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); - - expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); - expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); - expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); - expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); - }); - - it("Setting price config in incorrect decimals triggers incorrect pricing", async () => { - // we will use token with 5 decimals, but set prices in 18 decimals - const priceConfigIncorrect = { - maxPrice: parseUnits("234.46", decimalValues.eighteen), - minPrice: parseUnits("3.37", decimalValues.eighteen), - maxLength: BigNumber.from(20), - baseLength: BigNumber.from(2), - precisionMultiplier: BigNumber.from(1), - feePercentage: BigNumber.from(111), - isSet: true, - }; - - // see `token` in paymentConfig - const subdomainParentHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: rootHash, - domainLabel: "incorrectparent", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - accessType: AccessType.OPEN, - paymentType: PaymentType.STAKE, - }, - paymentConfig: { - // ! this token has 5 decimals ! - token: token5.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig: priceConfigIncorrect, - }, - }); - - const label = "incorrectchild"; - - const priceConfigCorrect = { - ...priceConfigIncorrect, - maxPrice: parseUnits("234.46", decimalValues.five), - minPrice: parseUnits("3.37", decimalValues.five), - }; - - // calc prices off-chain - const { - expectedPrice: priceIncorrect, - stakeFee: stakeFeeIncorrect, - } = getPriceObject(label, priceConfigIncorrect); - const protocolFeeIncorrect = getStakingOrProtocolFee(priceIncorrect.add(stakeFeeIncorrect)); - - const { - expectedPrice: priceCorrect, - stakeFee: stakeFeeCorrect, - } = getPriceObject(label, priceConfigCorrect); - const protocolFeeCorrect = getStakingOrProtocolFee(priceCorrect.add(stakeFeeCorrect)); - - // get prices from SC - const { - price: priceFromSC, - stakeFee: feeFromSC, - } = await zns.curvePricer.getPriceAndFee( - subdomainParentHash, - label - ); - const protocolFeeFromSC = await zns.curvePricer.getFeeForPrice( - ethers.constants.HashZero, - priceFromSC.add(feeFromSC) - ); - - expect(priceFromSC).to.not.eq(priceCorrect); - expect(priceFromSC).to.eq(priceIncorrect); - expect(feeFromSC).to.not.eq(stakeFeeCorrect); - expect(feeFromSC).to.eq(stakeFeeIncorrect); - expect(protocolFeeFromSC).to.not.eq(protocolFeeCorrect); - expect(protocolFeeFromSC).to.eq(protocolFeeIncorrect); - - const priceDiff = priceIncorrect.sub(priceCorrect); - // the difference should be very large - expect(priceDiff).to.be.gt( - BigNumber.from(10).pow(decimalValues.eighteen) - ); - - // let's see how much a user actually paid - - // we sending him 10^20 tokens - await token5.connect(deployer).transfer( - lvl3SubOwner.address, - parseUnits("10000000000000000000", decimalValues.five) - ); - - // client tx approving the correct price will fail - await token5.connect(lvl3SubOwner).approve( - zns.treasury.address, - priceCorrect.add(stakeFeeCorrect).add(protocolFeeCorrect) - ); - - await expect( - zns.subRegistrar.registerSubdomain( - subdomainParentHash, - label, - lvl3SubOwner.address, - defaultTokenURI, - distrConfigEmpty, - ) - ).to.be.revertedWith("ERC20: insufficient allowance"); - - // let's try to buy with the incorrect price - const userBalanceBefore = await token5.balanceOf(lvl3SubOwner.address); - - await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: subdomainParentHash, - domainLabel: label, - }); - - const userBalanceAfter = await token5.balanceOf(lvl3SubOwner.address); - - // user should have paid the incorrect price - expect(userBalanceBefore.sub(userBalanceAfter)).to.eq( - priceIncorrect.add(stakeFeeIncorrect).add(protocolFeeIncorrect) - ); - }); - }); - - describe("Registration access", () => { - let fixedPrice : BigNumber; - let domainConfigs : Array; - let regResults : Array; - let fixedFeePercentage : BigNumber; - - before(async () => { - [ - deployer, - zeroVault, - governor, - admin, - operator, - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - ] = await hre.ethers.getSigners(); - // zeroVault address is used to hold the fee charged to the user when registering - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address, governor.address], - adminAddresses: [admin.address], - priceConfig: priceConfigDefault, - zeroVaultAddress: zeroVault.address, - }); - - fixedPrice = ethers.utils.parseEther("397"); - fixedFeePercentage = BigNumber.from(200); - - await Promise.all( - [ - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - ].map(async ({ address }) => - zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) - ); - await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - // register root domain and 1 subdomain - domainConfigs = [ - { - user: rootOwner, - domainLabel: "root", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }, - { - user: lvl2SubOwner, - domainLabel: "levelone", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }, - ]; - - regResults = await registerDomainPath({ - zns, - domainConfigs, - }); - }); - - it("should allow parent owner to register a subdomain under himself even if accessType is LOCKED", async () => { - await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( - regResults[1].domainHash, - AccessType.LOCKED, - ); - - const balBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - - const hash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - parentHash: regResults[1].domainHash, - domainLabel: "ownercheck", - }); - - const latestBlock = await time.latestBlock(); - // look for an event where user pays himself - const filter = zns.zeroToken.filters.Transfer(lvl2SubOwner.address, lvl2SubOwner.address); - const events = await zns.zeroToken.queryFilter( - filter, - latestBlock - 50, - latestBlock - ); - // this means NO transfers have been executed, which is what we need - expect(events.length).to.eq(0); - - const balAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); - // the diff is 0 because user should not pay himself - expect(balAfter.sub(balBefore)).to.eq(0); - - // check registry - const dataFromReg = await zns.registry.getDomainRecord(hash); - expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); - expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); - - // check domain token - const tokenId = BigNumber.from(hash).toString(); - const tokenOwner = await zns.domainToken.ownerOf(tokenId); - expect(tokenOwner).to.eq(lvl2SubOwner.address); - - // revert back to OPEN - await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( - regResults[1].domainHash, - AccessType.OPEN, - ); - }); - - it("should NOT allow others to register a domain when parent's accessType is LOCKED", async () => { - // register parent with locked access - const res = await registerDomainPath({ - zns, - domainConfigs: [ - { - user: lvl3SubOwner, - domainLabel: "leveltwo", - parentHash: regResults[1].domainHash, - // when we do not specify accessType or config, it defaults to LOCKED - // we can also set it as 0 specifically if setting a config - fullConfig: fullDistrConfigEmpty, - }, - ], - }); - - // try to register child - await expect( - zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - res[0].domainHash, - "tobedenied", - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ) - ).to.be.revertedWith( - DISTRIBUTION_LOCKED_NOT_EXIST_ERR - ); - }); - - it("should allow anyone to register a domain when parent's accessType is OPEN", async () => { - const { domainHash: parentHash } = regResults[1]; - const domainLabel = "alloweded"; - - const { - expectedPrice, - } = getPriceObject( - domainLabel, - domainConfigs[1].fullConfig.priceConfig - ); - - const protocolFee = getStakingOrProtocolFee(expectedPrice); - // approve direct payment - await zns.zeroToken.connect(lvl5SubOwner).approve( - zns.treasury.address, - expectedPrice.add(protocolFee) - ); - - await zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - parentHash, - domainLabel, - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ); - - const hash = await getDomainHashFromEvent({ - zns, - user: lvl5SubOwner, - }); - - // check registry - const dataFromReg = await zns.registry.getDomainRecord(hash); - expect(dataFromReg.owner).to.eq(lvl5SubOwner.address); - expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); - - // check domain token - const tokenId = BigNumber.from(hash).toString(); - const tokenOwner = await zns.domainToken.ownerOf(tokenId); - expect(tokenOwner).to.eq(lvl5SubOwner.address); - }); - - // eslint-disable-next-line max-len - it("should ONLY allow mintlisted addresses and NOT allow other ones to register a domain when parent's accessType is MINTLIST", async () => { - // approve direct payment - await zns.zeroToken.connect(lvl3SubOwner).approve(zns.treasury.address, fixedPrice); - // register parent with mintlisted access - const parentHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: regResults[1].domainHash, - domainLabel: "mintlistparent", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.MINTLIST, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl3SubOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }); - - // mintlist potential child user - await zns.subRegistrar.connect(lvl3SubOwner).updateMintlistForDomain( - parentHash, - [lvl4SubOwner.address], - [true], - ); - - // register child - const hash = await registrationWithSetup({ - zns, - user: lvl4SubOwner, - parentHash, - domainLabel: "mintlisted", - }); - - // check registry - const dataFromReg = await zns.registry.getDomainRecord(hash); - expect(dataFromReg.owner).to.eq(lvl4SubOwner.address); - expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); - - // check domain token - const tokenId = BigNumber.from(hash).toString(); - const tokenOwner = await zns.domainToken.ownerOf(tokenId); - expect(tokenOwner).to.eq(lvl4SubOwner.address); - - // try to register child with non-mintlisted user - await expect( - zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - parentHash, - "notmintlisted", - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Sender is not approved for purchase" - ); - - // remove user from mintlist - await zns.subRegistrar.connect(lvl3SubOwner).updateMintlistForDomain( - parentHash, - [lvl4SubOwner.address], - [false], - ); - - // try to register again - await expect( - zns.subRegistrar.connect(lvl4SubOwner).registerSubdomain( - parentHash, - "notmintlistednow", - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Sender is not approved for purchase" - ); - }); - - // eslint-disable-next-line max-len - it("#updateMintlistForDomain() should NOT allow setting if called by non-authorized account or registrar", async () => { - const { domainHash } = regResults[1]; - - // assign operator in registry - // to see that he CAN do it - await zns.registry.connect(lvl2SubOwner).setOwnersOperator( - operator.address, - true, - ); - - // try with operator - await zns.subRegistrar.connect(operator).updateMintlistForDomain( - domainHash, - [lvl5SubOwner.address], - [true], - ); - - const mintlisted = await zns.subRegistrar.isMintlistedForDomain( - domainHash, - lvl5SubOwner.address - ); - assert.ok(mintlisted, "User did NOT get mintlisted, but should've"); - - // try with non-authorized - await expect( - zns.subRegistrar.connect(lvl5SubOwner).updateMintlistForDomain( - domainHash, - [lvl5SubOwner.address], - [true], - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Not authorized" - ); - }); - - it("#updateMintlistForDomain() should fire a #MintlistUpdated event with correct params", async () => { - const { domainHash } = regResults[1]; - - const candidatesArr = [ - lvl5SubOwner.address, - lvl6SubOwner.address, - lvl3SubOwner.address, - lvl4SubOwner.address, - ]; - - const allowedArr = [ - true, - true, - false, - true, - ]; - - await zns.subRegistrar.connect(lvl2SubOwner).updateMintlistForDomain( - domainHash, - candidatesArr, - allowedArr - ); - - const latestBlock = await time.latestBlock(); - const filter = zns.subRegistrar.filters.MintlistUpdated(domainHash); - const events = await zns.subRegistrar.queryFilter( - filter, - latestBlock - 3, - latestBlock - ); - const event = events[events.length - 1]; - - expect(event.args?.domainHash).to.eq(domainHash); - expect(event.args?.candidates).to.deep.eq(candidatesArr); - expect(event.args?.allowed).to.deep.eq(allowedArr); - }); - - it("should switch accessType for existing parent domain", async () => { - await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( - regResults[1].domainHash, - AccessType.LOCKED - ); - - await expect( - zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - regResults[1].domainHash, - "notallowed", - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ) - ).to.be.revertedWith( - DISTRIBUTION_LOCKED_NOT_EXIST_ERR - ); - - // switch to mintlist - await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( - regResults[1].domainHash, - AccessType.MINTLIST - ); - - // add to mintlist - await zns.subRegistrar.connect(lvl2SubOwner).updateMintlistForDomain( - regResults[1].domainHash, - [lvl5SubOwner.address], - [true], - ); - - const label = "alloweddddd"; - - // approve - const { - expectedPrice, - stakeFee, - } = getPriceObject( - label, - domainConfigs[1].fullConfig.priceConfig - ); - const paymentToParent = domainConfigs[1].fullConfig.distrConfig.paymentType === PaymentType.STAKE - ? expectedPrice.add(stakeFee) - : expectedPrice; - - const protocolFee = getStakingOrProtocolFee(paymentToParent); - await zns.zeroToken.connect(lvl5SubOwner).approve( - zns.treasury.address, - paymentToParent.add(protocolFee) - ); - - // register - await zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - regResults[1].domainHash, - "alloweddddd", - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ); - - const hash = await getDomainHashFromEvent({ - zns, - user: lvl5SubOwner, - }); - - // check registry - const dataFromReg = await zns.registry.getDomainRecord(hash); - expect(dataFromReg.owner).to.eq(lvl5SubOwner.address); - - // switch back to open - await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( - regResults[1].domainHash, - AccessType.OPEN - ); - }); - - // eslint-disable-next-line max-len - it("should NOT allow to register subdomains under the parent that hasn't set up his distribution config", async () => { - const parentHash = await registrationWithSetup({ - zns, - user: lvl3SubOwner, - parentHash: regResults[1].domainHash, - domainLabel: "parentnoconfig", - fullConfig: fullDistrConfigEmpty, // accessType is 0 when supplying empty config - }); - - await expect( - zns.subRegistrar.connect(lvl4SubOwner).registerSubdomain( - parentHash, - "notallowed", - ethers.constants.AddressZero, - defaultTokenURI, - distrConfigEmpty - ) - ).to.be.revertedWith( - DISTRIBUTION_LOCKED_NOT_EXIST_ERR - ); - }); - }); - - describe("Existing subdomain ops", () => { - let fixedPrice : BigNumber; - let domainConfigs : Array; - let regResults : Array; - let fixedFeePercentage : BigNumber; - - before(async () => { - [ - deployer, - zeroVault, - governor, - admin, - operator, - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - ] = await hre.ethers.getSigners(); - // zeroVault address is used to hold the fee charged to the user when registering - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address, governor.address], - adminAddresses: [admin.address], - priceConfig: priceConfigDefault, - zeroVaultAddress: zeroVault.address, - }); - - fixedPrice = ethers.utils.parseEther("397"); - fixedFeePercentage = BigNumber.from(200); - - await Promise.all( - [ - rootOwner, - lvl2SubOwner, - lvl3SubOwner, - lvl4SubOwner, - lvl5SubOwner, - lvl6SubOwner, - ].map(async ({ address }) => - zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) - ); - await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - // register root domain and 1 subdomain - domainConfigs = [ - { - user: rootOwner, - domainLabel: "root", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }, - { - user: lvl2SubOwner, - domainLabel: "leveltwo", - tokenURI: "http://example.com/leveltwo", - fullConfig: { - distrConfig: { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, - }, - }, - { - user: lvl3SubOwner, - domainLabel: "lvlthree", - tokenURI: "http://example.com/lvlthree", - fullConfig: { - distrConfig: { - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.DIRECT, - accessType: AccessType.OPEN, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl3SubOwner.address, - }, - priceConfig: priceConfigDefault, - }, - }, - ]; - - regResults = await registerDomainPath({ - zns, - domainConfigs, - }); - }); - - it("should NOT allow to register an existing subdomain that has not been revoked", async () => { - await expect( - zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - regResults[0].domainHash, - domainConfigs[1].domainLabel, - lvl2SubOwner.address, - defaultTokenURI, - domainConfigs[1].fullConfig.distrConfig - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Subdomain already exists" - ); - }); - - it("should NOT allow revoking when the caller is NOT an owner of both Name and Token", async () => { - // change owner of the domain - await zns.registry.connect(lvl2SubOwner).updateDomainOwner( - regResults[1].domainHash, - rootOwner.address - ); - - // fail - await expect( - zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - regResults[1].domainHash, - ) - ).to.be.revertedWith( - "ZNSRootRegistrar: Not the owner of both Name and Token" - ); - - // change owner back - await zns.registry.connect(rootOwner).updateDomainOwner( - regResults[1].domainHash, - lvl2SubOwner.address - ); - - // tranfer token - await zns.domainToken.connect(lvl2SubOwner).transferFrom( - lvl2SubOwner.address, - lvl3SubOwner.address, - regResults[1].domainHash - ); - - // fail again - await expect( - zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain( - regResults[1].domainHash, - ) - ).to.be.revertedWith( - "ZNSRootRegistrar: Not the owner of both Name and Token" - ); - - // give token back - await zns.domainToken.connect(lvl3SubOwner).transferFrom( - lvl3SubOwner.address, - lvl2SubOwner.address, - regResults[1].domainHash - ); - }); - - it("should allow to UPDATE domain data for subdomain", async () => { - const dataFromReg = await zns.registry.getDomainRecord(regResults[1].domainHash); - expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); - expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); - - await zns.registry.connect(lvl2SubOwner).updateDomainRecord( - regResults[1].domainHash, - lvl3SubOwner.address, - ethers.constants.AddressZero, - ); - - const dataFromRegAfter = await zns.registry.getDomainRecord(regResults[1].domainHash); - expect(dataFromRegAfter.owner).to.eq(lvl3SubOwner.address); - expect(dataFromRegAfter.resolver).to.eq(ethers.constants.AddressZero); - - // reclaim to switch ownership back to original owner - await zns.rootRegistrar.connect(lvl2SubOwner).reclaimDomain( - regResults[1].domainHash, - ); - - const dataFromRegAfterReclaim = await zns.registry.getDomainRecord(regResults[1].domainHash); - expect(dataFromRegAfterReclaim.owner).to.eq(lvl2SubOwner.address); - expect(dataFromRegAfterReclaim.resolver).to.eq(ethers.constants.AddressZero); - }); - - describe("#setDistributionConfigForDomain()", () => { - it("should re-set distribution config for an existing subdomain", async () => { - const domainHash = regResults[2].domainHash; - - const distrConfigBefore = await zns.subRegistrar.distrConfigs(domainHash); - expect(distrConfigBefore.accessType).to.not.eq(AccessType.MINTLIST); - expect(distrConfigBefore.pricerContract).to.not.eq(zns.fixedPricer.address); - expect( - distrConfigBefore.paymentType - ).to.not.eq( - PaymentType.STAKE - ); - - const newConfig = { - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.MINTLIST, - }; - - await zns.subRegistrar.connect(lvl3SubOwner).setDistributionConfigForDomain( - domainHash, - newConfig, - ); - - const distrConfigAfter = await zns.subRegistrar.distrConfigs(domainHash); - expect(distrConfigAfter.accessType).to.eq(newConfig.accessType); - expect(distrConfigAfter.pricerContract).to.eq(newConfig.pricerContract); - expect(distrConfigAfter.paymentType).to.eq(newConfig.paymentType); - - // assign operator in registry - await zns.registry.connect(lvl3SubOwner).setOwnersOperator( - operator.address, - true, - ); - - // reset it back - await zns.subRegistrar.connect(operator).setDistributionConfigForDomain( - domainHash, - domainConfigs[2].fullConfig.distrConfig, - ); - const origConfigAfter = await zns.subRegistrar.distrConfigs(domainHash); - expect(origConfigAfter.accessType).to.eq(domainConfigs[2].fullConfig.distrConfig.accessType); - expect(origConfigAfter.pricerContract).to.eq(domainConfigs[2].fullConfig.distrConfig.pricerContract); - expect( - origConfigAfter.paymentType - ).to.eq( - domainConfigs[2].fullConfig.distrConfig.paymentType - ); - - // remove operator - await zns.registry.connect(lvl3SubOwner).setOwnersOperator( - operator.address, - false, - ); - }); - - it("should NOT allow to set distribution config for a non-authorized account", async () => { - const domainHash = regResults[1].domainHash; - - const newConfig = { - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.STAKE, - accessType: AccessType.MINTLIST, - }; - - await expect( - zns.subRegistrar.connect(lvl3SubOwner).setDistributionConfigForDomain( - domainHash, - newConfig, - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Not authorized" - ); - }); - - it("should revert if pricerContract is passed as 0x0 address", async () => { - const domainHash = regResults[2].domainHash; - - const newConfig = { - pricerContract: ethers.constants.AddressZero, - paymentType: PaymentType.STAKE, - accessType: AccessType.MINTLIST, - }; - - await expect( - zns.subRegistrar.connect(lvl3SubOwner).setDistributionConfigForDomain( - domainHash, - newConfig, - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: pricerContract can not be 0x0 address" - ); - }); - }); - - describe("#setPricerContractForDomain()", () => { - it("should re-set pricer contract for an existing subdomain", async () => { - const domainHash = regResults[2].domainHash; - - const pricerContractBefore = await zns.subRegistrar.distrConfigs(domainHash); - expect(pricerContractBefore.pricerContract).to.eq(domainConfigs[2].fullConfig.distrConfig.pricerContract); - - await zns.subRegistrar.connect(lvl3SubOwner).setPricerContractForDomain( - domainHash, - zns.curvePricer.address, - ); - - const pricerContractAfter = await zns.subRegistrar.distrConfigs(domainHash); - expect(pricerContractAfter.pricerContract).to.eq(zns.curvePricer.address); - - // reset it back - await zns.subRegistrar.connect(lvl3SubOwner).setPricerContractForDomain( - domainHash, - domainConfigs[2].fullConfig.distrConfig.pricerContract, - ); - }); - - it("should NOT allow setting for non-authorized account", async () => { - const domainHash = regResults[2].domainHash; - - await expect( - zns.subRegistrar.connect(lvl2SubOwner).setPricerContractForDomain( - domainHash, - zns.curvePricer.address, - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Not authorized" - ); - }); - - it("should NOT set pricerContract to 0x0 address", async () => { - const domainHash = regResults[2].domainHash; - - await expect( - zns.subRegistrar.connect(lvl3SubOwner).setPricerContractForDomain( - domainHash, - ethers.constants.AddressZero, - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: pricerContract can not be 0x0 address" - ); - }); - }); - - describe("#setPaymentTypeForDomain()", () => { - it("should re-set payment type for an existing subdomain", async () => { - const domainHash = regResults[2].domainHash; - - const { paymentType: paymentTypeBefore } = await zns.subRegistrar.distrConfigs(domainHash); - expect(paymentTypeBefore).to.eq(domainConfigs[2].fullConfig.distrConfig.paymentType); - - await zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( - domainHash, - PaymentType.STAKE, - ); - - const { paymentType: paymentTypeAfter } = await zns.subRegistrar.distrConfigs(domainHash); - expect(paymentTypeAfter).to.eq(PaymentType.STAKE); - - // reset it back - await zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( - domainHash, - domainConfigs[2].fullConfig.distrConfig.paymentType, - ); - }); - - it("should NOT allow setting for non-authorized account", async () => { - const domainHash = regResults[2].domainHash; - - await expect( - zns.subRegistrar.connect(lvl2SubOwner).setPaymentTypeForDomain( - domainHash, - PaymentType.STAKE, - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Not authorized" - ); - }); - - it("should emit #PaymentTypeSet event with correct params", async () => { - const domainHash = regResults[2].domainHash; - - await expect( - zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( - domainHash, - PaymentType.STAKE, - ) - ).to.emit(zns.subRegistrar, "PaymentTypeSet").withArgs( - domainHash, - PaymentType.STAKE, - ); - - // reset back - await zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( - domainHash, - domainConfigs[2].fullConfig.distrConfig.paymentType, - ); - }); - }); - - // eslint-disable-next-line max-len - it("should TRANSFER ownership of a subdomain and let the receiver RECLAIM and then revoke with REFUND", async () => { - const tokenId = BigNumber.from(regResults[1].domainHash).toString(); - - const { amount: stakedBefore } = await zns.treasury.stakedForDomain(regResults[1].domainHash); - - await zns.domainToken.connect(lvl2SubOwner).transferFrom( - lvl2SubOwner.address, - lvl3SubOwner.address, - tokenId - ); - - // Verify owner in registry - const dataFromReg = await zns.registry.getDomainRecord(regResults[1].domainHash); - expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); - - // reclaim - await zns.rootRegistrar.connect(lvl3SubOwner).reclaimDomain( - regResults[1].domainHash, - ); - - // Verify domain token is still owned - const tokenOwner = await zns.domainToken.ownerOf(tokenId); - expect(tokenOwner).to.eq(lvl3SubOwner.address); - - // Verify owner in registry - const dataFromRegAfter = await zns.registry.getDomainRecord(regResults[1].domainHash); - expect(dataFromRegAfter.owner).to.eq(lvl3SubOwner.address); - - // verify stake still existing - const { amount: stakedAfter } = await zns.treasury.stakedForDomain(regResults[1].domainHash); - expect(stakedAfter).to.eq(stakedBefore); - - const userBalbefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - - // try revoking - await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( - regResults[1].domainHash, - ); - - // verify that refund has been acquired by the new owner - const userBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); - expect(userBalAfter.sub(userBalbefore)).to.eq(fixedPrice); - }); - }); - - describe("State setters", () => { - before(async () => { - [ - deployer, - admin, - random, - ] = await hre.ethers.getSigners(); - - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address], - adminAddresses: [admin.address], - }); - }); - - it("Should NOT let initialize the implementation contract", async () => { - const factory = new ZNSSubRegistrar__factory(deployer); - const impl = await getProxyImplAddress(zns.subRegistrar.address); - const implContract = factory.attach(impl); - - await expect( - implContract.initialize( - deployer.address, - deployer.address, - deployer.address, - ) - ).to.be.revertedWith(INITIALIZED_ERR); - }); - - it("#setRootRegistrar() should set the new root registrar correctly and emit #RootRegistrarSet event", async () => { - const tx = await zns.subRegistrar.connect(admin).setRootRegistrar(random.address); - - await expect(tx).to.emit(zns.subRegistrar, "RootRegistrarSet").withArgs(random.address); - - expect(await zns.subRegistrar.rootRegistrar()).to.equal(random.address); - }); - - it("#setRootRegistrar() should NOT be callable by anyone other than ADMIN_ROLE", async () => { - await expect( - zns.subRegistrar.connect(random).setRootRegistrar(random.address), - ).to.be.revertedWith( - getAccessRevertMsg(random.address, ADMIN_ROLE), - ); - }); - - it("#setRootRegistrar should NOT set registrar as 0x0 address", async () => { - await expect( - zns.subRegistrar.connect(admin).setRootRegistrar(ethers.constants.AddressZero), - ).to.be.revertedWith( - "ZNSSubRegistrar: _registrar can not be 0x0 address", - ); - }); - - it("#setRegistry() should set the new registry correctly and emit #RegistrySet event", async () => { - const tx = await zns.subRegistrar.connect(admin).setRegistry(random.address); - - await expect(tx).to.emit(zns.subRegistrar, "RegistrySet").withArgs(random.address); - - expect(await zns.subRegistrar.registry()).to.equal(random.address); - }); - - it("#setRegistry() should not be callable by anyone other than ADMIN_ROLE", async () => { - await expect( - zns.subRegistrar.connect(random).setRegistry(random.address), - ).to.be.revertedWith( - getAccessRevertMsg(random.address, ADMIN_ROLE), - ); - }); - - it("#setAccessController() should not be callable by anyone other than ADMIN_ROLE", async () => { - await expect( - zns.subRegistrar.connect(random).setAccessController(random.address), - ).to.be.revertedWith( - getAccessRevertMsg(random.address, ADMIN_ROLE), - ); - }); - - it("#getAccessController() should return the correct access controller", async () => { - expect( - await zns.subRegistrar.getAccessController() - ).to.equal(zns.accessController.address); - }); - - // eslint-disable-next-line max-len - it("#setAccessController() should set the new access controller correctly and emit #AccessControllerSet event", async () => { - const tx = await zns.subRegistrar.connect(admin).setAccessController(random.address); - - await expect(tx).to.emit(zns.subRegistrar, "AccessControllerSet").withArgs(random.address); - - expect(await zns.subRegistrar.getAccessController()).to.equal(random.address); - }); - }); - - describe("UUPS", () => { - let fixedPrice : BigNumber; - let rootHash : string; - - beforeEach(async () => { - [ - deployer, - zeroVault, - governor, - admin, - rootOwner, - lvl2SubOwner, - ] = await hre.ethers.getSigners(); - // zeroVault address is used to hold the fee charged to the user when registering - zns = await deployZNS({ - deployer, - governorAddresses: [deployer.address, governor.address], - adminAddresses: [admin.address], - priceConfig: priceConfigDefault, - zeroVaultAddress: zeroVault.address, - }); - - // Give funds to users - await Promise.all( - [ - rootOwner, - lvl2SubOwner, - ].map(async ({ address }) => - zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) - ); - await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - - fixedPrice = ethers.utils.parseEther("397.13"); - // register root domain - rootHash = await registrationWithSetup({ - zns, - user: rootOwner, - domainLabel: "root", - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: rootOwner.address, - }, - priceConfig: { - price: fixedPrice, - feePercentage: BigNumber.from(0), - }, - }, - }); - }); - - it("Allows an authorized user to upgrade the contract", async () => { - // SubRegistrar to upgrade to - const factory = new ZNSSubRegistrarUpgradeMock__factory(deployer); - const newRegistrar = await factory.deploy(); - await newRegistrar.deployed(); - - // Confirm the deployer is a governor, as set in `deployZNS` helper - await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; - - const tx = zns.subRegistrar.connect(deployer).upgradeTo(newRegistrar.address); - await expect(tx).to.not.be.reverted; - - await expect( - zns.subRegistrar.connect(deployer).initialize( - zns.accessController.address, - zns.registry.address, - zns.rootRegistrar.address, - ) - ).to.be.revertedWith(INITIALIZED_ERR); - }); - - it("Fails to upgrade if the caller is not authorized", async () => { - // SubRegistrar to upgrade to - const factory = new ZNSSubRegistrarUpgradeMock__factory(deployer); - const newRegistrar = await factory.deploy(); - await newRegistrar.deployed(); - - // Confirm the account is not a governor - await expect(zns.accessController.checkGovernor(lvl2SubOwner.address)).to.be.reverted; - - const tx = zns.subRegistrar.connect(lvl2SubOwner).upgradeTo(newRegistrar.address); - - await expect(tx).to.be.revertedWith( - getAccessRevertMsg(lvl2SubOwner.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 ZNSSubRegistrarUpgradeMock__factory(deployer); - const registrar = await registrarFactory.deploy(); - await registrar.deployed(); - - const domainLabel = "world"; - - await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); - await zns.zeroToken.mint(lvl2SubOwner.address, parseEther("1000000")); - - const domainHash = await registrationWithSetup({ - zns, - user: lvl2SubOwner, - domainLabel, - parentHash: rootHash, - fullConfig: { - distrConfig: { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - }, - priceConfig: { - price: fixedPrice, - feePercentage: BigNumber.from(0), - }, - paymentConfig: { - token: zns.zeroToken.address, - beneficiary: lvl2SubOwner.address, - }, - }, - }); - - await zns.subRegistrar.setRootRegistrar(lvl2SubOwner.address); - - const contractCalls = [ - zns.subRegistrar.getAccessController(), - zns.subRegistrar.registry(), - zns.subRegistrar.rootRegistrar(), - zns.registry.exists(domainHash), - zns.treasury.stakedForDomain(domainHash), - zns.domainToken.name(), - zns.domainToken.symbol(), - zns.fixedPricer.getPrice(rootHash, domainLabel), - ]; - - await validateUpgrade(deployer, zns.subRegistrar, registrar, registrarFactory, contractCalls); - }); - - it("Allows to add more fields to the existing struct in a mapping", async () => { - // SubRegistrar to upgrade to - const factory = new ZNSSubRegistrarUpgradeMock__factory(deployer); - const newRegistrar = await factory.deploy(); - await newRegistrar.deployed(); - - const tx = zns.subRegistrar.connect(deployer).upgradeTo(newRegistrar.address); - await expect(tx).to.not.be.reverted; - - // create new proxy object - const newRegistrarProxy = factory.attach(zns.subRegistrar.address); - - // check values in storage - const rootConfigBefore = await newRegistrarProxy.distrConfigs(rootHash); - expect(rootConfigBefore.accessType).to.eq(AccessType.OPEN); - expect(rootConfigBefore.pricerContract).to.eq(zns.fixedPricer.address); - expect(rootConfigBefore.paymentType).to.eq(PaymentType.DIRECT); - - await zns.zeroToken.mint(lvl2SubOwner.address, parseEther("1000000")); - await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, parseEther("1000000")); - - const subConfigToSet = { - accessType: AccessType.MINTLIST, - pricerContract: zns.curvePricer.address, - paymentType: PaymentType.STAKE, - newAddress: lvl2SubOwner.address, - newUint: BigNumber.from(1912171236), - }; - - // register a subdomain with new logic - await newRegistrarProxy.connect(lvl2SubOwner).registerSubdomain( - rootHash, - "subbb", - lvl2SubOwner.address, - defaultTokenURI, - subConfigToSet - ); - - const subHash = await getDomainHashFromEvent({ - zns, - user: lvl2SubOwner, - }); - - const rootConfigAfter = await zns.subRegistrar.distrConfigs(rootHash); - expect(rootConfigAfter.accessType).to.eq(rootConfigBefore.accessType); - expect(rootConfigAfter.pricerContract).to.eq(rootConfigBefore.pricerContract); - expect(rootConfigAfter.paymentType).to.eq(rootConfigBefore.paymentType); - expect(rootConfigAfter.length).to.eq(3); - - const updatedStructConfig = { - accessType: AccessType.OPEN, - pricerContract: zns.fixedPricer.address, - paymentType: PaymentType.DIRECT, - newAddress: lvl2SubOwner.address, - newUint: BigNumber.from(123), - }; - - // try setting new fields to the new struct - await newRegistrarProxy.connect(rootOwner).setDistributionConfigForDomain( - rootHash, - updatedStructConfig - ); - - // check what we got for new - const rootConfigFinal = await newRegistrarProxy.distrConfigs(rootHash); - const subConfigAfter = await newRegistrarProxy.distrConfigs(subHash); - - // validate the new config has been set correctly - expect(subConfigAfter.accessType).to.eq(subConfigToSet.accessType); - expect(subConfigAfter.pricerContract).to.eq(subConfigToSet.pricerContract); - expect(subConfigAfter.paymentType).to.eq(subConfigToSet.paymentType); - expect(subConfigAfter.newAddress).to.eq(subConfigToSet.newAddress); - expect(subConfigAfter.newUint).to.eq(subConfigToSet.newUint); - - // validate the old values stayed the same and new values been added - expect(rootConfigFinal.accessType).to.eq(rootConfigBefore.accessType); - expect(rootConfigFinal.pricerContract).to.eq(rootConfigBefore.pricerContract); - expect(rootConfigFinal.paymentType).to.eq(rootConfigBefore.paymentType); - expect(rootConfigFinal.newAddress).to.eq(updatedStructConfig.newAddress); - expect(rootConfigFinal.newUint).to.eq(updatedStructConfig.newUint); - - // check that crucial state vars stayed the same - expect(await newRegistrarProxy.getAccessController()).to.eq(zns.accessController.address); - expect(await newRegistrarProxy.registry()).to.eq(zns.registry.address); - expect(await newRegistrarProxy.rootRegistrar()).to.eq(zns.rootRegistrar.address); - }); - }); -}); +import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/signers"; +import { IDomainConfigForTest, IFixedPriceConfig, IPathRegResult, IZNSContracts } from "./helpers/types"; +import { + AccessType, + ADMIN_ROLE, + defaultTokenURI, + deployZNS, + distrConfigEmpty, + DISTRIBUTION_LOCKED_NOT_EXIST_ERR, + fullDistrConfigEmpty, + getAccessRevertMsg, + getPriceObject, + getStakingOrProtocolFee, + GOVERNOR_ROLE, + INITIALIZED_ERR, + INVALID_NAME_ERR, + INVALID_TOKENID_ERC_ERR, NO_BENEFICIARY_ERR, + ONLY_NAME_OWNER_REG_ERR, paymentConfigEmpty, + PaymentType, + precisionDefault, + priceConfigDefault, curvePriceConfigEmpty, + validateUpgrade, +} from "./helpers"; +import * as hre from "hardhat"; +import * as ethers from "ethers"; +import { BigNumber } from "ethers"; +import { expect } from "chai"; +import { registerDomainPath, validatePathRegistration } from "./helpers/flows/registration"; +import assert from "assert"; +import { defaultSubdomainRegistration, registrationWithSetup } from "./helpers/register-setup"; +import { getDomainHashFromEvent } from "./helpers/events"; +import { time } from "@nomicfoundation/hardhat-network-helpers"; +import { CustomDecimalTokenMock, ZNSSubRegistrar__factory, ZNSSubRegistrarUpgradeMock__factory } from "../typechain"; +import { parseEther, parseUnits } from "ethers/lib/utils"; +import { deployCustomDecToken } from "./helpers/deploy/mocks"; +import { getProxyImplAddress } from "./helpers/utils"; + + +describe("ZNSSubRegistrar", () => { + let deployer : SignerWithAddress; + let rootOwner : SignerWithAddress; + let governor : SignerWithAddress; + let admin : SignerWithAddress; + let lvl2SubOwner : SignerWithAddress; + let lvl3SubOwner : SignerWithAddress; + let lvl4SubOwner : SignerWithAddress; + let lvl5SubOwner : SignerWithAddress; + let lvl6SubOwner : SignerWithAddress; + let branchLvl1Owner : SignerWithAddress; + let branchLvl2Owner : SignerWithAddress; + let random : SignerWithAddress; + let operator : SignerWithAddress; + let multiOwner : SignerWithAddress; + + let zns : IZNSContracts; + let zeroVault : SignerWithAddress; + + describe("Single Subdomain Registration", () => { + let rootHash : string; + let rootPriceConfig : IFixedPriceConfig; + const subTokenURI = "https://token-uri.com/8756a4b6f"; + + before(async () => { + [ + deployer, + zeroVault, + governor, + admin, + rootOwner, + lvl2SubOwner, + ] = await hre.ethers.getSigners(); + // zeroVault address is used to hold the fee charged to the user when registering + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address, governor.address], + adminAddresses: [admin.address], + priceConfig: priceConfigDefault, + zeroVaultAddress: zeroVault.address, + }); + // Give funds to users + await Promise.all( + [ + rootOwner, + lvl2SubOwner, + ].map(async ({ address }) => + zns.zeroToken.mint(address, ethers.utils.parseEther("100000000000"))) + ); + await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + rootPriceConfig = { + price: ethers.utils.parseEther("1375.612"), + feePercentage: BigNumber.from(0), + }; + + // register root domain + rootHash = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "root", + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: rootPriceConfig, + }, + }); + }); + + // eslint-disable-next-line max-len + it("should revert when trying to register a subdomain before parent has set it's config with FixedPricer", async () => { + // register a new root domain + const newRootHash = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "rootunsetfixed", + setConfigs: false, + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: { + price: BigNumber.from(0), + feePercentage: BigNumber.from(0), + }, + }, + }); + + await expect( + zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + newRootHash, + "subunset", + lvl2SubOwner.address, + subTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + "ZNSFixedPricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" + ); + }); + + // eslint-disable-next-line max-len + it("should revert when trying to register a subdomain before parent has set it's config with CurvePricer", async () => { + // register a new root domain + const newRootHash = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "rootunsetcurve", + setConfigs: false, + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: curvePriceConfigEmpty, + }, + }); + + await expect( + zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + newRootHash, + "subunset", + lvl2SubOwner.address, + subTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + "ZNSCurvePricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" + ); + }); + + it("should register subdomain with the correct tokenURI assigned to the domain token minted", async () => { + const subHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "sub", + tokenURI: subTokenURI, + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig: { + price: ethers.utils.parseEther("777.325"), + feePercentage: BigNumber.from(0), + }, + }, + }); + + const tokenId = BigNumber.from(subHash).toString(); + const tokenURI = await zns.domainToken.tokenURI(tokenId); + expect(tokenURI).to.eq(subTokenURI); + }); + + it("Can register a subdomain with characters [a-z0-9]", async () => { + const alphaNumeric = "0x0dwidler0x0"; + + // Add allowance + await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + // While "to.not.be.reverted" isn't really a full "test" + // we don't emit a custom event here, only in the `rootRegistrar.coreRegister` + // call. So we can't use the `.to.emit` syntax + await expect(defaultSubdomainRegistration( + { + user: lvl2SubOwner, + zns, + parentHash: rootHash, + subdomainLabel: alphaNumeric, + domainContent: lvl2SubOwner.address, + tokenURI: subTokenURI, + distrConfig: distrConfigEmpty, + } + )).to.not.be.reverted; + }); + + it("Fails for a subdomain that uses any invalid characters", async () => { + const nameA = "WILDER"; + const nameB = "!?w1Id3r!?"; + const nameC = "!%$#^*?!#👍3^29"; + const nameD = "wo.rld"; + + await expect(defaultSubdomainRegistration( + { + user: lvl2SubOwner, + zns, + parentHash: rootHash, + subdomainLabel: nameA, + domainContent: lvl2SubOwner.address, + tokenURI: subTokenURI, + distrConfig: distrConfigEmpty, + } + )).to.be.revertedWith(INVALID_NAME_ERR); + + await expect(defaultSubdomainRegistration( + { + user: lvl2SubOwner, + zns, + parentHash: rootHash, + subdomainLabel: nameB, + domainContent: lvl2SubOwner.address, + tokenURI: subTokenURI, + distrConfig: distrConfigEmpty, + } + )).to.be.revertedWith(INVALID_NAME_ERR); + + await expect(defaultSubdomainRegistration( + { + user: lvl2SubOwner, + zns, + parentHash: rootHash, + subdomainLabel: nameC, + domainContent: lvl2SubOwner.address, + tokenURI: subTokenURI, + distrConfig: distrConfigEmpty, + } + )).to.be.revertedWith(INVALID_NAME_ERR); + + await expect(defaultSubdomainRegistration( + { + user: lvl2SubOwner, + zns, + parentHash: rootHash, + subdomainLabel: nameD, + domainContent: lvl2SubOwner.address, + tokenURI: subTokenURI, + distrConfig: distrConfigEmpty, + } + )).to.be.revertedWith(INVALID_NAME_ERR); + }); + + it("should revert when trying to register a subdomain under a non-existent parent", async () => { + // check that 0x0 hash can NOT be passed as parentHash + await expect( + zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + ethers.constants.HashZero, + "sub", + lvl2SubOwner.address, + subTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + DISTRIBUTION_LOCKED_NOT_EXIST_ERR + ); + + // check that a random non-existent hash can NOT be passed as parentHash + const randomHash = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("random")); + await expect( + zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + randomHash, + "sub", + lvl2SubOwner.address, + subTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + DISTRIBUTION_LOCKED_NOT_EXIST_ERR + ); + }); + + it("should register subdomain with a single char label", async () => { + const subHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "a", + tokenURI: subTokenURI, + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig: { + price: ethers.utils.parseEther("777.325"), + feePercentage: BigNumber.from(0), + }, + }, + }); + + const tokenId = BigNumber.from(subHash).toString(); + const tokenURI = await zns.domainToken.tokenURI(tokenId); + expect(tokenURI).to.eq(subTokenURI); + + // check registry + const dataFromReg = await zns.registry.getDomainRecord(subHash); + expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); + expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); + }); + + // ! this value can change based on the block gas limit ! + it("should register subdomain with a label length of 100000 chars", async () => { + const subHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "a".repeat(100000), + tokenURI: subTokenURI, + fullConfig: fullDistrConfigEmpty, + }); + + const tokenId = BigNumber.from(subHash).toString(); + const tokenURI = await zns.domainToken.tokenURI(tokenId); + expect(tokenURI).to.eq(subTokenURI); + + // check registry + const dataFromReg = await zns.registry.getDomainRecord(subHash); + expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); + expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); + }); + + it("should revert when user has insufficient funds", async () => { + const label = "subinsufficientfunds"; + const { expectedPrice } = getPriceObject(label, rootPriceConfig); + const userBalanceBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const userBalanceAfter = userBalanceBefore.sub(expectedPrice); + await zns.zeroToken.connect(lvl2SubOwner).transfer(deployer.address, userBalanceAfter); + + // add allowance + await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + await expect( + zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + rootHash, + label, + lvl2SubOwner.address, + subTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + "ERC20: transfer amount exceeds balance" + ); + + // transfer back for other tests + await zns.zeroToken.connect(deployer).transfer(lvl2SubOwner.address, userBalanceAfter); + }); + + it("should revert when user has insufficient allowance", async () => { + const label = "subinsufficientallowance"; + const { expectedPrice } = getPriceObject(label, rootPriceConfig); + + // add allowance + await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, expectedPrice.sub(1)); + + await expect( + zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + rootHash, + label, + lvl2SubOwner.address, + subTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + "ERC20: transfer amount exceeds allowance" + ); + }); + + it("should revert on payment when parent's beneficiary has not yet been set and when stakeFee is > 0", async () => { + // register a new parent with direct payment and no payment config + const parentHash1 = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "parentnoconfigdirect", + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: paymentConfigEmpty, + priceConfig: rootPriceConfig, + }, + }); + + // set the token address + await zns.treasury.connect(rootOwner).setPaymentToken(parentHash1, zns.zeroToken.address); + + // register a new parent with stake payment and no payment config + const parentHash2 = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "parentnoconfigstake", + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.STAKE, + }, + paymentConfig: paymentConfigEmpty, + priceConfig: priceConfigDefault, + }, + }); + + // set the token address + await zns.treasury.connect(rootOwner).setPaymentToken(parentHash2, zns.zeroToken.address); + + // register subdomains under new parents + await expect( + registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: parentHash1, + domainLabel: "sub1", + }) + ).to.be.revertedWith(NO_BENEFICIARY_ERR); + + await expect( + registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: parentHash2, + domainLabel: "sub2", + }) + ).to.be.revertedWith(NO_BENEFICIARY_ERR); + + // change stakeFee to 0 + await zns.curvePricer.connect(rootOwner).setFeePercentage( + parentHash2, + BigNumber.from(0) + ); + + let subHash; + // try register a subdomain again + await expect( + subHash = registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: parentHash2, + domainLabel: "sub2", + }) + ).to.be.fulfilled; + + await zns.registry.exists(subHash); + }); + }); + + describe("Operations within domain paths", () => { + let domainConfigs : Array; + let regResults : Array; + + const fixedPrice = ethers.utils.parseEther("1375.612"); + const fixedFeePercentage = BigNumber.from(200); + + before(async () => { + [ + deployer, + zeroVault, + governor, + admin, + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + branchLvl1Owner, + branchLvl2Owner, + multiOwner, + ] = await hre.ethers.getSigners(); + // zeroVault address is used to hold the fee charged to the user when registering + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address, governor.address], + adminAddresses: [admin.address], + priceConfig: priceConfigDefault, + zeroVaultAddress: zeroVault.address, + }); + + // Give funds to users + await Promise.all( + [ + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + branchLvl1Owner, + branchLvl2Owner, + multiOwner, + ].map(async ({ address }) => + zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) + ); + await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + domainConfigs = [ + { + user: rootOwner, + domainLabel: "root", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, + }, + }, + { + user: lvl2SubOwner, + domainLabel: "lvltwo", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig: priceConfigDefault, + }, + }, + { + user: lvl3SubOwner, + domainLabel: "lvlthree", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl3SubOwner.address, + }, + priceConfig: priceConfigDefault, + }, + }, + { + user: lvl4SubOwner, + domainLabel: "lvlfour", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl4SubOwner.address, + }, + priceConfig: priceConfigDefault, + + }, + }, + { + user: lvl5SubOwner, + domainLabel: "lvlfive", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl5SubOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + + }, + }, + { + user: lvl6SubOwner, + domainLabel: "lvlsix", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl6SubOwner.address, + }, + priceConfig: priceConfigDefault, + }, + }, + ]; + + regResults = await registerDomainPath({ + zns, + domainConfigs, + }); + + assert.equal(regResults.length, domainConfigs.length); + }); + + it("should register a path of 6 domains with different configs", async () => { + await validatePathRegistration({ + zns, + domainConfigs, + regResults, + }); + }); + + it("should be able to register multiple domains under multiple levels for the same owner", async () => { + const configs = [ + { + user: multiOwner, + domainLabel: "multiownerdomone", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: multiOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, + }, + }, + { + user: multiOwner, + domainLabel: "multiownerdomtwo", + parentHash: regResults[0].domainHash, + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.LOCKED, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: zeroVault.address, + }, + priceConfig: priceConfigDefault, + }, + }, + { + user: multiOwner, + domainLabel: "multiownerdomthree", + parentHash: regResults[1].domainHash, + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.MINTLIST, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: multiOwner.address, + }, + priceConfig: priceConfigDefault, + }, + }, + { + user: multiOwner, + domainLabel: "multiownerdomfour", + parentHash: regResults[2].domainHash, + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: zeroVault.address, + }, + priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, + }, + }, + { + user: multiOwner, + domainLabel: "multiownerdomfive", + parentHash: regResults[3].domainHash, + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: multiOwner.address, + }, + priceConfig: priceConfigDefault, + }, + }, + { + user: multiOwner, + domainLabel: "multiownerdomsix", + parentHash: regResults[4].domainHash, + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: zeroVault.address, + }, + priceConfig: priceConfigDefault, + }, + }, + { + user: multiOwner, + domainLabel: "multiownerdomseven", + parentHash: regResults[5].domainHash, + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: multiOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }, + ]; + + // prep + await zns.zeroToken.connect(multiOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + // register + const domainHashes = await configs.reduce( + async ( + acc : Promise>, + { + user, + parentHash, + domainLabel, + fullConfig, + }) : Promise> => { + const newAcc = await acc; + + const newHash = await registrationWithSetup({ + zns, + user, + parentHash, + domainLabel, + fullConfig, + }); + + return [...newAcc, newHash]; + }, Promise.resolve([]) + ); + + // check + await domainHashes.reduce( + async (acc, domainHash, idx) => { + await acc; + const { owner, resolver } = await zns.registry.getDomainRecord(domainHash); + expect(owner).to.eq(multiOwner.address); + expect(resolver).to.eq(zns.addressResolver.address); + + const tokenId = BigNumber.from(domainHash).toString(); + const tokenOwner = await zns.domainToken.ownerOf(tokenId); + expect(tokenOwner).to.eq(multiOwner.address); + + const { + pricerContract, + accessType, + paymentType, + } = await zns.subRegistrar.distrConfigs(domainHash); + expect(pricerContract).to.eq(configs[idx].fullConfig.distrConfig.pricerContract); + expect(accessType).to.eq(configs[idx].fullConfig.distrConfig.accessType); + expect(paymentType).to.eq(configs[idx].fullConfig.distrConfig.paymentType); + + const { + token, + beneficiary, + } = await zns.treasury.paymentConfigs(domainHash); + expect(token).to.eq(configs[idx].fullConfig.paymentConfig.token); + expect(beneficiary).to.eq(configs[idx].fullConfig.paymentConfig.beneficiary); + + const domainAddress = await zns.addressResolver.getAddress(domainHash); + expect(domainAddress).to.eq(multiOwner.address); + }, Promise.resolve() + ); + }); + + it("should revoke lvl 6 domain without refund, lock registration and remove mintlist", async () => { + const domainHash = regResults[5].domainHash; + + // add to mintlist + await zns.subRegistrar.connect(lvl6SubOwner).updateMintlistForDomain( + domainHash, + [lvl6SubOwner.address, lvl2SubOwner.address], + [true, true] + ); + + const userBalBefore = await zns.zeroToken.balanceOf(lvl6SubOwner.address); + + await zns.rootRegistrar.connect(lvl6SubOwner).revokeDomain( + domainHash, + ); + + const userBalAfter = await zns.zeroToken.balanceOf(lvl6SubOwner.address); + + expect(userBalAfter.sub(userBalBefore)).to.eq(0); + + // make sure that accessType has been set to LOCKED + // and nobody can register a subdomain under this domain + const { accessType: accessTypeFromSC } = await zns.subRegistrar.distrConfigs(domainHash); + expect(accessTypeFromSC).to.eq(AccessType.LOCKED); + + // make sure that mintlist has been removed + expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, lvl6SubOwner.address)).to.eq(false); + expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, lvl2SubOwner.address)).to.eq(false); + + await expect( + zns.subRegistrar.connect(lvl6SubOwner).registerSubdomain( + domainHash, + "newsubdomain", + lvl6SubOwner.address, + defaultTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + DISTRIBUTION_LOCKED_NOT_EXIST_ERR + ); + + const dataFromReg = await zns.registry.getDomainRecord(domainHash); + expect(dataFromReg.owner).to.eq(ethers.constants.AddressZero); + expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); + + const tokenId = BigNumber.from(domainHash).toString(); + await expect( + zns.domainToken.ownerOf(tokenId) + ).to.be.revertedWith( + INVALID_TOKENID_ERC_ERR + ); + + await expect( + zns.registry.connect(lvl6SubOwner).updateDomainRecord(domainHash, rootOwner.address, lvl6SubOwner.address) + ).to.be.revertedWith(ONLY_NAME_OWNER_REG_ERR); + }); + + it("should revoke lvl 5 domain with refund", async () => { + const domainHash = regResults[4].domainHash; + + const userBalanceBefore = await zns.zeroToken.balanceOf(lvl5SubOwner.address); + const parentBalBefore = await zns.zeroToken.balanceOf(lvl4SubOwner.address); + const paymentContractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); + + await zns.rootRegistrar.connect(lvl5SubOwner).revokeDomain(domainHash); + + const userBalAfter = await zns.zeroToken.balanceOf(lvl5SubOwner.address); + const parentBalAfter = await zns.zeroToken.balanceOf(lvl4SubOwner.address); + const paymentContractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); + + const { expectedPrice } = getPriceObject(domainConfigs[4].domainLabel); + + expect( + userBalAfter.sub(userBalanceBefore) + ).to.eq( + expectedPrice + ); + expect( + parentBalBefore.sub(parentBalAfter) + ).to.eq( + BigNumber.from(0) + ); + expect( + paymentContractBalBefore.sub(paymentContractBalAfter) + ).to.eq( + expectedPrice + ); + + // make sure that accessType has been set to LOCKED + // and nobody can register a subdomain under this domain + const { accessType: accessTypeFromSC } = await zns.subRegistrar.distrConfigs(domainHash); + expect(accessTypeFromSC).to.eq(AccessType.LOCKED); + + await expect( + zns.subRegistrar.connect(lvl6SubOwner).registerSubdomain( + domainHash, + "newsubdomain", + lvl6SubOwner.address, + defaultTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith( + DISTRIBUTION_LOCKED_NOT_EXIST_ERR + ); + + const dataFromReg = await zns.registry.getDomainRecord(domainHash); + expect(dataFromReg.owner).to.eq(ethers.constants.AddressZero); + expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); + + const tokenId = BigNumber.from(domainHash).toString(); + await expect( + zns.domainToken.ownerOf(tokenId) + ).to.be.revertedWith( + INVALID_TOKENID_ERC_ERR + ); + + await expect( + zns.registry.connect(lvl5SubOwner).updateDomainRecord(domainHash, rootOwner.address, lvl6SubOwner.address) + ).to.be.revertedWith(ONLY_NAME_OWNER_REG_ERR); + }); + + it("should register a new 2 lvl path at lvl 3 of the existing path", async () => { + const newConfigs = [ + { + user: branchLvl1Owner, + domainLabel: "lvlthreenew", + parentHash: regResults[2].domainHash, + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: branchLvl1Owner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }, + { + user: branchLvl2Owner, + domainLabel: "lvlfournew", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: branchLvl2Owner.address, + }, + priceConfig: priceConfigDefault, + }, + }, + ]; + + const newRegResults = await registerDomainPath({ + zns, + domainConfigs: newConfigs, + }); + + await validatePathRegistration({ + zns, + domainConfigs: newConfigs, + regResults: newRegResults, + }); + }); + + it("should revoke lvl 3 domain (child) with refund after lvl 2 (parent) has been revoked", async () => { + const lvl2Hash = regResults[1].domainHash; + const lvl3Hash = regResults[2].domainHash; + + const childExists = await zns.registry.exists(lvl3Hash); + assert.ok(childExists); + + // revoke parent + await zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain( + lvl2Hash, + ); + + // make sure all parent's distribution configs still exist + const parentDistrConfig = await zns.subRegistrar.distrConfigs(lvl2Hash); + const parentPaymentConfig = await zns.treasury.paymentConfigs(lvl2Hash); + expect(parentDistrConfig.pricerContract).to.eq(domainConfigs[1].fullConfig.distrConfig.pricerContract); + expect( + parentDistrConfig.paymentType + ).to.eq( + domainConfigs[1].fullConfig.distrConfig.paymentType + ); + expect( + parentPaymentConfig.token + ).to.eq( + domainConfigs[1].fullConfig.paymentConfig.token + ); + expect( + parentPaymentConfig.beneficiary + ).to.eq( + domainConfigs[1].fullConfig.paymentConfig.beneficiary + ); + + expect(parentDistrConfig.pricerContract).to.eq(zns.curvePricer.address); + + // check a couple of fields from price config + const priceConfig = await zns.curvePricer.priceConfigs(lvl2Hash); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if ("maxPrice" in domainConfigs[1].fullConfig.priceConfig!) { + expect(priceConfig.maxPrice).to.eq(domainConfigs[1].fullConfig.priceConfig.maxPrice); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + if ("minPrice" in domainConfigs[1].fullConfig.priceConfig!) { + expect(priceConfig.minPrice).to.eq(domainConfigs[1].fullConfig.priceConfig.minPrice); + } + + // make sure the child's stake is still there + const { amount: childStakedAmt } = await zns.treasury.stakedForDomain(lvl3Hash); + const { expectedPrice } = getPriceObject(domainConfigs[2].domainLabel); + + expect(childStakedAmt).to.eq(expectedPrice); + + const userBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + + // revoke child + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + lvl3Hash, + ); + + const userBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + + expect(userBalAfter.sub(userBalBefore)).to.eq(expectedPrice); + + const childExistsAfter = await zns.registry.exists(lvl3Hash); + assert.ok(!childExistsAfter); + + const { amount: stakedAfterRevoke } = await zns.treasury.stakedForDomain(lvl3Hash); + expect(stakedAfterRevoke).to.eq(0); + + const dataFromReg = await zns.registry.getDomainRecord(lvl3Hash); + expect(dataFromReg.owner).to.eq(ethers.constants.AddressZero); + expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); + + const tokenId = BigNumber.from(lvl3Hash).toString(); + await expect( + zns.domainToken.ownerOf(tokenId) + ).to.be.revertedWith( + INVALID_TOKENID_ERC_ERR + ); + + await expect( + zns.registry.connect(lvl3SubOwner).updateDomainRecord(lvl3Hash, rootOwner.address, lvl4SubOwner.address) + ).to.be.revertedWith(ONLY_NAME_OWNER_REG_ERR); + }); + + it("should let anyone register a previously revoked domain", async () => { + const lvl2Hash = regResults[1].domainHash; + const parentHash = regResults[0].domainHash; + + const exists = await zns.registry.exists(lvl2Hash); + if (!exists) { + const newHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash, + domainLabel: domainConfigs[1].domainLabel, + fullConfig: domainConfigs[1].fullConfig, + }); + + expect(newHash).to.eq(lvl2Hash); + } + + // revoke subdomain + await zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain( + lvl2Hash, + ); + + // someone else is taking it + const newConfig = [ + { + user: branchLvl1Owner, + domainLabel: "lvltwonew", + parentHash, + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: branchLvl1Owner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }, + ]; + + const newResult = await registerDomainPath({ + zns, + domainConfigs: newConfig, + }); + + await validatePathRegistration({ + zns, + domainConfigs: newConfig, + regResults: newResult, + }); + }); + + it("should NOT register a child (subdomain) under a parent (root domain) that has been revoked", async () => { + const lvl1Hash = regResults[0].domainHash; + + // revoke parent + await zns.rootRegistrar.connect(rootOwner).revokeDomain( + lvl1Hash + ); + + const exists = await zns.registry.exists(lvl1Hash); + assert.ok(!exists); + + await expect( + zns.subRegistrar.connect(branchLvl1Owner).registerSubdomain( + lvl1Hash, + "newsubdomain", + branchLvl1Owner.address, + defaultTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); + + // register root back for other tests + await registrationWithSetup({ + zns, + user: rootOwner, + parentHash: ethers.constants.HashZero, + domainLabel: domainConfigs[0].domainLabel, + fullConfig: domainConfigs[0].fullConfig, + }); + }); + + it("should NOT register a child (subdomain) under a parent (subdomain) that has been revoked", async () => { + const lvl4Hash = regResults[3].domainHash; + + // revoke parent + await zns.rootRegistrar.connect(lvl4SubOwner).revokeDomain( + lvl4Hash, + ); + + const exists = await zns.registry.exists(lvl4Hash); + assert.ok(!exists); + + await expect( + zns.subRegistrar.connect(branchLvl2Owner).registerSubdomain( + lvl4Hash, + "newsubdomain", + branchLvl2Owner.address, + defaultTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); + }); + + // eslint-disable-next-line max-len + it("should allow setting a new config and start distributing subdomain when registering a previously revoked parent", async () => { + if (!await zns.registry.exists(regResults[1].domainHash)) { + await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: regResults[0].domainHash, + domainLabel: domainConfigs[1].domainLabel, + fullConfig: domainConfigs[1].fullConfig, + }); + } + + // revoke parent + await zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain(regResults[1].domainHash); + + expect(await zns.registry.exists(regResults[1].domainHash)).to.eq(false); + + // register again with new owner and config + const newHash = await registrationWithSetup({ + zns, + user: branchLvl1Owner, + parentHash: regResults[0].domainHash, + domainLabel: domainConfigs[1].domainLabel, + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.MINTLIST, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: branchLvl1Owner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: BigNumber.from(0) }, + }, + }); + + expect(newHash).to.eq(regResults[1].domainHash); + + // add new child owner to mintlist + await zns.subRegistrar.connect(branchLvl1Owner).updateMintlistForDomain( + newHash, + [ branchLvl2Owner.address ], + [ true ], + ); + + const parentOwnerFromReg = await zns.registry.getDomainOwner(newHash); + expect(parentOwnerFromReg).to.eq(branchLvl1Owner.address); + + const childBalBefore = await zns.zeroToken.balanceOf(branchLvl2Owner.address); + + // try register a new child under the new parent + const newChildHash = await registrationWithSetup({ + zns, + user: branchLvl2Owner, + parentHash: newHash, + domainLabel: "newchildddd", + fullConfig: fullDistrConfigEmpty, + }); + + const childBalAfter = await zns.zeroToken.balanceOf(branchLvl2Owner.address); + + // check that the new child has been registered + const childOwnerFromReg = await zns.registry.getDomainOwner(newChildHash); + expect(childOwnerFromReg).to.eq(branchLvl2Owner.address); + + const protocolFee = getStakingOrProtocolFee(fixedPrice); + + // make sure child payed based on the new parent config + expect(childBalBefore.sub(childBalAfter)).to.eq(fixedPrice.add(protocolFee)); + }); + }); + + describe("Token movements with different distr setups", () => { + let rootHash : string; + let fixedPrice : BigNumber; + let feePercentage : BigNumber; + let token2 : CustomDecimalTokenMock; + let token5 : CustomDecimalTokenMock; + let token8 : CustomDecimalTokenMock; + let token13 : CustomDecimalTokenMock; + let token18 : CustomDecimalTokenMock; + + const decimalValues = { + two: BigNumber.from(2), + five: BigNumber.from(5), + eight: BigNumber.from(8), + thirteen: BigNumber.from(13), + eighteen: BigNumber.from(18), + }; + + before(async () => { + [ + deployer, + zeroVault, + governor, + admin, + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + branchLvl1Owner, + branchLvl2Owner, + ] = await hre.ethers.getSigners(); + // zeroVault address is used to hold the fee charged to the user when registering + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address, governor.address], + adminAddresses: [admin.address], + priceConfig: priceConfigDefault, + zeroVaultAddress: zeroVault.address, + }); + + ([ + token2, + token5, + token8, + token13, + token18, + ] = await Object.values(decimalValues).reduce( + async (acc : Promise>, decimals) => { + const newAcc = await acc; + + const token = await deployCustomDecToken(deployer, decimals); + + return [...newAcc, token]; + }, Promise.resolve([]) + )); + + // Give funds to users + await Promise.all( + [ + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + branchLvl1Owner, + branchLvl2Owner, + ].map(async ({ address }) => + zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) + ); + await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + // register root domain + rootHash = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "root", + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: { + price: ethers.utils.parseEther("1375.612"), + feePercentage: BigNumber.from(0), + }, + }, + }); + }); + + it("FixedPricer - StakePayment - stake fee - 5 decimals", async () => { + const decimals = await token5.decimals(); + expect(decimals).to.eq(decimalValues.five); + + fixedPrice = parseUnits("1375.17", decimalValues.five); + feePercentage = BigNumber.from(200); + + const priceConfig = { + price: fixedPrice, + feePercentage, + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "fixedstake", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: token5.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const label = "fixedstakechild"; + + const { + expectedPrice, + stakeFee: stakeFee, + } = getPriceObject(label, priceConfig); + const protocolFee = getStakingOrProtocolFee( + expectedPrice.add(stakeFee) + ); + + // send future child some tokens + await token5.connect(deployer).transfer(lvl3SubOwner.address, expectedPrice.add(stakeFee).add(protocolFee)); + + const contractBalBefore = await token5.balanceOf(zns.treasury.address); + const parentBalBefore = await token5.balanceOf(lvl2SubOwner.address); + const childBalBefore = await token5.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await token5.balanceOf(zeroVault.address); + + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + fullConfig: fullDistrConfigEmpty, + }); + + const parentBalAfter = await token5.balanceOf(lvl2SubOwner.address); + const childBalAfter = await token5.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await token5.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await token5.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(stakeFee); + expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(stakeFee).add(protocolFee)); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should offer refund ! + const contractBalAfterRevoke = await token5.balanceOf(zns.treasury.address); + const childBalAfterRevoke = await token5.balanceOf(lvl3SubOwner.address); + const parentBalAfterRevoke = await token5.balanceOf(lvl2SubOwner.address); + const zeroVaultBalanceAfterRevoke = await token5.balanceOf(zeroVault.address); + + expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("FixedPricer - StakePayment - no fee - 18 decimals", async () => { + const priceConfig = { + price: parseUnits("397.77", decimalValues.eighteen), + feePercentage: BigNumber.from(0), + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "fixedstakenofee", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: token18.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const label = "fixedstakenofeechild"; + + const { expectedPrice } = getPriceObject(label, priceConfig); + const protocolFee = getStakingOrProtocolFee(expectedPrice); + + // send future child some tokens + await token18.connect(deployer).transfer( + lvl3SubOwner.address, + expectedPrice.add(protocolFee) + ); + + const contractBalBefore = await token18.balanceOf(zns.treasury.address); + const parentBalBefore = await token18.balanceOf(lvl2SubOwner.address); + const childBalBefore = await token18.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await token18.balanceOf(zeroVault.address); + + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const parentBalAfter = await token18.balanceOf(lvl2SubOwner.address); + const childBalAfter = await token18.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await token18.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await token18.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); + expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should offer refund ! + const contractBalAfterRevoke = await token18.balanceOf(zns.treasury.address); + const childBalAfterRevoke = await token18.balanceOf(lvl3SubOwner.address); + const parentBalAfterRevoke = await token18.balanceOf(lvl2SubOwner.address); + const zeroVaultBalanceAfterRevoke = await token18.balanceOf(zeroVault.address); + + expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("FixedPricer - DirectPayment - no fee - 8 decimals", async () => { + const priceConfig = { + price: parseUnits("11.371", decimalValues.eight), + feePercentage: BigNumber.from(0), + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "fixeddirectnofee", + fullConfig: { + distrConfig: { + paymentType: PaymentType.DIRECT, + pricerContract: zns.fixedPricer.address, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: token8.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const label = "fixeddirectnofeechild"; + const { expectedPrice } = getPriceObject(label, priceConfig); + const protocolFee = getStakingOrProtocolFee(expectedPrice); + + // send future child some tokens + await token8.connect(deployer).transfer( + lvl3SubOwner.address, + expectedPrice.add(protocolFee) + ); + + const parentBalBefore = await token8.balanceOf(lvl2SubOwner.address); + const childBalBefore = await token8.balanceOf(lvl3SubOwner.address); + const contractBalBefore = await token8.balanceOf(zns.treasury.address); + const zeroVaultBalanceBefore = await token8.balanceOf(zeroVault.address); + + + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + fullConfig: fullDistrConfigEmpty, + }); + + const parentBalAfter = await token8.balanceOf(lvl2SubOwner.address); + const childBalAfter = await token8.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await token8.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await token8.balanceOf(zeroVault.address); + + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(expectedPrice); + expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should NOT offer refund ! + const parentBalAfterRevoke = await token8.balanceOf(lvl2SubOwner.address); + const childBalAfterRevoke = await token8.balanceOf(lvl3SubOwner.address); + const contractBalAfterRevoke = await token8.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfterRevoke = await token8.balanceOf(zeroVault.address); + + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); + expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("CurvePricer - StakePayment - stake fee - 13 decimals", async () => { + const priceConfig = { + maxPrice: parseUnits("30000.93", decimalValues.thirteen), + minPrice: parseUnits("2000.11", decimalValues.thirteen), + maxLength: BigNumber.from(50), + baseLength: BigNumber.from(4), + precisionMultiplier: BigNumber.from(10).pow( + decimalValues.thirteen + .sub(precisionDefault) + ), + feePercentage: BigNumber.from(185), + isSet: true, + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "asympstake", + fullConfig: { + distrConfig: { + paymentType: PaymentType.STAKE, + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: token13.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const label = "curvestakechild"; + + const { + expectedPrice, + stakeFee: stakeFee, + } = getPriceObject(label, priceConfig); + const protocolFee = getStakingOrProtocolFee( + expectedPrice.add(stakeFee) + ); + + // send future child some tokens + await token13.connect(deployer).transfer( + lvl3SubOwner.address, + expectedPrice.add(stakeFee).add(protocolFee) + ); + + const contractBalBefore = await token13.balanceOf(zns.treasury.address); + const parentBalBefore = await token13.balanceOf(lvl2SubOwner.address); + const childBalBefore = await token13.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await token13.balanceOf(zeroVault.address); + + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + fullConfig: fullDistrConfigEmpty, + }); + + const contractBalAfter = await token13.balanceOf(zns.treasury.address); + const parentBalAfter = await token13.balanceOf(lvl2SubOwner.address); + const childBalAfter = await token13.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceAfter = await token13.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(stakeFee); + expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee).add(stakeFee)); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should offer refund ! + const contractBalAfterRevoke = await token13.balanceOf(zns.treasury.address); + const childBalAfterRevoke = await token13.balanceOf(lvl3SubOwner.address); + const parentBalAfterRevoke = await token13.balanceOf(lvl2SubOwner.address); + const zeroVaultBalanceAfterRevoke = await token13.balanceOf(zeroVault.address); + + expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("CurvePricer - StakePayment - no fee - 2 decimals", async () => { + const priceConfig = { + maxPrice: parseUnits("234.46", decimalValues.two), + minPrice: parseUnits("3.37", decimalValues.two), + maxLength: BigNumber.from(20), + baseLength: BigNumber.from(2), + precisionMultiplier: BigNumber.from(1), + feePercentage: BigNumber.from(0), + isSet: true, + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "curvestakenofee", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: token2.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const label = "curvestakenofeechild"; + + const { expectedPrice } = getPriceObject(label, priceConfig); + const protocolFee = getStakingOrProtocolFee(expectedPrice); + + // send future child some tokens + await token2.connect(deployer).transfer( + lvl3SubOwner.address, + expectedPrice.add(protocolFee) + ); + + const contractBalBefore = await token2.balanceOf(zns.treasury.address); + const parentBalBefore = await token2.balanceOf(lvl2SubOwner.address); + const childBalBefore = await token2.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await token2.balanceOf(zeroVault.address); + + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const contractBalAfter = await token2.balanceOf(zns.treasury.address); + const parentBalAfter = await token2.balanceOf(lvl2SubOwner.address); + const childBalAfter = await token2.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceAfter = await token2.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); + expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(expectedPrice); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should offer refund ! + const contractBalAfterRevoke = await token2.balanceOf(zns.treasury.address); + const childBalAfterRevoke = await token2.balanceOf(lvl3SubOwner.address); + const parentBalAfterRevoke = await token2.balanceOf(lvl2SubOwner.address); + const zeroVaultBalanceAfterRevoke = await token2.balanceOf(zeroVault.address); + + expect(contractBalAfter.sub(contractBalAfterRevoke)).to.eq(expectedPrice); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(expectedPrice); + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("CurvePricer - DirectPayment - no fee - 18 decimals", async () => { + const priceConfig = { + ...priceConfigDefault, + feePercentage: BigNumber.from(0), + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "curvedirectnofee", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + // zero has 18 decimals + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const label = "asdirectnofeechild"; + + const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); + const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); + + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); + + const { expectedPrice } = getPriceObject(label, priceConfig); + const protocolFee = getStakingOrProtocolFee(expectedPrice); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(expectedPrice); + expect(childBalBefore.sub(childBalAfter)).to.eq(expectedPrice.add(protocolFee)); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(protocolFee); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should NOT offer refund ! + const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); + expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("FixedPricer + DirectPayment with price = 0 - should NOT perform any transfers", async () => { + const priceConfig = { + price: BigNumber.from(0), + feePercentage: BigNumber.from(0), + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "zeroprice", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); + const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); + + const label = "zeropricechild"; + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); + expect(childBalBefore.sub(childBalAfter)).to.eq(0); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); + + // validate transfer events are not happenning + const latestBlock = await time.latestBlock(); + const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); + const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); + const transfersToParent = await zns.zeroToken.queryFilter( + transferFilterToParent, + latestBlock - 3, + latestBlock + ); + const transfersToTreasury = await zns.zeroToken.queryFilter( + transferFilterToTreasury, + latestBlock - 3, + latestBlock + ); + expect(transfersToParent.length).to.eq(0); + expect(transfersToTreasury.length).to.eq(0); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should NOT offer refund ! + const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); + expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("CurvePricer + DirectPayment with price = 0 - should NOT perform any transfers", async () => { + const priceConfig = { + ...priceConfigDefault, + maxPrice: BigNumber.from(0), + minPrice: BigNumber.from(0), + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "zeropricead", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); + const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); + + const label = "zeropricechildad"; + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); + expect(childBalBefore.sub(childBalAfter)).to.eq(0); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); + + // validate transfer events are not happenning + const latestBlock = await time.latestBlock(); + const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); + const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); + const transfersToParent = await zns.zeroToken.queryFilter( + transferFilterToParent, + latestBlock - 3, + latestBlock + ); + const transfersToTreasury = await zns.zeroToken.queryFilter( + transferFilterToTreasury, + latestBlock - 3, + latestBlock + ); + expect(transfersToParent.length).to.eq(0); + expect(transfersToTreasury.length).to.eq(0); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should NOT offer refund ! + const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); + expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("CurvePricer + StakePayment with price = 0 - should NOT perform any transfers", async () => { + const priceConfig = { + ...priceConfigDefault, + maxPrice: BigNumber.from(0), + minPrice: BigNumber.from(0), + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "zeropriceas", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); + const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); + + const label = "zeropricechildas"; + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); + expect(childBalBefore.sub(childBalAfter)).to.eq(0); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); + + // validate transfer events are not happenning + const latestBlock = await time.latestBlock(); + const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); + const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); + const transfersToParent = await zns.zeroToken.queryFilter( + transferFilterToParent, + latestBlock - 3, + latestBlock + ); + const transfersToTreasury = await zns.zeroToken.queryFilter( + transferFilterToTreasury, + latestBlock - 3, + latestBlock + ); + expect(transfersToParent.length).to.eq(0); + expect(transfersToTreasury.length).to.eq(0); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should NOT offer refund ! + const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); + expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("FixedPricer + StakePayment with price = 0 - should NOT perform any transfers", async () => { + const priceConfig = { + price: BigNumber.from(0), + // we are trying to set a feePercentage, but that should still result to 0 fee + // since fee is based on price + feePercentage: BigNumber.from(5), + }; + + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "zeropricefs", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig, + }, + }); + + const contractBalBefore = await zns.zeroToken.balanceOf(zns.treasury.address); + const parentBalBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalBefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const zeroVaultBalanceBefore = await zns.zeroToken.balanceOf(zeroVault.address); + + const label = "zeropricechildfs"; + const childHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const parentBalAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfter = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfter = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfter.sub(parentBalBefore)).to.eq(0); + expect(childBalBefore.sub(childBalAfter)).to.eq(0); + expect(contractBalAfter.sub(contractBalBefore)).to.eq(0); + expect(zeroVaultBalanceAfter.sub(zeroVaultBalanceBefore)).to.eq(0); + + // validate transfer events are not happenning + const latestBlock = await time.latestBlock(); + const transferFilterToParent = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, lvl2SubOwner.address); + const transferFilterToTreasury = zns.zeroToken.filters.Transfer(lvl3SubOwner.address, zns.treasury.address); + const transfersToParent = await zns.zeroToken.queryFilter( + transferFilterToParent, + latestBlock - 3, + latestBlock + ); + const transfersToTreasury = await zns.zeroToken.queryFilter( + transferFilterToTreasury, + latestBlock - 3, + latestBlock + ); + expect(transfersToParent.length).to.eq(0); + expect(transfersToTreasury.length).to.eq(0); + + // revoke + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + childHash, + ); + + // should NOT offer refund ! + const parentBalAfterRevoke = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + const childBalAfterRevoke = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + const contractBalAfterRevoke = await zns.zeroToken.balanceOf(zns.treasury.address); + const zeroVaultBalanceAfterRevoke = await zns.zeroToken.balanceOf(zeroVault.address); + + expect(parentBalAfterRevoke.sub(parentBalAfter)).to.eq(0); + expect(childBalAfterRevoke.sub(childBalAfter)).to.eq(0); + expect(contractBalAfterRevoke.sub(contractBalAfter)).to.eq(0); + expect(zeroVaultBalanceAfterRevoke.sub(zeroVaultBalanceAfter)).to.eq(0); + }); + + it("Setting price config in incorrect decimals triggers incorrect pricing", async () => { + // we will use token with 5 decimals, but set prices in 18 decimals + const priceConfigIncorrect = { + maxPrice: parseUnits("234.46", decimalValues.eighteen), + minPrice: parseUnits("3.37", decimalValues.eighteen), + maxLength: BigNumber.from(20), + baseLength: BigNumber.from(2), + precisionMultiplier: BigNumber.from(1), + feePercentage: BigNumber.from(111), + isSet: true, + }; + + // see `token` in paymentConfig + const subdomainParentHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: rootHash, + domainLabel: "incorrectparent", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + accessType: AccessType.OPEN, + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + // ! this token has 5 decimals ! + token: token5.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig: priceConfigIncorrect, + }, + }); + + const label = "incorrectchild"; + + const priceConfigCorrect = { + ...priceConfigIncorrect, + maxPrice: parseUnits("234.46", decimalValues.five), + minPrice: parseUnits("3.37", decimalValues.five), + }; + + // calc prices off-chain + const { + expectedPrice: priceIncorrect, + stakeFee: stakeFeeIncorrect, + } = getPriceObject(label, priceConfigIncorrect); + const protocolFeeIncorrect = getStakingOrProtocolFee(priceIncorrect.add(stakeFeeIncorrect)); + + const { + expectedPrice: priceCorrect, + stakeFee: stakeFeeCorrect, + } = getPriceObject(label, priceConfigCorrect); + const protocolFeeCorrect = getStakingOrProtocolFee(priceCorrect.add(stakeFeeCorrect)); + + // get prices from SC + const { + price: priceFromSC, + stakeFee: feeFromSC, + } = await zns.curvePricer.getPriceAndFee( + subdomainParentHash, + label, + true + ); + const protocolFeeFromSC = await zns.curvePricer.getFeeForPrice( + ethers.constants.HashZero, + priceFromSC.add(feeFromSC) + ); + + expect(priceFromSC).to.not.eq(priceCorrect); + expect(priceFromSC).to.eq(priceIncorrect); + expect(feeFromSC).to.not.eq(stakeFeeCorrect); + expect(feeFromSC).to.eq(stakeFeeIncorrect); + expect(protocolFeeFromSC).to.not.eq(protocolFeeCorrect); + expect(protocolFeeFromSC).to.eq(protocolFeeIncorrect); + + const priceDiff = priceIncorrect.sub(priceCorrect); + // the difference should be very large + expect(priceDiff).to.be.gt( + BigNumber.from(10).pow(decimalValues.eighteen) + ); + + // let's see how much a user actually paid + + // we sending him 10^20 tokens + await token5.connect(deployer).transfer( + lvl3SubOwner.address, + parseUnits("10000000000000000000", decimalValues.five) + ); + + // client tx approving the correct price will fail + await token5.connect(lvl3SubOwner).approve( + zns.treasury.address, + priceCorrect.add(stakeFeeCorrect).add(protocolFeeCorrect) + ); + + await expect( + zns.subRegistrar.registerSubdomain( + subdomainParentHash, + label, + lvl3SubOwner.address, + defaultTokenURI, + distrConfigEmpty, + ) + ).to.be.revertedWith("ERC20: insufficient allowance"); + + // let's try to buy with the incorrect price + const userBalanceBefore = await token5.balanceOf(lvl3SubOwner.address); + + await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: subdomainParentHash, + domainLabel: label, + }); + + const userBalanceAfter = await token5.balanceOf(lvl3SubOwner.address); + + // user should have paid the incorrect price + expect(userBalanceBefore.sub(userBalanceAfter)).to.eq( + priceIncorrect.add(stakeFeeIncorrect).add(protocolFeeIncorrect) + ); + }); + }); + + describe("Registration access", () => { + let fixedPrice : BigNumber; + let domainConfigs : Array; + let regResults : Array; + let fixedFeePercentage : BigNumber; + + before(async () => { + [ + deployer, + zeroVault, + governor, + admin, + operator, + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + ] = await hre.ethers.getSigners(); + // zeroVault address is used to hold the fee charged to the user when registering + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address, governor.address], + adminAddresses: [admin.address], + priceConfig: priceConfigDefault, + zeroVaultAddress: zeroVault.address, + }); + + fixedPrice = ethers.utils.parseEther("397"); + fixedFeePercentage = BigNumber.from(200); + + await Promise.all( + [ + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + ].map(async ({ address }) => + zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) + ); + await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + // register root domain and 1 subdomain + domainConfigs = [ + { + user: rootOwner, + domainLabel: "root", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }, + { + user: lvl2SubOwner, + domainLabel: "levelone", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }, + ]; + + regResults = await registerDomainPath({ + zns, + domainConfigs, + }); + }); + + it("should allow parent owner to register a subdomain under himself even if accessType is LOCKED", async () => { + await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( + regResults[1].domainHash, + AccessType.LOCKED, + ); + + const balBefore = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + + const hash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + parentHash: regResults[1].domainHash, + domainLabel: "ownercheck", + }); + + const latestBlock = await time.latestBlock(); + // look for an event where user pays himself + const filter = zns.zeroToken.filters.Transfer(lvl2SubOwner.address, lvl2SubOwner.address); + const events = await zns.zeroToken.queryFilter( + filter, + latestBlock - 50, + latestBlock + ); + // this means NO transfers have been executed, which is what we need + expect(events.length).to.eq(0); + + const balAfter = await zns.zeroToken.balanceOf(lvl2SubOwner.address); + // the diff is 0 because user should not pay himself + expect(balAfter.sub(balBefore)).to.eq(0); + + // check registry + const dataFromReg = await zns.registry.getDomainRecord(hash); + expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); + expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); + + // check domain token + const tokenId = BigNumber.from(hash).toString(); + const tokenOwner = await zns.domainToken.ownerOf(tokenId); + expect(tokenOwner).to.eq(lvl2SubOwner.address); + + // revert back to OPEN + await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( + regResults[1].domainHash, + AccessType.OPEN, + ); + }); + + it("should NOT allow others to register a domain when parent's accessType is LOCKED", async () => { + // register parent with locked access + const res = await registerDomainPath({ + zns, + domainConfigs: [ + { + user: lvl3SubOwner, + domainLabel: "leveltwo", + parentHash: regResults[1].domainHash, + // when we do not specify accessType or config, it defaults to LOCKED + // we can also set it as 0 specifically if setting a config + fullConfig: fullDistrConfigEmpty, + }, + ], + }); + + // try to register child + await expect( + zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( + res[0].domainHash, + "tobedenied", + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ) + ).to.be.revertedWith( + DISTRIBUTION_LOCKED_NOT_EXIST_ERR + ); + }); + + it("should allow anyone to register a domain when parent's accessType is OPEN", async () => { + const { domainHash: parentHash } = regResults[1]; + const domainLabel = "alloweded"; + + const { + expectedPrice, + } = getPriceObject( + domainLabel, + domainConfigs[1].fullConfig.priceConfig + ); + + const protocolFee = getStakingOrProtocolFee(expectedPrice); + // approve direct payment + await zns.zeroToken.connect(lvl5SubOwner).approve( + zns.treasury.address, + expectedPrice.add(protocolFee) + ); + + await zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( + parentHash, + domainLabel, + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ); + + const hash = await getDomainHashFromEvent({ + zns, + user: lvl5SubOwner, + }); + + // check registry + const dataFromReg = await zns.registry.getDomainRecord(hash); + expect(dataFromReg.owner).to.eq(lvl5SubOwner.address); + expect(dataFromReg.resolver).to.eq(ethers.constants.AddressZero); + + // check domain token + const tokenId = BigNumber.from(hash).toString(); + const tokenOwner = await zns.domainToken.ownerOf(tokenId); + expect(tokenOwner).to.eq(lvl5SubOwner.address); + }); + + // eslint-disable-next-line max-len + it("should ONLY allow mintlisted addresses and NOT allow other ones to register a domain when parent's accessType is MINTLIST", async () => { + // approve direct payment + await zns.zeroToken.connect(lvl3SubOwner).approve(zns.treasury.address, fixedPrice); + // register parent with mintlisted access + const parentHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: regResults[1].domainHash, + domainLabel: "mintlistparent", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.MINTLIST, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl3SubOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }); + + // mintlist potential child user + await zns.subRegistrar.connect(lvl3SubOwner).updateMintlistForDomain( + parentHash, + [lvl4SubOwner.address], + [true], + ); + + // register child + const hash = await registrationWithSetup({ + zns, + user: lvl4SubOwner, + parentHash, + domainLabel: "mintlisted", + }); + + // check registry + const dataFromReg = await zns.registry.getDomainRecord(hash); + expect(dataFromReg.owner).to.eq(lvl4SubOwner.address); + expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); + + // check domain token + const tokenId = BigNumber.from(hash).toString(); + const tokenOwner = await zns.domainToken.ownerOf(tokenId); + expect(tokenOwner).to.eq(lvl4SubOwner.address); + + // try to register child with non-mintlisted user + await expect( + zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( + parentHash, + "notmintlisted", + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: Sender is not approved for purchase" + ); + + // remove user from mintlist + await zns.subRegistrar.connect(lvl3SubOwner).updateMintlistForDomain( + parentHash, + [lvl4SubOwner.address], + [false], + ); + + // try to register again + await expect( + zns.subRegistrar.connect(lvl4SubOwner).registerSubdomain( + parentHash, + "notmintlistednow", + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: Sender is not approved for purchase" + ); + }); + + // eslint-disable-next-line max-len + it("#updateMintlistForDomain() should NOT allow setting if called by non-authorized account or registrar", async () => { + const { domainHash } = regResults[1]; + + // assign operator in registry + // to see that he CAN do it + await zns.registry.connect(lvl2SubOwner).setOwnersOperator( + operator.address, + true, + ); + + // try with operator + await zns.subRegistrar.connect(operator).updateMintlistForDomain( + domainHash, + [lvl5SubOwner.address], + [true], + ); + + const mintlisted = await zns.subRegistrar.isMintlistedForDomain( + domainHash, + lvl5SubOwner.address + ); + assert.ok(mintlisted, "User did NOT get mintlisted, but should've"); + + // try with non-authorized + await expect( + zns.subRegistrar.connect(lvl5SubOwner).updateMintlistForDomain( + domainHash, + [lvl5SubOwner.address], + [true], + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: Not authorized" + ); + }); + + it("#updateMintlistForDomain() should fire a #MintlistUpdated event with correct params", async () => { + const { domainHash } = regResults[1]; + + const candidatesArr = [ + lvl5SubOwner.address, + lvl6SubOwner.address, + lvl3SubOwner.address, + lvl4SubOwner.address, + ]; + + const allowedArr = [ + true, + true, + false, + true, + ]; + + await zns.subRegistrar.connect(lvl2SubOwner).updateMintlistForDomain( + domainHash, + candidatesArr, + allowedArr + ); + + const latestBlock = await time.latestBlock(); + const filter = zns.subRegistrar.filters.MintlistUpdated(domainHash); + const events = await zns.subRegistrar.queryFilter( + filter, + latestBlock - 3, + latestBlock + ); + const event = events[events.length - 1]; + + expect(event.args?.domainHash).to.eq(domainHash); + expect(event.args?.candidates).to.deep.eq(candidatesArr); + expect(event.args?.allowed).to.deep.eq(allowedArr); + }); + + it("should switch accessType for existing parent domain", async () => { + await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( + regResults[1].domainHash, + AccessType.LOCKED + ); + + await expect( + zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( + regResults[1].domainHash, + "notallowed", + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ) + ).to.be.revertedWith( + DISTRIBUTION_LOCKED_NOT_EXIST_ERR + ); + + // switch to mintlist + await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( + regResults[1].domainHash, + AccessType.MINTLIST + ); + + // add to mintlist + await zns.subRegistrar.connect(lvl2SubOwner).updateMintlistForDomain( + regResults[1].domainHash, + [lvl5SubOwner.address], + [true], + ); + + const label = "alloweddddd"; + + // approve + const { + expectedPrice, + stakeFee, + } = getPriceObject( + label, + domainConfigs[1].fullConfig.priceConfig + ); + const paymentToParent = domainConfigs[1].fullConfig.distrConfig.paymentType === PaymentType.STAKE + ? expectedPrice.add(stakeFee) + : expectedPrice; + + const protocolFee = getStakingOrProtocolFee(paymentToParent); + await zns.zeroToken.connect(lvl5SubOwner).approve( + zns.treasury.address, + paymentToParent.add(protocolFee) + ); + + // register + await zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( + regResults[1].domainHash, + "alloweddddd", + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ); + + const hash = await getDomainHashFromEvent({ + zns, + user: lvl5SubOwner, + }); + + // check registry + const dataFromReg = await zns.registry.getDomainRecord(hash); + expect(dataFromReg.owner).to.eq(lvl5SubOwner.address); + + // switch back to open + await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( + regResults[1].domainHash, + AccessType.OPEN + ); + }); + + // eslint-disable-next-line max-len + it("should NOT allow to register subdomains under the parent that hasn't set up his distribution config", async () => { + const parentHash = await registrationWithSetup({ + zns, + user: lvl3SubOwner, + parentHash: regResults[1].domainHash, + domainLabel: "parentnoconfig", + fullConfig: fullDistrConfigEmpty, // accessType is 0 when supplying empty config + }); + + await expect( + zns.subRegistrar.connect(lvl4SubOwner).registerSubdomain( + parentHash, + "notallowed", + ethers.constants.AddressZero, + defaultTokenURI, + distrConfigEmpty + ) + ).to.be.revertedWith( + DISTRIBUTION_LOCKED_NOT_EXIST_ERR + ); + }); + }); + + describe("Existing subdomain ops", () => { + let fixedPrice : BigNumber; + let domainConfigs : Array; + let regResults : Array; + let fixedFeePercentage : BigNumber; + + before(async () => { + [ + deployer, + zeroVault, + governor, + admin, + operator, + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + ] = await hre.ethers.getSigners(); + // zeroVault address is used to hold the fee charged to the user when registering + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address, governor.address], + adminAddresses: [admin.address], + priceConfig: priceConfigDefault, + zeroVaultAddress: zeroVault.address, + }); + + fixedPrice = ethers.utils.parseEther("397"); + fixedFeePercentage = BigNumber.from(200); + + await Promise.all( + [ + rootOwner, + lvl2SubOwner, + lvl3SubOwner, + lvl4SubOwner, + lvl5SubOwner, + lvl6SubOwner, + ].map(async ({ address }) => + zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) + ); + await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + // register root domain and 1 subdomain + domainConfigs = [ + { + user: rootOwner, + domainLabel: "root", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }, + { + user: lvl2SubOwner, + domainLabel: "leveltwo", + tokenURI: "http://example.com/leveltwo", + fullConfig: { + distrConfig: { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + priceConfig: { price: fixedPrice, feePercentage: fixedFeePercentage }, + }, + }, + { + user: lvl3SubOwner, + domainLabel: "lvlthree", + tokenURI: "http://example.com/lvlthree", + fullConfig: { + distrConfig: { + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl3SubOwner.address, + }, + priceConfig: priceConfigDefault, + }, + }, + ]; + + regResults = await registerDomainPath({ + zns, + domainConfigs, + }); + }); + + it("should NOT allow to register an existing subdomain that has not been revoked", async () => { + await expect( + zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + regResults[0].domainHash, + domainConfigs[1].domainLabel, + lvl2SubOwner.address, + defaultTokenURI, + domainConfigs[1].fullConfig.distrConfig + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: Subdomain already exists" + ); + }); + + it("should NOT allow revoking when the caller is NOT an owner of both Name and Token", async () => { + // change owner of the domain + await zns.registry.connect(lvl2SubOwner).updateDomainOwner( + regResults[1].domainHash, + rootOwner.address + ); + + // fail + await expect( + zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + regResults[1].domainHash, + ) + ).to.be.revertedWith( + "ZNSRootRegistrar: Not the owner of both Name and Token" + ); + + // change owner back + await zns.registry.connect(rootOwner).updateDomainOwner( + regResults[1].domainHash, + lvl2SubOwner.address + ); + + // tranfer token + await zns.domainToken.connect(lvl2SubOwner).transferFrom( + lvl2SubOwner.address, + lvl3SubOwner.address, + regResults[1].domainHash + ); + + // fail again + await expect( + zns.rootRegistrar.connect(lvl2SubOwner).revokeDomain( + regResults[1].domainHash, + ) + ).to.be.revertedWith( + "ZNSRootRegistrar: Not the owner of both Name and Token" + ); + + // give token back + await zns.domainToken.connect(lvl3SubOwner).transferFrom( + lvl3SubOwner.address, + lvl2SubOwner.address, + regResults[1].domainHash + ); + }); + + it("should allow to UPDATE domain data for subdomain", async () => { + const dataFromReg = await zns.registry.getDomainRecord(regResults[1].domainHash); + expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); + expect(dataFromReg.resolver).to.eq(zns.addressResolver.address); + + await zns.registry.connect(lvl2SubOwner).updateDomainRecord( + regResults[1].domainHash, + lvl3SubOwner.address, + ethers.constants.AddressZero, + ); + + const dataFromRegAfter = await zns.registry.getDomainRecord(regResults[1].domainHash); + expect(dataFromRegAfter.owner).to.eq(lvl3SubOwner.address); + expect(dataFromRegAfter.resolver).to.eq(ethers.constants.AddressZero); + + // reclaim to switch ownership back to original owner + await zns.rootRegistrar.connect(lvl2SubOwner).reclaimDomain( + regResults[1].domainHash, + ); + + const dataFromRegAfterReclaim = await zns.registry.getDomainRecord(regResults[1].domainHash); + expect(dataFromRegAfterReclaim.owner).to.eq(lvl2SubOwner.address); + expect(dataFromRegAfterReclaim.resolver).to.eq(ethers.constants.AddressZero); + }); + + describe("#setDistributionConfigForDomain()", () => { + it("should re-set distribution config for an existing subdomain", async () => { + const domainHash = regResults[2].domainHash; + + const distrConfigBefore = await zns.subRegistrar.distrConfigs(domainHash); + expect(distrConfigBefore.accessType).to.not.eq(AccessType.MINTLIST); + expect(distrConfigBefore.pricerContract).to.not.eq(zns.fixedPricer.address); + expect( + distrConfigBefore.paymentType + ).to.not.eq( + PaymentType.STAKE + ); + + const newConfig = { + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.MINTLIST, + }; + + await zns.subRegistrar.connect(lvl3SubOwner).setDistributionConfigForDomain( + domainHash, + newConfig, + ); + + const distrConfigAfter = await zns.subRegistrar.distrConfigs(domainHash); + expect(distrConfigAfter.accessType).to.eq(newConfig.accessType); + expect(distrConfigAfter.pricerContract).to.eq(newConfig.pricerContract); + expect(distrConfigAfter.paymentType).to.eq(newConfig.paymentType); + + // assign operator in registry + await zns.registry.connect(lvl3SubOwner).setOwnersOperator( + operator.address, + true, + ); + + // reset it back + await zns.subRegistrar.connect(operator).setDistributionConfigForDomain( + domainHash, + domainConfigs[2].fullConfig.distrConfig, + ); + const origConfigAfter = await zns.subRegistrar.distrConfigs(domainHash); + expect(origConfigAfter.accessType).to.eq(domainConfigs[2].fullConfig.distrConfig.accessType); + expect(origConfigAfter.pricerContract).to.eq(domainConfigs[2].fullConfig.distrConfig.pricerContract); + expect( + origConfigAfter.paymentType + ).to.eq( + domainConfigs[2].fullConfig.distrConfig.paymentType + ); + + // remove operator + await zns.registry.connect(lvl3SubOwner).setOwnersOperator( + operator.address, + false, + ); + }); + + it("should NOT allow to set distribution config for a non-authorized account", async () => { + const domainHash = regResults[1].domainHash; + + const newConfig = { + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.STAKE, + accessType: AccessType.MINTLIST, + }; + + await expect( + zns.subRegistrar.connect(lvl3SubOwner).setDistributionConfigForDomain( + domainHash, + newConfig, + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: Not authorized" + ); + }); + + it("should revert if pricerContract is passed as 0x0 address", async () => { + const domainHash = regResults[2].domainHash; + + const newConfig = { + pricerContract: ethers.constants.AddressZero, + paymentType: PaymentType.STAKE, + accessType: AccessType.MINTLIST, + }; + + await expect( + zns.subRegistrar.connect(lvl3SubOwner).setDistributionConfigForDomain( + domainHash, + newConfig, + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: pricerContract can not be 0x0 address" + ); + }); + }); + + describe("#setPricerContractForDomain()", () => { + it("should re-set pricer contract for an existing subdomain", async () => { + const domainHash = regResults[2].domainHash; + + const pricerContractBefore = await zns.subRegistrar.distrConfigs(domainHash); + expect(pricerContractBefore.pricerContract).to.eq(domainConfigs[2].fullConfig.distrConfig.pricerContract); + + await zns.subRegistrar.connect(lvl3SubOwner).setPricerContractForDomain( + domainHash, + zns.curvePricer.address, + ); + + const pricerContractAfter = await zns.subRegistrar.distrConfigs(domainHash); + expect(pricerContractAfter.pricerContract).to.eq(zns.curvePricer.address); + + // reset it back + await zns.subRegistrar.connect(lvl3SubOwner).setPricerContractForDomain( + domainHash, + domainConfigs[2].fullConfig.distrConfig.pricerContract, + ); + }); + + it("should NOT allow setting for non-authorized account", async () => { + const domainHash = regResults[2].domainHash; + + await expect( + zns.subRegistrar.connect(lvl2SubOwner).setPricerContractForDomain( + domainHash, + zns.curvePricer.address, + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: Not authorized" + ); + }); + + it("should NOT set pricerContract to 0x0 address", async () => { + const domainHash = regResults[2].domainHash; + + await expect( + zns.subRegistrar.connect(lvl3SubOwner).setPricerContractForDomain( + domainHash, + ethers.constants.AddressZero, + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: pricerContract can not be 0x0 address" + ); + }); + }); + + describe("#setPaymentTypeForDomain()", () => { + it("should re-set payment type for an existing subdomain", async () => { + const domainHash = regResults[2].domainHash; + + const { paymentType: paymentTypeBefore } = await zns.subRegistrar.distrConfigs(domainHash); + expect(paymentTypeBefore).to.eq(domainConfigs[2].fullConfig.distrConfig.paymentType); + + await zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( + domainHash, + PaymentType.STAKE, + ); + + const { paymentType: paymentTypeAfter } = await zns.subRegistrar.distrConfigs(domainHash); + expect(paymentTypeAfter).to.eq(PaymentType.STAKE); + + // reset it back + await zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( + domainHash, + domainConfigs[2].fullConfig.distrConfig.paymentType, + ); + }); + + it("should NOT allow setting for non-authorized account", async () => { + const domainHash = regResults[2].domainHash; + + await expect( + zns.subRegistrar.connect(lvl2SubOwner).setPaymentTypeForDomain( + domainHash, + PaymentType.STAKE, + ) + ).to.be.revertedWith( + "ZNSSubRegistrar: Not authorized" + ); + }); + + it("should emit #PaymentTypeSet event with correct params", async () => { + const domainHash = regResults[2].domainHash; + + await expect( + zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( + domainHash, + PaymentType.STAKE, + ) + ).to.emit(zns.subRegistrar, "PaymentTypeSet").withArgs( + domainHash, + PaymentType.STAKE, + ); + + // reset back + await zns.subRegistrar.connect(lvl3SubOwner).setPaymentTypeForDomain( + domainHash, + domainConfigs[2].fullConfig.distrConfig.paymentType, + ); + }); + }); + + // eslint-disable-next-line max-len + it("should TRANSFER ownership of a subdomain and let the receiver RECLAIM and then revoke with REFUND", async () => { + const tokenId = BigNumber.from(regResults[1].domainHash).toString(); + + const { amount: stakedBefore } = await zns.treasury.stakedForDomain(regResults[1].domainHash); + + await zns.domainToken.connect(lvl2SubOwner).transferFrom( + lvl2SubOwner.address, + lvl3SubOwner.address, + tokenId + ); + + // Verify owner in registry + const dataFromReg = await zns.registry.getDomainRecord(regResults[1].domainHash); + expect(dataFromReg.owner).to.eq(lvl2SubOwner.address); + + // reclaim + await zns.rootRegistrar.connect(lvl3SubOwner).reclaimDomain( + regResults[1].domainHash, + ); + + // Verify domain token is still owned + const tokenOwner = await zns.domainToken.ownerOf(tokenId); + expect(tokenOwner).to.eq(lvl3SubOwner.address); + + // Verify owner in registry + const dataFromRegAfter = await zns.registry.getDomainRecord(regResults[1].domainHash); + expect(dataFromRegAfter.owner).to.eq(lvl3SubOwner.address); + + // verify stake still existing + const { amount: stakedAfter } = await zns.treasury.stakedForDomain(regResults[1].domainHash); + expect(stakedAfter).to.eq(stakedBefore); + + const userBalbefore = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + + // try revoking + await zns.rootRegistrar.connect(lvl3SubOwner).revokeDomain( + regResults[1].domainHash, + ); + + // verify that refund has been acquired by the new owner + const userBalAfter = await zns.zeroToken.balanceOf(lvl3SubOwner.address); + expect(userBalAfter.sub(userBalbefore)).to.eq(fixedPrice); + }); + }); + + describe("State setters", () => { + before(async () => { + [ + deployer, + admin, + random, + ] = await hre.ethers.getSigners(); + + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address], + adminAddresses: [admin.address], + }); + }); + + it("Should NOT let initialize the implementation contract", async () => { + const factory = new ZNSSubRegistrar__factory(deployer); + const impl = await getProxyImplAddress(zns.subRegistrar.address); + const implContract = factory.attach(impl); + + await expect( + implContract.initialize( + deployer.address, + deployer.address, + deployer.address, + ) + ).to.be.revertedWith(INITIALIZED_ERR); + }); + + it("#setRootRegistrar() should set the new root registrar correctly and emit #RootRegistrarSet event", async () => { + const tx = await zns.subRegistrar.connect(admin).setRootRegistrar(random.address); + + await expect(tx).to.emit(zns.subRegistrar, "RootRegistrarSet").withArgs(random.address); + + expect(await zns.subRegistrar.rootRegistrar()).to.equal(random.address); + }); + + it("#setRootRegistrar() should NOT be callable by anyone other than ADMIN_ROLE", async () => { + await expect( + zns.subRegistrar.connect(random).setRootRegistrar(random.address), + ).to.be.revertedWith( + getAccessRevertMsg(random.address, ADMIN_ROLE), + ); + }); + + it("#setRootRegistrar should NOT set registrar as 0x0 address", async () => { + await expect( + zns.subRegistrar.connect(admin).setRootRegistrar(ethers.constants.AddressZero), + ).to.be.revertedWith( + "ZNSSubRegistrar: _registrar can not be 0x0 address", + ); + }); + + it("#setRegistry() should set the new registry correctly and emit #RegistrySet event", async () => { + const tx = await zns.subRegistrar.connect(admin).setRegistry(random.address); + + await expect(tx).to.emit(zns.subRegistrar, "RegistrySet").withArgs(random.address); + + expect(await zns.subRegistrar.registry()).to.equal(random.address); + }); + + it("#setRegistry() should not be callable by anyone other than ADMIN_ROLE", async () => { + await expect( + zns.subRegistrar.connect(random).setRegistry(random.address), + ).to.be.revertedWith( + getAccessRevertMsg(random.address, ADMIN_ROLE), + ); + }); + + it("#setAccessController() should not be callable by anyone other than ADMIN_ROLE", async () => { + await expect( + zns.subRegistrar.connect(random).setAccessController(random.address), + ).to.be.revertedWith( + getAccessRevertMsg(random.address, ADMIN_ROLE), + ); + }); + + it("#getAccessController() should return the correct access controller", async () => { + expect( + await zns.subRegistrar.getAccessController() + ).to.equal(zns.accessController.address); + }); + + // eslint-disable-next-line max-len + it("#setAccessController() should set the new access controller correctly and emit #AccessControllerSet event", async () => { + const tx = await zns.subRegistrar.connect(admin).setAccessController(random.address); + + await expect(tx).to.emit(zns.subRegistrar, "AccessControllerSet").withArgs(random.address); + + expect(await zns.subRegistrar.getAccessController()).to.equal(random.address); + }); + }); + + describe("UUPS", () => { + let fixedPrice : BigNumber; + let rootHash : string; + + beforeEach(async () => { + [ + deployer, + zeroVault, + governor, + admin, + rootOwner, + lvl2SubOwner, + ] = await hre.ethers.getSigners(); + // zeroVault address is used to hold the fee charged to the user when registering + zns = await deployZNS({ + deployer, + governorAddresses: [deployer.address, governor.address], + adminAddresses: [admin.address], + priceConfig: priceConfigDefault, + zeroVaultAddress: zeroVault.address, + }); + + // Give funds to users + await Promise.all( + [ + rootOwner, + lvl2SubOwner, + ].map(async ({ address }) => + zns.zeroToken.mint(address, ethers.utils.parseEther("1000000"))) + ); + await zns.zeroToken.connect(rootOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + + fixedPrice = ethers.utils.parseEther("397.13"); + // register root domain + rootHash = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "root", + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: rootOwner.address, + }, + priceConfig: { + price: fixedPrice, + feePercentage: BigNumber.from(0), + }, + }, + }); + }); + + it("Allows an authorized user to upgrade the contract", async () => { + // SubRegistrar to upgrade to + const factory = new ZNSSubRegistrarUpgradeMock__factory(deployer); + const newRegistrar = await factory.deploy(); + await newRegistrar.deployed(); + + // Confirm the deployer is a governor, as set in `deployZNS` helper + await expect(zns.accessController.checkGovernor(deployer.address)).to.not.be.reverted; + + const tx = zns.subRegistrar.connect(deployer).upgradeTo(newRegistrar.address); + await expect(tx).to.not.be.reverted; + + await expect( + zns.subRegistrar.connect(deployer).initialize( + zns.accessController.address, + zns.registry.address, + zns.rootRegistrar.address, + ) + ).to.be.revertedWith(INITIALIZED_ERR); + }); + + it("Fails to upgrade if the caller is not authorized", async () => { + // SubRegistrar to upgrade to + const factory = new ZNSSubRegistrarUpgradeMock__factory(deployer); + const newRegistrar = await factory.deploy(); + await newRegistrar.deployed(); + + // Confirm the account is not a governor + await expect(zns.accessController.checkGovernor(lvl2SubOwner.address)).to.be.reverted; + + const tx = zns.subRegistrar.connect(lvl2SubOwner).upgradeTo(newRegistrar.address); + + await expect(tx).to.be.revertedWith( + getAccessRevertMsg(lvl2SubOwner.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 ZNSSubRegistrarUpgradeMock__factory(deployer); + const registrar = await registrarFactory.deploy(); + await registrar.deployed(); + + const domainLabel = "world"; + + await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, ethers.constants.MaxUint256); + await zns.zeroToken.mint(lvl2SubOwner.address, parseEther("1000000")); + + const domainHash = await registrationWithSetup({ + zns, + user: lvl2SubOwner, + domainLabel, + parentHash: rootHash, + fullConfig: { + distrConfig: { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + }, + priceConfig: { + price: fixedPrice, + feePercentage: BigNumber.from(0), + }, + paymentConfig: { + token: zns.zeroToken.address, + beneficiary: lvl2SubOwner.address, + }, + }, + }); + + await zns.subRegistrar.setRootRegistrar(lvl2SubOwner.address); + + const contractCalls = [ + zns.subRegistrar.getAccessController(), + zns.subRegistrar.registry(), + zns.subRegistrar.rootRegistrar(), + zns.registry.exists(domainHash), + zns.treasury.stakedForDomain(domainHash), + zns.domainToken.name(), + zns.domainToken.symbol(), + zns.fixedPricer.getPrice(rootHash, domainLabel, true), + ]; + + await validateUpgrade(deployer, zns.subRegistrar, registrar, registrarFactory, contractCalls); + }); + + it("Allows to add more fields to the existing struct in a mapping", async () => { + // SubRegistrar to upgrade to + const factory = new ZNSSubRegistrarUpgradeMock__factory(deployer); + const newRegistrar = await factory.deploy(); + await newRegistrar.deployed(); + + const tx = zns.subRegistrar.connect(deployer).upgradeTo(newRegistrar.address); + await expect(tx).to.not.be.reverted; + + // create new proxy object + const newRegistrarProxy = factory.attach(zns.subRegistrar.address); + + // check values in storage + const rootConfigBefore = await newRegistrarProxy.distrConfigs(rootHash); + expect(rootConfigBefore.accessType).to.eq(AccessType.OPEN); + expect(rootConfigBefore.pricerContract).to.eq(zns.fixedPricer.address); + expect(rootConfigBefore.paymentType).to.eq(PaymentType.DIRECT); + + await zns.zeroToken.mint(lvl2SubOwner.address, parseEther("1000000")); + await zns.zeroToken.connect(lvl2SubOwner).approve(zns.treasury.address, parseEther("1000000")); + + const subConfigToSet = { + accessType: AccessType.MINTLIST, + pricerContract: zns.curvePricer.address, + paymentType: PaymentType.STAKE, + newAddress: lvl2SubOwner.address, + newUint: BigNumber.from(1912171236), + }; + + // register a subdomain with new logic + await newRegistrarProxy.connect(lvl2SubOwner).registerSubdomain( + rootHash, + "subbb", + lvl2SubOwner.address, + defaultTokenURI, + subConfigToSet + ); + + const subHash = await getDomainHashFromEvent({ + zns, + user: lvl2SubOwner, + }); + + const rootConfigAfter = await zns.subRegistrar.distrConfigs(rootHash); + expect(rootConfigAfter.accessType).to.eq(rootConfigBefore.accessType); + expect(rootConfigAfter.pricerContract).to.eq(rootConfigBefore.pricerContract); + expect(rootConfigAfter.paymentType).to.eq(rootConfigBefore.paymentType); + expect(rootConfigAfter.length).to.eq(3); + + const updatedStructConfig = { + accessType: AccessType.OPEN, + pricerContract: zns.fixedPricer.address, + paymentType: PaymentType.DIRECT, + newAddress: lvl2SubOwner.address, + newUint: BigNumber.from(123), + }; + + // try setting new fields to the new struct + await newRegistrarProxy.connect(rootOwner).setDistributionConfigForDomain( + rootHash, + updatedStructConfig + ); + + // check what we got for new + const rootConfigFinal = await newRegistrarProxy.distrConfigs(rootHash); + const subConfigAfter = await newRegistrarProxy.distrConfigs(subHash); + + // validate the new config has been set correctly + expect(subConfigAfter.accessType).to.eq(subConfigToSet.accessType); + expect(subConfigAfter.pricerContract).to.eq(subConfigToSet.pricerContract); + expect(subConfigAfter.paymentType).to.eq(subConfigToSet.paymentType); + expect(subConfigAfter.newAddress).to.eq(subConfigToSet.newAddress); + expect(subConfigAfter.newUint).to.eq(subConfigToSet.newUint); + + // validate the old values stayed the same and new values been added + expect(rootConfigFinal.accessType).to.eq(rootConfigBefore.accessType); + expect(rootConfigFinal.pricerContract).to.eq(rootConfigBefore.pricerContract); + expect(rootConfigFinal.paymentType).to.eq(rootConfigBefore.paymentType); + expect(rootConfigFinal.newAddress).to.eq(updatedStructConfig.newAddress); + expect(rootConfigFinal.newUint).to.eq(updatedStructConfig.newUint); + + // check that crucial state vars stayed the same + expect(await newRegistrarProxy.getAccessController()).to.eq(zns.accessController.address); + expect(await newRegistrarProxy.registry()).to.eq(zns.registry.address); + expect(await newRegistrarProxy.rootRegistrar()).to.eq(zns.rootRegistrar.address); + }); + }); +}); diff --git a/test/ZNSTreasury.test.ts b/test/ZNSTreasury.test.ts index ffc30a540..0c2304d17 100644 --- a/test/ZNSTreasury.test.ts +++ b/test/ZNSTreasury.test.ts @@ -5,7 +5,9 @@ import { checkBalance, defaultTokenURI, deployTreasury, deployZNS, distrConfigEmpty, - getPriceObject, NO_BENEFICIARY_ERR, NOT_AUTHORIZED_REG_WIRED_ERR, + getPriceObject, + NO_BENEFICIARY_ERR, + NOT_AUTHORIZED_REG_WIRED_ERR, INITIALIZED_ERR, priceConfigDefault, validateUpgrade, @@ -143,7 +145,8 @@ describe("ZNSTreasury", () => { const expectedStake = await zns.curvePricer.getPrice( ethers.constants.HashZero, - domainName + domainName, + false ); const fee = await zns.curvePricer.getFeeForPrice(ethers.constants.HashZero, expectedStake); diff --git a/test/gas/gas-costs.json b/test/gas/gas-costs.json index 52b8fd0d0..a6a1bc9c4 100644 --- a/test/gas/gas-costs.json +++ b/test/gas/gas-costs.json @@ -1,4 +1,4 @@ { - "Root Domain Price": "425483", - "Subdomain Price": "419495" + "Root Domain Price": "427687", + "Subdomain Price": "423989" } \ No newline at end of file diff --git a/test/helpers/errors.ts b/test/helpers/errors.ts index ca8e96d03..fa371d16c 100644 --- a/test/helpers/errors.ts +++ b/test/helpers/errors.ts @@ -1,36 +1,40 @@ -export const getAccessRevertMsg = (addr : string, role : string) : string => - `AccessControl: account ${addr.toLowerCase()} is missing role ${role}`; - -// revert messages -// When adding a revert test, check if this message is already present in other tests -// if it is, add a new constant here and use it in all tests - -// ZNSCurvePricer.sol -export const MULTIPLIER_BELOW_MIN_ERR = "ZNSCurvePricer: Multiplier must be >= baseLength + 1"; -export const NO_ZERO_MULTIPLIER_ERR = "ZNSCurvePricer: Multiplier cannot be 0"; -export const MULTIPLIER_OUT_OF_RANGE_ORA_ERR = "ZNSCurvePricer: Multiplier out of range"; -export const CURVE_NO_ZERO_PRECISION_MULTIPLIER_ERR = "ZNSCurvePricer: precisionMultiplier cannot be 0"; -export const CURVE_PRICE_CONFIG_ERR = "ZNSCurvePricer: incorrect value set causes the price spike at maxLength."; - -// ZNSRegistry -export const ONLY_NAME_OWNER_REG_ERR = "ZNSRegistry: Not the Name Owner"; -export const ONLY_OWNER_REGISTRAR_REG_ERR = "ZNSRegistry: Only Name Owner or Registrar allowed to call"; -export const NOT_AUTHORIZED_REG_WIRED_ERR = "ARegistryWired: Not authorized. Only Owner or Operator allowed"; -export const NOT_AUTHORIZED_REG_ERR = "ZNSRegistry: Not authorized"; -export const OWNER_NOT_ZERO_REG_ERR = "ZNSRegistry: Owner cannot be zero address"; - -// ZNSRootRegistrar.sol -export const NOT_NAME_OWNER_RAR_ERR = "ZNSRootRegistrar: Not the owner of the Name"; -export const NOT_TOKEN_OWNER_RAR_ERR = "ZNSRootRegistrar: Not the owner of the Token"; -export const NOT_BOTH_OWNER_RAR_ERR = "ZNSRootRegistrar: Not the owner of both Name and Token"; - -// Subdomain Registrar -// eslint-disable-next-line max-len -export const DISTRIBUTION_LOCKED_NOT_EXIST_ERR = "ZNSSubRegistrar: Parent domain's distribution is locked or parent does not exist"; - -// Treasury -export const NO_BENEFICIARY_ERR = "ZNSTreasury: parent domain has no beneficiary set"; - -// OpenZeppelin -export const INVALID_TOKENID_ERC_ERR = "ERC721: invalid token ID"; -export const INITIALIZED_ERR = "Initializable: contract is already initialized"; +export const getAccessRevertMsg = (addr : string, role : string) : string => + `AccessControl: account ${addr.toLowerCase()} is missing role ${role}`; + +// revert messages +// When adding a revert test, check if this message is already present in other tests +// if it is, add a new constant here and use it in all tests + +// ZNSCurvePricer.sol +export const MULTIPLIER_BELOW_MIN_ERR = "ZNSCurvePricer: Multiplier must be >= baseLength + 1"; +export const NO_ZERO_MULTIPLIER_ERR = "ZNSCurvePricer: Multiplier cannot be 0"; +export const MULTIPLIER_OUT_OF_RANGE_ORA_ERR = "ZNSCurvePricer: Multiplier out of range"; +export const CURVE_NO_ZERO_PRECISION_MULTIPLIER_ERR = "ZNSCurvePricer: precisionMultiplier cannot be 0"; +export const CURVE_PRICE_CONFIG_ERR = "ZNSCurvePricer: incorrect value set causes the price spike at maxLength."; + +// ZNSRegistry +export const ONLY_NAME_OWNER_REG_ERR = "ZNSRegistry: Not the Name Owner"; +export const ONLY_OWNER_REGISTRAR_REG_ERR = "ZNSRegistry: Only Name Owner or Registrar allowed to call"; +export const NOT_AUTHORIZED_REG_WIRED_ERR = "ARegistryWired: Not authorized. Only Owner or Operator allowed"; +export const NOT_AUTHORIZED_REG_ERR = "ZNSRegistry: Not authorized"; +export const OWNER_NOT_ZERO_REG_ERR = "ZNSRegistry: Owner cannot be zero address"; + +// ZNSRootRegistrar.sol +export const NOT_NAME_OWNER_RAR_ERR = "ZNSRootRegistrar: Not the owner of the Name"; +export const NOT_TOKEN_OWNER_RAR_ERR = "ZNSRootRegistrar: Not the owner of the Token"; +export const NOT_BOTH_OWNER_RAR_ERR = "ZNSRootRegistrar: Not the owner of both Name and Token"; + +// Subdomain Registrar +// eslint-disable-next-line max-len +export const DISTRIBUTION_LOCKED_NOT_EXIST_ERR = "ZNSSubRegistrar: Parent domain's distribution is locked or parent does not exist"; + +// StringUtils +export const INVALID_NAME_ERR = "StringUtils: Invalid domain label"; +export const INVALID_LENGTH_ERR = "StringUtils: Domain label too long or nonexistent"; + +// Treasury +export const NO_BENEFICIARY_ERR = "ZNSTreasury: parent domain has no beneficiary set"; + +// OpenZeppelin +export const INVALID_TOKENID_ERC_ERR = "ERC721: invalid token ID"; +export const INITIALIZED_ERR = "Initializable: contract is already initialized"; diff --git a/test/helpers/flows/registration.ts b/test/helpers/flows/registration.ts index d98341851..0367805d5 100644 --- a/test/helpers/flows/registration.ts +++ b/test/helpers/flows/registration.ts @@ -157,7 +157,7 @@ export const validatePathRegistration = async ({ ({ price: expectedPrice, fee: stakeFee, - } = await zns.fixedPricer.getPriceAndFee(parentHashFound, domainLabel)); + } = await zns.fixedPricer.getPriceAndFee(parentHashFound, domainLabel, false)); } else { const { maxPrice, diff --git a/test/helpers/register-setup.ts b/test/helpers/register-setup.ts index 186fb3e92..0b9f560e7 100644 --- a/test/helpers/register-setup.ts +++ b/test/helpers/register-setup.ts @@ -54,9 +54,9 @@ export const approveForParent = async ({ let price = BigNumber.from(0); let parentFee = BigNumber.from(0); if (pricerContract === zns.curvePricer.address) { - [price, parentFee] = await zns.curvePricer.getPriceAndFee(parentHash, domainLabel); + [price, parentFee] = await zns.curvePricer.getPriceAndFee(parentHash, domainLabel, false); } else if (pricerContract === zns.fixedPricer.address) { - [price, parentFee] = await zns.fixedPricer.getPriceAndFee(parentHash, domainLabel); + [price, parentFee] = await zns.fixedPricer.getPriceAndFee(parentHash, domainLabel, false); } const { token: tokenAddress } = await zns.treasury.paymentConfigs(parentHash);