diff --git a/.gitignore b/.gitignore index a9de5f05..f55a0d81 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ docker*.tgz # We don't ever use the generated manifests .openzeppelin +/.vscode diff --git a/contracts/price/IZNSCurvePricer.sol b/contracts/price/IZNSCurvePricer.sol index f1ff10fa..0551a4c2 100644 --- a/contracts/price/IZNSCurvePricer.sol +++ b/contracts/price/IZNSCurvePricer.sol @@ -11,18 +11,7 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer { * @notice Reverted when multiplier passed by the domain owner * is equal to 0 or more than 10^18, which is too large. */ - error InvalidMultiplierPassed(uint256 multiplier); - - /** - * @notice Reverted when `priceConfig` set by the owner does not result in a proper asymptotic curve - * and one of it's incorrect values causes the price spike at maxLength, meaning that the price - * for a domain label shorter than `baseLength` (the one before `minPrice`) becomes higher than `minPrice`. - */ - error InvalidConfigCausingPriceSpikes( - bytes32 configsDomainHash, - uint256 minPrice, - uint256 previousToMinPrice - ); + error InvalidPrecisionMultiplierPassed(bytes32 domainHash); /** * @notice Emitted when the `maxPrice` is set in `CurvePriceConfig` @@ -30,12 +19,6 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer { */ event MaxPriceSet(bytes32 domainHash, uint256 price); - /** - * @notice Emitted when the `minPrice` is set in `CurvePriceConfig` - * @param price The new minPrice value - */ - event MinPriceSet(bytes32 domainHash, uint256 price); - /** * @notice Emitted when the `baseLength` is set in `CurvePriceConfig` * @param length The new baseLength value @@ -60,10 +43,17 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer { */ event FeePercentageSet(bytes32 domainHash, uint256 feePercentage); + /** + * @notice Emitted when the `curveMultiplier` is set in state + * @param curveMultiplier The new curveMultiplier value + */ + event CurveMultiplierSet(bytes32 domainHash, uint256 curveMultiplier); + + /** * @notice Emitted when the full `CurvePriceConfig` is set in state * @param maxPrice The new `maxPrice` value - * @param minPrice The new `minPrice` value + * @param curveMultiplier The new `curveMultiplier` value * @param maxLength The new `maxLength` value * @param baseLength The new `baseLength` value * @param precisionMultiplier The new `precisionMultiplier` value @@ -71,7 +61,7 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer { event PriceConfigSet( bytes32 domainHash, uint256 maxPrice, - uint256 minPrice, + uint256 curveMultiplier, uint256 maxLength, uint256 baseLength, uint256 precisionMultiplier, @@ -111,12 +101,12 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer { function setMaxPrice(bytes32 domainHash, uint256 maxPrice) external; - function setMinPrice(bytes32 domainHash, uint256 minPrice) external; - function setBaseLength(bytes32 domainHash, uint256 length) external; function setMaxLength(bytes32 domainHash, uint256 length) external; + function setCurveMultiplier(bytes32 domainHash, uint256 curveMultiplier) external; + function setPrecisionMultiplier(bytes32 domainHash, uint256 multiplier) external; function setFeePercentage(bytes32 domainHash, uint256 feePercentage) external; diff --git a/contracts/price/ZNSCurvePricer.sol b/contracts/price/ZNSCurvePricer.sol index 3524261e..87a06742 100644 --- a/contracts/price/ZNSCurvePricer.sol +++ b/contracts/price/ZNSCurvePricer.sol @@ -7,15 +7,17 @@ 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`. + * This module uses an hyperbolic curve that starts at (`baseLength`; `maxPrice`) + * for all domains <= `baseLength`. + * Then the price is reduced using the price calculation function below. + * The price after `maxLength` is fixed and equals the price on the hyperbola graph at the point `maxLength` + * and is determined using the formula where `length` = `maxLength`. */ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSCurvePricer { + using StringUtils for string; /** @@ -24,6 +26,13 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I */ uint256 public constant PERCENTAGE_BASIS = 10000; + /** + * @notice Multiply the entire hyperbola formula by this number to be able to reduce the `curveMultiplier` + * by 3 digits, which gives us more flexibility in defining the hyperbola function. + * @dev > Canot be "0". + */ + uint256 public constant FACTOR_SCALE = 1000; + /** * @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. @@ -41,7 +50,6 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I * @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. @@ -122,9 +130,9 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I /** * @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, + * @dev Validates the value of the `precisionMultiplier`. * fires `PriceConfigSet` event. - * Only the owner of the domain or an allowed operator can call this function + * Only the owner of the domain or an allowed operator can call this function. * > This function should ALWAYS be used to set the config, since it's the only place where `isSet` is set to true. * > Use the other individual setters to modify only, since they do not set this variable! * @param domainHash The domain hash to set the price config for @@ -134,20 +142,18 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I bytes32 domainHash, CurvePriceConfig calldata priceConfig ) public override { - setPrecisionMultiplier(domainHash, priceConfig.precisionMultiplier); - priceConfigs[domainHash].baseLength = priceConfig.baseLength; + _setPrecisionMultiplier(domainHash, priceConfig.precisionMultiplier); + _validateSetBaseLength(domainHash, priceConfig.baseLength, priceConfig); priceConfigs[domainHash].maxPrice = priceConfig.maxPrice; - priceConfigs[domainHash].minPrice = priceConfig.minPrice; - priceConfigs[domainHash].maxLength = priceConfig.maxLength; - setFeePercentage(domainHash, priceConfig.feePercentage); + _validateSetCurveMultiplier(domainHash, priceConfig.curveMultiplier, priceConfig); + _validateSetMaxLength(domainHash, priceConfig.maxLength, priceConfig); + _validateSetFeePercentage(domainHash, priceConfig.feePercentage); priceConfigs[domainHash].isSet = true; - _validateConfig(domainHash); - emit PriceConfigSet( domainHash, priceConfig.maxPrice, - priceConfig.minPrice, + priceConfig.curveMultiplier, priceConfig.maxLength, priceConfig.baseLength, priceConfig.precisionMultiplier, @@ -159,9 +165,12 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I * @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. + * > `maxPrice` can be set to 0 along with `baseLength` to make all domains free! + * > `maxPrice` cannot be 0 when: + * - `maxLength` is 0; + * - `baseLength` AND `curveMultiplier` are 0; + * @dev In the case of 0 we do not validate, since setting it to 0 will make all subdomains free. + * @param domainHash The domain hash to set the `maxPrice` for it * @param maxPrice The maximum price to set */ function setMaxPrice( @@ -169,28 +178,40 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I 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 + * @notice Sets the multiplier for domains calculations + * to allow the hyperbolic price curve to be bent all the way to a straight line. + * Validates the config with the new multiplier in case where `baseLength` is 0 too. + * Fires `CurveMultiplier` event. + * Only domain owner can call this function. + * - If `curveMultiplier` = 1.000 - default. Makes a canonical hyperbola fucntion. + * - It can be "0", which makes all domain prices max. + * - If it is less than 1.000, then it pulls the bend towards the straight line. + * - If it is bigger than 1.000, then it makes bigger slope on the chart. + * @param domainHash The domain hash to set the price config for + * @param curveMultiplier Multiplier for bending the price function (graph) */ - function setMinPrice( + function setCurveMultiplier( bytes32 domainHash, - uint256 minPrice + uint256 curveMultiplier ) external override onlyOwnerOrOperator(domainHash) { - priceConfigs[domainHash].minPrice = minPrice; + CurvePriceConfig memory config = priceConfigs[domainHash]; + _validateSetCurveMultiplier(domainHash, curveMultiplier, config); + emit CurveMultiplierSet(domainHash, curveMultiplier); + } - _validateConfig(domainHash); + function _validateSetCurveMultiplier( + bytes32 domainHash, + uint256 curveMultiplier, + CurvePriceConfig memory config + ) internal onlyOwnerOrOperator(domainHash) { + if (curveMultiplier == 0 && config.baseLength == 0) + revert DivisionByZero(domainHash); - emit MinPriceSet(domainHash, minPrice); + priceConfigs[domainHash].curveMultiplier = curveMultiplier; } /** @@ -198,44 +219,69 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I * 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`! + * > `baseLength` can be set to 0 to make all domains free. + * > `baseLength` can be = `maxLength` to make all domain prices max. * > 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 + * @param baseLength Boundary to set */ function setBaseLength( bytes32 domainHash, - uint256 length + uint256 baseLength ) external override onlyOwnerOrOperator(domainHash) { - priceConfigs[domainHash].baseLength = length; + CurvePriceConfig memory config = priceConfigs[domainHash]; + _validateSetBaseLength(domainHash, baseLength, config); + emit BaseLengthSet(domainHash, baseLength); + } + + function _validateSetBaseLength( + bytes32 domainHash, + uint256 baseLength, + CurvePriceConfig memory config + ) internal onlyOwnerOrOperator(domainHash) { + + if (config.maxLength < baseLength) + revert MaxLengthSmallerThanBaseLength(domainHash); - _validateConfig(domainHash); + if (baseLength == 0 && config.curveMultiplier == 0) + revert DivisionByZero(domainHash); - emit BaseLengthSet(domainHash, length); + priceConfigs[domainHash].baseLength = baseLength; } /** * @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. + * All domain names (labels) that are longer than this value will cost the lowest price at maxLength. * 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`! + * > `maxLength` can't be set to 0 or less than `baseLength`! + * > If `maxLength` = `baseLength` it makes all domain prices max. * @param domainHash The domain hash to set the `maxLength` for - * @param length The maximum length to set + * @param maxLength The maximum length to set */ function setMaxLength( bytes32 domainHash, - uint256 length + uint256 maxLength ) external override onlyOwnerOrOperator(domainHash) { - priceConfigs[domainHash].maxLength = length; - - if (length != 0) _validateConfig(domainHash); + CurvePriceConfig memory config = priceConfigs[domainHash]; + _validateSetMaxLength(domainHash, maxLength, config); + emit MaxLengthSet(domainHash, maxLength); + } - emit MaxLengthSet(domainHash, length); + function _validateSetMaxLength( + bytes32 domainHash, + uint256 maxLength, + CurvePriceConfig memory config + ) internal onlyOwnerOrOperator(domainHash) { + if ( + (maxLength < config.baseLength) || + maxLength == 0 + ) revert MaxLengthSmallerThanBaseLength(domainHash); + + priceConfigs[domainHash].maxLength = maxLength; } /** @@ -247,17 +293,24 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I * 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 domainHash The domain hash to set `PrecisionMultiplier` * @param multiplier The multiplier to set */ function setPrecisionMultiplier( bytes32 domainHash, uint256 multiplier ) public override onlyOwnerOrOperator(domainHash) { - if (multiplier == 0 || multiplier > 10**18) revert InvalidMultiplierPassed(multiplier); + _setPrecisionMultiplier(domainHash, multiplier); + emit PrecisionMultiplierSet(domainHash, multiplier); + } - priceConfigs[domainHash].precisionMultiplier = multiplier; + function _setPrecisionMultiplier( + bytes32 domainHash, + uint256 multiplier + ) internal { + if (multiplier == 0 || multiplier > 10**18) revert InvalidPrecisionMultiplierPassed(domainHash); - emit PrecisionMultiplierSet(domainHash, multiplier); + priceConfigs[domainHash].precisionMultiplier = multiplier; } /** @@ -268,15 +321,25 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I * @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) { + function setFeePercentage( + bytes32 domainHash, + uint256 feePercentage + ) public override onlyOwnerOrOperator(domainHash) { + _validateSetFeePercentage(domainHash, feePercentage); + emit FeePercentageSet(domainHash, feePercentage); + } + + function _validateSetFeePercentage( + bytes32 domainHash, + uint256 feePercentage + ) internal onlyOwnerOrOperator(domainHash) { if (feePercentage > PERCENTAGE_BASIS) - revert FeePercentageValueTooLarge(feePercentage, PERCENTAGE_BASIS); + revert FeePercentageValueTooLarge( + feePercentage, + PERCENTAGE_BASIS + ); priceConfigs[domainHash].feePercentage = feePercentage; - emit FeePercentageSet(domainHash, feePercentage); } /** @@ -290,17 +353,25 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I /** * @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: + * @dev Before we calculate the price, 6 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` + * 2. `baseLength` is 0, which means prices for all domains = 0 (free). + * 3. `length` is less or equal to `baseLength`, which means a domain will cost `maxPrice` + * 4. `length` is greater than `maxLength`, which means a domain will cost price by fomula at `maxLength` + * 5. The numerator can be less than the denominator, which is achieved by setting a huge value + * for `curveMultiplier` or by decreasing the `baseLength` and `maxPrice`, which means all domains + * which are longer than `baseLength` will be free. + * 6. `curveMultiplier` is 0, which means all domains will cost `maxPrice`. * - * 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 + * The formula itself creates an hyperbolic curve that decreases in pricing based on domain name length, + * base length, max price and curve multiplier. + * `FACTOR_SCALE` allows to perceive `curveMultiplier` as fraction number in regular formula, + * which helps to bend a curve of price chart. + * 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 parentHash The parent hash * @param length The length of the domain name */ function _getPrice( @@ -315,29 +386,13 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I // 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; - } + if (length > config.maxLength) length = config.maxLength; - /** - * @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); - if (priceConfigs[domainHash].minPrice > prevToMinPrice) - revert InvalidConfigCausingPriceSpikes( - domainHash, - priceConfigs[domainHash].minPrice, - prevToMinPrice - ); + return ((config.baseLength * config.maxPrice * FACTOR_SCALE) / + (config.baseLength * FACTOR_SCALE + config.curveMultiplier * (length - config.baseLength))) / + config.precisionMultiplier * config.precisionMultiplier; } /** diff --git a/contracts/registrar/IZNSSubRegistrar.sol b/contracts/registrar/IZNSSubRegistrar.sol index a8feb7b5..3ea949c7 100644 --- a/contracts/registrar/IZNSSubRegistrar.sol +++ b/contracts/registrar/IZNSSubRegistrar.sol @@ -10,8 +10,8 @@ import { IZNSPricer } from "../types/IZNSPricer.sol"; */ interface IZNSSubRegistrar is IDistributionConfig { /** - * @notice Reverted when someone other than parent owner is trying to buy - a subdomain under the parent that is locked\ + * @notice Reverted when someone other than parent owner is trying to buy a subdomain + * under the parent that is locked * or when the parent provided does not exist. */ error ParentLockedOrDoesntExist(bytes32 parentHash); diff --git a/contracts/types/ICurvePriceConfig.sol b/contracts/types/ICurvePriceConfig.sol index b721e1a1..4a3fd5af 100644 --- a/contracts/types/ICurvePriceConfig.sol +++ b/contracts/types/ICurvePriceConfig.sol @@ -6,8 +6,8 @@ pragma solidity 0.8.26; * @dev **`CurvePriceConfig` struct properties:** * * - `maxPrice` (uint256): Maximum price for a domain returned at <= `baseLength` - * - `minPrice` (uint256): Minimum price for a domain returned at > `maxLength` - * - `maxLength` (uint256): Maximum length of a domain name. If the name is longer - we return the `minPrice` + * - `maxLength` (uint256): Maximum length of a domain name. If the name is longer - + * we return the price that was at the `maxLength`. * - `baseLength` (uint256): Base length of a domain name. If the name is shorter or equal - we return the `maxPrice` * - `precisionMultiplier` (uint256): The precision multiplier of the price. This multiplier * should be picked based on the number of token decimals to calculate properly. @@ -25,12 +25,12 @@ interface ICurvePriceConfig { */ uint256 maxPrice; /** - * @notice Minimum price for a domain returned at > `maxLength` + * @notice Multiplier which we use to bend a curve of price on interval from `baseLength` to `maxLength`. */ - uint256 minPrice; + uint256 curveMultiplier; /** * @notice Maximum length of a domain name. If the name is longer than this - * value we return the `minPrice` + * value we return the price that was at the `maxLength` */ uint256 maxLength; /** diff --git a/contracts/types/IZNSPricer.sol b/contracts/types/IZNSPricer.sol index 9f59dbe9..4b8e1624 100644 --- a/contracts/types/IZNSPricer.sol +++ b/contracts/types/IZNSPricer.sol @@ -18,6 +18,16 @@ interface IZNSPricer { */ error FeePercentageValueTooLarge(uint256 feePercentage, uint256 maximum); + /** + * @notice Reverted when `maxLength` smaller than `baseLength`. + */ + error MaxLengthSmallerThanBaseLength(bytes32 domainHash); + + /** + * @notice Reverted when `curveMultiplier` AND `baseLength` are 0. + */ + error DivisionByZero(bytes32 domainHash); + /** * @dev `parentHash` param is here to allow pricer contracts * to have different price configs for different subdomains diff --git a/src/deploy/campaign/environments.ts b/src/deploy/campaign/environments.ts index 1a585b6e..971c001b 100644 --- a/src/deploy/campaign/environments.ts +++ b/src/deploy/campaign/environments.ts @@ -8,7 +8,6 @@ import { DEFAULT_DECIMALS, DECAULT_PRECISION, DEFAULT_PRICE_CONFIG, - getCurvePrice, NO_MOCK_PROD_ERR, STAKING_TOKEN_ERR, INVALID_CURVE_ERR, @@ -109,11 +108,12 @@ export const getConfig = async ({ domainToken: { name: process.env.DOMAIN_TOKEN_NAME ? process.env.DOMAIN_TOKEN_NAME : ZNS_DOMAIN_TOKEN_NAME, symbol: process.env.DOMAIN_TOKEN_SYMBOL ? process.env.DOMAIN_TOKEN_SYMBOL : ZNS_DOMAIN_TOKEN_SYMBOL, - defaultRoyaltyReceiver: royaltyReceiver, + defaultRoyaltyReceiver: royaltyReceiver!, defaultRoyaltyFraction: royaltyFraction, }, rootPriceConfig: priceConfig, - zeroVaultAddress: zeroVaultAddressConf, + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion + zeroVaultAddress: zeroVaultAddressConf!, mockMeowToken: process.env.MOCK_MEOW_TOKEN === "true", stakingTokenAddress: process.env.STAKING_TOKEN_ADDRESS!, postDeploy: { @@ -211,10 +211,10 @@ const getValidateRootPriceConfig = () => { ? ethers.parseEther(process.env.MAX_PRICE) : DEFAULT_PRICE_CONFIG.maxPrice; - const minPrice = - process.env.MIN_PRICE - ? ethers.parseEther(process.env.MIN_PRICE) - : DEFAULT_PRICE_CONFIG.minPrice; + const curveMultiplier = + process.env.curveMultiplier + ? process.env.curveMultiplier + : DEFAULT_PRICE_CONFIG.curveMultiplier; const maxLength = process.env.MAX_LENGTH @@ -237,7 +237,7 @@ const getValidateRootPriceConfig = () => { const priceConfig : ICurvePriceConfig = { maxPrice, - minPrice, + curveMultiplier: BigInt(curveMultiplier), maxLength, baseLength, precisionMultiplier, @@ -245,7 +245,7 @@ const getValidateRootPriceConfig = () => { isSet: true, }; - requires(validatePrice(priceConfig), INVALID_CURVE_ERR); + validateConfig(priceConfig); return priceConfig; }; @@ -256,14 +256,16 @@ const requires = (condition : boolean, message : string) => { } }; -// No price spike before `minPrice` kicks in at `maxLength` -const validatePrice = (config : ICurvePriceConfig) => { - const strA = "a".repeat(Number(config.maxLength)); - const strB = "b".repeat(Number(config.maxLength + 1n)); - - const priceA = getCurvePrice(strA, config); - const priceB = getCurvePrice(strB, config); - - // if B > A, then the price spike is invalid - return (priceB <= priceA); +const validateConfig = (config : ICurvePriceConfig) => { + const PERCENTAGE_BASIS = 10000n; + + if ( + (config.curveMultiplier === 0n && config.baseLength === 0n) || + (config.maxLength < config.baseLength) || + ((config.maxLength < config.baseLength) || config.maxLength === 0n) || + (config.curveMultiplier === 0n || config.curveMultiplier > 10n**18n) || + (config.feePercentage > PERCENTAGE_BASIS) + ) { + requires(false, INVALID_CURVE_ERR); + } }; diff --git a/src/deploy/missions/types.ts b/src/deploy/missions/types.ts index 70470545..7a5de5e2 100644 --- a/src/deploy/missions/types.ts +++ b/src/deploy/missions/types.ts @@ -1,7 +1,7 @@ export interface ICurvePriceConfig { maxPrice : bigint; - minPrice : bigint; + curveMultiplier : bigint; maxLength : bigint; baseLength : bigint; precisionMultiplier : bigint; diff --git a/test/DeployCampaignInt.test.ts b/test/DeployCampaignInt.test.ts index 98bf69e0..6c393c9e 100644 --- a/test/DeployCampaignInt.test.ts +++ b/test/DeployCampaignInt.test.ts @@ -21,7 +21,6 @@ import { INVALID_ENV_ERR, NO_MOCK_PROD_ERR, STAKING_TOKEN_ERR, - INVALID_CURVE_ERR, MONGO_URI_ERR, } from "./helpers"; import { @@ -796,28 +795,6 @@ describe("Deploy Campaign Test", () => { } }); - it("Fails to validate if invalid curve for pricing", async () => { - process.env.MOCK_MEOW_TOKEN = "false"; - process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; - process.env.BASE_LENGTH = "3"; - process.env.MAX_LENGTH = "5"; - process.env.MAX_PRICE = "0"; - process.env.MIN_PRICE = ethers.parseEther("3").toString(); - - try { - await getConfig({ - env: "prod", - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [deployAdmin.address, governor.address], - admins: [deployAdmin.address, admin.address], - }); - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (e : any) { - expect(e.message).includes(INVALID_CURVE_ERR); - } - }); - it("Fails to validate if no mongo uri or local URI in prod", async () => { process.env.MOCK_MEOW_TOKEN = "false"; process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; diff --git a/test/ZNSCurvePricer.test.ts b/test/ZNSCurvePricer.test.ts index 8c272288..f0cd4a40 100644 --- a/test/ZNSCurvePricer.test.ts +++ b/test/ZNSCurvePricer.test.ts @@ -6,13 +6,14 @@ import { deployZNS, getCurvePrice, DEFAULT_PRECISION_MULTIPLIER, - INVALID_PRICE_CONFIG_ERR, validateUpgrade, PaymentType, NOT_AUTHORIZED_ERR, - INVALID_MULTIPLIER_ERR, + INVALID_PRECISION_MULTIPLIER_ERR, INVALID_LENGTH_ERR, INVALID_LABEL_ERR, INITIALIZED_ERR, AC_UNAUTHORIZED_ERR, ZERO_ADDRESS_ERR, FEE_TOO_LARGE_ERR, + INVALID_BASE_OR_MAX_LENGTH_ERR, + DIVISION_BY_ZERO_ERR, } from "./helpers"; import { AccessType, @@ -23,8 +24,9 @@ import { import { ADMIN_ROLE, GOVERNOR_ROLE } from "../src/deploy/constants"; import { ZNSCurvePricer, ZNSCurvePricerUpgradeMock__factory, ZNSCurvePricer__factory } from "../typechain"; import { registrationWithSetup } from "./helpers/register-setup"; -import { getProxyImplAddress } from "./helpers/utils"; +import { getProxyImplAddress, getRandomString } from "./helpers/utils"; import { IZNSContractsLocal } from "./helpers/types"; +import { getMongoAdapter } from "@zero-tech/zdc"; require("@nomicfoundation/hardhat-chai-matchers"); @@ -57,7 +59,7 @@ describe("ZNSCurvePricer", () => { }); await zns.meowToken.connect(user).approve(await zns.treasury.getAddress(), ethers.MaxUint256); - await zns.meowToken.mint(user.address, DEFAULT_PRICE_CONFIG.maxPrice); + await zns.meowToken.mint(user.address, 26000000000000000000000n); const fullConfig = { distrConfig: { @@ -80,6 +82,11 @@ describe("ZNSCurvePricer", () => { }); }); + after(async () => { + const dbAdapter = await getMongoAdapter(); + await dbAdapter.dropDB(); + }); + it("Should NOT let initialize the implementation contract", async () => { const factory = new ZNSCurvePricer__factory(deployer); const impl = await getProxyImplAddress(await zns.curvePricer.getAddress()); @@ -171,12 +178,13 @@ describe("ZNSCurvePricer", () => { // 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 = BigInt("4545450000000000000000"); - const domainTwoRefValue = BigInt("7692300000000000000000"); const domainOneExpPrice = await getCurvePrice(domainOne, DEFAULT_PRICE_CONFIG); const domainTwoExpPrice = await getCurvePrice(domainTwo, DEFAULT_PRICE_CONFIG); + const domainOneRefValue = BigInt("4545450000000000000000"); + const domainTwoRefValue = BigInt("7692300000000000000000"); + const domainOnePriceSC = await zns.curvePricer.getPrice(domainHash, domainOne, true); const domainTwoPriceSC = await zns.curvePricer.getPrice(domainHash, domainTwo, true); @@ -233,48 +241,6 @@ describe("ZNSCurvePricer", () => { 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, BigInt("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 > 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 > 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", () => { @@ -301,7 +267,7 @@ describe("ZNSCurvePricer", () => { baseLength: BigInt("6"), maxLength: BigInt("35"), maxPrice: ethers.parseEther("150"), - minPrice: ethers.parseEther("10"), + curveMultiplier: DEFAULT_PRICE_CONFIG.curveMultiplier, precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, isSet: true, @@ -318,7 +284,7 @@ describe("ZNSCurvePricer", () => { 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.curveMultiplier).to.eq(newConfig.curveMultiplier); expect(configUser.precisionMultiplier).to.eq(newConfig.precisionMultiplier); expect(configUser.feePercentage).to.eq(newConfig.feePercentage); @@ -327,56 +293,17 @@ describe("ZNSCurvePricer", () => { 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.curveMultiplier).to.eq(newConfig.curveMultiplier); 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: BigInt("6"), - maxLength: BigInt("20"), - maxPrice: ethers.parseEther("10"), - minPrice: ethers.parseEther("6"), - precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, - feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, - isSet: true, - }; - - await expect( - zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig) - ).to.be.revertedWithCustomError( - zns.curvePricer, - INVALID_PRICE_CONFIG_ERR - ); - }); - - it("Cannot go below the set minPrice", async () => { - // Using config numbers from audit - const newConfig = { - baseLength: BigInt("5"), - maxLength: BigInt("10"), - maxPrice: ethers.parseEther("10"), - minPrice: ethers.parseEther("5.5"), - precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, - feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, - isSet: true, - }; - - await expect( - zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig) - ).to.be.revertedWithCustomError( - zns.curvePricer, - INVALID_PRICE_CONFIG_ERR - ); - }); - it("Should revert if called by anyone other than owner or operator", async () => { const newConfig = { baseLength: BigInt("6"), maxLength: BigInt("20"), maxPrice: ethers.parseEther("10"), - minPrice: ethers.parseEther("6"), + curveMultiplier: DEFAULT_PRICE_CONFIG.curveMultiplier, precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, isSet: true, @@ -402,7 +329,7 @@ describe("ZNSCurvePricer", () => { baseLength: BigInt("6"), maxLength: BigInt("35"), maxPrice: ethers.parseEther("150"), - minPrice: ethers.parseEther("10"), + curveMultiplier: DEFAULT_PRICE_CONFIG.curveMultiplier, precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, isSet: true, @@ -413,29 +340,13 @@ describe("ZNSCurvePricer", () => { await expect(tx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( domainHash, newConfig.maxPrice, - newConfig.minPrice, + newConfig.curveMultiplier, newConfig.maxLength, newConfig.baseLength, newConfig.precisionMultiplier, newConfig.feePercentage, ); }); - - it("Fails validation when maxPrice < minPrice", async () => { - const newConfig = { - baseLength: BigInt("3"), - maxLength: BigInt("35"), - maxPrice: ethers.parseEther("1"), - minPrice: ethers.parseEther("2"), - precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, - feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, - isSet: true, - }; - - const tx = zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); - - await expect(tx).to.be.revertedWithCustomError(zns.curvePricer, INVALID_PRICE_CONFIG_ERR); - }); }); describe("#setMaxPrice", () => { @@ -464,6 +375,24 @@ describe("ZNSCurvePricer", () => { expect(params.maxPrice).to.eq(newMaxPrice); }); + it("Return 0 price for any domain when maxPrice is 0", async () => { + const newMaxPrice = BigInt("0"); + + await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); + + // run 5 times to cover it more + for (let i = 1; i < 6; i++) { + const label = getRandomString(i * 2); + expect( + await zns.curvePricer.getPrice( + domainHash, + label, + false + ) + ).to.eq(0n); + } + }); + it("Correctly sets max price", async () => { const newMaxPrice = DEFAULT_PRICE_CONFIG.maxPrice + ethers.parseEther("553"); await zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice); @@ -472,16 +401,6 @@ describe("ZNSCurvePricer", () => { expect(params.maxPrice).to.eq(newMaxPrice); }); - it("Should revert when setting maxPrice that causes a spike at maxLength", async () => { - const newMaxPrice = ethers.parseEther("500"); - await expect( - zns.curvePricer.connect(user).setMaxPrice(domainHash, newMaxPrice) - ).to.be.revertedWithCustomError( - zns.curvePricer, - INVALID_PRICE_CONFIG_ERR - ); - }); - it("Causes any length domain to have a price of 0 if the maxPrice is 0", async () => { const newMaxPrice = BigInt("0"); @@ -521,79 +440,111 @@ describe("ZNSCurvePricer", () => { }); }); - describe("#setMinPrice", async () => { - it("Allows an authorized user to set the min price", async () => { - const newMinPrice = ethers.parseEther("0.1"); + describe("#setCurveMultiplier", async () => { + it("Return max price if curve multiplier set to 0", async () => { + const newMultiplier = BigInt("0"); - await zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice); + await zns.curvePricer.connect(user).setCurveMultiplier(domainHash, newMultiplier); - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.minPrice).to.eq(newMinPrice); - }); + // run 5 times to cover it more + for (let i = 1; i < 6; i++) { + const domainString = getRandomString(i * i); - it("Disallows an unauthorized user from setting the min price", async () => { - const newMinPrice = ethers.parseEther("0.1"); + const price = await zns.curvePricer.getPrice( + domainHash, + domainString, + false + ); - const tx = zns.curvePricer.connect(admin).setMinPrice(domainHash, newMinPrice); - await expect(tx).to.be.revertedWithCustomError(zns.curvePricer, NOT_AUTHORIZED_ERR); + await expect( + price + ).to.be.equal( + DEFAULT_PRICE_CONFIG.maxPrice + ); + } }); - it("Allows setting to zero", async () => { - const zeroPrice = BigInt("0"); + it("Reverts when the method is called by a non-owner or operator", async () => { + await expect( + zns.curvePricer.connect(deployer).setCurveMultiplier(domainHash, 2000n) + ).to.be.revertedWithCustomError( + zns.curvePricer, + NOT_AUTHORIZED_ERR + ).withArgs( + deployer, + domainHash + ); + }); - await zns.curvePricer.connect(user).setMinPrice(domainHash, zeroPrice); - const params = await zns.curvePricer.priceConfigs(domainHash); + it("Should revert when `baseLength` is already 0 and we pass `curveMultiplier` as 0", async () => { + await zns.curvePricer.connect(user).setBaseLength( + domainHash, + 0n + ); - expect(params.minPrice).to.eq(zeroPrice); + await expect( + zns.curvePricer.connect(user).setCurveMultiplier( + domainHash, + 0n + ) + ).to.be.revertedWithCustomError( + zns.curvePricer, + DIVISION_BY_ZERO_ERR + ); }); - it("Successfully sets the min price correctly", async () => { - const newMinPrice = ethers.parseEther("0.1"); - await zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice); + it("Should return max price for base length domain labels and 0 for other, which longer", async () => { + // Case where we can set domain strings longer than `baseLength` for free + // (numerator must be less than denominator) - const params = await zns.curvePricer.priceConfigs(domainHash); - expect(params.minPrice).to.eq(newMinPrice); - }); + // constants for playing the scenario (one of many cases): + // `maxPrice` = 25 000 + // `baseLength` <= 40 + // `curveMultiplier` >= 10 000 000 000 - 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 newConfig = { + maxPrice: ethers.parseEther("25000"), + curveMultiplier: BigInt("10000000000"), + maxLength: BigInt(100), + baseLength: BigInt(40), + precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, + feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, + isSet: true, + }; - const minPrice = ethers.parseEther("50"); - await zns.curvePricer.connect(user).setMinPrice(domainHash, minPrice); + await zns.curvePricer.connect(user).setPriceConfig(domainHash, newConfig); - // 16 characters - const short = "abcdefghijklmnop"; - // 30 characters - const medium = "abcdefghijklmnoabcdefghijklmno"; - // 60 characters - const long = "abcdefghijklmnoabcdefghijklmnoabcdefghijklmnoabcdefghijklmno"; + const check = async (label : string, res : bigint) => { + const price = await zns.curvePricer.getPrice( + domainHash, + label, + false + ); + + expect( + price + ).to.equal( + res + ); + }; - const priceCalls = [ - zns.curvePricer.getPrice(domainHash, short, true), - zns.curvePricer.getPrice(domainHash, medium, true), - zns.curvePricer.getPrice(domainHash, long, true), - ]; + // `baseLength` = 40, so run this 4 times + for (let i = 1; i <= newConfig.baseLength / 10n; i++) { + await check( + getRandomString(i * 10), + newConfig.maxPrice + ); + } - const [ - shortPrice, - mediumPrice, - longPrice, - ] = await Promise.all(priceCalls); + for (let i = 5; i <= 15; i++) { + await check( + getRandomString(i * 10), + 0n + ); + } - expect(shortPrice).to.eq(minPrice); - expect(mediumPrice).to.eq(minPrice); - expect(longPrice).to.eq(minPrice); - }); + await zns.curvePricer.connect(user).setPriceConfig(domainHash, DEFAULT_PRICE_CONFIG); - it("Should revert when setting minPrice that causes a spike at maxLength", async () => { - const newMinPrice = DEFAULT_PRICE_CONFIG.minPrice + ethers.parseEther("231"); - await expect( - zns.curvePricer.connect(user).setMinPrice(domainHash, newMinPrice) - ).to.be.revertedWithCustomError( - zns.curvePricer, - INVALID_PRICE_CONFIG_ERR - ); }); }); @@ -608,18 +559,24 @@ describe("ZNSCurvePricer", () => { }); it("Disallows an unauthorized user from setting the precision multiplier", async () => { - const newMultiplier = BigInt("1"); + const newMultiplier = BigInt("2"); - - const tx = zns.curvePricer.connect(admin).setMinPrice(domainHash, newMultiplier); + const tx = zns.curvePricer.connect(admin).setCurveMultiplier(domainHash, newMultiplier); await expect(tx).to.be.revertedWithCustomError(zns.curvePricer, NOT_AUTHORIZED_ERR); }); - it("Fails when setting to zero", async () => { + it("Fails when setting `precisionMultiplier` to zero", async () => { const zeroMultiplier = BigInt("0"); - const tx = zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, zeroMultiplier); - await expect(tx).to.be.revertedWithCustomError(zns.curvePricer, INVALID_MULTIPLIER_ERR); + await expect( + zns.curvePricer.connect(user).setPrecisionMultiplier( + domainHash, + zeroMultiplier + ) + ).to.be.revertedWithCustomError( + zns.curvePricer, + INVALID_PRECISION_MULTIPLIER_ERR + ).withArgs(domainHash); }); it("Successfuly sets the precision multiplier when above 0", async () => { @@ -658,7 +615,7 @@ describe("ZNSCurvePricer", () => { zns.curvePricer.connect(user).setPrecisionMultiplier(domainHash, newMultiplier) ).to.be.revertedWithCustomError( zns.curvePricer, - INVALID_MULTIPLIER_ERR + INVALID_PRECISION_MULTIPLIER_ERR ); }); }); @@ -689,40 +646,27 @@ describe("ZNSCurvePricer", () => { expect(params.baseLength).to.eq(newLength); }); - it("Always returns the minPrice if both baseLength and maxLength are their min values", async () => { - const newConfig = { - baseLength: BigInt(1), - maxLength: BigInt(1), - maxPrice: BigInt(100), - minPrice: BigInt(10), - precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, - feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, - 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); + it("Should revert when `curveMultiplier` is already 0 and we pass `baseLength` as 0", async () => { + await zns.curvePricer.connect(user).setCurveMultiplier( + domainHash, + 0n + ); - expect(shortPrice).to.eq(newConfig.minPrice); - expect(mediumPrice).to.eq(newConfig.minPrice); - expect(longPrice).to.eq(newConfig.minPrice); + await expect( + zns.curvePricer.connect(user).setBaseLength( + domainHash, + 0n + ) + ).to.be.revertedWithCustomError( + zns.curvePricer, + DIVISION_BY_ZERO_ERR + ); }); it("Causes any length domain to cost the base fee when set to max length of 255", async () => { const newLength = 255; + // We have to set `maxLength` firstly, cause `baseLength` cannot be bigger than `maxLength` + await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); await zns.curvePricer.connect(user).setBaseLength(domainHash, newLength); const params = await zns.curvePricer.priceConfigs(domainHash); @@ -792,48 +736,49 @@ describe("ZNSCurvePricer", () => { expect(priceAfter).to.not.eq(paramsAfter.maxPrice); }); - it("Returns the maxPrice whenever the baseLength is 0", async () => { + it("Returns the price = 0 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); + const 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, - DEFAULT_PRICE_CONFIG.maxPrice + 15n + expect( + price + ).to.eq( + 0n ); - - 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 = { - ...DEFAULT_PRICE_CONFIG, - baseLength: BigInt(newRootLength), - }; - - const expectedRootPrice = await getCurvePrice(defaultDomain, newConfig); - const rootPrice = await zns.curvePricer.getPrice(domainHash, defaultDomain, true); - - expect(rootPrice).to.eq(expectedRootPrice); + for (let i = 0; i < 5; i++) { + const newRootLength = i * 2; + await zns.curvePricer.connect(user).setBaseLength(domainHash, newRootLength); + const newConfig = { + ...DEFAULT_PRICE_CONFIG, + baseLength: BigInt(newRootLength), + }; + + const expectedRootPrice = await getCurvePrice(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 = DEFAULT_PRICE_CONFIG.baseLength - 1n; + it("Should revert when `baseLength` bigger than `maxLength`", async () => { + await zns.curvePricer.connect(user).setMaxLength( + domainHash, + 10n + ); + await expect( - zns.curvePricer.connect(user).setBaseLength(domainHash, newBaseLength) + zns.curvePricer.connect(user).setBaseLength( + domainHash, + 20n + ) ).to.be.revertedWithCustomError( zns.curvePricer, - INVALID_PRICE_CONFIG_ERR + INVALID_BASE_OR_MAX_LENGTH_ERR ); }); }); @@ -855,45 +800,36 @@ describe("ZNSCurvePricer", () => { await expect(tx).to.be.revertedWithCustomError(zns.curvePricer, NOT_AUTHORIZED_ERR); }); - it("Allows setting the max length to zero", async () => { + it("Doesn't allow setting the `maxLength` to zero", async () => { + // require setting `baseLength` smaller or equal `maxLength` const newLength = 0; - await zns.curvePricer.connect(user).setMaxLength(domainHash, newLength); - const params = await zns.curvePricer.priceConfigs(domainHash); + await zns.curvePricer.connect(user).setBaseLength(domainHash, 0n); - expect(params.maxLength).to.eq(newLength); + await expect( + zns.curvePricer.connect(user).setMaxLength(domainHash, newLength) + ).to.be.revertedWithCustomError( + zns.curvePricer, + INVALID_BASE_OR_MAX_LENGTH_ERR + ).withArgs( + domainHash + ); }); - 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(DEFAULT_PRICE_CONFIG.maxPrice); - expect(longPrice).to.eq(DEFAULT_PRICE_CONFIG.maxPrice); - expect(beyondPrice).to.eq(DEFAULT_PRICE_CONFIG.minPrice); - }); + it("Should revert when `maxLength` smaller than `baseLength`", async () => { + await zns.curvePricer.connect(user).setBaseLength( + domainHash, + 20n + ); - it("Should revert when setting maxLength that causes a spike at maxLength", async () => { - const newMaxLength = DEFAULT_PRICE_CONFIG.maxLength + 10n; await expect( - zns.curvePricer.connect(user).setMaxLength(domainHash, newMaxLength) + zns.curvePricer.connect(user).setMaxLength( + domainHash, + 10n + ) ).to.be.revertedWithCustomError( zns.curvePricer, - INVALID_PRICE_CONFIG_ERR + INVALID_BASE_OR_MAX_LENGTH_ERR ); }); }); @@ -983,6 +919,29 @@ describe("ZNSCurvePricer", () => { }); }); + describe("General validation", () => { + it("Should revert when all passed variables are 0", async () => { + await expect( + zns.curvePricer.connect(user).setPriceConfig( + domainHash, + { + maxPrice: 0n, + curveMultiplier: 0n, + maxLength: 0n, + baseLength: 0n, + precisionMultiplier: 0n, + feePercentage: 0n, + isSet: true, + } + ) + ).to.be.revertedWithCustomError( + zns.curvePricer, + // because its a first check + INVALID_PRECISION_MULTIPLIER_ERR + ).withArgs(domainHash); + }); + }); + describe("Events", () => { it("Emits MaxPriceSet", async () => { const newMaxPrice = DEFAULT_PRICE_CONFIG.maxPrice + 1n; @@ -1055,4 +1014,4 @@ describe("ZNSCurvePricer", () => { await validateUpgrade(deployer, zns.curvePricer, newCurvePricer, factory, contractCalls); }); }); -}); +}); \ No newline at end of file diff --git a/test/ZNSRootRegistrar.test.ts b/test/ZNSRootRegistrar.test.ts index f4018b04..74606197 100644 --- a/test/ZNSRootRegistrar.test.ts +++ b/test/ZNSRootRegistrar.test.ts @@ -258,7 +258,7 @@ describe("ZNSRootRegistrar", () => { baseLength: BigInt("6"), maxLength: BigInt("35"), maxPrice: ethers.parseEther("150"), - minPrice: ethers.parseEther("10"), + curveMultiplier: BigInt(1000), precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, feePercentage: DEFAULT_PROTOCOL_FEE_PERCENT, isSet: true, @@ -272,7 +272,7 @@ describe("ZNSRootRegistrar", () => { await expect(pricerTx).to.emit(zns.curvePricer, "PriceConfigSet").withArgs( ethers.ZeroHash, newPricerConfig.maxPrice, - newPricerConfig.minPrice, + newPricerConfig.curveMultiplier, newPricerConfig.maxLength, newPricerConfig.baseLength, newPricerConfig.precisionMultiplier, @@ -803,7 +803,6 @@ describe("ZNSRootRegistrar", () => { it("Should NOT charge any tokens if price and/or stake fee is 0", async () => { // set config on CurvePricer for the price to be 0 await zns.curvePricer.connect(deployer).setMaxPrice(ethers.ZeroHash, "0"); - await zns.curvePricer.connect(deployer).setMinPrice(ethers.ZeroHash, "0"); const userBalanceBefore = await zns.meowToken.balanceOf(user.address); const vaultBalanceBefore = await zns.meowToken.balanceOf(zeroVault.address); diff --git a/test/ZNSSubRegistrar.test.ts b/test/ZNSSubRegistrar.test.ts index ac20e980..7a339b98 100644 --- a/test/ZNSSubRegistrar.test.ts +++ b/test/ZNSSubRegistrar.test.ts @@ -1148,10 +1148,6 @@ describe("ZNSSubRegistrar", () => { 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); @@ -1749,7 +1745,7 @@ describe("ZNSSubRegistrar", () => { it("CurvePricer - StakePayment - stake fee - 13 decimals", async () => { const priceConfig = { maxPrice: ethers.parseUnits("30000.93", decimalValues.thirteen), - minPrice: ethers.parseUnits("2000.11", decimalValues.thirteen), + curveMultiplier: BigInt(1000), maxLength: BigInt(50), baseLength: BigInt(4), precisionMultiplier: BigInt(10) ** ( @@ -1838,7 +1834,7 @@ describe("ZNSSubRegistrar", () => { it("CurvePricer - StakePayment - no fee - 2 decimals", async () => { const priceConfig = { maxPrice: ethers.parseUnits("234.46", decimalValues.two), - minPrice: ethers.parseUnits("3.37", decimalValues.two), + curveMultiplier: BigInt(1000), maxLength: BigInt(20), baseLength: BigInt(2), precisionMultiplier: BigInt(1), @@ -2076,7 +2072,6 @@ describe("ZNSSubRegistrar", () => { const priceConfig = { ...DEFAULT_PRICE_CONFIG, maxPrice: BigInt(0), - minPrice: BigInt(0), }; const subdomainParentHash = await registrationWithSetup({ @@ -2165,7 +2160,6 @@ describe("ZNSSubRegistrar", () => { const priceConfig = { ...DEFAULT_PRICE_CONFIG, maxPrice: BigInt(0), - minPrice: BigInt(0), }; const subdomainParentHash = await registrationWithSetup({ @@ -2338,7 +2332,7 @@ describe("ZNSSubRegistrar", () => { // we will use token with 5 decimals, but set prices in 18 decimals const priceConfigIncorrect = { maxPrice: ethers.parseUnits("234.46", decimalValues.eighteen), - minPrice: ethers.parseUnits("3.37", decimalValues.eighteen), + curveMultiplier: BigInt(1000), maxLength: BigInt(20), baseLength: BigInt(2), precisionMultiplier: BigInt(1), @@ -2372,7 +2366,6 @@ describe("ZNSSubRegistrar", () => { const priceConfigCorrect = { ...priceConfigIncorrect, maxPrice: ethers.parseUnits("234.46", decimalValues.five), - minPrice: ethers.parseUnits("3.37", decimalValues.five), }; // calc prices off-chain diff --git a/test/helpers/constants.ts b/test/helpers/constants.ts index a3adc1bc..24407add 100644 --- a/test/helpers/constants.ts +++ b/test/helpers/constants.ts @@ -2,8 +2,8 @@ import { ethers } from "hardhat"; import { ICurvePriceConfig } from "../../src/deploy/missions/types"; export const DEFAULT_RESOLVER_TYPE = "address"; -export const ZNS_DOMAIN_TOKEN_NAME = "ZERO ID"; -export const ZNS_DOMAIN_TOKEN_SYMBOL = "ZID"; +export const ZNS_DOMAIN_TOKEN_NAME = "Zero Name Service"; +export const ZNS_DOMAIN_TOKEN_SYMBOL = "ZNS"; export const DEFAULT_ROYALTY_FRACTION = BigInt("200"); export const DEFAULT_TOKEN_URI = "https://www.zns.domains/7c654a5f"; @@ -36,7 +36,7 @@ export const PaymentType = { export const DEFAULT_PRICE_CONFIG : ICurvePriceConfig = { maxPrice: ethers.parseEther("25000"), - minPrice: ethers.parseEther("2000"), + curveMultiplier: BigInt("1000"), maxLength: BigInt(50), baseLength: BigInt(4), precisionMultiplier: DEFAULT_PRECISION_MULTIPLIER, @@ -46,7 +46,7 @@ export const DEFAULT_PRICE_CONFIG : ICurvePriceConfig = { export const curvePriceConfigEmpty : ICurvePriceConfig = { maxPrice: BigInt(0), - minPrice: BigInt(0), + curveMultiplier: BigInt(0), maxLength: BigInt(0), baseLength: BigInt(0), precisionMultiplier: BigInt(0), diff --git a/test/helpers/errors.ts b/test/helpers/errors.ts index cbb42de1..fbb51b43 100644 --- a/test/helpers/errors.ts +++ b/test/helpers/errors.ts @@ -22,8 +22,10 @@ export const PARENT_CONFIG_NOT_SET_ERR = "ParentPriceConfigNotSet"; export const FEE_TOO_LARGE_ERR = "FeePercentageValueTooLarge"; // ZNSCurvePricer.sol -export const INVALID_MULTIPLIER_ERR = "InvalidMultiplierPassed"; +export const INVALID_PRECISION_MULTIPLIER_ERR = "InvalidPrecisionMultiplierPassed"; export const INVALID_PRICE_CONFIG_ERR = "InvalidConfigCausingPriceSpikes"; +export const INVALID_BASE_OR_MAX_LENGTH_ERR = "MaxLengthSmallerThanBaseLength"; +export const DIVISION_BY_ZERO_ERR = "DivisionByZero"; // ZNSRootRegistrar.sol export const NOT_OWNER_OF_ERR = "NotTheOwnerOf"; diff --git a/test/helpers/flows/registration.ts b/test/helpers/flows/registration.ts index 7cef8dfa..0628d2ee 100644 --- a/test/helpers/flows/registration.ts +++ b/test/helpers/flows/registration.ts @@ -119,7 +119,7 @@ export const validatePathRegistration = async ({ const { maxPrice: curveMaxPrice, - minPrice: curveMinPrice, + curveMultiplier, maxLength: curveMaxLength, baseLength: curveBaseLength, precisionMultiplier: curvePrecisionMultiplier, @@ -136,7 +136,7 @@ export const validatePathRegistration = async ({ domainLabel, { maxPrice: curveMaxPrice, - minPrice: curveMinPrice, + curveMultiplier, maxLength: curveMaxLength, baseLength: curveBaseLength, precisionMultiplier: curvePrecisionMultiplier, @@ -160,7 +160,7 @@ export const validatePathRegistration = async ({ } else { const { maxPrice, - minPrice, + curveMultiplier, maxLength, baseLength, precisionMultiplier, @@ -174,7 +174,7 @@ export const validatePathRegistration = async ({ domainLabel, { maxPrice, - minPrice, + curveMultiplier, maxLength, baseLength, precisionMultiplier, diff --git a/test/helpers/index.ts b/test/helpers/index.ts index fb5133ca..e09196e6 100644 --- a/test/helpers/index.ts +++ b/test/helpers/index.ts @@ -6,6 +6,7 @@ export * from "./constants"; export * from "./balances"; export * from "./errors"; export * from "./validate-upgrade"; +export * from "./utils"; export { EXECUTOR_ROLE } from "../../src/deploy/constants"; export { REGISTRAR_ROLE } from "../../src/deploy/constants"; export { ADMIN_ROLE } from "../../src/deploy/constants"; diff --git a/test/helpers/pricing.ts b/test/helpers/pricing.ts index 7afae8a9..4395af5a 100644 --- a/test/helpers/pricing.ts +++ b/test/helpers/pricing.ts @@ -18,23 +18,26 @@ export const getCurvePrice = ( // Get price configuration for contract const { maxPrice, - minPrice, + curveMultiplier, baseLength, maxLength, precisionMultiplier, } = priceConfig; - if (baseLength === 0n) return maxPrice; + let length = BigInt(name.length); - if (BigInt(name.length) <= baseLength) { + if (length <= baseLength) { return maxPrice; } if (BigInt(name.length) > maxLength) { - return minPrice; + length = maxLength; } - const base = baseLength * maxPrice / BigInt(name.length); + const MULTIPLIER_BASIS = 1000n; + + const base = (baseLength * maxPrice * MULTIPLIER_BASIS) + / (baseLength * MULTIPLIER_BASIS + curveMultiplier * (length - baseLength)); return base / precisionMultiplier * precisionMultiplier; }; diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts index ff18b478..50f12059 100644 --- a/test/helpers/utils.ts +++ b/test/helpers/utils.ts @@ -20,3 +20,15 @@ export const loggerMock = { info: () => {}, debug: () => {}, }; + +export const getRandomString = (length : number) => { + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + + for (let i = 0; i < length; i++) { + const randomIndex = Math.floor(Math.random() * alphabet.length); + result += alphabet.charAt(randomIndex); + } + + return result; +}; \ No newline at end of file