Skip to content

Commit

Permalink
Merge pull request #387 from ensdomains/migration-helper
Browse files Browse the repository at this point in the history
Initial revision of MigrationHelper
  • Loading branch information
Arachnid authored Oct 7, 2024
2 parents 14b73e1 + 6e15e59 commit 5421b56
Show file tree
Hide file tree
Showing 4 changed files with 373 additions and 0 deletions.
68 changes: 68 additions & 0 deletions contracts/utils/MigrationHelper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;

import {IBaseRegistrar} from "../ethregistrar/IBaseRegistrar.sol";
import {INameWrapper} from "../wrapper/INameWrapper.sol";
import {Controllable} from "../wrapper/Controllable.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract MigrationHelper is Ownable, Controllable {
IBaseRegistrar public immutable registrar;
INameWrapper public immutable wrapper;
address public migrationTarget;

error MigrationTargetNotSet();

event MigrationTargetUpdated(address indexed target);

constructor(IBaseRegistrar _registrar, INameWrapper _wrapper) {
registrar = _registrar;
wrapper = _wrapper;
}

function setMigrationTarget(address target) external onlyOwner {
migrationTarget = target;
emit MigrationTargetUpdated(target);
}

function migrateNames(
address nameOwner,
uint256[] memory tokenIds,
bytes memory data
) external onlyController {
if (migrationTarget == address(0)) {
revert MigrationTargetNotSet();
}

for (uint256 i = 0; i < tokenIds.length; i++) {
registrar.safeTransferFrom(
nameOwner,
migrationTarget,
tokenIds[i],
data
);
}
}

function migrateWrappedNames(
address nameOwner,
uint256[] memory tokenIds,
bytes memory data
) external onlyController {
if (migrationTarget == address(0)) {
revert MigrationTargetNotSet();
}

uint256[] memory amounts = new uint256[](tokenIds.length);
for (uint256 i = 0; i < amounts.length; i++) {
amounts[i] = 1;
}
wrapper.safeBatchTransferFrom(
nameOwner,
migrationTarget,
tokenIds,
amounts,
data
);
}
}
26 changes: 26 additions & 0 deletions deploy/utils/10_deploy_migration_helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { DeployFunction } from 'hardhat-deploy/types.js'

const func: DeployFunction = async function (hre) {
const { deployments, viem } = hre
const { deploy } = deployments

const { deployer, owner } = await viem.getNamedClients()

const registrar = await viem.getContract('BaseRegistrarImplementation')
const wrapper = await viem.getContract('NameWrapper')

await viem.deploy('MigrationHelper', [registrar.address, wrapper.address])

if (owner !== undefined && owner.address !== deployer.address) {
const migrationHelper = await viem.getContract('MigrationHelper')
const hash = await migrationHelper.write.transferOwnership([owner.address])
console.log(`Transfer ownership to ${owner.address} (tx: ${hash})...`)
await viem.waitForTransactionSuccess(hash)
}
}

func.id = 'migration-helper'
func.tags = ['utils', 'MigrationHelper']
func.dependencies = ['BaseRegistrarImplementation', 'NameWrapper']

export default func
1 change: 1 addition & 0 deletions scripts/deploy-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ execSync('bun run hardhat --network localhost deploy', {
env: {
...process.env,
NODE_OPTIONS: '--experimental-loader ts-node/esm/transpile-only',
BATCH_GATEWAY_URLS: '["https://example.com/"]',
},
})

Expand Down
278 changes: 278 additions & 0 deletions test/utils/TestMigrationHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js'
import { expect } from 'chai'
import hre from 'hardhat'
import {
hexToBigInt,
labelhash,
namehash,
stringToHex,
zeroAddress,
zeroHash,
} from 'viem'

const getAccounts = async () => {
const [ownerClient, registrantClient, otherClient] =
await hre.viem.getWalletClients()
return {
ownerAccount: ownerClient.account,
ownerClient,
registrantAccount: registrantClient.account,
registrantClient,
otherAccount: otherClient.account,
otherClient,
}
}

async function fixture() {
const accounts = await getAccounts()
const ensRegistry = await hre.viem.deployContract('ENSRegistry', [])
const baseRegistrar = await hre.viem.deployContract(
'BaseRegistrarImplementation',
[ensRegistry.address, namehash('eth')],
)
const reverseRegistrar = await hre.viem.deployContract('ReverseRegistrar', [
ensRegistry.address,
])

await ensRegistry.write.setSubnodeOwner([
zeroHash,
labelhash('reverse'),
accounts.ownerAccount.address,
])
await ensRegistry.write.setSubnodeOwner([
namehash('reverse'),
labelhash('addr'),
reverseRegistrar.address,
])

const nameWrapper = await hre.viem.deployContract('NameWrapper', [
ensRegistry.address,
baseRegistrar.address,
accounts.ownerAccount.address,
])

await ensRegistry.write.setSubnodeOwner([
zeroHash,
labelhash('eth'),
baseRegistrar.address,
])

await baseRegistrar.write.addController([nameWrapper.address])
await baseRegistrar.write.addController([accounts.ownerAccount.address])
await nameWrapper.write.setController([accounts.ownerAccount.address, true])

const migrationHelper = await hre.viem.deployContract('MigrationHelper', [
baseRegistrar.address,
nameWrapper.address,
])
await migrationHelper.write.setController([
accounts.ownerAccount.address,
true,
])

return {
ensRegistry,
baseRegistrar,
reverseRegistrar,
nameWrapper,
migrationHelper,
...accounts,
}
}

describe('MigrationHelper', () => {
it('should allow the owner to set a migration target', async () => {
const { migrationHelper, ownerAccount } = await loadFixture(fixture)

await expect(migrationHelper)
.write('setMigrationTarget', [ownerAccount.address])
.toEmitEvent('MigrationTargetUpdated')
.withArgs(ownerAccount.address)
expect(await migrationHelper.read.migrationTarget()).toEqualAddress(
ownerAccount.address,
)
})

it('should not allow non-owners to set migration targets', async () => {
const { migrationHelper, ownerAccount, registrantAccount } =
await loadFixture(fixture)
await expect(migrationHelper)
.write('setMigrationTarget', [ownerAccount.address], {
account: registrantAccount,
})
.toBeRevertedWithString('Ownable: caller is not the owner')
})

it('should refuse to migrate unwrapped names to the zero address', async () => {
const { baseRegistrar, migrationHelper, registrantAccount } =
await loadFixture(fixture)
const ids = [labelhash('test'), labelhash('test2')].map((v) =>
hexToBigInt(v),
)
for (let id of ids) {
await baseRegistrar.write.register([
id,
registrantAccount.address,
86400n,
])
}
await baseRegistrar.write.setApprovalForAll(
[migrationHelper.address, true],
{ account: registrantAccount },
)
await expect(migrationHelper)
.write('migrateNames', [
registrantAccount.address,
ids,
stringToHex('test'),
])
.toBeRevertedWithCustomError('MigrationTargetNotSet')
})

it('should migrate unwrapped names', async () => {
const { baseRegistrar, migrationHelper, ownerAccount, registrantAccount } =
await loadFixture(fixture)
const ids = [labelhash('test'), labelhash('test2')].map((v) =>
hexToBigInt(v),
)
for (let id of ids) {
await baseRegistrar.write.register([
id,
registrantAccount.address,
86400n,
])
}
await baseRegistrar.write.setApprovalForAll(
[migrationHelper.address, true],
{ account: registrantAccount },
)
await migrationHelper.write.setMigrationTarget([ownerAccount.address])
const tx = await migrationHelper.write.migrateNames([
registrantAccount.address,
ids,
stringToHex('test'),
])
await expect(migrationHelper)
.transaction(tx)
.toEmitEventFrom(baseRegistrar, 'Transfer')
.withArgs(registrantAccount.address, ownerAccount.address, ids[0])
await expect(migrationHelper)
.transaction(tx)
.toEmitEventFrom(baseRegistrar, 'Transfer')
.withArgs(registrantAccount.address, ownerAccount.address, ids[1])
})

it('should only allow controllers to migrate unwrapped names', async () => {
const { baseRegistrar, migrationHelper, ownerAccount, registrantAccount } =
await loadFixture(fixture)
const ids = [labelhash('test'), labelhash('test2')].map((v) =>
hexToBigInt(v),
)
for (let id of ids) {
await baseRegistrar.write.register([
id,
registrantAccount.address,
86400n,
])
}
await migrationHelper.write.setMigrationTarget([ownerAccount.address])
await baseRegistrar.write.setApprovalForAll(
[migrationHelper.address, true],
{ account: registrantAccount },
)
await expect(migrationHelper)
.write(
'migrateNames',
[registrantAccount.address, ids, stringToHex('test')],
{ account: registrantAccount },
)
.toBeRevertedWithString('Controllable: Caller is not a controller')
})

it('should migrate wrapped names', async () => {
const { nameWrapper, migrationHelper, ownerAccount, registrantAccount } =
await loadFixture(fixture)
const labels = ['test', 'test2']
const ids = labels.map((label) => hexToBigInt(namehash(label + '.eth')))
for (let label of labels) {
await nameWrapper.write.registerAndWrapETH2LD([
label,
registrantAccount.address,
86400n,
zeroAddress,
0,
])
}
await migrationHelper.write.setMigrationTarget([ownerAccount.address])
await nameWrapper.write.setApprovalForAll([migrationHelper.address, true], {
account: registrantAccount,
})
await expect(migrationHelper)
.write('migrateWrappedNames', [
registrantAccount.address,
ids,
stringToHex('test'),
])
.toEmitEventFrom(nameWrapper, 'TransferBatch')
.withArgs(
migrationHelper.address,
registrantAccount.address,
ownerAccount.address,
ids,
ids.map(() => 1n),
)
})

it('should refuse to migrate wrapped names to the zero address', async () => {
const { nameWrapper, migrationHelper, registrantAccount } =
await loadFixture(fixture)
const labels = ['test', 'test2']
const ids = labels.map((label) => hexToBigInt(namehash(label + '.eth')))
for (let label of labels) {
await nameWrapper.write.registerAndWrapETH2LD([
label,
registrantAccount.address,
86400n,
zeroAddress,
0,
])
}
await nameWrapper.write.setApprovalForAll([migrationHelper.address, true], {
account: registrantAccount,
})
await expect(migrationHelper)
.write('migrateWrappedNames', [
registrantAccount.address,
ids,
stringToHex('test'),
])
.toBeRevertedWithCustomError('MigrationTargetNotSet')
})

it('should only allow controllers to migrate wrapped names', async () => {
const { nameWrapper, migrationHelper, ownerAccount, registrantAccount } =
await loadFixture(fixture)
const labels = ['test', 'test2']
const ids = labels.map((label) => hexToBigInt(namehash(label + '.eth')))
for (let label of labels) {
await nameWrapper.write.registerAndWrapETH2LD([
label,
registrantAccount.address,
86400n,
zeroAddress,
0,
])
}
await migrationHelper.write.setMigrationTarget([ownerAccount.address])
await nameWrapper.write.setApprovalForAll([migrationHelper.address, true], {
account: registrantAccount,
})
await expect(migrationHelper)
.write(
'migrateWrappedNames',
[registrantAccount.address, ids, stringToHex('test')],
{ account: registrantAccount },
)
.toBeRevertedWithString('Controllable: Caller is not a controller')
})
})

0 comments on commit 5421b56

Please sign in to comment.