Overview
The StandardRentPriceOracle contract calculates registration and renewal prices for ENS names. It implements a sophisticated pricing model with:
- Variable base rates by name length (codepoints)
- Duration-based discounts for longer registrations
- Premium pricing for recently expired names (exponential decay)
- Multi-token payment support with configurable exchange rates
Interface ID: 0x53b53cee
Key Features
- Length-based pricing (shorter names cost more)
- Discount incentives for multi-year registrations
- Premium pricing that decays exponentially after expiration
- Support for multiple ERC-20 payment tokens
- Owner-controlled pricing configuration
Constants
REGISTRY
IPermissionedRegistry public immutable REGISTRY
The ENS registry contract used to check name expiry status.
State Variables
premiumPriceInitial
uint256 public premiumPriceInitial
The initial premium price for a name immediately after expiration, in base units.
premiumHalvingPeriod
uint64 public premiumHalvingPeriod
Duration (in seconds) for the premium price to decay by half.
premiumPeriod
uint64 public premiumPeriod
Total duration (in seconds) of the premium pricing period. Premium becomes zero after this time.
Data Structures
DiscountPoint
struct DiscountPoint {
uint64 t; // Incremental time interval, in seconds
uint128 value; // Discount percentage relative to type(uint128).max
}
Defines a point in the discount function. The discount is applied incrementally based on registration duration.
The time interval for this discount tier, in seconds
The discount percentage for this tier, where type(uint128).max represents 100%
PaymentRatio
struct PaymentRatio {
IERC20 token; // The payment token address
uint128 numer; // Exchange rate numerator
uint128 denom; // Exchange rate denominator
}
Configures a payment token with its exchange rate to base units.
The ERC-20 token contract address
Exchange rate denominator. Price in tokens = (base price * numer) / denom
Functions
updateBaseRates
function updateBaseRates(uint256[] calldata ratePerCp) external onlyOwner
Update the base registration rates per codepoint.
Array of base rates in base units per second. ratePerCp[i] is the rate for i+1 codepoints. Names longer than the array length use the last rate. Set rate to 0 to disable a specific length. Use empty array to disable all registrations.
Events Emitted:
BaseRatesChanged(uint256[] ratePerCp)
Access Control: Only callable by contract owner.
Example:
// Set rates for 1-5 character names
// 1 char: 1000 wei/sec, 2 char: 500 wei/sec, 3+ char: 100 wei/sec
uint256[] memory rates = new uint256[](3);
rates[0] = 1000; // 1 character
rates[1] = 500; // 2 characters
rates[2] = 100; // 3+ characters
oracle.updateBaseRates(rates);
updateDiscountPoints
function updateDiscountPoints(DiscountPoint[] calldata points) external onlyOwner
Update the discount function for longer registration durations.
Array of discount points defining the discount tiers. Each point specifies an incremental time interval and its corresponding discount percentage. Use empty array to disable discounts.
Events Emitted:
DiscountPointsChanged(DiscountPoint[] points)
Access Control: Only callable by contract owner.
Reverts:
InvalidDiscountPoint() - If any point has t = 0
Example:
// Configure discount tiers:
// Year 1: 0% discount
// Year 2+: 10% discount
DiscountPoint[] memory points = new DiscountPoint[](2);
points[0] = DiscountPoint({
t: 365 days,
value: 0 // 0% discount for first year
});
points[1] = DiscountPoint({
t: 365 days,
value: type(uint128).max / 10 // 10% discount for subsequent years
});
oracle.updateDiscountPoints(points);
updatePremiumPricing
function updatePremiumPricing(
uint256 initialPrice,
uint64 halvingPeriod,
uint64 period
) external onlyOwner
Update the premium pricing function for recently expired names.
The initial premium price in base units. Set to 0 to disable premium pricing.
Duration (in seconds) for the premium to decay by half
Total duration (in seconds) until premium reaches zero
Events Emitted:
PremiumPricingChanged(uint256 indexed initialPrice, uint64 indexed halvingPeriod, uint64 indexed period)
Access Control: Only callable by contract owner.
Notes:
premiumPriceAfter(0) returns the exact starting premium
premiumPriceAfter(halvingPeriod) ≈ initialPrice / 2
premiumPriceAfter(halvingPeriod * x) ≈ initialPrice / 2^x
premiumPriceAfter(period) = 0
Example:
// Set premium to start at 100 ETH equivalent,
// halve every 7 days, reach zero after 28 days
oracle.updatePremiumPricing(
100 ether, // Initial premium
7 days, // Halving period
28 days // Total premium period
);
updatePaymentToken
function updatePaymentToken(
IERC20 paymentToken,
uint128 numer,
uint128 denom
) external onlyOwner
Add, update, or remove a payment token and its exchange rate.
The ERC-20 token contract address
Exchange rate numerator. Must be non-zero to add/update.
Exchange rate denominator. Set to 0 to remove the token.
Events Emitted:
PaymentTokenAdded(IERC20 indexed paymentToken) - When token is newly supported
PaymentTokenRemoved(IERC20 indexed paymentToken) - When token support is removed
Access Control: Only callable by contract owner.
Reverts:
InvalidRatio() - If numer = 0 when denom > 0
Example:
// Add USDC with 6 decimals
// If base unit is wei (18 decimals), convert: price_usdc = price_wei * 1 / 10^12
oracle.updatePaymentToken(
IERC20(usdcAddress),
1, // numerator
10**12 // denominator
);
// Remove a token
oracle.updatePaymentToken(
IERC20(tokenToRemove),
0,
0
);
getBaseRates
function getBaseRates() external view returns (uint256[] memory)
Get all configured base rates.
Array of base rates in base units per second, indexed by codepoint count minus one
Example:
uint256[] memory rates = oracle.getBaseRates();
// rates[0] = rate for 1 codepoint
// rates[1] = rate for 2 codepoints
// etc.
getDiscountPoints
function getDiscountPoints() external view returns (DiscountPoint[] memory)
Get all configured discount points.
Array of discount points defining the discount function
Example:
DiscountPoint[] memory points = oracle.getDiscountPoints();
for (uint i = 0; i < points.length; i++) {
// Process each discount tier
}
isValid
function isValid(string calldata label) external view returns (bool)
Check if a label is valid for registration according to pricing rules.
The name to validate (without .eth suffix)
true if the label has a non-zero base rate configured (and is therefore valid for registration)
Note: This function does not check if the label is normalized. It only checks if a base rate exists.
Example:
if (oracle.isValid("myname")) {
// Name length has a configured price
}
isPaymentToken
function isPaymentToken(IERC20 paymentToken) public view returns (bool)
Check if a token is supported for payment.
The ERC-20 token contract address to check
true if the token is accepted for payments
Example:
if (oracle.isPaymentToken(IERC20(usdcAddress))) {
// USDC is accepted
}
baseRate
function baseRate(string memory label) public view returns (uint256)
Get the base rate per second for a label.
The base rate in base units per second, or 0 if the label is not valid (too long, too short, or no rate configured)
Example:
uint256 rate = oracle.baseRate("myname");
uint256 annualCost = rate * 365 days;
integratedDiscount
function integratedDiscount(uint64 duration) public view returns (uint256)
Compute the integral of the discount function over a duration.
The registration duration in seconds
The integrated discount value. Divide by duration to get the average discount percentage.
Usage: integratedDiscount(t) / t gives the average discount percentage over duration t.
Example:
// Calculate average discount for 2 year registration
uint64 twoYears = 730 days;
uint256 avgDiscount = oracle.integratedDiscount(twoYears) / twoYears;
premiumPrice
function premiumPrice(uint64 expiry) public view returns (uint256)
Get the current premium price for a name based on its expiry time.
The expiration timestamp of the name
The premium price in base units. Returns 0 if the name hasn’t expired yet or if the premium period has passed.
Example:
// Get premium for a name that expired
IPermissionedRegistry.State memory state = registry.getState(tokenId);
uint256 premium = oracle.premiumPrice(state.expiry);
premiumPriceAfter
function premiumPriceAfter(uint64 duration) public view returns (uint256)
Get the premium price at a specific duration after expiration.
Time elapsed since expiration, in seconds
The premium price in base units at that point in time. Returns 0 if duration >= premiumPeriod.
Note: Defined over [0, premiumPeriod).
Example:
// What would the premium be 3 days after expiration?
uint256 premium = oracle.premiumPriceAfter(3 days);
rentPrice
function rentPrice(
string memory label,
address owner,
uint64 duration,
IERC20 paymentToken
) public view returns (uint256 base, uint256 premium)
Calculate the complete registration or renewal price for a name.
The name to price (without .eth suffix)
The prospective owner address. If this matches the current owner, no premium is charged. Use zero address to exclude premium for price estimation.
The registration duration in seconds
The ERC-20 token to use for payment
The base price in payment token units, including duration discounts if applicable
The premium price in payment token units (0 if owner matches current owner or name is not expired)
Reverts:
PaymentTokenNotSupported(IERC20 paymentToken) - If the payment token isn’t supported
NotValid(string label) - If the label has no configured base rate
Pricing Formula:
- Calculate base units:
baseRate(label) * duration
- Apply duration discount based on current expiry and registration duration
- Calculate premium if applicable (owner doesn’t match current owner)
- Convert to payment token using configured exchange rate
Example:
// Get price for new registration
(uint256 base, uint256 premium) = oracle.rentPrice(
"myname",
msg.sender,
365 days,
IERC20(usdcAddress)
);
uint256 totalCost = base + premium;
// Get renewal price (current owner pays no premium)
(uint256 renewalBase, ) = oracle.rentPrice(
"myname",
currentOwner, // Same as registered owner
365 days,
IERC20(usdcAddress)
);
// Get price estimate without premium
(uint256 estimateBase, ) = oracle.rentPrice(
"myname",
address(0), // Null address excludes premium
365 days,
IERC20(usdcAddress)
);
Events
DiscountPointsChanged
event DiscountPointsChanged(DiscountPoint[] points)
Emitted when discount points are updated.
The new discount point configuration
BaseRatesChanged
event BaseRatesChanged(uint256[] ratePerCp)
Emitted when base rates are updated.
The new base rate configuration
PremiumPricingChanged
event PremiumPricingChanged(
uint256 indexed initialPrice,
uint64 indexed halvingPeriod,
uint64 indexed period
)
Emitted when premium pricing parameters are updated.
The new initial premium price
The new total premium period
PaymentTokenAdded
event PaymentTokenAdded(IERC20 indexed paymentToken)
Emitted when a payment token is added or updated.
The token that was added or updated
PaymentTokenRemoved
event PaymentTokenRemoved(IERC20 indexed paymentToken)
Emitted when a payment token is removed.
The token that was removed
Errors
InvalidRatio
The payment token exchange rate is invalid (numerator is zero when denominator is non-zero).
Error Selector: 0x648564d3
InvalidDiscountPoint
error InvalidDiscountPoint()
A discount point has an invalid time interval (t = 0).
Error Selector: 0xd1be8bbe
NotValid
error NotValid(string label)
The label is not valid for registration (no base rate configured).
Error Selector: 0xdbfa2886
PaymentTokenNotSupported
error PaymentTokenNotSupported(IERC20 paymentToken)
The payment token is not supported.
Error Selector: 0x02e2ae9e
Pricing Model
Base Pricing
Base prices are determined by the number of Unicode codepoints in the name:
- Count codepoints using
StringUtils.strlen()
- Look up the rate in the
_baseRatePerCp array
- Multiply rate by duration to get base units
Example Configuration:
1 codepoint: 1000 wei/second → $100,000/year
2 codepoints: 100 wei/second → $10,000/year
3+ codepoints: 10 wei/second → $1,000/year
Duration Discounts
Discounts incentivize longer registration periods:
- Each
DiscountPoint defines an incremental time period and discount rate
- The integral of the discount function is computed over the registration duration
- The average discount is applied to the base price
Example: To achieve a 5% average discount for 2-year registrations:
- Year 1: 0% discount
- Year 2: 10% discount
- Average: (0% + 10%) / 2 = 5%
Premium Pricing
Premium pricing prevents immediate re-registration of expired names:
- Premium starts at
premiumPriceInitial when a name expires
- Decays exponentially with half-life of
premiumHalvingPeriod
- Reaches zero after
premiumPeriod seconds
- Current owner pays no premium on renewals
Formula: Uses LibHalving.halving() for exponential decay calculation.
Example:
Initial: 100 ETH
Halving: 7 days
Period: 28 days
Day 0: 100 ETH
Day 7: ~50 ETH
Day 14: ~25 ETH
Day 21: ~12.5 ETH
Day 28: 0 ETH
Multi-Token Support
Prices are calculated in base units then converted to payment tokens:
price_token = (price_baseUnits * numer) / denom
Example: For USDC (6 decimals) with base units in wei (18 decimals):
numer = 1
denom = 10^12
price_usdc = price_wei / 10^12
Integration Examples
Calculating Multi-Year Discount
contract PriceCalculator {
StandardRentPriceOracle public oracle;
function compareYearlyPrices(
string memory label,
IERC20 paymentToken
) external view returns (
uint256 oneYearTotal,
uint256 threeYearTotal,
uint256 savings
) {
address nullOwner = address(0); // Exclude premium
// 1 year price
(uint256 base1, ) = oracle.rentPrice(
label,
nullOwner,
365 days,
paymentToken
);
oneYearTotal = base1 * 3; // Cost to renew 3 times
// 3 year price with discount
(uint256 base3, ) = oracle.rentPrice(
label,
nullOwner,
3 * 365 days,
paymentToken
);
threeYearTotal = base3;
savings = oneYearTotal - threeYearTotal;
}
}
Premium Decay Tracker
contract PremiumTracker {
StandardRentPriceOracle public oracle;
IPermissionedRegistry public registry;
function trackPremiumDecay(
uint256 tokenId,
IERC20 paymentToken
) external view returns (
uint256 currentPremium,
uint256 premiumIn1Week,
uint256 premiumIn2Weeks
) {
IPermissionedRegistry.State memory state = registry.getState(tokenId);
string memory label = "example"; // Get from token metadata
// Current premium
(, currentPremium) = oracle.rentPrice(
label,
msg.sender, // Not current owner
365 days,
paymentToken
);
// Premium in 1 week
uint64 timeAfterExpiry = uint64(block.timestamp) - state.expiry + 7 days;
uint256 premiumUnits1Week = oracle.premiumPriceAfter(timeAfterExpiry);
// Convert to payment token
// (Simplified - actual conversion needs the payment ratio)
premiumIn1Week = premiumUnits1Week;
// Premium in 2 weeks
uint64 timeAfterExpiry2 = uint64(block.timestamp) - state.expiry + 14 days;
premiumIn2Weeks = oracle.premiumPriceAfter(timeAfterExpiry2);
}
}
Custom Pricing Oracle
contract CustomOracle is IRentPriceOracle {
StandardRentPriceOracle public standardOracle;
// Override pricing for specific labels
mapping(bytes32 => uint256) public customPrices;
function rentPrice(
string memory label,
address owner,
uint64 duration,
IERC20 paymentToken
) external view returns (uint256 base, uint256 premium) {
bytes32 labelHash = keccak256(bytes(label));
// Check for custom price
if (customPrices[labelHash] > 0) {
base = customPrices[labelHash] * duration;
premium = 0; // No premium for custom prices
} else {
// Fall back to standard pricing
return standardOracle.rentPrice(label, owner, duration, paymentToken);
}
}
// Implement other IRentPriceOracle functions...
}