Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ docker*.tgz

# We don't ever use the generated manifests
.openzeppelin
/.vscode
32 changes: 11 additions & 21 deletions contracts/price/IZNSCurvePricer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,12 @@ interface IZNSCurvePricer is ICurvePriceConfig, IZNSPricer {
*/
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
);

/**
* @notice Emitted when the `maxPrice` is set in `CurvePriceConfig`
* @param price The new maxPrice value
*/
event MaxPriceSet(bytes32 domainHash, uint256 price);

/**
* @notice Emitted when the `minPrice` is set in `CurvePriceConfig`
* @param price The new minPrice value
*/
event MinPriceSet(bytes32 domainHash, uint256 price);

/**
* @notice Emitted when the `baseLength` is set in `CurvePriceConfig`
* @param length The new baseLength value
Expand All @@ -60,18 +43,25 @@ 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
*/
event PriceConfigSet(
bytes32 domainHash,
uint256 maxPrice,
uint256 minPrice,
uint256 curveMultiplier,
uint256 maxLength,
uint256 baseLength,
uint256 precisionMultiplier,
Expand Down Expand Up @@ -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;
Expand Down
102 changes: 63 additions & 39 deletions contracts/price/ZNSCurvePricer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ 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`.
* It then decreases in price, using the calculated price function below.
* At `maxLength` length of the domain name. Price after `maxLength` is fixed
* and is determined using the formula where `length` = `maxLength`.
*/
contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, IZNSCurvePricer {

using StringUtils for string;

/**
Expand All @@ -24,6 +26,12 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I
*/
uint256 public constant PERCENTAGE_BASIS = 10000;

/**
* @notice Value used as a basis to multiply the multiplier,
* since Solidity does not support fractions.
*/
uint256 public constant MULTIPLIER_BASIS = 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.
Expand All @@ -41,7 +49,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.
Expand Down Expand Up @@ -122,9 +129,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
Expand All @@ -137,7 +144,7 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I
setPrecisionMultiplier(domainHash, priceConfig.precisionMultiplier);
priceConfigs[domainHash].baseLength = priceConfig.baseLength;
priceConfigs[domainHash].maxPrice = priceConfig.maxPrice;
priceConfigs[domainHash].minPrice = priceConfig.minPrice;
priceConfigs[domainHash].curveMultiplier = priceConfig.curveMultiplier;
priceConfigs[domainHash].maxLength = priceConfig.maxLength;
setFeePercentage(domainHash, priceConfig.feePercentage);
priceConfigs[domainHash].isSet = true;
Expand All @@ -147,7 +154,7 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I
emit PriceConfigSet(
domainHash,
priceConfig.maxPrice,
priceConfig.minPrice,
priceConfig.curveMultiplier,
priceConfig.maxLength,
priceConfig.baseLength,
priceConfig.precisionMultiplier,
Expand All @@ -159,9 +166,9 @@ 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!
* @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(
Expand All @@ -176,21 +183,24 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I
}

/**
* @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.
* > `curveMultiplier` can be set to 0 to set a maximum price for all domains!
* @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;
priceConfigs[domainHash].curveMultiplier = curveMultiplier;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you have any limitations on what this value could be?
Are there any values that would break the formula if set as curveMultiplier?
If you have limits for this value, it would be good to add checks here that will revert if incorrect value is set.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • curveMultiplier = 1.000 - default. Makes a canonical hyperbola (regular).
  • 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.

I'm adding these comments to contract to be clear


_validateConfig(domainHash);

emit MinPriceSet(domainHash, minPrice);
emit CurveMultiplierSet(domainHash, curveMultiplier);
}

/**
Expand Down Expand Up @@ -218,12 +228,11 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I

/**
* @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 be set to 0!
* @param domainHash The domain hash to set the `maxLength` for
* @param length The maximum length to set
*/
Expand All @@ -233,6 +242,8 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I
) external override onlyOwnerOrOperator(domainHash) {
priceConfigs[domainHash].maxLength = length;

_validateConfig(domainHash);

if (length != 0) _validateConfig(domainHash);

emit MaxLengthSet(domainHash, length);
Expand All @@ -247,14 +258,14 @@ 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);

priceConfigs[domainHash].precisionMultiplier = multiplier;

emit PrecisionMultiplierSet(domainHash, multiplier);
Expand All @@ -274,7 +285,6 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I
onlyOwnerOrOperator(domainHash) {
if (feePercentage > PERCENTAGE_BASIS)
revert FeePercentageValueTooLarge(feePercentage, PERCENTAGE_BASIS);

priceConfigs[domainHash].feePercentage = feePercentage;
emit FeePercentageSet(domainHash, feePercentage);
}
Expand All @@ -290,17 +300,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`
* 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 for 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.
* `MULTIPLIER_BASIS` 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(
Expand All @@ -317,26 +335,32 @@ contract ZNSCurvePricer is AAccessControlled, ARegistryWired, UUPSUpgradeable, I
// 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;

//
return ((config.baseLength * config.maxPrice * MULTIPLIER_BASIS) / (
config.baseLength * MULTIPLIER_BASIS + config.curveMultiplier * (length - config.baseLength)
))
/ 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.
* @dev We are checking here for possible incorrect passed values: `maxLength`, `baselength` or `curveMultiplier`.
* @param domainHash The domain hash to validate its config
*/
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
if (priceConfigs[domainHash].maxLength < priceConfigs[domainHash].baseLength)
revert InvalidBaseLengthOrMaxLength(
domainHash
);

if (priceConfigs[domainHash].baseLength == 0 && priceConfigs[domainHash].curveMultiplier == 0)
revert DivisionByZero(
domainHash
);
}

Expand Down
4 changes: 2 additions & 2 deletions contracts/types/ICurvePriceConfig.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ 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`
Expand Down
12 changes: 11 additions & 1 deletion contracts/types/IZNSPricer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@ interface IZNSPricer {
error ParentPriceConfigNotSet(bytes32 parentHash);

/**
* @notice Reverted when domain owner is trying to set it's stake fee percentage higher than 100% (uint256 "10,000").
* @notice Reverted when domain owner is trying to set it's stake fee percentage higher than 100% (uint256 "10,000")
*/
error FeePercentageValueTooLarge(uint256 feePercentage, uint256 maximum);

/**
* @notice Reverted when `maxLength` smaller than `baseLength`.
*/
error InvalidBaseLengthOrMaxLength(bytes32 domainHash);

/**
* @notice Reverted when `curveMultiplier` AND (&&) `baseLength` is 0.
*/
error DivisionByZero(bytes32 domainHash);

/**
* @dev `parentHash` param is here to allow pricer contracts
* to have different price configs for different subdomains
Expand Down
2 changes: 1 addition & 1 deletion src/deploy/missions/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

export interface ICurvePriceConfig {
maxPrice : bigint;
minPrice : bigint;
curveMultiplier : bigint;
maxLength : bigint;
baseLength : bigint;
precisionMultiplier : bigint;
Expand Down
Loading