Skip to main content
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:
  1. Resolves the current ETHRegistrarController via the ENS registry
  2. Calculates the total price for all renewals
  3. Calls renew() on the controller for each name
  4. 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)
names
string[]
required
Array of labels to renew (without .eth suffix)
duration
uint256
required
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
names
string[]
required
Array of labels to renew
duration
uint256
required
Duration in seconds to add to each name
referrer
bytes32
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:
  1. Unchecked arithmetic - Uses unchecked blocks where overflow is impossible
  2. Single controller lookup - Resolves the controller once, not per name
  3. 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:
  1. Getting the resolver for the .eth TLD
  2. Querying for the IETHRegistrarController interface implementer
  3. 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

MethodGas CostTransactionsConvenience
Individual renewals~50k per nameN transactionsManual
BulkRenewal~45k per name1 transactionAutomatic
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.

Build docs developers (and LLMs) love