The BulkRenewal contract provides a convenient way to renew multiple .eth names in a single transaction, saving gas and time compared to individual renewals.
Overview
Key features:
- Renew multiple names in one transaction
- Automatic price calculation for all names
- Returns excess payment to sender
- Works with the current ETHRegistrarController via ENS resolution
How It Works
The BulkRenewal contract:
- Resolves the current ETHRegistrarController via the ENS registry
- Calculates the total price for all renewals
- Calls
renew() on the controller for each name
- Returns any excess ETH to the sender
Source: BulkRenewal.sol:13
Core Functions
rentPrice
Calculates the total price to renew multiple names.
function rentPrice(
string[] calldata names,
uint256 duration
) external view returns (uint256 total)
Array of labels to renew (without .eth suffix)
Duration in seconds to add to each name
Returns: Total price in wei for all renewals (base + premium for each name)
Source: BulkRenewal.sol:34
renewAll
Renews multiple names in a single transaction.
function renewAll(
string[] calldata names,
uint256 duration,
bytes32 referrer
) external payable
Duration in seconds to add to each name
Optional referrer identifier for analytics
Requirements:
msg.value must be >= total price for all renewals
- All names must be registered (not expired past grace period)
Source: BulkRenewal.sol:52
Example Usage
Calculate Bulk Renewal Price
import { ethers } from 'ethers';
import { BulkRenewal } from '@ensdomains/ens-contracts';
const bulkRenewal = new ethers.Contract(
bulkRenewalAddress,
BulkRenewal.abi,
provider
);
const names = ['alice', 'bob', 'charlie'];
const duration = 31536000; // 1 year in seconds
const totalPrice = await bulkRenewal.rentPrice(names, duration);
console.log(`Total price: ${ethers.utils.formatEther(totalPrice)} ETH`);
console.log(`Average per name: ${ethers.utils.formatEther(totalPrice.div(names.length))} ETH`);
Renew Multiple Names
import { ethers } from 'ethers';
const names = ['alice', 'bob', 'charlie'];
const duration = 31536000; // 1 year
// Get total price
const totalPrice = await bulkRenewal.rentPrice(names, duration);
// Add 10% buffer for price fluctuations
const priceWithBuffer = totalPrice.mul(110).div(100);
// Renew all names
const tx = await bulkRenewal.renewAll(
names,
duration,
ethers.constants.HashZero, // no referrer
{ value: priceWithBuffer }
);
await tx.wait();
console.log('All names renewed!');
// Any excess ETH is automatically returned
Renew with Price Check
const names = ['alice', 'bob', 'charlie'];
const duration = 31536000;
// Calculate expected price
const expectedPrice = await bulkRenewal.rentPrice(names, duration);
// Set maximum acceptable price (10% buffer)
const maxPrice = expectedPrice.mul(110).div(100);
// Check if current price is acceptable
const currentPrice = await bulkRenewal.rentPrice(names, duration);
if (currentPrice.gt(maxPrice)) {
throw new Error('Price too high!');
}
// Proceed with renewal
const tx = await bulkRenewal.renewAll(names, duration, ethers.constants.HashZero, {
value: currentPrice
});
await tx.wait();
Gas Optimization
The BulkRenewal contract uses several gas optimization techniques:
- Unchecked arithmetic - Uses unchecked blocks where overflow is impossible
- Single controller lookup - Resolves the controller once, not per name
- Batch processing - Processes all names in one transaction
Source: BulkRenewal.sol:45-47
Integration Notes
Controller Resolution
The contract automatically finds the current ETHRegistrarController by:
- Getting the resolver for the .eth TLD
- Querying for the IETHRegistrarController interface implementer
- Using that address for all renewal calls
This means the BulkRenewal contract doesn’t need to be updated if the controller address changes.
Source: BulkRenewal.sol:23
Excess Funds
Any ETH sent beyond the total renewal cost is automatically returned to the sender:
payable(msg.sender).transfer(address(this).balance);
Source: BulkRenewal.sol:73
Empty Array Handling
Sending an empty array is valid and will:
- Return 0 from
rentPrice()
- Complete successfully in
renewAll() (no-op)
- Return all sent ETH
Common Patterns
Portfolio Management
Renew all names in a portfolio:
const portfolio = await getMyNames(ownerAddress);
const labels = portfolio.map(name => name.replace('.eth', ''));
const duration = 31536000; // 1 year
const price = await bulkRenewal.rentPrice(labels, duration);
await bulkRenewal.renewAll(labels, duration, ethers.constants.HashZero, {
value: price
});
Selective Renewal
Renew only names expiring soon:
const portfolio = await getMyNames(ownerAddress);
const now = Math.floor(Date.now() / 1000);
const thirtyDays = 30 * 24 * 60 * 60;
// Filter names expiring in the next 30 days
const expiringSoon = portfolio.filter(name => {
return name.expires < now + thirtyDays;
});
const labels = expiringSoon.map(name => name.replace('.eth', ''));
if (labels.length > 0) {
const price = await bulkRenewal.rentPrice(labels, 31536000);
await bulkRenewal.renewAll(labels, 31536000, ethers.constants.HashZero, {
value: price
});
}
Comparison with Individual Renewals
| Method | Gas Cost | Transactions | Convenience |
|---|
| Individual renewals | ~50k per name | N transactions | Manual |
| BulkRenewal | ~45k per name | 1 transaction | Automatic |
For 10 names, bulk renewal can save:
- ~50k gas (10% savings)
- 9 transactions
- Significant time and UX improvement
Error Handling
The contract will revert if:
- Any name is not registered or past grace period
- Insufficient ETH is sent
- The controller cannot be resolved
- Any individual renewal fails
Since it’s an all-or-nothing transaction, a failure for one name means no names are renewed.