Price oracles determine the cost of registering and renewing .eth names. ENS provides several oracle implementations with different pricing models.
Overview
All price oracles implement the IPriceOracle interface and return a Price struct:
struct Price {
uint256 base; // Base price in wei
uint256 premium; // Premium price in wei (for expired names)
}
Total price = base + premium
StablePriceOracle
A price oracle that returns prices in USD, converted to ETH using a Chainlink oracle.
Source: StablePriceOracle.sol:14
Features
- Prices denominated in USD (stable pricing)
- Length-based pricing (shorter names cost more)
- Uses Chainlink oracle for ETH/USD conversion
- No premium decay (base implementation)
Price Structure
Prices are set per name length:
uint256 public immutable price1Letter; // Premium price for 1-char names
uint256 public immutable price2Letter; // Price for 2-char names
uint256 public immutable price3Letter; // Price for 3-char names
uint256 public immutable price4Letter; // Price for 4-char names
uint256 public immutable price5Letter; // Price for 5+ char names
Source: StablePriceOracle.sol:18-22
Constructor
constructor(
AggregatorInterface _usdOracle,
uint256[] memory _rentPrices
)
_usdOracle
AggregatorInterface
required
Chainlink oracle address for ETH/USD price feed
Array of 5 prices in attoUSD (10^-18 USD) for 1-char through 5+ char names
Source: StablePriceOracle.sol:29
price
Returns the price for a name.
function price(
string calldata name,
uint256 expires,
uint256 duration
) external view returns (IPriceOracle.Price memory)
Current expiration timestamp (0 for new registrations)
Registration duration in seconds
Returns: Price struct with base and premium in wei
Source: StablePriceOracle.sol:38
Example Usage
import { ethers } from 'ethers';
const oracle = new ethers.Contract(oracleAddress, oracleABI, provider);
// Price for registering "alice" for 1 year
const price = await oracle.price('alice', 0, 31536000);
console.log(`Base: ${ethers.utils.formatEther(price.base)} ETH`);
console.log(`Premium: ${ethers.utils.formatEther(price.premium)} ETH`);
console.log(`Total: ${ethers.utils.formatEther(price.base.add(price.premium))} ETH`);
LinearPremiumPriceOracle
Extends StablePriceOracle with a linearly decaying premium for recently expired names.
Source: LinearPremiumPriceOracle.sol:7
Features
- All features of StablePriceOracle
- Linear premium decay for expired names
- Premium starts at
initialPremium and decreases by premiumDecreaseRate per second
- 90-day grace period (no premium during grace period)
Premium Calculation
The premium decay works as follows:
time since expiry = block.timestamp - (expires + 90 days)
if time since expiry <= 0:
premium = 0 // Still in grace period or not expired
else:
discount = premiumDecreaseRate * time since expiry
premium = initialPremium - discount
if premium < 0:
premium = 0
Source: LinearPremiumPriceOracle.sol:28
Constructor
constructor(
AggregatorInterface _usdOracle,
uint256[] memory _rentPrices,
uint256 _initialPremium,
uint256 _premiumDecreaseRate
)
_usdOracle
AggregatorInterface
required
Chainlink oracle for ETH/USD
Array of 5 base prices in attoUSD
Starting premium in attoUSD immediately after grace period ends
Amount in attoUSD the premium decreases per second
Source: LinearPremiumPriceOracle.sol:18
timeUntilPremium
Calculates when the premium will reach a specific amount.
function timeUntilPremium(
uint256 expires,
uint256 amount
) external view returns (uint256)
The name’s expiration timestamp
Desired premium amount in wei
Returns: Timestamp when premium will equal amount
Source: LinearPremiumPriceOracle.sol:57
Example Usage
// Find when premium drops to 0.1 ETH
const expires = await baseRegistrar.nameExpires(tokenId);
const targetPremium = ethers.utils.parseEther('0.1');
const timestamp = await oracle.timeUntilPremium(expires, targetPremium);
const date = new Date(timestamp * 1000);
console.log(`Premium will be 0.1 ETH at: ${date}`);
ExponentialPremiumPriceOracle
Extends StablePriceOracle with an exponentially decaying premium (halves each day).
Source: ExponentialPremiumPriceOracle.sol:6
Features
- All features of StablePriceOracle
- Exponential decay: premium halves every day
- High initial premium to discourage immediate re-registration
- Premium eventually reaches
endValue (minimum floor)
Premium Calculation
The premium uses exponential decay:
premium(t) = startPremium * (0.5 ^ days_elapsed) - endValue
Where:
days_elapsed = time since grace period ended, in days
startPremium = Initial premium value
endValue = Minimum premium floor (never goes below this)
Source: ExponentialPremiumPriceOracle.sol:40
Constructor
constructor(
AggregatorInterface _usdOracle,
uint256[] memory _rentPrices,
uint256 _startPremium,
uint256 totalDays
)
_usdOracle
AggregatorInterface
required
Chainlink oracle for ETH/USD
Array of 5 base prices in attoUSD
Initial premium immediately after grace period (in attoUSD)
Number of days over which premium decays. Used to calculate endValue = _startPremium >> totalDays
Source: ExponentialPremiumPriceOracle.sol:11
decayedPremium
Calculates the premium at a specific elapsed time.
function decayedPremium(
uint256 startPremium,
uint256 elapsed
) public pure returns (uint256)
The starting premium value
Returns: The decayed premium value
Source: ExponentialPremiumPriceOracle.sol:61
Example Decay Timeline
With startPremium = 1000 USD and totalDays = 21:
| Days After Grace Period | Premium |
|---|
| 0 | 1000 USD |
| 1 | 500 USD |
| 2 | 250 USD |
| 3 | 125 USD |
| 7 | 7.8 USD |
| 14 | 0.06 USD |
| 21 | ~0 USD (reaches endValue) |
Example Usage
// Check premium decay for an expired name
const expires = await baseRegistrar.nameExpires(tokenId);
const gracePeriod = 90 * 24 * 60 * 60; // 90 days
const now = Math.floor(Date.now() / 1000);
if (expires + gracePeriod < now) {
const elapsed = now - (expires + gracePeriod);
const premium = await oracle.decayedPremium(
ethers.utils.parseEther('1000'), // 1000 ETH equivalent in attoUSD
elapsed
);
console.log(`Premium after ${elapsed / 86400} days: ${ethers.utils.formatEther(premium)} ETH`);
}
Comparison
| Oracle | Premium Decay | Best For | Complexity |
|---|
| StablePriceOracle | None | Custom implementations | Low |
| LinearPremiumPriceOracle | Linear | Gradual, predictable decay | Medium |
| ExponentialPremiumPriceOracle | Exponential | Fast initial decay, then gradual | High |
Integration Notes
Price Units
Prices are stored in “attoUSD” (10^-18 USD):
// $5 USD per year = 5 * 10^18 attoUSD
const priceInAttoUSD = ethers.utils.parseEther('5');
ETH/USD Oracle
The Chainlink oracle must return price with 8 decimals:
// If ETH = $2000
// Oracle returns: 200000000000 (2000 * 10^8)
The conversion from attoUSD to wei:
function attoUSDToWei(uint256 amount) internal view returns (uint256) {
uint256 ethPrice = uint256(usdOracle.latestAnswer());
return (amount * 1e8) / ethPrice;
}
Source: StablePriceOracle.sol:83
Grace Period Handling
All oracles treat names in the grace period (90 days after expiry) as renewals with no premium:
expires = expires + GRACE_PERIOD;
if (expires > block.timestamp) {
return 0; // No premium
}
Custom Implementations
To create a custom price oracle:
- Implement the
IPriceOracle interface
- Return a
Price struct with base and premium in wei
- Consider inheriting from
StablePriceOracle for USD pricing support
- Override
_premium() for custom premium logic
contract CustomOracle is StablePriceOracle {
function _premium(
string memory name,
uint256 expires,
uint256 duration
) internal view override returns (uint256) {
// Your custom premium logic
return 0;
}
}