diff --git a/bun.lockb b/bun.lockb index d88687ed..c48dfd25 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 2183b675..252b11ba 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "main": "index.js", "devDependencies": { "@ensdomains/dnsprovejs": "^0.5.1", - "@ensdomains/hardhat-chai-matchers-viem": "^0.0.5", + "@ensdomains/hardhat-chai-matchers-viem": "^0.0.6", "@ensdomains/test-utils": "^1.3.0", "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", "@openzeppelin/test-helpers": "^0.5.11", diff --git a/test/wrapper/TestNameWrapper.ts b/test/wrapper/TestNameWrapper.ts index 72a91411..2bc89a3d 100644 --- a/test/wrapper/TestNameWrapper.ts +++ b/test/wrapper/TestNameWrapper.ts @@ -1,24 +1,57 @@ import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' -import { zeroAddress } from 'viem' -import { DAY, FUSES } from '../fixtures/constants.js' +import { expect } from 'chai' +import { getAddress, labelhash, namehash, zeroAddress, zeroHash } from 'viem' +import { DAY } from '../fixtures/constants.js' +import { dnsEncodeName } from '../fixtures/dnsEncodeName.js' import { toLabelId, toNameId } from '../fixtures/utils.js' import { shouldRespectConstraints } from './Constraints.behaviour.js' import { shouldBehaveLikeErc1155 } from './ERC1155.behaviour.js' import { shouldSupportInterfaces } from './SupportsInterface.behaviour.js' -import { deployNameWrapperFixture } from './fixtures/deploy.js' +import { + CANNOT_CREATE_SUBDOMAIN, + CANNOT_TRANSFER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from './fixtures/utils.js' + +import { approveTests } from './functions/approve.js' +import { extendExpiryTests } from './functions/extendExpiry.js' +import { getApprovedTests } from './functions/getApproved.js' +import { getDataTests } from './functions/getData.js' +import { isWrappedTests } from './functions/isWrapped.js' +import { onERC721ReceivedTests } from './functions/onERC721Received.js' +import { ownerOfTests } from './functions/ownerOf.js' +import { registerAndWrapETH2LDTests } from './functions/registerAndWrapETH2LD.js' +import { renewTests } from './functions/renew.js' +import { setChildFusesTests } from './functions/setChildFuses.js' +import { setFusesTests } from './functions/setFuses.js' +import { setRecordTests } from './functions/setRecord.js' +import { setResolverTests } from './functions/setResolver.js' +import { setSubnodeOwnerTests } from './functions/setSubnodeOwner.js' +import { setSubnodeRecordTests } from './functions/setSubnodeRecord.js' +import { setTTLTests } from './functions/setTTL.js' +import { setUpgradeContractTests } from './functions/setUpgradeContract.js' +import { unwrapTests } from './functions/unwrap.js' +import { unwrapETH2LDTests } from './functions/unwrapETH2LD.js' +import { upgradeTests } from './functions/upgrade.js' +import { wrapTests } from './functions/wrap.js' +import { wrapETH2LDTests } from './functions/wrapETH2LD.js' describe('NameWrapper', () => { shouldSupportInterfaces({ - contract: () => - loadFixture(deployNameWrapperFixture).then( - ({ nameWrapper }) => nameWrapper, - ), + contract: () => loadFixture(fixture).then(({ nameWrapper }) => nameWrapper), interfaces: ['INameWrapper', 'IERC721Receiver'], }) shouldBehaveLikeErc1155({ contracts: () => - loadFixture(deployNameWrapperFixture).then((contracts) => ({ + loadFixture(fixture).then((contracts) => ({ contract: contracts.nameWrapper, ...contracts, })), @@ -28,35 +61,1069 @@ describe('NameWrapper', () => { toNameId('doesnotexist.eth'), ], mint: async ( - { nameWrapper, baseRegistrar, accounts }, + { accounts, actions }, [firstTokenHolder, secondTokenHolder], ) => { - await baseRegistrar.write.setApprovalForAll([nameWrapper.address, true]) - await baseRegistrar.write.register([ - toLabelId('test1'), - accounts[0].address, - 1n * DAY, + await actions.setBaseRegistrarApprovalForWrapper() + await actions.register({ + label: 'test1', + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.wrapEth2ld({ + label: 'test1', + owner: firstTokenHolder, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + await actions.register({ + label: 'test2', + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.wrapEth2ld({ + label: 'test2', + owner: secondTokenHolder, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + }, + }) + + shouldRespectConstraints() + + approveTests() + extendExpiryTests() + getApprovedTests() + getDataTests() + isWrappedTests() + onERC721ReceivedTests() + ownerOfTests() + registerAndWrapETH2LDTests() + renewTests() + setChildFusesTests() + setFusesTests() + setRecordTests() + setResolverTests() + setSubnodeOwnerTests() + setSubnodeRecordTests() + setTTLTests() + setUpgradeContractTests() + unwrapTests() + unwrapETH2LDTests() + upgradeTests() + wrapTests() + wrapETH2LDTests() + + describe('Transfer', () => { + const label = 'transfer' + const name = `${label}.eth` + + async function transferFixture() { + const initial = await loadFixture(fixture) + const { actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + return initial + } + + it('safeTransfer cannot be called if CANNOT_TRANSFER is burned and is not expired', async () => { + const { nameWrapper, accounts } = await loadFixture(transferFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_TRANSFER]) + + await expect(nameWrapper) + .write('safeTransferFrom', [ + accounts[0].address, + accounts[1].address, + toNameId(name), + 1n, + '0x', + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('safeBatchTransfer cannot be called if CANNOT_TRANSFER is burned and is not expired', async () => { + const { nameWrapper, accounts } = await loadFixture(transferFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_TRANSFER]) + + await expect(nameWrapper) + .write('safeBatchTransferFrom', [ + accounts[0].address, + accounts[1].address, + [toNameId(name)], + [1n], + '0x', + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + }) + + describe('Controllable', () => { + it('allows the owner to add and remove controllers', async () => { + const { nameWrapper, accounts } = await loadFixture(fixture) + + await expect(nameWrapper) + .write('setController', [accounts[0].address, true]) + .toEmitEvent('ControllerChanged') + .withArgs(accounts[0].address, true) + + await expect(nameWrapper) + .write('setController', [accounts[0].address, false]) + .toEmitEvent('ControllerChanged') + .withArgs(accounts[0].address, false) + }) + + it('does not allow non-owners to add or remove controllers', async () => { + const { nameWrapper, accounts } = await loadFixture(fixture) + + await nameWrapper.write.setController([accounts[0].address, true]) + + await expect(nameWrapper) + .write('setController', [accounts[1].address, true], { + account: accounts[1], + }) + .toBeRevertedWithString('Ownable: caller is not the owner') + + await expect(nameWrapper) + .write('setController', [accounts[0].address, false], { + account: accounts[1], + }) + .toBeRevertedWithString('Ownable: caller is not the owner') + }) + }) + + describe('MetadataService', () => { + it('uri() returns url', async () => { + const { nameWrapper } = await loadFixture(fixture) + + await expect(nameWrapper.read.uri([123n])).resolves.toEqual( + 'https://ens.domains', + ) + }) + + it('owner can set a new MetadataService', async () => { + const { nameWrapper, accounts } = await loadFixture(fixture) + + await nameWrapper.write.setMetadataService([accounts[1].address]) + + await expect(nameWrapper.read.metadataService()).resolves.toEqualAddress( + accounts[1].address, + ) + }) + + it('non-owner cannot set a new MetadataService', async () => { + const { nameWrapper, accounts } = await loadFixture(fixture) + + await expect(nameWrapper) + .write('setMetadataService', [accounts[1].address], { + account: accounts[1], + }) + .toBeRevertedWithString('Ownable: caller is not the owner') + }) + }) + + describe('NameWrapper.names preimage dictionary', () => { + it('Does not allow manipulating the preimage db by manually setting owner as NameWrapper', async () => { + const { + baseRegistrar, + ensRegistry, + nameWrapper, + accounts, + testClient, + publicClient, + actions, + } = await loadFixture(fixture) + + const label = 'base' + const name = `${label}.eth` + + await actions.register({ + label, + owner: accounts[2].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper({ account: 2 }) + await actions.wrapEth2ld({ + label, + owner: accounts[2].address, + fuses: CANNOT_UNWRAP, + resolver: zeroAddress, + account: 2, + }) + + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[2]) + + // signed a submomain for the hacker, with a soon-expired expiry + const sublabel1 = 'sub1' + const subname1 = `${sublabel1}.${name}` // sub1.base.eth + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel1, + owner: accounts[2].address, + fuses: 0, + expiry: timestamp + 3600n, // soonly expired + account: 2, + }) + + await expectOwnerOf(subname1).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(subname1).on(nameWrapper).toBe(accounts[2]) + + const [, fuses] = await nameWrapper.read.getData([toNameId(subname1)]) + expect(fuses).toEqual(0) + + // the hacker unwraps their wrappedSubTokenId + await testClient.increaseTime({ seconds: 7200 }) + await actions.unwrapName({ + parentName: name, + label: sublabel1, + controller: accounts[2].address, + account: 2, + }) + await expectOwnerOf(subname1).on(ensRegistry).toBe(accounts[2]) + + // the hacker setSubnodeOwner, to set the owner of subname2 as NameWrapper + const sublabel2 = 'sub2' + const subname2 = `${sublabel2}.${subname1}` // sub2.sub1.base.eth + + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: subname1, + label: sublabel2, + owner: nameWrapper.address, + account: 2, + }) + + await expectOwnerOf(subname2).on(ensRegistry).toBe(nameWrapper) + + // the hacker re-wraps the subname1 + await actions.setRegistryApprovalForWrapper({ account: 2 }) + await actions.wrapName({ + name: subname1, + owner: accounts[2].address, + resolver: zeroAddress, + account: 2, + }) + await expectOwnerOf(subname1).on(nameWrapper).toBe(accounts[2]) + + // the hackers setSubnodeOwner + // XXX: till now, the hacker gets sub2Domain with no name in Namewrapper + await actions.setSubnodeOwner.onNameWrapper({ + parentName: subname1, + label: sublabel2, + owner: accounts[2].address, + fuses: CAN_DO_EVERYTHING, + expiry: MAX_EXPIRY, + account: 2, + }) + await expectOwnerOf(subname2).on(nameWrapper).toBe(accounts[2]) + await expect( + nameWrapper.read.names([namehash(subname2)]), + ).resolves.toEqual(dnsEncodeName(subname2)) + + // the hacker forge a fake root node + const sublabel3 = 'eth' + const subname3 = `${sublabel3}.${subname2}` // eth.sub2.sub1.base.eth + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: subname2, + label: sublabel3, + owner: accounts[2].address, + fuses: CAN_DO_EVERYTHING, + expiry: MAX_EXPIRY, + account: 2, + }) + + await expectOwnerOf(subname3).on(nameWrapper).toBe(accounts[2]) + await expect( + nameWrapper.read.names([namehash(subname3)]), + ).resolves.toEqual(dnsEncodeName(subname3)) + }) + }) + + describe('Grace period tests', () => { + const label = 'test' + const name = `${label}.eth` + const sublabel = 'sub' + const subname = `${sublabel}.${name}` + + async function gracePeriodFixture() { + const initial = await loadFixture(fixture) + const { nameWrapper, actions, accounts, testClient, publicClient } = + initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [, , parentExpiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + + // create a subdomain for other tests + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: parentExpiry - DAY / 2n, + }) + + // move .eth name to expired and be within grace period + await testClient.increaseTime({ seconds: Number(2n * DAY) }) + await testClient.mine({ blocks: 1 }) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + // expect name to be expired, but inside grace period + expect(parentExpiry - GRACE_PERIOD).toBeLessThan(timestamp) + expect(parentExpiry + GRACE_PERIOD).toBeGreaterThan(timestamp) + + const [, , subExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + // subdomain is not expired + expect(subExpiry).toBeGreaterThan(timestamp) + + return { ...initial, parentExpiry } + } + + it('When a .eth name is in grace period it cannot call setSubnodeOwner', async () => { + const { nameWrapper, parentExpiry, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[1].address, + PARENT_CANNOT_CONTROL, + parentExpiry - DAY / 2n, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('When a .eth name is in grace period it cannot call setSubnodeRecord', async () => { + const { nameWrapper, parentExpiry, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[1].address, + zeroAddress, + 0n, + PARENT_CANNOT_CONTROL, + parentExpiry - DAY / 2n, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('When a .eth name is in grace period it cannot call setRecord', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await expect(nameWrapper) + .write('setRecord', [ + namehash(name), + accounts[1].address, + zeroAddress, + 0n, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('When a .eth name is in grace period it cannot call safeTransferFrom', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await expect(nameWrapper) + .write('safeTransferFrom', [ + accounts[0].address, + accounts[1].address, + toNameId(name), + 1n, + '0x', + ]) + .toBeRevertedWithString('ERC1155: insufficient balance for transfer') + }) + + it('When a .eth name is in grace period it cannot call batchSafeTransferFrom', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await expect(nameWrapper) + .write('safeBatchTransferFrom', [ + accounts[0].address, + accounts[1].address, + [toNameId(name)], + [1n], + '0x', + ]) + .toBeRevertedWithString('ERC1155: insufficient balance for transfer') + }) + + it('When a .eth name is in grace period it cannot call setResolver', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await expect(nameWrapper) + .write('setResolver', [namehash(name), zeroAddress]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('When a .eth name is in grace period it cannot call setTTL', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await expect(nameWrapper) + .write('setTTL', [namehash(name), 0n]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('When a .eth name is in grace period it cannot call setFuses', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await expect(nameWrapper) + .write('setFuses', [namehash(name), 0]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('When a .eth name is in grace period it cannot call setChildFuses', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await expect(nameWrapper) + .write('setChildFuses', [namehash(name), labelhash(sublabel), 0, 0n]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('When a .eth name is in grace period, unexpired subdomains can call setFuses', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await nameWrapper.write.setFuses([namehash(subname), CANNOT_UNWRAP], { + account: accounts[1], + }) + + const [, fuses] = await nameWrapper.read.getData([toNameId(subname)]) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + }) + + it('When a .eth name is in grace period, unexpired subdomains can transfer', async () => { + const { nameWrapper, accounts } = await loadFixture(gracePeriodFixture) + + await nameWrapper.write.safeTransferFrom( + [accounts[1].address, accounts[0].address, toNameId(subname), 1n, '0x'], + { account: accounts[1] }, + ) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + }) + + it('When a .eth name is in grace period, unexpired subdomains can set resolver', async () => { + const { ensRegistry, nameWrapper, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await nameWrapper.write.setResolver( + [namehash(subname), accounts[0].address], + { + account: accounts[1], + }, + ) + + await expect( + ensRegistry.read.resolver([namehash(subname)]), + ).resolves.toEqualAddress(accounts[0].address) + }) + + it('When a .eth name is in grace period, unexpired subdomains can set ttl', async () => { + const { ensRegistry, nameWrapper, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await nameWrapper.write.setTTL([namehash(subname), 100n], { + account: accounts[1], + }) + + await expect(ensRegistry.read.ttl([namehash(subname)])).resolves.toEqual( + 100n, + ) + }) + + it('When a .eth name is in grace period, unexpired subdomains can call setRecord', async () => { + const { ensRegistry, nameWrapper, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await nameWrapper.write.setRecord( + [namehash(subname), accounts[0].address, accounts[1].address, 100n], + { + account: accounts[1], + }, + ) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + await expectOwnerOf(subname).on(ensRegistry).toBe(nameWrapper) + await expect( + ensRegistry.read.resolver([namehash(subname)]), + ).resolves.toEqualAddress(accounts[1].address) + await expect(ensRegistry.read.ttl([namehash(subname)])).resolves.toBe( + 100n, + ) + }) + + it('When a .eth name is in grace period, unexpired subdomains can call setSubnodeOwner', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: subname, + label: 'sub2', + owner: accounts[1].address, + fuses: 0, + expiry: 0n, + account: 1, + }) + + await expectOwnerOf(`sub2.${subname}`).on(nameWrapper).toBe(accounts[1]) + }) + + it('When a .eth name is in grace period, unexpired subdomains can call setSubnodeRecord', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: subname, + label: 'sub2', + owner: accounts[1].address, + resolver: zeroAddress, + ttl: 0n, + fuses: 0, + expiry: 0n, + account: 1, + }) + + await expectOwnerOf(`sub2.${subname}`).on(nameWrapper).toBe(accounts[1]) + }) + + it('When a .eth name is in grace period, unexpired subdomains can call setChildFuses if the subdomain exists', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + gracePeriodFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: subname, + label: 'sub2', + owner: accounts[1].address, + fuses: 0, + expiry: 0n, + account: 1, + }) + + await nameWrapper.write.setChildFuses( + [namehash(subname), labelhash('sub2'), 0, 100n], + { + account: accounts[1], + }, + ) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(`sub2.${subname}`), + ]) + expect(owner).toEqualAddress(accounts[1].address) + expect(expiry).toEqual(100n) + expect(fuses).toEqual(0) + }) + }) + + describe('Registrar tests', () => { + const label = 'sub1' + const name = `${label}.eth` + const sublabel = 'sub2' + const subname = `${sublabel}.${name}` + + it('Reverts when attempting to call token owner protected function on an unwrapped name', async () => { + const { + ensRegistry, + nameWrapper, + baseRegistrar, + actions, + accounts, + testClient, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + // wait the ETH2LD expired and re-register to the hacker themselves + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + // XXX: note that at this step, the hackler should use the current .eth + // registrar to directly register `sub1.eth` to himself, without wrapping + // the name. + await actions.register({ + label, + owner: accounts[2].address, + duration: 10n * DAY, + }) + await expectOwnerOf(name).on(ensRegistry).toBe(accounts[2]) + await expectOwnerOf(label).on(baseRegistrar).toBe(accounts[2]) + + // set `EnsRegistry.owner` as NameWrapper. Note that this step is used to + // bypass the newly-introduced checks for [ZZ-001] + // + // XXX: corrently, `sub1.eth` becomes a normal node + await ensRegistry.write.setOwner([namehash(name), nameWrapper.address], { + account: accounts[2], + }) + + // create `sub2.sub1.eth` to the victim user with `PARENT_CANNOT_CONTROL` + // burnt. + await expect(nameWrapper) + .write( + 'setSubnodeOwner', + [ + namehash(name), + sublabel, + accounts[1].address, + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + MAX_EXPIRY, + ], + { account: accounts[2] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[2].address)) + }) + }) + + describe('ERC1155 additional tests', () => { + const label = 'erc1155' + const name = `${label}.eth` + + it('Transferring a token that is not owned by the owner reverts', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await expect(nameWrapper) + .write( + 'safeTransferFrom', + [accounts[2].address, accounts[0].address, toNameId(name), 1n, '0x'], + { account: accounts[2] }, + ) + .toBeRevertedWithString('ERC1155: insufficient balance for transfer') + }) + + it('Approval on the Wrapper does not give permission to wrap the .eth name', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + + await nameWrapper.write.setApprovalForAll([accounts[2].address, true]) + + await expect(nameWrapper) + .write('wrapETH2LD', [label, accounts[2].address, 0, zeroAddress], { + account: accounts[2], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(label + '.eth'), getAddress(accounts[2].address)) + }) + + it('Approval on the Wrapper does not give permission to wrap a non .eth name', async () => { + const { nameWrapper, ensRegistry, accounts, actions } = await loadFixture( + fixture, + ) + + await expectOwnerOf('xyz').on(ensRegistry).toBe(accounts[0]) + + await nameWrapper.write.setApprovalForAll([accounts[2].address, true]) + + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write( + 'wrap', + [dnsEncodeName('xyz'), accounts[2].address, zeroAddress], + { + account: accounts[2], + }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash('xyz'), getAddress(accounts[2].address)) + }) + + it('When .eth name expires, it is untransferrable', async () => { + const { nameWrapper, actions, accounts, testClient } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('safeTransferFrom', [ + accounts[0].address, + accounts[1].address, + toNameId(name), + 1n, + '0x', + ]) + .toBeRevertedWithString('ERC1155: insufficient balance for transfer') + }) + + it('Approval on the Wrapper does not give permission to transfer after expiry', async () => { + const { nameWrapper, actions, accounts, testClient } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + await nameWrapper.write.setApprovalForAll([accounts[2].address, true]) + + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('safeTransferFrom', [ + accounts[0].address, + accounts[1].address, + toNameId(name), + 1n, + '0x', + ]) + .toBeRevertedWithString('ERC1155: insufficient balance for transfer') + + await expect(nameWrapper) + .write( + 'safeTransferFrom', + [accounts[0].address, accounts[2].address, toNameId(name), 1n, '0x'], + { account: accounts[2] }, + ) + .toBeRevertedWithString('ERC1155: insufficient balance for transfer') + }) + + it('When emancipated names expire, they are untransferrible', async () => { + const { nameWrapper, actions, accounts, testClient, publicClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'test', + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: 3600n + timestamp, + }) + + await testClient.increaseTime({ seconds: 3601 }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('safeTransferFrom', [ + accounts[0].address, + accounts[1].address, + toNameId(`test.${name}`), + 1n, + '0x', + ]) + .toBeRevertedWithString('ERC1155: insufficient balance for transfer') + }) + + it('Returns a balance of 0 for expired names', async () => { + const { nameWrapper, actions, accounts, testClient } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect( + nameWrapper.read.balanceOf([accounts[0].address, toNameId(name)]), + ).resolves.toEqual(1n) + + await testClient.increaseTime({ seconds: Number(86401n + GRACE_PERIOD) }) + await testClient.mine({ blocks: 1 }) + + await expect( + nameWrapper.read.balanceOf([accounts[0].address, toNameId(name)]), + ).resolves.toEqual(0n) + }) + + it('Reregistering an expired name does not inherit its previous parent fuses', async () => { + const { nameWrapper, actions, accounts, testClient, publicClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + // Mint the subdomain + const timestamp1 = await publicClient.getBlock().then((b) => b.timestamp) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'test', + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: 3600n + timestamp1, + }) + + // Let it expire + await testClient.increaseTime({ seconds: 3601 }) + await testClient.mine({ blocks: 1 }) + + // Mint it again, without PCC + const timestamp2 = await publicClient.getBlock().then((b) => b.timestamp) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'test', + owner: accounts[0].address, + fuses: 0, + expiry: 3600n + timestamp2, + }) + + // Check PCC isn't set + const [, fuses] = await nameWrapper.read.getData([ + toNameId(`test.${name}`), ]) - await nameWrapper.write.wrapETH2LD([ - 'test1', - firstTokenHolder, - FUSES.CAN_DO_EVERYTHING, + expect(fuses).toEqual(0) + }) + }) + + describe('Implicit unwrap tests', () => { + const label = 'sub1' + const name = `${label}.eth` + const sublabel = 'sub2' + const subname = `${sublabel}.${name}` + + async function implicitUnwrapFixture() { + const initial = await loadFixture(fixture) + const { nameWrapper, baseRegistrar, accounts } = initial + + await baseRegistrar.write.addController([nameWrapper.address]) + await nameWrapper.write.setController([accounts[0].address, true]) + + return initial + } + + it('Trying to burn child fuses when re-registering a name on the old controller reverts', async () => { + const { + ensRegistry, + nameWrapper, + baseRegistrar, + actions, + accounts, + testClient, + } = await loadFixture(implicitUnwrapFixture) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[2].address, + 1n * DAY, zeroAddress, + CANNOT_UNWRAP, ]) - await baseRegistrar.write.register([ - toLabelId('test2'), + // create `sub2.sub1.eth` w/o fuses burnt + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[2].address, + fuses: CAN_DO_EVERYTHING, + expiry: MAX_EXPIRY, + account: 2, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[2]) + + // wait the ETH2LD expired and re-register to the hacker themselves + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + // XXX: note that at this step, the hacker should use the current .eth + // registrar to directly register `sub1.eth` to themselves, without wrapping + // the name. + await actions.register({ + label, + owner: accounts[2].address, + duration: 10n * DAY, + }) + await expectOwnerOf(name).on(ensRegistry).toBe(accounts[2]) + await expectOwnerOf(label).on(baseRegistrar).toBe(accounts[2]) + + // XXX: PREPARE HACK! + // set `EnsRegistry.owner` of `sub1.eth` as the hacker themselves. + await ensRegistry.write.setOwner([namehash(name), accounts[2].address], { + account: accounts[2], + }) + + // XXX: PREPARE HACK! + // set controller owner as the NameWrapper contract, to bypass the check + await baseRegistrar.write.transferFrom( + [accounts[2].address, nameWrapper.address, toLabelId(label)], + { account: accounts[2] }, + ) + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + + // set `sub2.sub1.eth` to the victim user w fuses burnt + // XXX: do this via `setChildFuses` + // Cannot setChildFuses as the owner has not been updated in the wrapper when reregistering + await expect(nameWrapper) + .write( + 'setChildFuses', + [ + namehash(name), + labelhash(sublabel), + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_CREATE_SUBDOMAIN, + MAX_EXPIRY, + ], + { account: accounts[2] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[2].address)) + }) + + it('Renewing a wrapped, but expired name .eth in the wrapper, but unexpired on the registrar resyncs expiry', async () => { + const { ensRegistry, nameWrapper, baseRegistrar, accounts, testClient } = + await loadFixture(implicitUnwrapFixture) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, accounts[0].address, 1n * DAY, - ]) - await nameWrapper.write.wrapETH2LD([ - 'test2', - secondTokenHolder, - FUSES.CAN_DO_EVERYTHING, zeroAddress, + CANNOT_UNWRAP, ]) - }, + + await baseRegistrar.write.renew([toLabelId(label), 365n * DAY]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // expired but in grace period + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expectOwnerOf(name).on(nameWrapper).toBe(zeroAccount) + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + + await nameWrapper.write.renew([toLabelId(label), 1n]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + const [, , expiry] = await nameWrapper.read.getData([toNameId(name)]) + const registrarExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + expect(expiry).toEqual(registrarExpiry + GRACE_PERIOD) + }) }) - shouldRespectConstraints() + describe('TLD recovery', () => { + it('Wraps a name which get stuck forever can be recovered by ROOT owner', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + await expectOwnerOf('xyz').on(nameWrapper).toBe(zeroAccount) + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: 'xyz', + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expectOwnerOf('xyz').on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setChildFuses([ + zeroHash, + labelhash('xyz'), + PARENT_CANNOT_CONTROL, + 0n, + ]) + + await expectOwnerOf('xyz').on(nameWrapper).toBe(zeroAccount) + await expectOwnerOf('xyz').on(ensRegistry).toBe(nameWrapper) + + await expect(nameWrapper) + .write('setChildFuses', [ + zeroHash, + labelhash('xyz'), + PARENT_CANNOT_CONTROL, + 100000000000000n, + ]) + .toBeRevertedWithCustomError('NameIsNotWrapped') + + await ensRegistry.write.setSubnodeOwner([ + zeroHash, + labelhash('xyz'), + accounts[1].address, + ]) + await actions.setRegistryApprovalForWrapper({ account: 1 }) + await actions.wrapName({ + name: 'xyz', + owner: accounts[1].address, + resolver: zeroAddress, + account: 1, + }) + + await expectOwnerOf('xyz').on(nameWrapper).toBe(accounts[1]) + await expectOwnerOf('xyz').on(ensRegistry).toBe(nameWrapper) + }) + }) }) diff --git a/test/wrapper/fixtures/deploy.ts b/test/wrapper/fixtures/deploy.ts index 013218ed..ad18623f 100644 --- a/test/wrapper/fixtures/deploy.ts +++ b/test/wrapper/fixtures/deploy.ts @@ -84,3 +84,7 @@ export async function deployNameWrapperFixture() { testClient, } } + +export type DeployNameWrapperFixtureResult = Awaited< + ReturnType +> diff --git a/test/wrapper/fixtures/utils.ts b/test/wrapper/fixtures/utils.ts new file mode 100644 index 00000000..2444fb7b --- /dev/null +++ b/test/wrapper/fixtures/utils.ts @@ -0,0 +1,310 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { + getAbiItem, + labelhash, + namehash, + padHex, + zeroAddress, + type Address, +} from 'viem' +import { DAY, FUSES } from '../../fixtures/constants.js' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + deployNameWrapperFixture as baseFixture, + type DeployNameWrapperFixtureResult as Fixture, +} from './deploy.js' + +export const zeroAccount = { address: zeroAddress } + +export const { + CANNOT_UNWRAP, + CANNOT_BURN_FUSES, + CANNOT_TRANSFER, + CANNOT_SET_RESOLVER, + CANNOT_SET_TTL, + CANNOT_CREATE_SUBDOMAIN, + PARENT_CANNOT_CONTROL, + CAN_DO_EVERYTHING, + IS_DOT_ETH, + CAN_EXTEND_EXPIRY, + CANNOT_APPROVE, +} = FUSES +export const MAX_EXPIRY = 2n ** 64n - 1n +export const GRACE_PERIOD = 90n * DAY +export const DUMMY_ADDRESS = padHex('0x01', { size: 20 }) + +export async function deployNameWrapperWithUtils() { + const initial = await loadFixture(baseFixture) + const { publicClient, ensRegistry, baseRegistrar, nameWrapper, accounts } = + initial + + const setSubnodeOwner = { + onEnsRegistry: async ({ + parentName, + label, + owner, + account = 0, + }: { + parentName: string + label: string + owner: Address + account?: number + }) => + ensRegistry.write.setSubnodeOwner( + [namehash(parentName), labelhash(label), owner], + { account: accounts[account] }, + ), + onNameWrapper: async ({ + parentName, + label, + owner, + fuses, + expiry, + account = 0, + }: { + parentName: string + label: string + owner: Address + fuses: number + expiry: bigint + account?: number + }) => + nameWrapper.write.setSubnodeOwner( + [namehash(parentName), label, owner, fuses, expiry], + { account: accounts[account] }, + ), + } + const setSubnodeRecord = { + onEnsRegistry: async ({ + parentName, + label, + owner, + resolver, + ttl, + account = 0, + }: { + parentName: string + label: string + owner: Address + resolver: Address + ttl: bigint + account?: number + }) => + ensRegistry.write.setSubnodeRecord( + [namehash(parentName), labelhash(label), owner, resolver, ttl], + { account: accounts[account] }, + ), + onNameWrapper: async ({ + parentName, + label, + owner, + resolver, + ttl, + fuses, + expiry, + account = 0, + }: { + parentName: string + label: string + owner: Address + resolver: Address + ttl: bigint + fuses: number + expiry: bigint + account?: number + }) => + nameWrapper.write.setSubnodeRecord( + [namehash(parentName), label, owner, resolver, ttl, fuses, expiry], + { account: accounts[account] }, + ), + } + const register = async ({ + label, + owner, + duration, + account = 0, + }: { + label: string + owner: Address + duration: bigint + account?: number + }) => + baseRegistrar.write.register([toLabelId(label), owner, duration], { + account: accounts[account], + }) + const wrapName = async ({ + name, + owner, + resolver, + account = 0, + }: { + name: string + owner: Address + resolver: Address + account?: number + }) => + nameWrapper.write.wrap([dnsEncodeName(name), owner, resolver], { + account: accounts[account], + }) + const wrapEth2ld = async ({ + label, + owner, + fuses, + resolver, + account = 0, + }: { + label: string + owner: Address + fuses: number + resolver: Address + account?: number + }) => + nameWrapper.write.wrapETH2LD([label, owner, fuses, resolver], { + account: accounts[account], + }) + const unwrapName = async ({ + parentName, + label, + controller, + account = 0, + }: { + parentName: string + label: string + controller: Address + account?: number + }) => + nameWrapper.write.unwrap( + [namehash(parentName), labelhash(label), controller], + { account: accounts[account] }, + ) + const unwrapEth2ld = async ({ + label, + registrant, + controller, + account = 0, + }: { + label: string + registrant: Address + controller: Address + account?: number + }) => + nameWrapper.write.unwrapETH2LD([labelhash(label), registrant, controller], { + account: accounts[account], + }) + const setRegistryApprovalForWrapper = async ({ + account = 0, + }: { account?: number } = {}) => + ensRegistry.write.setApprovalForAll([nameWrapper.address, true], { + account: accounts[account], + }) + const setBaseRegistrarApprovalForWrapper = async ({ + account = 0, + }: { account?: number } = {}) => + baseRegistrar.write.setApprovalForAll([nameWrapper.address, true], { + account: accounts[account], + }) + const registerSetupAndWrapName = async ({ + label, + fuses, + resolver = zeroAddress, + duration = 1n * DAY, + account = 0, + }: { + label: string + fuses: number + resolver?: Address + duration?: bigint + account?: number + }) => { + const owner = accounts[account] + + await register({ label, owner: owner.address, duration, account }) + await setBaseRegistrarApprovalForWrapper({ account }) + await wrapEth2ld({ + label, + owner: owner.address, + fuses, + resolver, + account, + }) + } + const getBlockTimestamp = async () => + publicClient.getBlock().then((b) => b.timestamp) + + const actions = { + setSubnodeOwner, + setSubnodeRecord, + register, + wrapName, + wrapEth2ld, + unwrapName, + unwrapEth2ld, + setRegistryApprovalForWrapper, + setBaseRegistrarApprovalForWrapper, + registerSetupAndWrapName, + getBlockTimestamp, + } + + return { + ...initial, + actions, + } +} + +export const runForContract = ({ + contract, + onNameWrapper, + onBaseRegistrar, + onEnsRegistry, +}: { + contract: + | Fixture['nameWrapper'] + | Fixture['ensRegistry'] + | Fixture['baseRegistrar'] + onNameWrapper?: (nameWrapper: Fixture['nameWrapper']) => Promise + onEnsRegistry?: (ensRegistry: Fixture['ensRegistry']) => Promise + onBaseRegistrar?: (baseRegistrar: Fixture['baseRegistrar']) => Promise +}) => { + if (getAbiItem({ abi: contract.abi, name: 'isWrapped' })) { + if (!onNameWrapper) throw new Error('onNameWrapper not provided') + return onNameWrapper(contract as Fixture['nameWrapper']) + } + + if (getAbiItem({ abi: contract.abi, name: 'ownerOf' })) { + if (!onBaseRegistrar) throw new Error('onBaseRegistrar not provided') + return onBaseRegistrar(contract as Fixture['baseRegistrar']) + } + + if (!onEnsRegistry) throw new Error('onEnsRegistry not provided') + return onEnsRegistry(contract as Fixture['ensRegistry']) +} + +export const expectOwnerOf = (name: string) => ({ + on: ( + contract: + | Fixture['nameWrapper'] + | Fixture['baseRegistrar'] + | Fixture['ensRegistry'], + ) => ({ + toBe: (owner: { address: Address }) => + runForContract({ + contract, + onNameWrapper: async (nameWrapper) => + expect( + nameWrapper.read.ownerOf([toNameId(name)]), + ).resolves.toEqualAddress(owner.address), + onBaseRegistrar: async (baseRegistrar) => { + if (name.includes('.')) throw new Error('Not a label') + return expect( + baseRegistrar.read.ownerOf([toLabelId(name)]), + ).resolves.toEqualAddress(owner.address) + }, + onEnsRegistry: async (ensRegistry) => + expect( + ensRegistry.read.owner([namehash(name)]), + ).resolves.toEqualAddress(owner.address), + }), + }), +}) diff --git a/test/wrapper/functions/approve.ts b/test/wrapper/functions/approve.ts new file mode 100644 index 00000000..73a83d29 --- /dev/null +++ b/test/wrapper/functions/approve.ts @@ -0,0 +1,562 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, labelhash, namehash, zeroAddress } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toNameId } from '../../fixtures/utils.js' +import { + CANNOT_APPROVE, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + CAN_EXTEND_EXPIRY, + GRACE_PERIOD, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const approveTests = () => { + describe('approve()', () => { + const label = 'subdomain' + const sublabel = 'sub' + const name = `${label}.eth` + const subname = `${sublabel}.${name}` + + async function approveFixture() { + const initial = await loadFixture(fixture) + const { nameWrapper, actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + const [, , parentExpiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + + return { ...initial, parentExpiry } + } + + it('Sets an approval address if owner', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('Sets an approval address if is an operator', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + await nameWrapper.write.approve([accounts[2].address, toNameId(name)], { + account: accounts[1], + }) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[2].address) + }) + + it('Reverts if called by an approved address', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect(nameWrapper) + .write('approve', [accounts[2].address, toNameId(name)], { + account: accounts[1], + }) + .toBeRevertedWithString( + 'ERC721: approve caller is not token owner or approved for all', + ) + }) + + it('Reverts if called by non-owner or approved', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await expect(nameWrapper) + .write('approve', [accounts[1].address, toNameId(name)], { + account: accounts[2], + }) + .toBeRevertedWithString( + 'ERC721: approve caller is not token owner or approved for all', + ) + }) + + it('Emits Approval event', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await expect(nameWrapper) + .write('approve', [accounts[1].address, toNameId(name)]) + .toEmitEvent('Approval') + .withArgs(accounts[0].address, accounts[1].address, toNameId(name)) + }) + + it('Allows approved address to call extendExpiry()', async () => { + const { nameWrapper, accounts, actions } = await loadFixture( + approveFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + expiry: 0n, + fuses: CAN_DO_EVERYTHING, + }) + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), 100n], + { account: accounts[1] }, + ) + + const [, , expiry] = await nameWrapper.read.getData([toNameId(subname)]) + expect(expiry).toEqual(100n) + }) + + it('Does not allows approved address to call setSubnodeOwner()', async () => { + const { nameWrapper, accounts, actions } = await loadFixture( + approveFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + expiry: 0n, + fuses: CAN_DO_EVERYTHING, + }) + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + + await expect(nameWrapper) + .write( + 'setSubnodeOwner', + [namehash(name), sublabel, accounts[2].address, 0, 1000n], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Allows approved address to call setSubnodeRecord()', async () => { + const { nameWrapper, accounts, actions } = await loadFixture( + approveFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + expiry: 0n, + fuses: CAN_DO_EVERYTHING, + }) + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + + await expect(nameWrapper) + .write( + 'setSubnodeRecord', + [ + namehash(name), + sublabel, + accounts[1].address, + zeroAddress, + 0n, + 0, + 10000n, + ], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Does not allow approved address to call setChildFuses()', async () => { + const { nameWrapper, accounts, actions, parentExpiry } = + await loadFixture(approveFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_UNWRAP]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + expiry: 0n, + fuses: CAN_DO_EVERYTHING, + }) + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + + await expect(nameWrapper) + .write( + 'setChildFuses', + [ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | CAN_EXTEND_EXPIRY, + parentExpiry, + ], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Does not allow approved accounts to extend expiry when expired', async () => { + const { nameWrapper, accounts, actions, testClient } = await loadFixture( + approveFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + expiry: 0n, + fuses: CAN_DO_EVERYTHING, + }) + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await testClient.increaseTime({ + seconds: Number(2n * DAY), + }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('extendExpiry', [namehash(name), labelhash(sublabel), 1000n], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Approved address can be replaced and previous approved is removed', async () => { + const { nameWrapper, accounts, actions, parentExpiry } = + await loadFixture(approveFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_UNWRAP]) + // Make sure there are no lingering approvals + await nameWrapper.write.setApprovalForAll([accounts[1].address, false]) + await nameWrapper.write.setApprovalForAll([accounts[2].address, false]) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + expiry: parentExpiry - 1000n, + fuses: CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | CAN_EXTEND_EXPIRY, + }) + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + await nameWrapper.write.approve([accounts[2].address, toNameId(name)]) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), parentExpiry - 500n], + { account: accounts[2] }, + ) + + await expect(nameWrapper) + .write( + 'extendExpiry', + [namehash(name), labelhash(sublabel), parentExpiry], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(subname), getAddress(accounts[1].address)) + + const [, , expiry] = await nameWrapper.read.getData([toNameId(subname)]) + expect(expiry).toEqual(parentExpiry - 500n) + }) + + it('Approved address cannot be removed/replaced when fuse is burnt', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + await nameWrapper.write.setFuses([ + namehash(name), + CANNOT_UNWRAP | CANNOT_APPROVE, + ]) + + await expect(nameWrapper) + .write('approve', [zeroAddress, toNameId(name)]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + + await expect(nameWrapper) + .write('approve', [accounts[0].address, toNameId(name)]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Approved address cannot transfer the name', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect(nameWrapper) + .write( + 'safeTransferFrom', + [accounts[0].address, accounts[1].address, toNameId(name), 1n, '0x'], + { account: accounts[1] }, + ) + .toBeRevertedWithString('ERC1155: caller is not owner nor approved') + }) + + it('Approved address cannot transfer the name with setRecord()', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect(nameWrapper) + .write( + 'setRecord', + [namehash(name), accounts[1].address, zeroAddress, 0n], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Approved address cannot call setResolver()', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect(nameWrapper) + .write('setResolver', [namehash(name), accounts[1].address], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Approved address cannot call setTTL()', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect(nameWrapper) + .write('setTTL', [namehash(name), 100n], { account: accounts[1] }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Approved address cannot unwrap .eth', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect(nameWrapper) + .write( + 'unwrapETH2LD', + [labelhash(label), accounts[1].address, accounts[1].address], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Approved address cannot unwrap non .eth', async () => { + const { nameWrapper, accounts, actions } = await loadFixture( + approveFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + expiry: 0n, + fuses: CAN_DO_EVERYTHING, + }) + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect(nameWrapper) + .write( + 'unwrap', + [namehash(name), labelhash(sublabel), accounts[1].address], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(subname), getAddress(accounts[1].address)) + }) + + it('Approval is cleared on transfer', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await nameWrapper.write.safeTransferFrom([ + accounts[0].address, + accounts[2].address, + toNameId(name), + 1n, + '0x', + ]) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(zeroAddress) + }) + + it('Approval is cleared on unwrapETH2LD()', async () => { + const { nameWrapper, accounts, actions } = await loadFixture( + approveFixture, + ) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[1].address) + + await nameWrapper.write.unwrapETH2LD([ + labelhash(label), + accounts[0].address, + accounts[0].address, + ]) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(zeroAddress) + + // rewrapping to test approval is still cleared + await actions.wrapEth2ld({ + label, + fuses: 0, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(zeroAddress) + + // reapprove to show approval can be reinstated + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('Approval is cleared on unwrap()', async () => { + const { nameWrapper, accounts, actions } = await loadFixture( + approveFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + expiry: 0n, + fuses: CAN_DO_EVERYTHING, + }) + + await nameWrapper.write.approve([accounts[1].address, toNameId(subname)]) + await expect( + nameWrapper.read.getApproved([toNameId(subname)]), + ).resolves.toEqualAddress(accounts[1].address) + + await actions.unwrapName({ + parentName: name, + label: sublabel, + controller: accounts[0].address, + }) + await expect( + nameWrapper.read.getApproved([toNameId(subname)]), + ).resolves.toEqualAddress(zeroAddress) + + await actions.setRegistryApprovalForWrapper() + + // rewrapping to test approval is still cleared + await actions.wrapName({ + name: subname, + owner: accounts[0].address, + resolver: zeroAddress, + }) + await expect( + nameWrapper.read.getApproved([toNameId(subname)]), + ).resolves.toEqualAddress(zeroAddress) + + // reapprove to show approval can be reinstated + await nameWrapper.write.approve([accounts[1].address, toNameId(subname)]) + await expect( + nameWrapper.read.getApproved([toNameId(subname)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('Approval is cleared on re-registration and wrap of expired name', async () => { + const { nameWrapper, accounts, actions, testClient } = await loadFixture( + approveFixture, + ) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + await nameWrapper.write.setFuses([ + namehash(name), + CANNOT_UNWRAP | CANNOT_APPROVE, + ]) + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[1].address) + + await testClient.increaseTime({ + seconds: Number(2n * DAY + GRACE_PERIOD), + }) + await testClient.mine({ blocks: 1 }) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(zeroAddress) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + // rewrapping to test approval is still cleared + await actions.wrapEth2ld({ + label, + fuses: CAN_DO_EVERYTHING, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(zeroAddress) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + }) + + it('Approval is not cleared on transfer if CANNOT_APPROVE is burnt', async () => { + const { nameWrapper, accounts } = await loadFixture(approveFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + await nameWrapper.write.setFuses([ + namehash(name), + CANNOT_UNWRAP | CANNOT_APPROVE, + ]) + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[1].address) + + await nameWrapper.write.safeTransferFrom([ + accounts[0].address, + accounts[2].address, + toNameId(name), + 1n, + '0x', + ]) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + }) +} diff --git a/test/wrapper/functions/extendExpiry.ts b/test/wrapper/functions/extendExpiry.ts new file mode 100644 index 00000000..05854499 --- /dev/null +++ b/test/wrapper/functions/extendExpiry.ts @@ -0,0 +1,1138 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, labelhash, namehash, zeroAddress } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_UNWRAP, + CAN_EXTEND_EXPIRY, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const extendExpiryTests = () => { + describe('extendExpiry()', () => { + const label = 'fuses' + const name = `${label}.eth` + const sublabel = 'sub' + const subname = `${sublabel}.${name}` + + it('Allows parent owner to set expiry without CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry([ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Allows parent owner to set expiry with CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry([ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Allows parent owner to set expiry with same child owner and CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry([ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Allows approved operators of parent owner to set expiry without CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + // approve hacker for anything account owns + await nameWrapper.write.setApprovalForAll([accounts[2].address, true]) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[2] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Allows approved operators of parent owner to set expiry with CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + // approve hacker for anything account owns + await nameWrapper.write.setApprovalForAll([accounts[2].address, true]) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[2] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Does not allow child owner to set expiry without CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await expect(nameWrapper) + .write( + 'extendExpiry', + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Allows child owner to set expiry with CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[1] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Does not allow approved operator of child owner to set expiry without CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + // approve hacker for anything accounts[1] owns + await nameWrapper.write.setApprovalForAll([accounts[2].address, true], { + account: accounts[1], + }) + + await expect(nameWrapper) + .write( + 'extendExpiry', + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[2] }, + ) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Allows approved operator of child owner to set expiry with CAN_EXTEND_EXPIRY burned', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + // approve hacker for anything accounts[1] owns + await nameWrapper.write.setApprovalForAll([accounts[2].address, true], { + account: accounts[1], + }) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[2] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Does not allow accounts other than parent/child owners or approved operators to set expiry', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await expect(nameWrapper) + .write( + 'extendExpiry', + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[2] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(subname), getAddress(accounts[2].address)) + }) + + it('Does not allow owner of .eth 2LD to set expiry', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [, initialFuses, expiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + + expect(initialFuses).toEqual( + IS_DOT_ETH | PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + ) + + await expect(nameWrapper) + .write('extendExpiry', [namehash('eth'), labelhash(label), expiry]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Allows parent owner of non-Emancipated name to set expiry', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: 0, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(0) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry([ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual(0) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Allows child owner of non-Emancipated name to set expiry', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(CAN_EXTEND_EXPIRY) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[1] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual(CAN_EXTEND_EXPIRY) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Expiry is normalized to old expiry if too low', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), parentExpiry - 3601n], + { account: accounts[1] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry - 3600n) + }) + + it('Expiry is normalized to parent expiry if too high', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), parentExpiry + GRACE_PERIOD + 1n], + { account: accounts[1] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Expiry is not normalized to new value if between old expiry and parent expiry', async () => { + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry - 3600n) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), parentExpiry - 1800n], + { account: accounts[1] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry - 1800n) + }) + + it('Does not allow .eth 2LD owner to set expiry on child if the .eth 2LD is expired but grace period has not ended', async () => { + const { baseRegistrar, nameWrapper, testClient, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: parentExpiry + GRACE_PERIOD - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + expect(initialExpiry).toEqual(parentExpiry + GRACE_PERIOD - 3600n) + + // Fast forward until the 2LD expires + await testClient.increaseTime({ seconds: Number(DAY + 1n) }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('extendExpiry', [ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(subname), getAddress(accounts[0].address)) + }) + + it('Allows child owner to set expiry if parent .eth 2LD is expired but grace period has not ended', async () => { + const { baseRegistrar, nameWrapper, testClient, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry + GRACE_PERIOD - 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(initialExpiry).toEqual(parentExpiry + GRACE_PERIOD - 3600n) + + // Fast forward until the 2LD expires + await testClient.increaseTime({ seconds: Number(DAY + 1n) }) + await testClient.mine({ blocks: 1 }) + + await nameWrapper.write.extendExpiry( + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[1] }, + ) + + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + ) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Does not allow child owner to set expiry if Emancipated child name has expired', async () => { + const { nameWrapper, actions, accounts, baseRegistrar, testClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - DAY + 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(PARENT_CANNOT_CONTROL | CAN_EXTEND_EXPIRY) + expect(initialExpiry).toEqual(parentExpiry - DAY + 3600n) + + // Fast forward until the child name expires + await testClient.increaseTime({ seconds: 3601 }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write( + 'extendExpiry', + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('NameIsNotWrapped') + }) + + it('Does not allow child owner to set expiry if non-Emancipated child name has reached its expiry', async () => { + const { nameWrapper, actions, accounts, baseRegistrar, testClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: CAN_EXTEND_EXPIRY, + expiry: parentExpiry - DAY + 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(CAN_EXTEND_EXPIRY) + expect(initialExpiry).toEqual(parentExpiry - DAY + 3600n) + + // Fast forward until the child name expires + await testClient.increaseTime({ seconds: 3601 }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write( + 'extendExpiry', + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Does not allow parent owner to set expiry if Emancipated child name has expired', async () => { + const { nameWrapper, actions, accounts, baseRegistrar, testClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry - DAY + 3600n, + }) + + const [owner, initialFuses, initialExpiry] = + await nameWrapper.read.getData([toNameId(subname)]) + + expect(owner).toEqualAddress(accounts[1].address) + expect(initialFuses).toEqual(PARENT_CANNOT_CONTROL) + expect(initialExpiry).toEqual(parentExpiry - DAY + 3600n) + + // Fast forward until the child name expires + await testClient.increaseTime({ seconds: 3601 }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('extendExpiry', [ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('NameIsNotWrapped') + }) + + it('Allows parent owner to set expiry if non-Emancipated child name has reached its expiry', async () => { + const { nameWrapper, actions, accounts, baseRegistrar, testClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: 0, + expiry: parentExpiry - DAY + 3600n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(0) + expect(initialExpiry).toEqual(parentExpiry - DAY + 3600n) + + // Fast forward until the child name expires + await testClient.increaseTime({ seconds: 3601 }) + await testClient.mine({ blocks: 1 }) + + await nameWrapper.write.extendExpiry([ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + + const [owner, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(owner).toEqualAddress(accounts[1].address) + expect(newFuses).toEqual(0) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Does not allow extendExpiry() to be called on unregistered names (not registered ever)', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [owner, initialFuses, initialExpiry] = + await nameWrapper.read.getData([toNameId(subname)]) + + expect(owner).toEqual(zeroAddress) + expect(initialFuses).toEqual(0) + expect(initialExpiry).toEqual(0n) + + await expect(nameWrapper) + .write('extendExpiry', [ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('NameIsNotWrapped') + }) + + it('Does not allow extendExpiry() to be called on unregistered names (expired w/ PCC burnt)', async () => { + const { baseRegistrar, nameWrapper, accounts, actions, testClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + duration: 10n * DAY, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry - 5n * DAY, + }) + + // Advance time so the subdomain expires, but not the parent + await testClient.increaseTime({ seconds: Number(5n * DAY + 1n) }) + await testClient.mine({ blocks: 1 }) + + // extendExpiry() on the unregistered name will be reverted + await expect(nameWrapper) + .write('extendExpiry', [ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('NameIsNotWrapped') + }) + + it('Allow extendExpiry() to be called on wrapped names', async () => { + const { baseRegistrar, nameWrapper, accounts, actions, testClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + duration: 10n * DAY, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 5n * DAY, + }) + + // Advance time so the subdomain expires, but not the parent + await testClient.increaseTime({ seconds: Number(5n * DAY + 1n) }) + await testClient.mine({ blocks: 1 }) + + await nameWrapper.write.extendExpiry([ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + + const [owner, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(owner).toEqualAddress(accounts[0].address) + expect(newFuses).toEqual(0) + expect(newExpiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('Does not allow extendExpiry() to be called on unwrapped names', async () => { + const { ensRegistry, baseRegistrar, nameWrapper, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: 0, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: parentExpiry - 3600n, + }) + + // First unwrap the parent + await actions.unwrapEth2ld({ + label, + controller: accounts[0].address, + registrant: accounts[0].address, + }) + // Then manually change the registry owner outside of the wrapper + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + }) + // Rewrap the parent + await actions.wrapEth2ld({ + label, + fuses: CANNOT_UNWRAP, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(owner).toEqualAddress(accounts[0].address) + expect(fuses).toEqual(0) + expect(expiry).toEqual(parentExpiry - 3600n) + + // Verify the registry owner is the account and not the wrapper contract + await expectOwnerOf(subname).on(ensRegistry).toBe(accounts[0]) + + await expect(nameWrapper) + .write('extendExpiry', [ + namehash(name), + labelhash(sublabel), + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('NameIsNotWrapped') + }) + + it('Emits Expiry Extended event', async () => { + const { baseRegistrar, nameWrapper, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CAN_EXTEND_EXPIRY, + expiry: parentExpiry - 3600n, + }) + + await expect(nameWrapper) + .write( + 'extendExpiry', + [namehash(name), labelhash(sublabel), MAX_EXPIRY], + { account: accounts[1] }, + ) + .toEmitEvent('ExpiryExtended') + .withArgs(namehash(subname), parentExpiry + GRACE_PERIOD) + }) + }) +} diff --git a/test/wrapper/functions/getApproved.ts b/test/wrapper/functions/getApproved.ts new file mode 100644 index 00000000..1e8167f9 --- /dev/null +++ b/test/wrapper/functions/getApproved.ts @@ -0,0 +1,48 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { zeroAddress } from 'viem' +import { toNameId } from '../../fixtures/utils.js' +import { + CAN_DO_EVERYTHING, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const getApprovedTests = () => { + describe('getApproved()', () => { + const label = 'subdomain' + const name = `${label}.eth` + + async function getApprovedFixture() { + const initial = await loadFixture(fixture) + const { actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + return initial + } + + it('Returns returns zero address when ownerOf() is zero', async () => { + const { nameWrapper } = await loadFixture(getApprovedFixture) + + await expectOwnerOf('unminted.eth').on(nameWrapper).toBe(zeroAccount) + await expect( + nameWrapper.read.getApproved([toNameId('unminted.eth')]), + ).resolves.toEqualAddress(zeroAddress) + }) + + it('Returns the approved address', async () => { + const { nameWrapper, accounts } = await loadFixture(getApprovedFixture) + + await nameWrapper.write.approve([accounts[1].address, toNameId(name)]) + + await expect( + nameWrapper.read.getApproved([toNameId(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + }) +} diff --git a/test/wrapper/functions/getData.ts b/test/wrapper/functions/getData.ts new file mode 100644 index 00000000..cab5e976 --- /dev/null +++ b/test/wrapper/functions/getData.ts @@ -0,0 +1,80 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_SET_RESOLVER, + CANNOT_UNWRAP, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const getDataTests = () => { + describe('getData()', () => { + const label = 'getfuses' + const name = `${label}.eth` + const sublabel = 'sub' + const subname = `${sublabel}.${name}` + + it('returns the correct fuses and expiry', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + const initialFuses = CANNOT_UNWRAP | CANNOT_SET_RESOLVER + + await actions.registerSetupAndWrapName({ + label, + fuses: initialFuses, + }) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + const [, fuses, expiry] = await nameWrapper.read.getData([toNameId(name)]) + + expect(fuses).toEqual(initialFuses | PARENT_CANNOT_CONTROL | IS_DOT_ETH) + expect(expiry).toEqual(expectedExpiry + GRACE_PERIOD) + }) + + it('clears fuses when domain is expired', async () => { + const { baseRegistrar, nameWrapper, accounts, actions, testClient } = + await loadFixture(fixture) + + const initialFuses = PARENT_CANNOT_CONTROL | CANNOT_UNWRAP + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setRegistryApprovalForWrapper() + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: initialFuses, + expiry: MAX_EXPIRY, + }) + + await testClient.increaseTime({ + seconds: Number(DAY + 1n + GRACE_PERIOD), + }) + await testClient.mine({ blocks: 1 }) + + const [, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + expect(fuses).toEqual(0) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + }) +} diff --git a/test/wrapper/functions/isWrapped.ts b/test/wrapper/functions/isWrapped.ts new file mode 100644 index 00000000..2bcd1530 --- /dev/null +++ b/test/wrapper/functions/isWrapped.ts @@ -0,0 +1,276 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { labelhash, namehash, zeroHash } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_UNWRAP, + GRACE_PERIOD, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const isWrappedTests = () => { + describe('isWrapped(bytes32 node)', () => { + const label = 'something' + const name = `${label}.eth` + + async function isWrappedFixture() { + const initial = await loadFixture(fixture) + const { nameWrapper, actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [, , parentExpiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + + return { ...initial, parentExpiry } + } + + it('identifies a wrapped .eth name', async () => { + const { nameWrapper } = await loadFixture(isWrappedFixture) + + await expect( + nameWrapper.read.isWrapped([namehash(name)]) as Promise, + ).resolves.toBe(true) + }) + + it('identifies an expired .eth name as unwrapped', async () => { + const { nameWrapper, testClient } = await loadFixture(isWrappedFixture) + + await testClient.increaseTime({ seconds: Number(1n * DAY + 1n) }) + await testClient.mine({ blocks: 1 }) + + await expect( + nameWrapper.read.isWrapped([namehash(name)]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies an eth name registered on old controller as unwrapped', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + fixture, + ) + + await baseRegistrar.write.register([ + toLabelId(label), + accounts[0].address, + 1n * DAY, + ]) + + await expectOwnerOf(label).on(baseRegistrar).toBe(accounts[0]) + await expect( + nameWrapper.read.isWrapped([namehash(name)]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies an unregistered .eth name as unwrapped', async () => { + const { nameWrapper } = await loadFixture(isWrappedFixture) + + await expect( + nameWrapper.read.isWrapped([ + namehash('abcdefghijklmnop.eth'), + ]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies an unregistered tld as unwrapped', async () => { + const { nameWrapper } = await loadFixture(isWrappedFixture) + + await expect( + nameWrapper.read.isWrapped([namehash('abc')]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies a wrapped subname', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + isWrappedFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + await expect( + nameWrapper.read.isWrapped([ + namehash(`sub.${name}`), + ]) as Promise, + ).resolves.toBe(true) + }) + + it('identifies an expired wrapped subname with PCC burnt as unwrapped', async () => { + const { nameWrapper, actions, accounts, testClient, parentExpiry } = + await loadFixture(isWrappedFixture) + + const subname = `sub.${name}` + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry + 100n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + + await testClient.increaseTime({ + seconds: Number(DAY + GRACE_PERIOD + 101n), + }) + await testClient.mine({ blocks: 1 }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(zeroAccount) + await expect( + nameWrapper.read.isWrapped([namehash(subname)]) as Promise, + ).resolves.toBe(false) + }) + }) + + describe('isWrapped(bytes32 parentNode, bytes32 labelhash)', () => { + const label = 'something' + const name = `${label}.eth` + const sublabel = 'sub' + const subname = `${sublabel}.${name}` + + async function isWrappedFixture() { + const initial = await loadFixture(fixture) + const { nameWrapper, actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [, , parentExpiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + + return { ...initial, parentExpiry } + } + + it('identifies a wrapped .eth name', async () => { + const { nameWrapper } = await loadFixture(isWrappedFixture) + + await expect( + nameWrapper.read.isWrapped([ + namehash('eth'), + labelhash(label), + ]) as Promise, + ).resolves.toBe(true) + }) + + it('identifies an expired .eth name as unwrapped', async () => { + const { nameWrapper, testClient } = await loadFixture(isWrappedFixture) + + await testClient.increaseTime({ seconds: Number(1n * DAY + 1n) }) + await testClient.mine({ blocks: 1 }) + + await expect( + nameWrapper.read.isWrapped([ + namehash('eth'), + labelhash(label), + ]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies an eth name registered on old controller as unwrapped', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + fixture, + ) + + await baseRegistrar.write.register([ + toLabelId(label), + accounts[0].address, + 1n * DAY, + ]) + + await expectOwnerOf(label).on(baseRegistrar).toBe(accounts[0]) + await expect( + nameWrapper.read.isWrapped([ + namehash('eth'), + labelhash(label), + ]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies an unregistered .eth name as unwrapped', async () => { + const { nameWrapper } = await loadFixture(isWrappedFixture) + + await expect( + nameWrapper.read.isWrapped([ + namehash('eth'), + labelhash('abcdefghijklmnop'), + ]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies an unregistered tld as unwrapped', async () => { + const { nameWrapper } = await loadFixture(isWrappedFixture) + + await expect( + nameWrapper.read.isWrapped([ + zeroHash, + labelhash('abc'), + ]) as Promise, + ).resolves.toBe(false) + }) + + it('identifies a wrapped subname', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + isWrappedFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + await expect( + nameWrapper.read.isWrapped([ + namehash(name), + labelhash(sublabel), + ]) as Promise, + ).resolves.toBe(true) + }) + + it('identifies an expired wrapped subname with PCC burnt as unwrapped', async () => { + const { nameWrapper, actions, accounts, testClient, parentExpiry } = + await loadFixture(isWrappedFixture) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry + 100n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + + await testClient.increaseTime({ + seconds: Number(DAY + GRACE_PERIOD + 101n), + }) + await testClient.mine({ blocks: 1 }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(zeroAccount) + await expect( + nameWrapper.read.isWrapped([ + namehash(name), + labelhash(sublabel), + ]) as Promise, + ).resolves.toBe(false) + }) + }) +} diff --git a/test/wrapper/functions/onERC721Received.ts b/test/wrapper/functions/onERC721Received.ts new file mode 100644 index 00000000..1ca892ec --- /dev/null +++ b/test/wrapper/functions/onERC721Received.ts @@ -0,0 +1,460 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import hre from 'hardhat' +import { + encodeAbiParameters, + encodeFunctionData, + keccak256, + labelhash, + namehash, + zeroAddress, + type Address, + type Hex, +} from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId, toTokenId } from '../../fixtures/utils.js' +import { + CANNOT_TRANSFER, + CANNOT_UNWRAP, + GRACE_PERIOD, + IS_DOT_ETH, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const onERC721ReceivedTests = () => { + describe('onERC721Received', () => { + const label = 'send2contract' + const name = `${label}.eth` + + const encodeExtraData = ({ + label, + owner, + ownerControlledFuses, + resolver, + }: { + label: string + owner: Address + ownerControlledFuses: number + resolver: Address + }) => + encodeAbiParameters( + [ + { type: 'string' }, + { type: 'address' }, + { type: 'uint16' }, + { type: 'address' }, + ], + [label, owner, ownerControlledFuses, resolver], + ) + + async function onERC721ReceivedFixture() { + const initial = await loadFixture(fixture) + const { actions, accounts } = initial + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + + return initial + } + + it('Wraps a name transferred to it and sets the owner to the provided address', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + await baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[1].address, + ownerControlledFuses: 0, + resolver: zeroAddress, + }), + ]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[1]) + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + }) + + it('Reverts if called by anything other than the ENS registrar address', async () => { + const { nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + await expect(nameWrapper) + .write('onERC721Received', [ + accounts[0].address, + accounts[0].address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: 1, + resolver: zeroAddress, + }), + ]) + .toBeRevertedWithCustomError('IncorrectTokenType') + }) + + it('Accepts fuse values from the data field', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + await baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: 1, + resolver: zeroAddress, + }), + ]) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + + expect(fuses).toEqual(CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH) + await expect( + nameWrapper.read.allFusesBurned([namehash(name), CANNOT_UNWRAP]), + ).resolves.toEqual(true) + }) + + it('Allows specifiying resolver address', async () => { + const { baseRegistrar, nameWrapper, ensRegistry, accounts } = + await loadFixture(onERC721ReceivedFixture) + + await baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: 1, + resolver: accounts[1].address, + }), + ]) + + await expect( + ensRegistry.read.resolver([namehash(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('Reverts if transferred without data', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + await expect(baseRegistrar) + .write('safeTransferFrom', [ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + '0x', + ]) + .toBeRevertedWithString( + 'ERC721: transfer to non ERC721Receiver implementer', + ) + }) + + it('Rejects transfers where the data field label does not match the tokenId', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + const tx = baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label: 'incorrectlabel', + owner: accounts[0].address, + ownerControlledFuses: 0, + resolver: zeroAddress, + }), + ]) + + await expect(nameWrapper) + .transaction(tx) + .toBeRevertedWithCustomError('LabelMismatch') + .withArgs(labelhash('incorrectlabel'), labelhash(label)) + }) + + it('Reverts if CANNOT_UNWRAP is not burned and attempts to burn other fuses', async () => { + const { baseRegistrar, ensRegistry, nameWrapper, accounts } = + await loadFixture(onERC721ReceivedFixture) + + await ensRegistry.write.setOwner([namehash(name), accounts[1].address]) + + const tx = baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: 2, + resolver: zeroAddress, + }), + ]) + + await expect(nameWrapper) + .transaction(tx) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Reverts when manually changing fuse calldata to incorrect type', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + const [walletClient] = await hre.viem.getWalletClients() + + let data = encodeFunctionData({ + abi: baseRegistrar.abi, + functionName: 'safeTransferFrom', + args: [ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: 273, + resolver: zeroAddress, + }), + ], + }) + const rogueFuse = '40000' // 2 ** 18 in hex + data = data.replace('00111', rogueFuse) as Hex + + const tx = { + to: baseRegistrar.address, + data, + } + + await expect(baseRegistrar) + .transaction(walletClient.sendTransaction(tx)) + .toBeRevertedWithString( + 'ERC721: transfer to non ERC721Receiver implementer', + ) + }) + + it('Allows burning other fuses if CAN_UNWRAP has been burnt', async () => { + const { baseRegistrar, ensRegistry, nameWrapper, accounts } = + await loadFixture(onERC721ReceivedFixture) + + await ensRegistry.write.setOwner([namehash(name), accounts[1].address]) + + await baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: CANNOT_UNWRAP | CANNOT_TRANSFER, + resolver: zeroAddress, + }), + ]) + + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + + expect(fuses).toEqual( + CANNOT_UNWRAP | CANNOT_TRANSFER | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + await expect( + nameWrapper.read.allFusesBurned([ + namehash(name), + CANNOT_UNWRAP | CANNOT_TRANSFER | PARENT_CANNOT_CONTROL, + ]), + ).resolves.toEqual(true) + }) + + it('Allows burning other fuses if CAN_UNWRAP has been burnt, but resets fuses if expired', async () => { + const { baseRegistrar, ensRegistry, nameWrapper, accounts, testClient } = + await loadFixture(onERC721ReceivedFixture) + + await ensRegistry.write.setOwner([namehash(name), accounts[1].address]) + + await baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: CANNOT_UNWRAP | CANNOT_TRANSFER, + resolver: zeroAddress, + }), + ]) + + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY), + }) + await testClient.mine({ blocks: 1 }) + + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + // owner should be 0 as expired + await expectOwnerOf(name).on(nameWrapper).toBe(zeroAccount) + expect(fuses).toEqual(0) + + await expect( + nameWrapper.read.allFusesBurned([ + namehash(name), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | CANNOT_TRANSFER, + ]), + ).resolves.toEqual(false) + }) + + it('Sets the controller in the ENS registry to the wrapper contract', async () => { + const { baseRegistrar, ensRegistry, nameWrapper, accounts } = + await loadFixture(onERC721ReceivedFixture) + + await baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: 0, + resolver: zeroAddress, + }), + ]) + + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + }) + + it('Can wrap a name even if the controller address is different to the registrant address', async () => { + const { baseRegistrar, ensRegistry, nameWrapper, accounts } = + await loadFixture(onERC721ReceivedFixture) + + await ensRegistry.write.setOwner([namehash(name), accounts[1].address]) + + await baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: 0, + resolver: zeroAddress, + }), + ]) + + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + }) + + it('emits NameWrapped Event', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + const tx = baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: CANNOT_UNWRAP | CANNOT_TRANSFER, + resolver: zeroAddress, + }), + ]) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(name), + dnsEncodeName(name), + accounts[0].address, + CANNOT_UNWRAP | CANNOT_TRANSFER | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + expectedExpiry + GRACE_PERIOD, + ) + }) + + it('emits TransferSingle Event', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + onERC721ReceivedFixture, + ) + + const tx = baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + toLabelId(label), + encodeExtraData({ + label, + owner: accounts[0].address, + ownerControlledFuses: CANNOT_UNWRAP | CANNOT_TRANSFER, + resolver: zeroAddress, + }), + ]) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('TransferSingle') + .withArgs( + baseRegistrar.address, + zeroAddress, + accounts[0].address, + toNameId(name), + 1n, + ) + }) + + it('will not wrap a name with an empty label', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + fixture, + ) + + const emptyLabelId = toTokenId(keccak256(new Uint8Array(0))) + + await baseRegistrar.write.register([ + emptyLabelId, + accounts[0].address, + 1n * DAY, + ]) + + const tx = baseRegistrar.write.safeTransferFrom([ + accounts[0].address, + nameWrapper.address, + emptyLabelId, + encodeExtraData({ + label: '', + owner: accounts[0].address, + ownerControlledFuses: 0, + resolver: zeroAddress, + }), + ]) + + await expect(nameWrapper) + .transaction(tx) + .toBeRevertedWithCustomError('LabelTooShort') + }) + }) +} diff --git a/test/wrapper/functions/ownerOf.ts b/test/wrapper/functions/ownerOf.ts new file mode 100644 index 00000000..9b47ba80 --- /dev/null +++ b/test/wrapper/functions/ownerOf.ts @@ -0,0 +1,43 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { DAY } from '../../fixtures/constants.js' +import { + CAN_DO_EVERYTHING, + GRACE_PERIOD, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const ownerOfTests = () => { + describe('ownerOf()', () => { + const label = 'subdomain' + const name = `${label}.eth` + + it('Returns the owner', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + }) + + it('Returns 0 when owner is expired', async () => { + const { nameWrapper, actions, testClient } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await testClient.increaseTime({ + seconds: Number(1n * DAY + GRACE_PERIOD + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expectOwnerOf(name).on(nameWrapper).toBe(zeroAccount) + }) + }) +} diff --git a/test/wrapper/functions/registerAndWrapETH2LD.ts b/test/wrapper/functions/registerAndWrapETH2LD.ts new file mode 100644 index 00000000..fe3911de --- /dev/null +++ b/test/wrapper/functions/registerAndWrapETH2LD.ts @@ -0,0 +1,317 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import hre from 'hardhat' +import { encodeFunctionData, namehash, zeroAddress, type Hex } from 'viem' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_SET_RESOLVER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const registerAndWrapETH2LDTests = () => { + describe('registerAndWrapETH2LD()', () => { + const label = 'register' + const name = `${label}.eth` + + async function registerAndWrapETH2LDFixture() { + const initial = await loadFixture(fixture) + const { baseRegistrar, nameWrapper, accounts } = initial + + await baseRegistrar.write.addController([nameWrapper.address]) + await nameWrapper.write.setController([accounts[0].address, true]) + + return initial + } + + it('should register and wrap names', async () => { + const { ensRegistry, baseRegistrar, nameWrapper, accounts } = + await loadFixture(registerAndWrapETH2LDFixture) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + }) + + it('allows specifying a resolver address', async () => { + const { ensRegistry, nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + 86400n, + accounts[1].address, + CAN_DO_EVERYTHING, + ]) + + await expect( + ensRegistry.read.resolver([namehash(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('does not allow non controllers to register names', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + await nameWrapper.write.setController([accounts[0].address, false]) + + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + label, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + .toBeRevertedWithString('Controllable: Caller is not a controller') + }) + + it('Transfers the wrapped token to the target address.', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[1].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[1]) + }) + + it('Does not allow wrapping with a target address of 0x0', async () => { + const { nameWrapper } = await loadFixture(registerAndWrapETH2LDFixture) + + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + label, + zeroAddress, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + .toBeRevertedWithString('ERC1155: mint to the zero address') + }) + + it('Does not allow wrapping with a target address of the wrapper contract address.', async () => { + const { nameWrapper } = await loadFixture(registerAndWrapETH2LDFixture) + + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + label, + nameWrapper.address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + .toBeRevertedWithString( + 'ERC1155: newOwner cannot be the NameWrapper contract', + ) + }) + + it('Does not allows fuse to be burned if CANNOT_UNWRAP has not been burned.', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + label, + accounts[0].address, + 86400n, + zeroAddress, + CANNOT_SET_RESOLVER, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Allows fuse to be burned if CANNOT_UNWRAP has been burned and expiry set', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + const initialFuses = CANNOT_UNWRAP | CANNOT_SET_RESOLVER + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + 86400n, + zeroAddress, + initialFuses, + ]) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + + expect(fuses).toEqual(initialFuses | PARENT_CANNOT_CONTROL | IS_DOT_ETH) + }) + + it('automatically sets PARENT_CANNOT_CONTROL and IS_DOT_ETH', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + + expect(fuses).toEqual(PARENT_CANNOT_CONTROL | IS_DOT_ETH) + }) + + it('Errors when adding a number greater than uint16 for fuses', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + const [walletClient] = await hre.viem.getWalletClients() + + let data = encodeFunctionData({ + abi: nameWrapper.abi, + functionName: 'registerAndWrapETH2LD', + args: [label, accounts[0].address, 86400n, zeroAddress, 273], + }) + const rogueFuse = '40000' // 2 ** 18 in hex + data = data.replace('00111', rogueFuse) as Hex + + const tx = { + to: nameWrapper.address, + data, + } + + await expect(nameWrapper) + .transaction(walletClient.sendTransaction(tx)) + .toBeRevertedWithoutReason() + }) + + it('Errors when passing a parent-controlled fuse', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + for (let i = 0; i < 7; i++) { + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + label, + accounts[0].address, + 86400n, + zeroAddress, + IS_DOT_ETH * 2 ** i, + ]) + .toBeRevertedWithoutReason() + } + }) + + it('Will not wrap a name with an empty label', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + '', + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + .toBeRevertedWithCustomError('LabelTooShort') + }) + + it('Will not wrap a name with a label more than 255 characters', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + const longString = + 'yutaioxtcsbzrqhdjmltsdfkgomogohhcchjoslfhqgkuhduhxqsldnurwrrtoicvthwxytonpcidtnkbrhccaozdtoznedgkfkifsvjukxxpkcmgcjprankyzerzqpnuteuegtfhqgzcxqwttyfewbazhyilqhyffufxrookxrnjkmjniqpmntcbrowglgdpkslzechimsaonlcvjkhhvdvkvvuztihobmivifuqtvtwinljslusvhhbwhuhzty' + expect(longString.length).toEqual(256) + + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + longString, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + .toBeRevertedWithCustomError('LabelTooLong') + .withArgs(longString) + }) + + it('emits Wrap event', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + const tx = await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + + const expiry = await baseRegistrar.read.nameExpires([toLabelId(label)]) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(name), + dnsEncodeName(name), + accounts[0].address, + PARENT_CANNOT_CONTROL | IS_DOT_ETH, + expiry + GRACE_PERIOD, + ) + }) + + it('Emits TransferSingle event', async () => { + const { nameWrapper, accounts } = await loadFixture( + registerAndWrapETH2LDFixture, + ) + + await expect(nameWrapper) + .write('registerAndWrapETH2LD', [ + label, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[0].address, + zeroAddress, + accounts[0].address, + toNameId(name), + 1n, + ) + }) + }) +} diff --git a/test/wrapper/functions/renew.ts b/test/wrapper/functions/renew.ts new file mode 100644 index 00000000..8afa1a10 --- /dev/null +++ b/test/wrapper/functions/renew.ts @@ -0,0 +1,120 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { zeroAddress } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_SET_RESOLVER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + PARENT_CANNOT_CONTROL, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const renewTests = () => { + describe('renew', () => { + const label = 'register' + const name = `${label}.eth` + + async function renewFixture() { + const initial = await loadFixture(fixture) + const { baseRegistrar, nameWrapper, accounts } = initial + + await baseRegistrar.write.addController([nameWrapper.address]) + await nameWrapper.write.setController([accounts[0].address, true]) + + return initial + } + + it('Renews names', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + renewFixture, + ) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + + const expires = await baseRegistrar.read.nameExpires([toLabelId(label)]) + + await nameWrapper.write.renew([toLabelId(label), 86400n]) + + const newExpires = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + expect(newExpires).toEqual(expires + 86400n) + }) + + it('Renews names and can extend wrapper expiry', async () => { + const { baseRegistrar, nameWrapper, accounts } = await loadFixture( + renewFixture, + ) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + 86400n, + zeroAddress, + CAN_DO_EVERYTHING, + ]) + + const expires = await baseRegistrar.read.nameExpires([toLabelId(label)]) + const expectedExpiry = expires + 86400n + + await nameWrapper.write.renew([toLabelId(label), 86400n]) + + const [owner, , expiry] = await nameWrapper.read.getData([toNameId(name)]) + + expect(expiry).toEqual(expectedExpiry + GRACE_PERIOD) + expect(owner).toEqualAddress(accounts[0].address) + }) + + it('Renewing name less than required to unexpire it still has original owner/fuses', async () => { + const { nameWrapper, accounts, testClient, publicClient } = + await loadFixture(renewFixture) + + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[0].address, + DAY, + zeroAddress, + CANNOT_UNWRAP | CANNOT_SET_RESOLVER, + ]) + + await testClient.increaseTime({ seconds: Number(DAY * 2n) }) + await testClient.mine({ blocks: 1 }) + + const [, , expiryBefore] = await nameWrapper.read.getData([ + toNameId(name), + ]) + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + // confirm expired + expect(expiryBefore).toBeLessThanOrEqual(timestamp + GRACE_PERIOD) + + // renew for less than the grace period + await nameWrapper.write.renew([toLabelId(label), 1n * DAY]) + + const [ownerAfter, fusesAfter, expiryAfter] = + await nameWrapper.read.getData([toNameId(name)]) + + expect(ownerAfter).toEqualAddress(accounts[0].address) + // fuses remain the same + expect(fusesAfter).toEqual( + CANNOT_UNWRAP | + CANNOT_SET_RESOLVER | + IS_DOT_ETH | + PARENT_CANNOT_CONTROL, + ) + // still expired + expect(expiryAfter).toBeLessThanOrEqual(timestamp + GRACE_PERIOD) + }) + }) +} diff --git a/test/wrapper/functions/setChildFuses.ts b/test/wrapper/functions/setChildFuses.ts new file mode 100644 index 00000000..fd77d17e --- /dev/null +++ b/test/wrapper/functions/setChildFuses.ts @@ -0,0 +1,675 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, labelhash, namehash, zeroAddress, zeroHash } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_BURN_FUSES, + CANNOT_SET_RESOLVER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const setChildFusesTests = () => { + describe('setChildFuses()', () => { + const label = 'fuses' + const name = `${label}.eth` + const sublabel = 'sub' + const subname = `${sublabel}.${name}` + + it('Allows parent owners to set fuses/expiry', async () => { + const { baseRegistrar, nameWrapper, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(0) + expect(initialExpiry).toEqual(0n) + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual(CANNOT_UNWRAP | PARENT_CANNOT_CONTROL) + expect(newExpiry).toEqual(expectedExpiry + GRACE_PERIOD) + }) + + it('Emits a FusesSet event and ExpiryExtended event', async () => { + const { baseRegistrar, nameWrapper, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(0) + expect(initialExpiry).toEqual(0n) + + const tx = await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual(CANNOT_UNWRAP | PARENT_CANNOT_CONTROL) + expect(newExpiry).toEqual(expectedExpiry + GRACE_PERIOD) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('FusesSet') + .withArgs(namehash(subname), CANNOT_UNWRAP | PARENT_CANNOT_CONTROL) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('ExpiryExtended') + .withArgs(namehash(subname), expectedExpiry + GRACE_PERIOD) + }) + + it('Allows special cased TLD owners to set fuses/expiry', async () => { + const { nameWrapper, actions, accounts, publicClient } = + await loadFixture(fixture) + + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label: 'anothertld', + owner: accounts[0].address, + }) + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: 'anothertld', + owner: accounts[0].address, + resolver: zeroAddress, + }) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + const expectedExpiry = timestamp + 1000n + + await nameWrapper.write.setChildFuses([ + zeroHash, + labelhash('anothertld'), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + expectedExpiry, + ]) + + const [, fuses, expiry] = await nameWrapper.read.getData([ + toNameId('anothertld'), + ]) + + expect(fuses).toEqual(CANNOT_UNWRAP | PARENT_CANNOT_CONTROL) + expect(expiry).toEqual(expectedExpiry) + }) + + it('does not allow parent owners to burn IS_DOT_ETH fuse', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(fuses).toEqual(0) + expect(expiry).toEqual(0n) + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Allow parent owners to burn parent controlled fuses without burning PCC', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(0) + expect(initialExpiry).toEqual(0n) + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + IS_DOT_ETH * 2, // Next undefined parent controlled fuse + MAX_EXPIRY, + ]) + + const [, fusesAfter] = await nameWrapper.read.getData([toNameId(subname)]) + + expect(fusesAfter).toEqual(IS_DOT_ETH * 2) + }) + + it('Does not allow parent owners to burn parent controlled fuses after burning PCC', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(fuses).toEqual(0) + expect(expiry).toEqual(0n) + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + IS_DOT_ETH * 2, // Next undefined parent controlled fuse + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Allows accounts authorised by the parent node owner to set fuses/expiry', async () => { + const { baseRegistrar, nameWrapper, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, initialFuses, initialExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(initialFuses).toEqual(0) + expect(initialExpiry).toEqual(0n) + + // approve accounts[1] for anything accounts[0] owns + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await nameWrapper.write.setChildFuses( + [ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ], + { account: accounts[1] }, + ) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newFuses).toEqual(CANNOT_UNWRAP | PARENT_CANNOT_CONTROL) + expect(newExpiry).toEqual(expectedExpiry + GRACE_PERIOD) + }) + + it('Does not allow non-parent owners to set child fuses', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(fuses).toEqual(0) + expect(expiry).toEqual(0n) + + await expect(nameWrapper) + .write( + 'setChildFuses', + [ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Normalises expiry to the parent expiry', async () => { + const { baseRegistrar, nameWrapper, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const [, , expiry] = await nameWrapper.read.getData([toNameId(subname)]) + + expect(expiry).toEqual(0n) + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + + const [, , expectedExpiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + const [, , newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(newExpiry).toEqual(expectedExpiry) + }) + + it('Normalises expiry to the old expiry', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 1000n, + }) + + const [, , expiry] = await nameWrapper.read.getData([toNameId(subname)]) + + expect(expiry).toEqual(1000n) + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + 500n, + ]) + + const [, , newExpiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + // normalises to 1000 instead of using 500 + expect(newExpiry).toEqual(1000n) + }) + + it('Does not allow burning fuses if PARENT_CANNOT_CONTROL is not burnt', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + CANNOT_UNWRAP, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('should not allow .eth to call setChildFuses()', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash('eth'), + labelhash(label), + CANNOT_SET_RESOLVER, + 0n, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash('eth'), getAddress(accounts[0].address)) + }) + + it('Does not allow burning fuses if CANNOT_UNWRAP is not burnt', async () => { + const { nameWrapper, actions, accounts, publicClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + // set up child's PCC + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: timestamp + 10000n, + }) + + // attempt to burn a fuse without CANNOT_UNWRAP + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + CANNOT_SET_RESOLVER, + 0n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Does not allow burning fuses if PARENT_CANNOT_CONTROL is already burned', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const originalFuses = PARENT_CANNOT_CONTROL | CANNOT_UNWRAP + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + originalFuses, + MAX_EXPIRY, + ]) + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + CANNOT_SET_RESOLVER | CANNOT_BURN_FUSES, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Does not allow burning fuses if PARENT_CANNOT_CONTROL is already burned even if PARENT_CANNOT_CONTROL is added as a fuse', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const originalFuses = PARENT_CANNOT_CONTROL | CANNOT_UNWRAP + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + originalFuses, + MAX_EXPIRY, + ]) + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + PARENT_CANNOT_CONTROL | + CANNOT_UNWRAP | + CANNOT_SET_RESOLVER | + CANNOT_BURN_FUSES, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Does not allow burning PARENT_CANNOT_CONTROL if CU on the parent is not burned', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + const originalFuses = PARENT_CANNOT_CONTROL | CANNOT_UNWRAP + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + originalFuses, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Fuses and owner are set to 0 if expired', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_SET_RESOLVER, + 0n, + ]) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(fuses).toEqual(0) + expect(expiry).toEqual(0n) + expect(owner).toEqual(zeroAddress) + }) + + it('Fuses and owner are set to 0 if expired and fuses cannot be burnt after expiry using setChildFuses()', async () => { + const { nameWrapper, actions, accounts, publicClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + await nameWrapper.write.setChildFuses([ + namehash(name), + labelhash(sublabel), + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + 0n, + ]) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(fuses).toEqual(0) + expect(expiry).toEqual(0n) + expect(owner).toEqual(zeroAddress) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + await expect(nameWrapper) + .write('setChildFuses', [ + namehash(name), + labelhash(sublabel), + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + timestamp + 1n * DAY, + ]) + .toBeRevertedWithCustomError('NameIsNotWrapped') + }) + }) +} diff --git a/test/wrapper/functions/setFuses.ts b/test/wrapper/functions/setFuses.ts new file mode 100644 index 00000000..f65cc362 --- /dev/null +++ b/test/wrapper/functions/setFuses.ts @@ -0,0 +1,475 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import hre from 'hardhat' +import { encodeFunctionData, getAddress, namehash, type Hex } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_BURN_FUSES, + CANNOT_CREATE_SUBDOMAIN, + CANNOT_SET_RESOLVER, + CANNOT_SET_TTL, + CANNOT_TRANSFER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const setFusesTests = () => { + describe('setFuses()', () => { + const label = 'fuses' + const name = `${label}.eth` + + it('cannot burn PARENT_CANNOT_CONTROL', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setFuses', [namehash(`sub.${name}`), PARENT_CANNOT_CONTROL]) + .toBeRevertedWithoutReason() + }) + + it('cannot burn any parent controlled fuse', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + }) + + // check the 7 fuses above PCC + for (let i = 0; i < 7; i++) { + await expect(nameWrapper) + .write('setFuses', [namehash(`sub.${name}`), IS_DOT_ETH * 2 ** i]) + .toBeRevertedWithoutReason() + } + }) + + // TODO: why is this tested? + it('Errors when manually changing calldata to incorrect type', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + const [walletClient] = await hre.viem.getWalletClients() + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + }) + + let data = encodeFunctionData({ + abi: nameWrapper.abi, + functionName: 'setFuses', + args: [namehash(`sub.${name}`), 4], + }) + const rogueFuse = '40000' // 2 ** 18 in hex + data = data.substring(0, data.length - rogueFuse.length) as Hex + data += rogueFuse + + const tx = walletClient.sendTransaction({ + to: nameWrapper.address, + data: data as Hex, + }) + + await expect(nameWrapper).transaction(tx).toBeRevertedWithoutReason() + }) + + it('cannot burn fuses as the previous owner of a .eth when the name has expired', async () => { + const { nameWrapper, actions, accounts, testClient } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('setFuses', [namehash(name), CANNOT_UNWRAP]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('Will not allow burning fuses if PARENT_CANNOT_CONTROL has not been burned', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setFuses', [ + namehash(`sub.${name}`), + CANNOT_UNWRAP | CANNOT_TRANSFER, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(`sub.${name}`)) + }) + + it('Will not allow burning fuses of subdomains if CANNOT_UNWRAP has not been burned', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: PARENT_CANNOT_CONTROL, + }) + + await expect(nameWrapper) + .write('setFuses', [namehash(`sub.${name}`), CANNOT_TRANSFER]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(`sub.${name}`)) + }) + + it('Will not allow burning fuses of .eth names unless CANNOT_UNWRAP is also burned.', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setFuses', [namehash(name), CANNOT_TRANSFER]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Can be called by the owner', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [, initialFuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(initialFuses).toEqual( + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_TRANSFER]) + + const [, newFuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(newFuses).toEqual( + CANNOT_UNWRAP | CANNOT_TRANSFER | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + }) + + it('Emits FusesSet event', async () => { + const { nameWrapper, baseRegistrar, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const expectedExpiry = await baseRegistrar.read + .nameExpires([toLabelId(label)]) + .then((e) => e + GRACE_PERIOD) + + await expect(nameWrapper) + .write('setFuses', [namehash(name), CANNOT_TRANSFER]) + .toEmitEvent('FusesSet') + .withArgs( + namehash(name), + CANNOT_UNWRAP | CANNOT_TRANSFER | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + + const [, fuses, expiry] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual( + CANNOT_UNWRAP | CANNOT_TRANSFER | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + expect(expiry).toEqual(expectedExpiry) + }) + + it('Returns the correct fuses', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + // The `simulate` function is called to get the return value of the function. + // Note: simulate does not modify the state of the contract. + const { result: fusesReturned } = await nameWrapper.simulate.setFuses([ + namehash(name), + CANNOT_TRANSFER, + ]) + expect(fusesReturned).toEqual( + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + }) + + it('Can be called by an account authorised by the owner', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_UNWRAP], { + account: accounts[1], + }) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual(CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH) + }) + + it('Cannot be called by an unauthorised account', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setFuses', [namehash(name), CANNOT_UNWRAP], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Allows burning unknown fuses', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + // Each fuse is represented by the next bit, 64 is the next undefined fuse + await nameWrapper.write.setFuses([namehash(name), 64]) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual( + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH | 64, + ) + }) + + it('Logically ORs passed in fuses with already-burned fuses.', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP | CANNOT_TRANSFER, + }) + + await nameWrapper.write.setFuses([namehash(name), 64 | CANNOT_TRANSFER]) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual( + CANNOT_UNWRAP | + PARENT_CANNOT_CONTROL | + IS_DOT_ETH | + 64 | + CANNOT_TRANSFER, + ) + }) + + it('can set fuses and then burn ability to burn fuses', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_BURN_FUSES]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // check flag in the wrapper + await expect( + nameWrapper.read.allFusesBurned([namehash(name), CANNOT_BURN_FUSES]), + ).resolves.toEqual(true) + + // try to set the resolver and ttl + await expect(nameWrapper) + .write('setFuses', [namehash(name), CANNOT_TRANSFER]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('can set fuses and burn transfer', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_TRANSFER]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // check flag in the wrapper + await expect( + nameWrapper.read.allFusesBurned([namehash(name), CANNOT_TRANSFER]), + ).resolves.toEqual(true) + + // Transfer should revert + await expect(nameWrapper) + .write('safeTransferFrom', [ + accounts[0].address, + accounts[1].address, + toNameId(name), + 1n, + '0x', + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('can set fuses and burn canSetResolver and canSetTTL', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await nameWrapper.write.setFuses([ + namehash(name), + CANNOT_SET_RESOLVER | CANNOT_SET_TTL, + ]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // check flag in the wrapper + await expect( + nameWrapper.read.allFusesBurned([ + namehash(name), + CANNOT_SET_RESOLVER | CANNOT_SET_TTL, + ]), + ).resolves.toEqual(true) + + // try to set the resolver and ttl + await expect(nameWrapper) + .write('setResolver', [namehash(name), accounts[1].address]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + + await expect(nameWrapper) + .write('setTTL', [namehash(name), 1000n]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('can set fuses and burn canCreateSubdomains', async () => { + const { ensRegistry, nameWrapper, actions, accounts } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await expect( + nameWrapper.read.allFusesBurned([ + namehash(name), + CANNOT_CREATE_SUBDOMAIN, + ]), + ).resolves.toEqual(false) + + // can create before burn + // revert not approved and isn't sender because subdomain isnt owned by contract? + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'creatable', + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + expiry: 0n, + }) + + await expectOwnerOf(`creatable.${name}`).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(`creatable.${name}`).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setFuses([ + namehash(name), + CAN_DO_EVERYTHING | CANNOT_CREATE_SUBDOMAIN, + ]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await expect( + nameWrapper.read.allFusesBurned([ + namehash(name), + CANNOT_CREATE_SUBDOMAIN, + ]), + ).resolves.toEqual(true) + + // try to create a subdomain + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + 'uncreatable', + accounts[0].address, + 0, + 86400n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(`uncreatable.${name}`)) + }) + }) +} diff --git a/test/wrapper/functions/setRecord.ts b/test/wrapper/functions/setRecord.ts new file mode 100644 index 00000000..c8f0804d --- /dev/null +++ b/test/wrapper/functions/setRecord.ts @@ -0,0 +1,236 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, namehash, zeroAddress } from 'viem' +import { + CANNOT_SET_RESOLVER, + CANNOT_SET_TTL, + CANNOT_TRANSFER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const setRecordTests = () => { + describe('setRecord', () => { + const label = 'setrecord' + const name = `${label}.eth` + + async function setRecordFixture() { + const initial = await loadFixture(fixture) + const { actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + return initial + } + + it('Can be called by the owner', async () => { + const { nameWrapper, accounts } = await loadFixture(setRecordFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setRecord([ + namehash(name), + accounts[1].address, + accounts[0].address, + 50n, + ]) + }) + + it('Performs the appropriate function on the ENS registry and Wrapper', async () => { + const { ensRegistry, nameWrapper, accounts } = await loadFixture( + setRecordFixture, + ) + + await nameWrapper.write.setRecord([ + namehash(name), + accounts[1].address, + accounts[0].address, + 50n, + ]) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[1]) + await expect( + ensRegistry.read.resolver([namehash(name)]), + ).resolves.toEqualAddress(accounts[0].address) + await expect(ensRegistry.read.ttl([namehash(name)])).resolves.toEqual(50n) + }) + + it('Can be called by an account authorised by the owner.', async () => { + const { nameWrapper, accounts } = await loadFixture(setRecordFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await nameWrapper.write.setRecord( + [namehash(name), accounts[1].address, accounts[0].address, 50n], + { account: accounts[1] }, + ) + }) + + it('Cannot be called by anyone else.', async () => { + const { nameWrapper, accounts } = await loadFixture(setRecordFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await expect(nameWrapper) + .write( + 'setRecord', + [namehash(name), accounts[1].address, accounts[0].address, 50n], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Cannot be called if CANNOT_TRANSFER is burned.', async () => { + const { nameWrapper, accounts } = await loadFixture(setRecordFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_TRANSFER]) + + await expect(nameWrapper) + .write('setRecord', [ + namehash(name), + accounts[1].address, + accounts[0].address, + 50n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Cannot be called if CANNOT_SET_RESOLVER is burned.', async () => { + const { nameWrapper, accounts } = await loadFixture(setRecordFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_SET_RESOLVER]) + + await expect(nameWrapper) + .write('setRecord', [ + namehash(name), + accounts[1].address, + accounts[0].address, + 50n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Cannot be called if CANNOT_SET_TTL is burned.', async () => { + const { nameWrapper, accounts } = await loadFixture(setRecordFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_SET_TTL]) + + await expect(nameWrapper) + .write('setRecord', [ + namehash(name), + accounts[1].address, + accounts[0].address, + 50n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Setting the owner to 0 reverts if CANNOT_UNWRAP is burned', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + const subname = `sub.${name}` + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + fuses: CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + + await expect(nameWrapper) + .write('setRecord', [ + namehash(subname), + zeroAddress, + accounts[0].address, + 50n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Setting the owner of a subdomain to 0 unwraps the name and passes through resolver/ttl', async () => { + const { ensRegistry, nameWrapper, actions, accounts } = await loadFixture( + fixture, + ) + + const subname = `sub.${name}` + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + + await expect(nameWrapper) + .write('setRecord', [ + namehash(subname), + zeroAddress, + accounts[0].address, + 50n, + ]) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(subname), zeroAddress) + + await expectOwnerOf(subname).on(nameWrapper).toBe(zeroAccount) + await expectOwnerOf(subname).on(ensRegistry).toBe(zeroAccount) + await expect( + ensRegistry.read.resolver([namehash(subname)]), + ).resolves.toEqualAddress(accounts[0].address) + await expect(ensRegistry.read.ttl([namehash(subname)])).resolves.toEqual( + 50n, + ) + }) + + it('Setting the owner to 0 on a .eth reverts', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await expect(nameWrapper) + .write('setRecord', [ + namehash(name), + zeroAddress, + accounts[0].address, + 50n, + ]) + .toBeRevertedWithCustomError('IncorrectTargetOwner') + .withArgs(zeroAddress) + }) + }) +} diff --git a/test/wrapper/functions/setResolver.ts b/test/wrapper/functions/setResolver.ts new file mode 100644 index 00000000..307e7e23 --- /dev/null +++ b/test/wrapper/functions/setResolver.ts @@ -0,0 +1,89 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, namehash, zeroAddress } from 'viem' +import { + CANNOT_SET_RESOLVER, + CANNOT_UNWRAP, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const setResolverTests = () => { + describe('setResolver', () => { + const label = 'setresolver' + const name = `${label}.eth` + + async function setResolverFixture() { + const initial = await loadFixture(fixture) + const { actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + return initial + } + + it('Can be called by the owner', async () => { + const { nameWrapper, accounts } = await loadFixture(setResolverFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setResolver([namehash(name), accounts[1].address]) + }) + + it('Performs the appropriate function on the ENS registry.', async () => { + const { ensRegistry, nameWrapper, accounts } = await loadFixture( + setResolverFixture, + ) + + await expect( + ensRegistry.read.resolver([namehash(name)]), + ).resolves.toEqualAddress(zeroAddress) + + await nameWrapper.write.setResolver([namehash(name), accounts[1].address]) + + await expect( + ensRegistry.read.resolver([namehash(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('Can be called by an account authorised by the owner.', async () => { + const { nameWrapper, accounts } = await loadFixture(setResolverFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await nameWrapper.write.setResolver( + [namehash(name), accounts[1].address], + { + account: accounts[1], + }, + ) + }) + + it('Cannot be called by anyone else.', async () => { + const { nameWrapper, accounts } = await loadFixture(setResolverFixture) + + await expect(nameWrapper) + .write('setResolver', [namehash(name), accounts[1].address], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Cannot be called if CANNOT_SET_RESOLVER is burned', async () => { + const { nameWrapper, accounts } = await loadFixture(setResolverFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_SET_RESOLVER]) + + await expect(nameWrapper) + .write('setResolver', [namehash(name), accounts[1].address]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + }) +} diff --git a/test/wrapper/functions/setSubnodeOwner.ts b/test/wrapper/functions/setSubnodeOwner.ts new file mode 100644 index 00000000..d80eb8e2 --- /dev/null +++ b/test/wrapper/functions/setSubnodeOwner.ts @@ -0,0 +1,772 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import hre from 'hardhat' +import { getAddress, labelhash, namehash, zeroAddress } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_CREATE_SUBDOMAIN, + CANNOT_SET_RESOLVER, + CANNOT_TRANSFER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const setSubnodeOwnerTests = () => + describe('setSubnodeOwner()', () => { + const label = 'ownerandwrap' + const name = `${label}.eth` + const sublabel = 'sub' + const subname = `${sublabel}.${name}` + + async function setSubnodeOwnerFixture() { + const initial = await loadFixture(fixture) + const { actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + return initial + } + + it('Can be called by the owner of a name and sets this contract as owner on the ENS registry.', async () => { + const { ensRegistry, nameWrapper, actions, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await actions.setRegistryApprovalForWrapper() + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + }) + + it('Can be called by an account authorised by the owner.', async () => { + const { ensRegistry, nameWrapper, actions, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + account: 1, + }) + + await expectOwnerOf(subname).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + }) + + it('Transfers the wrapped token to the target address.', async () => { + const { ensRegistry, nameWrapper, actions, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: CAN_DO_EVERYTHING, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + }) + + it('Will not allow wrapping with a target address of 0x0.', async () => { + const { nameWrapper, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + zeroAddress, + CAN_DO_EVERYTHING, + 0n, + ]) + .toBeRevertedWithString('ERC1155: mint to the zero address') + }) + + it('Will not allow wrapping with a target address of the wrapper contract address', async () => { + const { nameWrapper } = await loadFixture(setSubnodeOwnerFixture) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + nameWrapper.address, + CAN_DO_EVERYTHING, + 0n, + ]) + .toBeRevertedWithString( + 'ERC1155: newOwner cannot be the NameWrapper contract', + ) + }) + + it('Does not allow anyone else to wrap a name even if the owner has authorised the wrapper with the ENS registry.', async () => { + const { ensRegistry, nameWrapper, actions, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // TODO: this is not testing what the description of the test is + await ensRegistry.write.setApprovalForAll([accounts[1].address, true]) + + await expect(nameWrapper) + .write( + 'setSubnodeOwner', + [ + namehash(name), + sublabel, + accounts[0].address, + CAN_DO_EVERYTHING, + 0n, + ], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Fuses cannot be burned if the name does not have PARENT_CANNOT_CONTROL burned', async () => { + // note: not using suite specific fixture here + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[0].address, + CANNOT_UNWRAP | CANNOT_TRANSFER, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Does not allow fuses to be burned if CANNOT_UNWRAP is not burned.', async () => { + // note: not using suite specific fixture here + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[0].address, + PARENT_CANNOT_CONTROL | CANNOT_TRANSFER, + 0n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Allows fuses to be burned if CANNOT_UNWRAP and PARENT_CANNOT_CONTROL is burned and is not expired', async () => { + // note: not using suite specific fixture here + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | CANNOT_SET_RESOLVER, + expiry: MAX_EXPIRY, + }) + + await expect( + nameWrapper.read.allFusesBurned([ + namehash(subname), + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | CANNOT_SET_RESOLVER, + ]), + ).resolves.toBe(true) + }) + + it('Does not allow IS_DOT_ETH to be burned', async () => { + // note: not using suite specific fixture here + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[0].address, + CANNOT_UNWRAP | + PARENT_CANNOT_CONTROL | + CANNOT_SET_RESOLVER | + IS_DOT_ETH, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Does not allow fuses to be burned if CANNOT_UNWRAP and PARENT_CANNOT_CONTROL are burned, but the name is expired', async () => { + // note: not using suite specific fixture here + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING | CANNOT_UNWRAP, + }) + + const [, parentFuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(parentFuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | IS_DOT_ETH, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + expiry: 0n, // set expiry to 0 + }) + + await expect( + nameWrapper.read.allFusesBurned([ + namehash(subname), + PARENT_CANNOT_CONTROL, + ]), + ).resolves.toBe(false) + }) + + it("normalises the max expiry of a subdomain to the parent's expiry", async () => { + // note: not using suite specific fixture here + const { baseRegistrar, nameWrapper, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING | CANNOT_UNWRAP, + }) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + const [, , expiry] = await nameWrapper.read.getData([toNameId(subname)]) + + expect(expiry).toEqual(expectedExpiry + GRACE_PERIOD) + }) + + it('Emits Wrap event', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[1].address, + 0, + 0n, + ]) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(subname), + dnsEncodeName(subname), + accounts[1].address, + 0, + 0n, + ) + }) + + it('Emits TransferSingle event', async () => { + const { nameWrapper, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[1].address, + 0, + 0n, + ]) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[0].address, + zeroAddress, + accounts[1].address, + toNameId(subname), + 1n, + ) + }) + + it('Will not create a subdomain with an empty label', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + '', + accounts[0].address, + CAN_DO_EVERYTHING, + 0n, + ]) + .toBeRevertedWithCustomError('LabelTooShort') + }) + + it('should be able to call twice and change the owner', async () => { + const { nameWrapper, actions, accounts } = await loadFixture( + setSubnodeOwnerFixture, + ) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + }) + + it('setting owner to 0 burns and unwraps', async () => { + // note: not using suite specific fixture here + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: 0, + expiry: MAX_EXPIRY, + }) + + const tx = await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: zeroAddress, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(zeroAccount) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(subname), zeroAddress) + }) + + it('Unwrapping within an external contract does not create any state inconsistencies', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + const testReentrancy = await hre.viem.deployContract( + 'TestNameWrapperReentrancy', + [ + accounts[0].address, + nameWrapper.address, + namehash('test.eth'), + labelhash('sub'), + ], + ) + await nameWrapper.write.setApprovalForAll([testReentrancy.address, true]) + + // set self as sub owner + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + expiry: MAX_EXPIRY, + }) + + // attempt to move owner to testReentrancy, which unwraps domain itself to account while keeping ERC1155 to testReentrancy + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + testReentrancy.address, + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + + // reverts because CANNOT_UNWRAP/PCC are burned first, and then unwrap is attempted inside contract, which fails, because CU has already been burned + }) + + it('Unwrapping a previously wrapped unexpired name retains PCC and so reverts setSubnodeRecord', async () => { + // note: not using suite specific fixture here + const { nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + // Confirm fuses are set + const [, fusesBefore] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + + await actions.unwrapName({ + parentName: name, + label: sublabel, + controller: accounts[1].address, + account: 1, + }) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(owner).toEqual(zeroAddress) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[1].address, + 0, + 0n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Rewrapping a name that had PCC burned, but has now expired is possible and resets fuses', async () => { + // note: not using suite specific fixture here + const { + nameWrapper, + actions, + accounts, + baseRegistrar, + testClient, + publicClient, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry - DAY / 2n, + }) + + // Confirm fuses are set + const [, fusesBefore] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + + await actions.unwrapName({ + parentName: name, + label: sublabel, + controller: accounts[1].address, + account: 1, + }) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(owner).toEqual(zeroAddress) + expect(expiry).toEqual(parentExpiry - DAY / 2n) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL) + + // Advance time so the subdomain expires, but not the parent + await testClient.increaseTime({ seconds: Number(DAY / 2n + 1n) }) + await testClient.mine({ blocks: 1 }) + + const [, fusesAfter, expiryAfter] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(expiryAfter).toEqual(parentExpiry - DAY / 2n) + expect(fusesAfter).toEqual(0) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: 0, + expiry: 0n, + }) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + + const [rawOwner, rawFuses, expiry2] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + // TODO: removed active fuses check because it was redundant + expect(rawFuses).toEqual(0) + expect(rawOwner).toEqualAddress(accounts[1].address) + expect(expiry2).toBeLessThan(timestamp) + }) + + it('Expired subnames should still be protected by CANNOT_CREATE_SUBDOMAIN on the parent', async () => { + // note: not using suite specific fixture here + const { + nameWrapper, + actions, + accounts, + baseRegistrar, + testClient, + publicClient, + } = await loadFixture(fixture) + + const sublabel2 = 'sub2' + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry - DAY / 2n, + }) + + await nameWrapper.write.setFuses([ + namehash(name), + CANNOT_CREATE_SUBDOMAIN, + ]) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel2, + accounts[1].address, + 0, + parentExpiry - DAY / 2n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(`${sublabel2}.${name}`)) + + await testClient.increaseTime({ seconds: Number(DAY / 2n + 1n) }) + await testClient.mine({ blocks: 1 }) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + // subdomain is expired + expect(owner).toEqual(zeroAddress) + expect(fuses).toEqual(0) + expect(expiry).toBeLessThan(timestamp) + + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[1].address, + 0, + parentExpiry - DAY / 2n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Burning a name still protects it from the parent as long as it is unexpired and has PCC burnt', async () => { + // note: not using suite specific fixture here + const { + ensRegistry, + nameWrapper, + actions, + accounts, + baseRegistrar, + publicClient, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + // Confirm fuses are set + const [, fusesBefore] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + + // Unwrap and set owner to 0 to burn the name + await actions.unwrapName({ + parentName: name, + label: sublabel, + controller: accounts[1].address, + account: 1, + }) + await ensRegistry.write.setOwner([namehash(subname), zeroAddress], { + account: accounts[1], + }) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + expect(owner).toEqual(zeroAddress) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + expect(expiry).toBeGreaterThan(timestamp) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL) + await expectOwnerOf(subname).on(ensRegistry).toBe(zeroAccount) + + // attempt to take back the name + await expect(nameWrapper) + .write('setSubnodeOwner', [ + namehash(name), + sublabel, + accounts[0].address, + PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + }) diff --git a/test/wrapper/functions/setSubnodeRecord.ts b/test/wrapper/functions/setSubnodeRecord.ts new file mode 100644 index 00000000..0402276f --- /dev/null +++ b/test/wrapper/functions/setSubnodeRecord.ts @@ -0,0 +1,777 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import hre from 'hardhat' +import { getAddress, labelhash, namehash, zeroAddress } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_CREATE_SUBDOMAIN, + CANNOT_TRANSFER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const setSubnodeRecordTests = () => + describe('setSubnodeRecord()', () => { + const label = 'subdomain2' + const sublabel = 'sub' + const name = `${label}.eth` + const subname = `${sublabel}.${name}` + + async function setSubnodeRecordFixture() { + const initial = await loadFixture(fixture) + const { actions, accounts } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + return { ...initial, resolverAddress: accounts[0].address } + } + + it('Can be called by the owner of a name', async () => { + const { ensRegistry, nameWrapper, actions, accounts, resolverAddress } = + await loadFixture(setSubnodeRecordFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + resolver: resolverAddress, + ttl: 0n, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + }) + + it('Can be called by an account authorised by the owner.', async () => { + const { ensRegistry, nameWrapper, actions, accounts, resolverAddress } = + await loadFixture(setSubnodeRecordFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + resolver: resolverAddress, + ttl: 0n, + fuses: 0, + expiry: 0n, + account: 1, + }) + + await expectOwnerOf(subname).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + }) + + it('Transfers the wrapped token to the target address.', async () => { + const { nameWrapper, actions, accounts, resolverAddress } = + await loadFixture(setSubnodeRecordFixture) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + resolver: resolverAddress, + ttl: 0n, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + }) + + it('Will not allow wrapping with a target address of 0x0', async () => { + const { nameWrapper, resolverAddress } = await loadFixture( + setSubnodeRecordFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + zeroAddress, + resolverAddress, + 0n, + 0, + 0n, + ]) + .toBeRevertedWithString('ERC1155: mint to the zero address') + }) + + it('Will not allow wrapping with a target address of the wrapper contract address.', async () => { + const { nameWrapper, resolverAddress } = await loadFixture( + setSubnodeRecordFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + nameWrapper.address, + resolverAddress, + 0n, + 0, + 0n, + ]) + .toBeRevertedWithString( + 'ERC1155: newOwner cannot be the NameWrapper contract', + ) + }) + + it('Does not allow anyone else to wrap a name even if the owner has authorised the wrapper with the ENS registry.', async () => { + const { ensRegistry, nameWrapper, accounts, resolverAddress } = + await loadFixture(setSubnodeRecordFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await ensRegistry.write.setApprovalForAll([accounts[1].address, true]) + + await expect(nameWrapper) + .write( + 'setSubnodeRecord', + [ + namehash(name), + sublabel, + accounts[0].address, + resolverAddress, + 0n, + 0, + 0n, + ], + { account: accounts[1] }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Does not allow fuses to be burned if PARENT_CANNOT_CONTROL is not burned.', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[0].address, + accounts[0].address, + 0n, + CANNOT_UNWRAP, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Does not allow fuses to be burned if CANNOT_UNWRAP is not burned', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[0].address, + accounts[0].address, + 0n, + PARENT_CANNOT_CONTROL | CANNOT_TRANSFER, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Fuses will remain 0 if expired', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + resolver: accounts[0].address, + ttl: 0n, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER, + expiry: 0n, + }) + + const [, fuses] = await nameWrapper.read.getData([toNameId(subname)]) + + expect(fuses).toEqual(0) + }) + + it('Allows fuses to be burned if not expired and PARENT_CANNOT_CONTROL/CANNOT_UNWRAP are burned', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + resolver: accounts[0].address, + ttl: 0n, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER, + expiry: MAX_EXPIRY, + }) + + const [, fuses] = await nameWrapper.read.getData([toNameId(subname)]) + + expect(fuses).toEqual( + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER, + ) + }) + + it('does not allow burning IS_DOT_ETH', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[0].address, + accounts[0].address, + 0n, + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER | IS_DOT_ETH, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Emits Wrap event', async () => { + const { nameWrapper, accounts } = await loadFixture( + setSubnodeRecordFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[1].address, + accounts[0].address, + 0n, + 0, + 0n, + ]) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(subname), + dnsEncodeName(subname), + accounts[1].address, + 0, + 0n, + ) + }) + + it('Emits TransferSingle event', async () => { + const { nameWrapper, accounts } = await loadFixture( + setSubnodeRecordFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[1].address, + accounts[0].address, + 0n, + 0, + 0n, + ]) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[0].address, + zeroAddress, + accounts[1].address, + toNameId(subname), + 1n, + ) + }) + + it('Sets the appropriate values on the ENS registry', async () => { + const { ensRegistry, nameWrapper, actions, accounts } = await loadFixture( + setSubnodeRecordFixture, + ) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + resolver: accounts[0].address, + ttl: 100n, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(ensRegistry).toBe(nameWrapper) + await expect( + ensRegistry.read.resolver([namehash(subname)]), + ).resolves.toEqualAddress(accounts[0].address) + await expect(ensRegistry.read.ttl([namehash(subname)])).resolves.toEqual( + 100n, + ) + }) + + it('Will not create a subdomain with an empty label', async () => { + const { nameWrapper, resolverAddress } = await loadFixture( + setSubnodeRecordFixture, + ) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + '', + zeroAddress, + resolverAddress, + 0n, + 0, + 0n, + ]) + .toBeRevertedWithCustomError('LabelTooShort') + }) + + it('should be able to call twice and change the owner', async () => { + const { nameWrapper, actions, accounts, resolverAddress } = + await loadFixture(setSubnodeRecordFixture) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + resolver: resolverAddress, + ttl: 0n, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[0]) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + resolver: resolverAddress, + ttl: 0n, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + }) + + it('setting owner to 0 burns and unwraps', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeRecord to accounts[1] + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + resolver: accounts[0].address, + ttl: 0n, + fuses: 0, + expiry: MAX_EXPIRY, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + zeroAddress, + zeroAddress, + 0n, + PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(subname), zeroAddress) + + await expectOwnerOf(subname).on(nameWrapper).toBe(zeroAccount) + }) + + it('Unwrapping within an external contract does not create any state inconsistencies', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.setRegistryApprovalForWrapper() + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + const testReentrancy = await hre.viem.deployContract( + 'TestNameWrapperReentrancy', + [ + accounts[0].address, + nameWrapper.address, + namehash(name), + labelhash(sublabel), + ], + ) + await nameWrapper.write.setApprovalForAll([testReentrancy.address, true]) + + // set self as sub.test.eth owner + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[0].address, + resolver: zeroAddress, + ttl: 0n, + fuses: CAN_DO_EVERYTHING, + expiry: MAX_EXPIRY, + }) + + // move owner to testReentrancy, which unwraps domain itself to account while keeping ERC1155 to testReentrancy + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + testReentrancy.address, + zeroAddress, + 0n, + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + + // reverts because CANNOT_UNWRAP/PCC are burned first, and then unwrap is attempted inside contract, which fails, because CU has already been burned + }) + + it('Unwrapping a previously wrapped unexpired name retains PCC and so reverts setSubnodeRecord', async () => { + const { ensRegistry, nameWrapper, actions, accounts, baseRegistrar } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + // Confirm fuses are set + const [ownerBefore, fusesBefore, expiryBefore] = + await nameWrapper.read.getData([toNameId(subname)]) + + expect(ownerBefore).toEqualAddress(accounts[1].address) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + expect(expiryBefore).toEqual(parentExpiry + GRACE_PERIOD) + + await actions.unwrapName({ + parentName: name, + label: sublabel, + controller: accounts[1].address, + account: 1, + }) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(owner).toEqual(zeroAddress) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + + // attempt to rewrap with PCC still burnt + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[1].address, + zeroAddress, + 0n, + 0, + 0n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Rewrapping a name that had PCC burned, but has now expired is possible', async () => { + const { nameWrapper, actions, accounts, baseRegistrar, testClient } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry - DAY / 2n, + }) + + // Confirm fuses are set + const [, fusesBefore] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + + await actions.unwrapName({ + parentName: name, + label: sublabel, + controller: accounts[1].address, + account: 1, + }) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + expect(owner).toEqual(zeroAddress) + expect(expiry).toEqual(parentExpiry - DAY / 2n) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL) + + // Advance time so the subname expires, but not the parent + await testClient.increaseTime({ seconds: Number(DAY / 2n + 1n) }) + await testClient.mine({ blocks: 1 }) + + const [, fusesAfter, expiryAfter] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(expiryAfter).toEqual(parentExpiry - DAY / 2n) + expect(fusesAfter).toEqual(0) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + resolver: zeroAddress, + ttl: 0n, + fuses: 0, + expiry: 0n, + }) + + await expectOwnerOf(subname).on(nameWrapper).toBe(accounts[1]) + }) + + it('Expired subnames should still be protected by CANNOT_CREATE_SUBDOMAIN on the parent', async () => { + const { + nameWrapper, + actions, + accounts, + baseRegistrar, + testClient, + publicClient, + } = await loadFixture(fixture) + + const sublabel2 = 'sub2' + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeRecord to accounts[1] + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + resolver: zeroAddress, + ttl: 0n, + fuses: PARENT_CANNOT_CONTROL, + expiry: parentExpiry - DAY / 2n, + }) + + await nameWrapper.write.setFuses([ + namehash(name), + CANNOT_CREATE_SUBDOMAIN, + ]) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel2, + accounts[1].address, + zeroAddress, + 0n, + 0, + parentExpiry - DAY / 2n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(`${sublabel2}.${name}`)) + + await testClient.increaseTime({ seconds: Number(DAY / 2n + 1n) }) + await testClient.mine({ blocks: 1 }) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + // subdomain is expired + expect(owner).toEqual(zeroAddress) + expect(fuses).toEqual(0) + expect(expiry).toBeLessThan(timestamp) + + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[1].address, + zeroAddress, + 0n, + 0, + parentExpiry - DAY / 2n, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + + it('Burning a name still protects it from the parent as long as it is unexpired and has PCC burnt', async () => { + const { + ensRegistry, + nameWrapper, + actions, + accounts, + baseRegistrar, + publicClient, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that the name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeRecord.onNameWrapper({ + parentName: name, + label: sublabel, + owner: accounts[1].address, + resolver: zeroAddress, + ttl: 0n, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + // Confirm fuses are set + const [, fusesBefore] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + + // Unwrap and set owner to 0 to burn the name + await actions.unwrapName({ + parentName: name, + label: sublabel, + controller: accounts[1].address, + account: 1, + }) + await ensRegistry.write.setOwner([namehash(subname), zeroAddress], { + account: accounts[1], + }) + + const [owner, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + + const timestamp = await publicClient.getBlock().then((b) => b.timestamp) + + expect(owner).toEqual(zeroAddress) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + expect(expiry).toBeGreaterThan(timestamp) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL) + await expectOwnerOf(subname).on(ensRegistry).toBe(zeroAccount) + + // attempt to take back the name + await expect(nameWrapper) + .write('setSubnodeRecord', [ + namehash(name), + sublabel, + accounts[0].address, + zeroAddress, + 0n, + PARENT_CANNOT_CONTROL, + MAX_EXPIRY, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(subname)) + }) + }) diff --git a/test/wrapper/functions/setTTL.ts b/test/wrapper/functions/setTTL.ts new file mode 100644 index 00000000..69998869 --- /dev/null +++ b/test/wrapper/functions/setTTL.ts @@ -0,0 +1,79 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, namehash } from 'viem' +import { + CANNOT_SET_TTL, + CANNOT_UNWRAP, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const setTTLTests = () => + describe('setTTL', () => { + const label = 'setttl' + const name = `${label}.eth` + + async function setTTLFixture() { + const initial = await loadFixture(fixture) + const { actions } = initial + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + return initial + } + + it('Can be called by the owner', async () => { + const { nameWrapper, accounts } = await loadFixture(setTTLFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setTTL([namehash(name), 100n]) + }) + + it('Performs the appropriate function on the ENS registry.', async () => { + const { ensRegistry, nameWrapper } = await loadFixture(setTTLFixture) + + await expect(ensRegistry.read.ttl([namehash(name)])).resolves.toEqual(0n) + + await nameWrapper.write.setTTL([namehash(name), 100n]) + + await expect(ensRegistry.read.ttl([namehash(name)])).resolves.toEqual( + 100n, + ) + }) + + it('Can be called by an account authorised by the owner.', async () => { + const { nameWrapper, accounts } = await loadFixture(setTTLFixture) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await nameWrapper.write.setTTL([namehash(name), 100n], { + account: accounts[1], + }) + }) + + it('Cannot be called by anyone else.', async () => { + const { nameWrapper, accounts } = await loadFixture(setTTLFixture) + + await expect(nameWrapper) + .write('setTTL', [namehash(name), 3600n], { account: accounts[1] }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Cannot be called if CANNOT_SET_TTL is burned', async () => { + const { nameWrapper } = await loadFixture(setTTLFixture) + + await nameWrapper.write.setFuses([namehash(name), CANNOT_SET_TTL]) + + await expect(nameWrapper) + .write('setTTL', [namehash(name), 100n]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + }) diff --git a/test/wrapper/functions/setUpgradeContract.ts b/test/wrapper/functions/setUpgradeContract.ts new file mode 100644 index 00000000..9f23c945 --- /dev/null +++ b/test/wrapper/functions/setUpgradeContract.ts @@ -0,0 +1,129 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { zeroAddress } from 'viem' +import { + DUMMY_ADDRESS, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const setUpgradeContractTests = () => + describe('setUpgradeContract()', () => { + it('Reverts if called by someone that is not the owner', async () => { + const { nameWrapper, accounts } = await loadFixture(fixture) + + await expect(nameWrapper) + .write('setUpgradeContract', [accounts[1].address], { + account: accounts[1], + }) + .toBeRevertedWithString('Ownable: caller is not the owner') + }) + + it('Will setApprovalForAll for the upgradeContract addresses in the registrar and registry to true', async () => { + const { nameWrapper, nameWrapperUpgraded, baseRegistrar, ensRegistry } = + await loadFixture(fixture) + + await expect( + baseRegistrar.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(false) + await expect( + ensRegistry.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(false) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([nameWrapperUpgraded.address]) + + await expect( + baseRegistrar.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(true) + await expect( + ensRegistry.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(true) + }) + + it('Will setApprovalForAll for the old upgradeContract addresses in the registrar and registry to false', async () => { + const { nameWrapper, nameWrapperUpgraded, baseRegistrar, ensRegistry } = + await loadFixture(fixture) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([DUMMY_ADDRESS]) + + await expect( + baseRegistrar.read.isApprovedForAll([ + nameWrapper.address, + DUMMY_ADDRESS, + ]), + ).resolves.toBe(true) + await expect( + ensRegistry.read.isApprovedForAll([nameWrapper.address, DUMMY_ADDRESS]), + ).resolves.toBe(true) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([nameWrapperUpgraded.address]) + + await expect( + baseRegistrar.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(true) + await expect( + ensRegistry.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(true) + + await expect( + baseRegistrar.read.isApprovedForAll([ + nameWrapper.address, + DUMMY_ADDRESS, + ]), + ).resolves.toBe(false) + await expect( + ensRegistry.read.isApprovedForAll([nameWrapper.address, DUMMY_ADDRESS]), + ).resolves.toBe(false) + }) + + it('Will not setApprovalForAll for the new upgrade address if it is the address(0)', async () => { + const { nameWrapper, nameWrapperUpgraded, baseRegistrar, ensRegistry } = + await loadFixture(fixture) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([nameWrapperUpgraded.address]) + + await expect( + baseRegistrar.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(true) + await expect( + ensRegistry.read.isApprovedForAll([ + nameWrapper.address, + nameWrapperUpgraded.address, + ]), + ).resolves.toBe(true) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([zeroAddress]) + + await expect( + baseRegistrar.read.isApprovedForAll([nameWrapper.address, zeroAddress]), + ).resolves.toBe(false) + await expect( + ensRegistry.read.isApprovedForAll([nameWrapper.address, zeroAddress]), + ).resolves.toBe(false) + }) + }) diff --git a/test/wrapper/functions/unwrap.ts b/test/wrapper/functions/unwrap.ts new file mode 100644 index 00000000..20597e07 --- /dev/null +++ b/test/wrapper/functions/unwrap.ts @@ -0,0 +1,509 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, labelhash, namehash, zeroAddress, zeroHash } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const unwrapTests = () => + describe('unwrap()', () => { + it('Allows owner to unwrap name', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const parentLabel = 'xyz' + const childLabel = 'unwrapped' + const childName = `${childLabel}.${parentLabel}` + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: parentLabel, + owner: accounts[0].address, + resolver: zeroAddress, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: parentLabel, + label: childLabel, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + expiry: 0n, + }) + + await expectOwnerOf(childName).on(nameWrapper).toBe(accounts[0]) + + await actions.unwrapName({ + parentName: parentLabel, + label: childLabel, + controller: accounts[0].address, + }) + + // Transfers ownership in the ENS registry to the target address. + await expectOwnerOf(childName).on(ensRegistry).toBe(accounts[0]) + }) + + it('Will not allow previous owner to unwrap name when name expires', async () => { + const { baseRegistrar, nameWrapper, accounts, testClient, actions } = + await loadFixture(fixture) + + const parentLabel = 'unwrapped' + const parentName = `${parentLabel}.eth` + const childLabel = 'sub' + const childName = `${childLabel}.${parentName}` + + await actions.registerSetupAndWrapName({ + label: parentLabel, + fuses: CANNOT_UNWRAP, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label: childLabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + await testClient.increaseTime({ + seconds: Number(GRACE_PERIOD + 1n * DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('unwrap', [ + namehash(parentName), + labelhash(childLabel), + accounts[0].address, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(childName), getAddress(accounts[0].address)) + }) + + it('emits Unwrap event', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'xyz' + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expect(nameWrapper) + .write('unwrap', [zeroHash, labelhash(label), accounts[0].address]) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(label), getAddress(accounts[0].address)) + }) + + it('emits TransferSingle event', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'xyz' + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expect(nameWrapper) + .write('unwrap', [zeroHash, labelhash(label), accounts[0].address]) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[0].address, + accounts[0].address, + zeroAddress, + toNameId(label), + 1n, + ) + }) + + it('Allows an account authorised by the owner on the NFT Wrapper to unwrap a name', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'abc' + + // setup .abc with accounts[0] as owner + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label, + owner: accounts[0].address, + }) + await actions.setRegistryApprovalForWrapper() + + // wrap using accounts[0] + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: zeroAddress, + }) + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await expectOwnerOf(label).on(nameWrapper).toBe(accounts[0]) + + // unwrap using accounts[1] + await actions.unwrapName({ + parentName: '', + label, + controller: accounts[1].address, + account: 1, + }) + + await expectOwnerOf(label).on(ensRegistry).toBe(accounts[1]) + await expectOwnerOf(label).on(nameWrapper).toBe(zeroAccount) + }) + + it('Does not allow an account authorised by the owner on the ENS registry to unwrap a name', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'abc' + + // setup .abc with accounts[1] as owner + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label, + owner: accounts[1].address, + }) + // allow account to deal with all account[1]'s names + await ensRegistry.write.setApprovalForAll([accounts[0].address, true], { + account: accounts[1], + }) + await actions.setRegistryApprovalForWrapper({ account: 1 }) + + // confirm abc is owner by accounts[1] not accounts[0] + await expectOwnerOf(label).on(ensRegistry).toBe(accounts[1]) + await expect( + ensRegistry.read.isApprovedForAll([ + accounts[1].address, + accounts[0].address, + ]), + ).resolves.toBe(true) + + // wrap using accounts[0] + await actions.wrapName({ + name: label, + owner: accounts[1].address, + resolver: zeroAddress, + }) + }) + + it('Does not allow anyone else to unwrap a name', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'abc' + + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label, + owner: accounts[0].address, + }) + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expectOwnerOf(label).on(nameWrapper).toBe(accounts[0]) + + // unwrap using accounts[1] + await expect(nameWrapper) + .write('unwrap', [zeroHash, labelhash(label), accounts[1].address], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(label), getAddress(accounts[1].address)) + }) + + it('Will not unwrap .eth 2LDs.', async () => { + const { nameWrapper, baseRegistrar, accounts, actions } = + await loadFixture(fixture) + + const label = 'unwrapped' + + await actions.registerSetupAndWrapName({ + label, + fuses: 0, + }) + + await expectOwnerOf(`${label}.eth`).on(nameWrapper).toBe(accounts[0]) + + await expect(nameWrapper) + .write('unwrap', [ + namehash('eth'), + labelhash(label), + accounts[0].address, + ]) + .toBeRevertedWithCustomError('IncompatibleParent') + }) + + it('Will not allow a target address of 0x0 or the wrapper contract address.', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'abc' + + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label, + owner: accounts[0].address, + }) + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expect(nameWrapper) + .write('unwrap', [zeroHash, labelhash(label), zeroAddress]) + .toBeRevertedWithCustomError('IncorrectTargetOwner') + .withArgs(zeroAddress) + + await expect(nameWrapper) + .write('unwrap', [zeroHash, labelhash(label), nameWrapper.address]) + .toBeRevertedWithCustomError('IncorrectTargetOwner') + .withArgs(getAddress(nameWrapper.address)) + }) + + it('Will not allow to unwrap with PCC/CU burned if expired', async () => { + const { accounts, ensRegistry, nameWrapper, actions } = await loadFixture( + fixture, + ) + + const parentLabel = 'awesome' + const parentName = `${parentLabel}.eth` + const childLabel = 'sub' + const childName = `${childLabel}.${parentName}` + + await actions.register({ + label: parentLabel, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setSubnodeOwner.onEnsRegistry({ + parentName, + label: childLabel, + owner: accounts[0].address, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await actions.wrapEth2ld({ + label: parentLabel, + owner: accounts[0].address, + fuses: CANNOT_UNWRAP, + resolver: zeroAddress, + }) + await actions.setRegistryApprovalForWrapper() + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label: childLabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: 0n, + }) + + await expectOwnerOf(childName).on(ensRegistry).toBe(nameWrapper) + + await expect(nameWrapper) + .write('unwrap', [ + namehash(parentName), + labelhash(childLabel), + accounts[0].address, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(childName), getAddress(accounts[0].address)) + }) + + it('Will allow to unwrap with PCC/CU burned if expired and then extended without PCC/CU', async () => { + const { + baseRegistrar, + nameWrapper, + ensRegistry, + accounts, + publicClient, + testClient, + actions, + } = await loadFixture(fixture) + + const parentLabel = 'awesome' + const parentName = `${parentLabel}.eth` + const childLabel = 'sub' + const childName = `${childLabel}.${parentName}` + + await actions.register({ + label: parentLabel, + owner: accounts[0].address, + duration: 7n * DAY, + }) + await actions.setSubnodeOwner.onEnsRegistry({ + parentName, + label: childLabel, + owner: accounts[0].address, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await actions.wrapEth2ld({ + label: parentLabel, + owner: accounts[0].address, + fuses: CANNOT_UNWRAP, + resolver: zeroAddress, + }) + await actions.setRegistryApprovalForWrapper() + + const timestamp = await actions.getBlockTimestamp() + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label: childLabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: timestamp + DAY, + }) + + await expectOwnerOf(childName).on(ensRegistry).toBe(nameWrapper) + + await testClient.increaseTime({ seconds: Number(2n * DAY) }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('unwrap', [ + namehash(parentName), + labelhash(childLabel), + accounts[0].address, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(childName), getAddress(accounts[0].address)) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label: childLabel, + owner: accounts[0].address, + fuses: 0, + expiry: MAX_EXPIRY, + }) + + await actions.unwrapName({ + parentName, + label: childLabel, + controller: accounts[0].address, + }) + + await expectOwnerOf(childName).on(ensRegistry).toBe(accounts[0]) + }) + + it('Will not allow to unwrap a name with the CANNOT_UNWRAP fuse burned if not expired', async () => { + const { ensRegistry, baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + const parentLabel = 'abc' + const parentName = `${parentLabel}.eth` + const childLabel = 'sub' + const childName = `${childLabel}.${parentName}` + + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label: parentLabel, + owner: accounts[0].address, + }) + await actions.setRegistryApprovalForWrapper() + + await actions.register({ + label: parentLabel, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await actions.wrapEth2ld({ + label: parentLabel, + owner: accounts[0].address, + fuses: CANNOT_UNWRAP, + resolver: zeroAddress, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label: childLabel, + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: MAX_EXPIRY, + }) + + await expect(nameWrapper) + .write('unwrap', [ + namehash(parentName), + labelhash(childLabel), + accounts[0].address, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(childName)) + }) + + it('Unwrapping a previously wrapped unexpired name retains PCC and expiry', async () => { + const { ensRegistry, baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + const parentLabel = 'test' + const parentName = `${parentLabel}.eth` + const childLabel = 'sub' + const childName = `${childLabel}.${parentName}` + + await actions.registerSetupAndWrapName({ + label: parentLabel, + fuses: CANNOT_UNWRAP, + }) + + // Confirm that the name is wrapped + await expectOwnerOf(parentName).on(nameWrapper).toBe(accounts[0]) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(parentLabel), + ]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label: childLabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + // Confirm fuses are set + const [, fusesBefore] = await nameWrapper.read.getData([ + toNameId(childName), + ]) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + + await actions.unwrapName({ + parentName, + label: childLabel, + controller: accounts[1].address, + account: 1, + }) + + const [, fusesAfter, expiryAfter] = await nameWrapper.read.getData([ + toNameId(childName), + ]) + expect(fusesAfter).toEqual(PARENT_CANNOT_CONTROL) + expect(expiryAfter).toEqual(parentExpiry + GRACE_PERIOD) + }) + }) diff --git a/test/wrapper/functions/unwrapETH2LD.ts b/test/wrapper/functions/unwrapETH2LD.ts new file mode 100644 index 00000000..7bd79138 --- /dev/null +++ b/test/wrapper/functions/unwrapETH2LD.ts @@ -0,0 +1,205 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, labelhash, namehash, zeroAddress } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, +} from '../fixtures/utils.js' + +export const unwrapETH2LDTests = () => + describe('unwrapETH2LD()', () => { + const label = 'unwrapped' + const name = `${label}.eth` + + it('Allows the owner to unwrap a name.', async () => { + const { baseRegistrar, ensRegistry, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await actions.unwrapEth2ld({ + label, + controller: accounts[0].address, + registrant: accounts[0].address, + }) + + // transfers the controller on the registry to the target address. + await expectOwnerOf(name).on(ensRegistry).toBe(accounts[0]) + //Transfers the registrant on the .eth registrar to the target address + await expectOwnerOf(label).on(baseRegistrar).toBe(accounts[0]) + }) + + it('Does not allows the previous owner to unwrap when the name has expired.', async () => { + const { nameWrapper, accounts, testClient, actions } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + await testClient.increaseTime({ + seconds: Number(DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expect(nameWrapper) + .write('unwrapETH2LD', [ + labelhash(label), + accounts[0].address, + accounts[0].address, + ]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('emits Unwrap event', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('unwrapETH2LD', [ + labelhash(label), + accounts[0].address, + accounts[0].address, + ]) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(name), accounts[0].address) + }) + + it('Emits TransferSingle event', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('unwrapETH2LD', [ + labelhash(label), + accounts[0].address, + accounts[0].address, + ]) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[0].address, + accounts[0].address, + zeroAddress, + toNameId(name), + 1n, + ) + }) + + it('Does not allows an account authorised by the owner on the .eth registrar to unwrap a name', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await actions.setBaseRegistrarApprovalForWrapper() + await baseRegistrar.write.setApprovalForAll([accounts[1].address, true]) + + await expect(nameWrapper) + .write( + 'unwrapETH2LD', + [labelhash(label), accounts[1].address, accounts[1].address], + { + account: accounts[1], + }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Does not allow anyone else to unwrap a name even if the owner has authorised the wrapper with the ENS registry.', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + await actions.setBaseRegistrarApprovalForWrapper() + + await ensRegistry.write.setApprovalForAll([accounts[1].address, true]) + + await expect(nameWrapper) + .write( + 'unwrapETH2LD', + [labelhash(label), accounts[1].address, accounts[1].address], + { + account: accounts[1], + }, + ) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Does not allow a name to be unwrapped if CANNOT_UNWRAP fuse has been burned', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await expect(nameWrapper) + .write('unwrapETH2LD', [ + labelhash(label), + accounts[0].address, + accounts[0].address, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('Unwrapping a previously wrapped unexpired name retains PCC and expiry', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + // register and wrap a name with PCC + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // unwrap it + await actions.unwrapEth2ld({ + label, + controller: accounts[0].address, + registrant: accounts[0].address, + }) + + // check that the PCC is still there + const [, fuses, expiry] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL | IS_DOT_ETH) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + }) diff --git a/test/wrapper/functions/upgrade.ts b/test/wrapper/functions/upgrade.ts new file mode 100644 index 00000000..5c83f81a --- /dev/null +++ b/test/wrapper/functions/upgrade.ts @@ -0,0 +1,691 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { getAddress, namehash, zeroAddress } from 'viem' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_SET_RESOLVER, + CANNOT_TRANSFER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const upgradeTests = () => + describe('upgrade()', () => { + describe('.eth', () => { + const label = 'wrapped2' + const name = `${label}.eth` + + it('Upgrades a .eth name if sender is owner', async () => { + const { + nameWrapper, + baseRegistrar, + ensRegistry, + nameWrapperUpgraded, + actions, + accounts, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // make sure reclaim claimed ownership for the wrapper in registry + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + // check the upgraded namewrapper is called with all parameters required + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(name), + accounts[0].address, + PARENT_CANNOT_CONTROL | IS_DOT_ETH, + expectedExpiry + GRACE_PERIOD, + zeroAddress, + '0x00', + ) + }) + + it('Upgrades a .eth name if sender is authorised by the owner', async () => { + const { + nameWrapper, + baseRegistrar, + ensRegistry, + nameWrapperUpgraded, + actions, + accounts, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // make sure reclaim claimed ownership for the wrapper in registry + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + // check the upgraded namewrapper is called with all parameters required + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x'], { + account: accounts[1], + }) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(name), + accounts[0].address, + PARENT_CANNOT_CONTROL | IS_DOT_ETH, + expectedExpiry + GRACE_PERIOD, + zeroAddress, + '0x00', + ) + }) + + it('Cannot upgrade a name if the upgradeContract has not been set.', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toBeRevertedWithCustomError('CannotUpgrade') + }) + + it('Cannot upgrade a name if the upgradeContract has been set and then set back to the 0 address.', async () => { + const { nameWrapper, nameWrapperUpgraded, actions } = await loadFixture( + fixture, + ) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await expect( + nameWrapper.read.upgradeContract(), + ).resolves.toEqualAddress(nameWrapperUpgraded.address) + + await nameWrapper.write.setUpgradeContract([zeroAddress]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toBeRevertedWithCustomError('CannotUpgrade') + }) + + it('Will pass fuses and expiry to the upgradedContract without any changes.', async () => { + const { + nameWrapper, + baseRegistrar, + nameWrapperUpgraded, + actions, + accounts, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP | CANNOT_SET_RESOLVER, + }) + + const expectedExpiry = await baseRegistrar.read + .nameExpires([toLabelId(label)]) + .then((e) => e + GRACE_PERIOD) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + // assert the fuses and expiry have been passed through to the new NameWrapper + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(name), + accounts[0].address, + PARENT_CANNOT_CONTROL | + CANNOT_UNWRAP | + CANNOT_SET_RESOLVER | + IS_DOT_ETH, + expectedExpiry, + zeroAddress, + '0x00', + ) + }) + + // TODO: this label seems wrong ?? + it('Will burn the token, fuses and expiry of the name in the NameWrapper contract when upgraded.', async () => { + const { + nameWrapper, + baseRegistrar, + nameWrapperUpgraded, + actions, + accounts, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await nameWrapper.write.upgrade([dnsEncodeName(name), '0x']) + + await expectOwnerOf(name).on(nameWrapper).toBe(zeroAccount) + + const [, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + + expect(fuses).toEqual( + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + + it('will revert if called twice by the original owner', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await nameWrapper.write.upgrade([dnsEncodeName(name), '0x']) + + await expectOwnerOf(name).on(nameWrapper).toBe(zeroAccount) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('Will allow you to pass through extra data on upgrade', async () => { + const { + nameWrapper, + baseRegistrar, + nameWrapperUpgraded, + actions, + accounts, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP | CANNOT_SET_RESOLVER, + }) + + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + const expectedExpiry = await baseRegistrar.read + .nameExpires([toLabelId(label)]) + .then((e) => e + GRACE_PERIOD) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x01']) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(name), + accounts[0].address, + PARENT_CANNOT_CONTROL | + CANNOT_UNWRAP | + CANNOT_SET_RESOLVER | + IS_DOT_ETH, + expectedExpiry, + zeroAddress, + '0x01', + ) + }) + + it('Does not allow anyone else to upgrade a name even if the owner has authorised the wrapper with the ENS registry.', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x'], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + }) + + describe('other', () => { + const label = 'to-upgrade' + const parentLabel = 'wrapped2' + const parentName = `${parentLabel}.eth` + const name = `${label}.${parentName}` + + it('Allows owner to upgrade name', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label: parentLabel, + fuses: CANNOT_UNWRAP, + }) + await actions.setRegistryApprovalForWrapper() + + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label, + owner: accounts[0].address, + expiry: 0n, + fuses: 0, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(name), + accounts[0].address, + 0, + 0n, + zeroAddress, + '0x00', + ) + }) + + it('upgrades a name if sender is authorized by the owner', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + const xyzName = `${label}.xyz` + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: 'xyz', + owner: accounts[0].address, + resolver: zeroAddress, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: 'xyz', + label: label, + owner: accounts[0].address, + expiry: 0n, + fuses: 0, + }) + + await expectOwnerOf(xyzName).on(nameWrapper).toBe(accounts[0]) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await nameWrapper.write.setApprovalForAll([accounts[1].address, true]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(xyzName), '0x'], { + account: accounts[1], + }) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(xyzName), + accounts[0].address, + 0, + 0n, + zeroAddress, + '0x00', + ) + }) + + it('Cannot upgrade a name if the upgradeContract has not been set.', async () => { + const { nameWrapper, actions, accounts } = await loadFixture(fixture) + + const xyzName = `${label}.xyz` + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: 'xyz', + owner: accounts[0].address, + resolver: zeroAddress, + }) + await actions.setSubnodeOwner.onNameWrapper({ + parentName: 'xyz', + label, + owner: accounts[0].address, + expiry: 0n, + fuses: 0, + }) + + await expectOwnerOf(xyzName).on(nameWrapper).toBe(accounts[0]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(xyzName), '0x']) + .toBeRevertedWithCustomError('CannotUpgrade') + }) + + it('Will pass fuses and expiry to the upgradedContract without any changes.', async () => { + const { + nameWrapper, + nameWrapperUpgraded, + actions, + accounts, + baseRegistrar, + } = await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label: parentLabel, + fuses: CANNOT_UNWRAP, + }) + await actions.setRegistryApprovalForWrapper() + + const expectedFuses = + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER + + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label, + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: expectedFuses, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + const expectedExpiry = await baseRegistrar.read + .nameExpires([toLabelId(parentLabel)]) + .then((e) => e + GRACE_PERIOD) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(name), + accounts[0].address, + expectedFuses, + expectedExpiry, + zeroAddress, + '0x00', + ) + }) + + it('Will burn the token of the name in the NameWrapper contract when upgraded, but keep expiry and fuses', async () => { + const { + nameWrapper, + nameWrapperUpgraded, + actions, + accounts, + baseRegistrar, + } = await loadFixture(fixture) + + const expectedFuses = + PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER + + await actions.registerSetupAndWrapName({ + label: parentLabel, + fuses: CANNOT_UNWRAP, + }) + await actions.setRegistryApprovalForWrapper() + + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label, + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: expectedFuses, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + const expectedExpiry = await baseRegistrar.read + .nameExpires([toLabelId(parentLabel)]) + .then((e) => e + GRACE_PERIOD) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await nameWrapper.write.upgrade([dnsEncodeName(name), '0x']) + + await expectOwnerOf(name).on(nameWrapper).toBe(zeroAccount) + + const [, fuses, expiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + + expect(fuses).toEqual(expectedFuses) + expect(expiry).toEqual(expectedExpiry) + }) + + it('reverts if called twice by the original owner', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label: parentLabel, + fuses: CANNOT_UNWRAP, + }) + await actions.setRegistryApprovalForWrapper() + + await actions.setSubnodeOwner.onNameWrapper({ + parentName, + label, + owner: accounts[0].address, + expiry: MAX_EXPIRY, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP | CANNOT_TRANSFER, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await nameWrapper.write.upgrade([dnsEncodeName(name), '0x']) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(name), '0x']) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[0].address)) + }) + + it('Keeps approval information on upgrade', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + const xyzName = `${label}.xyz` + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: 'xyz', + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: 'xyz', + label, + owner: accounts[0].address, + resolver: accounts[1].address, + ttl: 0n, + expiry: 0n, + fuses: 0, + }) + + await expectOwnerOf(xyzName).on(nameWrapper).toBe(accounts[0]) + + await nameWrapper.write.approve([ + accounts[2].address, + toNameId(xyzName), + ]) + + await expect( + nameWrapper.read.getApproved([toNameId(xyzName)]), + ).resolves.toEqualAddress(accounts[2].address) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(xyzName), '0x']) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(xyzName), + accounts[0].address, + 0, + 0n, + accounts[2].address, + '0x', + ) + }) + + it('Will allow you to pass through extra data on upgrade', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + const xyzName = `${label}.xyz` + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: 'xyz', + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await actions.setSubnodeRecord.onNameWrapper({ + parentName: 'xyz', + label, + owner: accounts[0].address, + resolver: accounts[1].address, + ttl: 0n, + expiry: 0n, + fuses: 0, + }) + + await expectOwnerOf(xyzName).on(nameWrapper).toBe(accounts[0]) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(xyzName), '0x01']) + .toEmitEventFrom(nameWrapperUpgraded, 'NameUpgraded') + .withArgs( + dnsEncodeName(xyzName), + accounts[0].address, + 0, + 0n, + zeroAddress, + '0x01', + ) + }) + + it('Does not allow anyone else to upgrade a name even if the owner has authorised the wrapper with the ENS registry.', async () => { + const { nameWrapper, nameWrapperUpgraded, actions, accounts } = + await loadFixture(fixture) + + const xyzName = `${label}.xyz` + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: 'xyz', + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await actions.setSubnodeOwner.onNameWrapper({ + parentName: 'xyz', + label, + owner: accounts[0].address, + expiry: 0n, + fuses: 0, + }) + + await expectOwnerOf(xyzName).on(nameWrapper).toBe(accounts[0]) + + // set the upgradeContract of the NameWrapper contract + await nameWrapper.write.setUpgradeContract([ + nameWrapperUpgraded.address, + ]) + + await expect(nameWrapper) + .write('upgrade', [dnsEncodeName(xyzName), '0x'], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(xyzName), getAddress(accounts[1].address)) + }) + }) + }) diff --git a/test/wrapper/functions/wrap.ts b/test/wrapper/functions/wrap.ts new file mode 100644 index 00000000..97c3134b --- /dev/null +++ b/test/wrapper/functions/wrap.ts @@ -0,0 +1,390 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import hre from 'hardhat' +import { getAddress, namehash, zeroAddress } from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId } from '../../fixtures/utils.js' +import { + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const wrapTests = () => + describe('wrap()', () => { + it('Wraps a name if you are the owner', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'xyz' + + await expectOwnerOf(label).on(nameWrapper).toBe(zeroAccount) + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await expectOwnerOf(label).on(nameWrapper).toBe(accounts[0]) + }) + + it('Allows specifying resolver', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'xyz' + + await actions.setRegistryApprovalForWrapper() + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: accounts[1].address, + }) + + await expect( + ensRegistry.read.resolver([namehash(label)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('emits event for NameWrapped', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write('wrap', [dnsEncodeName('xyz'), accounts[0].address, zeroAddress]) + .toEmitEvent('NameWrapped') + .withArgs( + namehash('xyz'), + dnsEncodeName('xyz'), + accounts[0].address, + 0, + 0n, + ) + }) + + it('emits event for TransferSingle', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write('wrap', [dnsEncodeName('xyz'), accounts[0].address, zeroAddress]) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[0].address, + zeroAddress, + accounts[0].address, + toNameId('xyz'), + 1n, + ) + }) + + it('Cannot wrap a name if the owner has not authorised the wrapper with the ENS registry', async () => { + const { nameWrapper, accounts } = await loadFixture(fixture) + + await expect(nameWrapper) + .write('wrap', [dnsEncodeName('xyz'), accounts[0].address, zeroAddress]) + .toBeRevertedWithoutReason() + }) + + it('Will not allow wrapping with a target address of 0x0 or the wrapper contract address.', async () => { + const { nameWrapper, actions } = await loadFixture(fixture) + + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write('wrap', [dnsEncodeName('xyz'), zeroAddress, zeroAddress]) + .toBeRevertedWithString('ERC1155: mint to the zero address') + }) + + it('Will not allow wrapping with a target address of the wrapper contract address.', async () => { + const { ensRegistry, nameWrapper, actions } = await loadFixture(fixture) + + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write('wrap', [dnsEncodeName('xyz'), nameWrapper.address, zeroAddress]) + .toBeRevertedWithString( + 'ERC1155: newOwner cannot be the NameWrapper contract', + ) + }) + + it('Allows an account approved by the owner on the ENS registry to wrap a name.', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'abc' + + // setup .abc with accounts[1] as owner + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label, + owner: accounts[1].address, + }) + // allow account to deal with all accounts[1]'s names + await ensRegistry.write.setApprovalForAll([accounts[0].address, true], { + account: accounts[1], + }) + await actions.setRegistryApprovalForWrapper({ account: 1 }) + + // confirm abc is owner by accounts[1] not accounts[0] + await expectOwnerOf(label).on(ensRegistry).toBe(accounts[1]) + + // wrap using accounts[0] + await actions.wrapName({ + name: label, + owner: accounts[1].address, + resolver: zeroAddress, + }) + + await expectOwnerOf(label).on(nameWrapper).toBe(accounts[1]) + }) + + it('Does not allow anyone else to wrap a name even if the owner has authorised the wrapper with the ENS registry', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'abc' + + // setup .abc with accounts[1] as owner + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: '', + label, + owner: accounts[1].address, + }) + await actions.setRegistryApprovalForWrapper({ + account: 1, + }) + + // confirm abc is owner by accounts[1] not accounts[0] + await expectOwnerOf(label).on(ensRegistry).toBe(accounts[1]) + + // wrap using accounts[0] + await expect(nameWrapper) + .write('wrap', [dnsEncodeName(label), accounts[1].address, zeroAddress]) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(label), getAddress(accounts[0].address)) + }) + + it('Does not allow wrapping .eth 2LDs.', async () => { + const { ensRegistry, nameWrapper, baseRegistrar, accounts, actions } = + await loadFixture(fixture) + + const label = 'wrapped' + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write('wrap', [ + dnsEncodeName(`${label}.eth`), + accounts[1].address, + zeroAddress, + ]) + .toBeRevertedWithCustomError('IncompatibleParent') + }) + + it('Can re-wrap a name that was reassigned by an unwrapped parent', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const parentLabel = 'xyz' + const childLabel = 'sub' + const childName = `${childLabel}.${parentLabel}` + + await expectOwnerOf(parentLabel).on(nameWrapper).toBe(zeroAccount) + + await actions.setRegistryApprovalForWrapper() + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: parentLabel, + label: childLabel, + owner: accounts[0].address, + }) + await actions.wrapName({ + name: childName, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + await actions.setSubnodeOwner.onEnsRegistry({ + parentName: parentLabel, + label: childLabel, + owner: accounts[1].address, + }) + + await expectOwnerOf(childName).on(ensRegistry).toBe(accounts[1]) + await expectOwnerOf(childName).on(nameWrapper).toBe(accounts[0]) + + await actions.setRegistryApprovalForWrapper({ account: 1 }) + + const tx = await actions.wrapName({ + name: childName, + owner: accounts[1].address, + resolver: zeroAddress, + account: 1, + }) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(childName), zeroAddress) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[1].address, + accounts[0].address, + zeroAddress, + toNameId(childName), + 1n, + ) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(childName), + dnsEncodeName(childName), + accounts[1].address, + CAN_DO_EVERYTHING, + 0n, + ) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[1].address, + zeroAddress, + accounts[1].address, + toNameId(childName), + 1n, + ) + + await expectOwnerOf(childName).on(nameWrapper).toBe(accounts[1]) + await expectOwnerOf(childName).on(ensRegistry).toBe(nameWrapper) + }) + + it('Will not wrap a name with junk at the end', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + await actions.setRegistryApprovalForWrapper() + + await expect(nameWrapper) + .write('wrap', [ + `${dnsEncodeName('xyz')}123456`, + accounts[0].address, + zeroAddress, + ]) + .toBeRevertedWithString('namehash: Junk at end of name') + }) + + it('Does not allow wrapping a name you do not own', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + const label = 'xyz' + + await actions.setRegistryApprovalForWrapper() + // Register the name to accounts[0] + await actions.wrapName({ + name: label, + owner: accounts[0].address, + resolver: zeroAddress, + }) + + // Deploy the destroy-your-name contract + const nameGriefer = await hre.viem.deployContract('NameGriefer', [ + nameWrapper.address, + ]) + + const tx = nameGriefer.write.destroy([dnsEncodeName(label)]) + + // Try and burn the name + await expect(nameWrapper) + .transaction(tx) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(label), getAddress(nameGriefer.address)) + + // Make sure it didn't succeed + await expectOwnerOf(label).on(nameWrapper).toBe(accounts[0]) + }) + + it('Rewrapping a previously wrapped unexpired name retains PCC', async () => { + const { ensRegistry, baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + const label = 'test' + const name = `${label}.eth` + const subLabel = 'sub' + const subname = `${subLabel}.${name}` + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // Confirm that name is wrapped + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // NameWrapper.setSubnodeOwner to accounts[1] + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: subLabel, + owner: accounts[1].address, + fuses: PARENT_CANNOT_CONTROL, + expiry: MAX_EXPIRY, + }) + + // Confirm fuses are set + const [, fusesBefore] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(fusesBefore).toEqual(PARENT_CANNOT_CONTROL) + + await actions.unwrapName({ + parentName: name, + label: subLabel, + controller: accounts[1].address, + account: 1, + }) + await actions.setRegistryApprovalForWrapper({ + account: 1, + }) + await actions.wrapName({ + name: subname, + owner: accounts[1].address, + resolver: zeroAddress, + account: 1, + }) + + const [, fusesAfter, expiryAfter] = await nameWrapper.read.getData([ + toNameId(subname), + ]) + expect(fusesAfter).toEqual(PARENT_CANNOT_CONTROL) + expect(expiryAfter).toEqual(parentExpiry + GRACE_PERIOD) + }) + }) diff --git a/test/wrapper/functions/wrapETH2LD.ts b/test/wrapper/functions/wrapETH2LD.ts new file mode 100644 index 00000000..92cf95d1 --- /dev/null +++ b/test/wrapper/functions/wrapETH2LD.ts @@ -0,0 +1,775 @@ +import { loadFixture } from '@nomicfoundation/hardhat-toolbox-viem/network-helpers.js' +import { expect } from 'chai' +import { + getAddress, + keccak256, + namehash, + stringToBytes, + zeroAddress, +} from 'viem' +import { DAY } from '../../fixtures/constants.js' +import { dnsEncodeName } from '../../fixtures/dnsEncodeName.js' +import { toLabelId, toNameId, toTokenId } from '../../fixtures/utils.js' +import { + CANNOT_SET_RESOLVER, + CANNOT_TRANSFER, + CANNOT_UNWRAP, + CAN_DO_EVERYTHING, + GRACE_PERIOD, + IS_DOT_ETH, + MAX_EXPIRY, + PARENT_CANNOT_CONTROL, + expectOwnerOf, + deployNameWrapperWithUtils as fixture, + zeroAccount, +} from '../fixtures/utils.js' + +export const wrapETH2LDTests = () => + describe('wrapETH2LD()', () => { + const label = 'wrapped2' + const name = `${label}.eth` + + it('wraps a name if sender is owner', async () => { + const { ensRegistry, baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + + // allow the restricted name wrappper to transfer the name to itself and reclaim it + await actions.setBaseRegistrarApprovalForWrapper() + + await expectOwnerOf(name).on(nameWrapper).toBe(zeroAccount) + + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + + // make sure reclaim claimed ownership for the wrapper in registry + await expectOwnerOf(name).on(ensRegistry).toBe(nameWrapper) + + // make sure owner in the wrapper is the user + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + + // make sure registrar ERC721 is owned by Wrapper + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + }) + + it('Cannot wrap a name if the owner has not authorised the wrapper with the .eth registrar.', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + + await expect(nameWrapper) + .write('wrapETH2LD', [ + label, + accounts[0].address, + CAN_DO_EVERYTHING, + zeroAddress, + ]) + .toBeRevertedWithString('ERC721: caller is not token owner or approved') + }) + + it('Allows specifying resolver', async () => { + const { ensRegistry, baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + resolver: accounts[1].address, + }) + + await expect( + ensRegistry.read.resolver([namehash(name)]), + ).resolves.toEqualAddress(accounts[1].address) + }) + + it('Can re-wrap a name that was wrapped has already expired on the .eth registrar', async () => { + const { baseRegistrar, nameWrapper, accounts, testClient, actions } = + await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + + await testClient.increaseTime({ + seconds: Number(DAY * GRACE_PERIOD + DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expect( + baseRegistrar.read.available([toLabelId(label)]), + ).resolves.toBe(true) + + await actions.register({ + label, + owner: accounts[1].address, + duration: 1n * DAY, + account: 1, + }) + await expectOwnerOf(label).on(baseRegistrar).toBe(accounts[1]) + + await actions.setBaseRegistrarApprovalForWrapper({ account: 1 }) + + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + const tx = await actions.wrapEth2ld({ + label, + owner: accounts[1].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + account: 1, + }) + + // Check the 4 events + // UnwrapETH2LD of the original owner + // TransferSingle burn of the original token + // WrapETH2LD to the new owner with fuses + // TransferSingle to mint the new token + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(name), zeroAddress) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[1].address, + accounts[0].address, + zeroAddress, + toNameId(name), + 1n, + ) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(name), + dnsEncodeName(name), + accounts[1].address, + PARENT_CANNOT_CONTROL | IS_DOT_ETH, + expectedExpiry + GRACE_PERIOD, + ) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[1].address, + zeroAddress, + accounts[1].address, + toNameId(name), + 1n, + ) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[1]) + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + }) + + it('Can re-wrap a name that was wrapped has already expired even if CANNOT_TRANSFER was burned', async () => { + const { baseRegistrar, nameWrapper, accounts, testClient, actions } = + await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: CANNOT_UNWRAP | CANNOT_TRANSFER, + resolver: zeroAddress, + }) + + await testClient.increaseTime({ + seconds: Number(DAY * GRACE_PERIOD + DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + await expect( + baseRegistrar.read.available([toLabelId(label)]), + ).resolves.toBe(true) + + await actions.register({ + label, + owner: accounts[1].address, + duration: 1n * DAY, + account: 1, + }) + + await expectOwnerOf(label).on(baseRegistrar).toBe(accounts[1]) + const expectedExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + await actions.setBaseRegistrarApprovalForWrapper({ account: 1 }) + const tx = await actions.wrapEth2ld({ + label, + owner: accounts[1].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + account: 1, + }) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameUnwrapped') + .withArgs(namehash(name), zeroAddress) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[1].address, + accounts[0].address, + zeroAddress, + toNameId(name), + 1n, + ) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(name), + dnsEncodeName(name), + accounts[1].address, + PARENT_CANNOT_CONTROL | IS_DOT_ETH, + expectedExpiry + GRACE_PERIOD, + ) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[1]) + await expectOwnerOf(label).on(baseRegistrar).toBe(nameWrapper) + }) + + it('correctly reports fuses for a name that has expired and been rewrapped more permissively', async () => { + const { baseRegistrar, nameWrapper, accounts, testClient, actions } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [, initialFuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(initialFuses).toEqual( + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + + // Create a subdomain that can't be unwrapped + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: MAX_EXPIRY, + }) + + const [, subFuses] = await nameWrapper.read.getData([ + toNameId('sub.' + name), + ]) + expect(subFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + + // Fast forward until the 2LD expires + await testClient.increaseTime({ + seconds: Number(DAY * GRACE_PERIOD + DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + // Register from another address + await actions.registerSetupAndWrapName({ + label, + duration: 1n * DAY, + account: 1, + fuses: CAN_DO_EVERYTHING, + }) + + const expectedExpiry = await baseRegistrar.read + .nameExpires([toLabelId(label)]) + .then((e) => e + GRACE_PERIOD) + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + expect(newFuses).toEqual(PARENT_CANNOT_CONTROL | IS_DOT_ETH) + expect(newExpiry).toEqual(expectedExpiry) + + // subdomain fuses get reset + const [, newSubFuses] = await nameWrapper.read.getData([ + toNameId('sub.' + name), + ]) + expect(newSubFuses).toEqual(0) + }) + + it('correctly reports fuses for a name that has expired and been rewrapped more permissively with registerAndWrap()', async () => { + const { baseRegistrar, nameWrapper, accounts, testClient, actions } = + await loadFixture(fixture) + + await actions.registerSetupAndWrapName({ + label, + fuses: CANNOT_UNWRAP, + }) + + const [, initialFuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(initialFuses).toEqual( + CANNOT_UNWRAP | PARENT_CANNOT_CONTROL | IS_DOT_ETH, + ) + + // Create a subdomain that can't be unwrapped + await actions.setSubnodeOwner.onNameWrapper({ + parentName: name, + label: 'sub', + owner: accounts[0].address, + fuses: PARENT_CANNOT_CONTROL | CANNOT_UNWRAP, + expiry: MAX_EXPIRY, + }) + + const [, subFuses] = await nameWrapper.read.getData([ + toNameId('sub.' + name), + ]) + expect(subFuses).toEqual(PARENT_CANNOT_CONTROL | CANNOT_UNWRAP) + + // Fast forward until the 2LD expires + await testClient.increaseTime({ + seconds: Number(DAY * GRACE_PERIOD + DAY + 1n), + }) + await testClient.mine({ blocks: 1 }) + + // Register from another address with registerAndWrap() + await baseRegistrar.write.addController([nameWrapper.address]) + await nameWrapper.write.setController([accounts[0].address, true]) + await nameWrapper.write.registerAndWrapETH2LD([ + label, + accounts[1].address, + 1n * DAY, + zeroAddress, + 0, + ]) + + const expectedExpiry = await baseRegistrar.read + .nameExpires([toLabelId(label)]) + .then((e) => e + GRACE_PERIOD) + const [, newFuses, newExpiry] = await nameWrapper.read.getData([ + toNameId(name), + ]) + expect(newFuses).toEqual(PARENT_CANNOT_CONTROL | IS_DOT_ETH) + expect(newExpiry).toEqual(expectedExpiry) + + // subdomain fuses get reset + const [, newSubFuses] = await nameWrapper.read.getData([ + toNameId('sub.' + name), + ]) + expect(newSubFuses).toEqual(0) + }) + + it('emits Wrap event', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + const tx = await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + + const expiry = await baseRegistrar.read.nameExpires([toLabelId(label)]) + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('NameWrapped') + .withArgs( + namehash(name), + dnsEncodeName(name), + accounts[0].address, + CAN_DO_EVERYTHING | IS_DOT_ETH, + expiry + GRACE_PERIOD, + ) + }) + + it('emits TransferSingle event', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + const tx = await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + + await expect(nameWrapper) + .transaction(tx) + .toEmitEvent('TransferSingle') + .withArgs( + accounts[0].address, + zeroAddress, + accounts[0].address, + toNameId(name), + 1n, + ) + }) + + it('Transfers the wrapped token to the target address.', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await actions.wrapEth2ld({ + label, + owner: accounts[1].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[1]) + }) + + it('Does not allow wrapping with a target address of 0x0', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + + await expect(nameWrapper) + .write('wrapETH2LD', [ + label, + zeroAddress, + CAN_DO_EVERYTHING, + zeroAddress, + ]) + .toBeRevertedWithString('ERC1155: mint to the zero address') + }) + + it('Does not allow wrapping with a target address of the wrapper contract address.', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + + await expect(nameWrapper) + .write('wrapETH2LD', [ + label, + nameWrapper.address, + CAN_DO_EVERYTHING, + zeroAddress, + ]) + .toBeRevertedWithString( + 'ERC1155: newOwner cannot be the NameWrapper contract', + ) + }) + + it('Allows an account approved by the owner on the .eth registrar to wrap a name.', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + await baseRegistrar.write.setApprovalForAll([accounts[1].address, true]) + + await actions.wrapEth2ld({ + label, + owner: accounts[1].address, + fuses: 0, + resolver: zeroAddress, + account: 1, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[1]) + }) + + it('Does not allow anyone else to wrap a name even if the owner has authorised the wrapper with the ENS registry.', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + + await actions.setRegistryApprovalForWrapper() + await actions.setBaseRegistrarApprovalForWrapper() + + await expect(nameWrapper) + .write('wrapETH2LD', [label, accounts[1].address, 0, zeroAddress], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Can wrap a name even if the controller address is different to the registrant address.', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await ensRegistry.write.setOwner([namehash(name), accounts[1].address]) + await actions.setBaseRegistrarApprovalForWrapper() + + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: 0, + resolver: zeroAddress, + }) + + await expectOwnerOf(name).on(nameWrapper).toBe(accounts[0]) + }) + + it('Does not allow the controller of a name to wrap it if they are not also the registrant.', async () => { + const { ensRegistry, nameWrapper, accounts, actions } = await loadFixture( + fixture, + ) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await ensRegistry.write.setOwner([namehash(name), accounts[1].address]) + await actions.setBaseRegistrarApprovalForWrapper() + + await expect(nameWrapper) + .write('wrapETH2LD', [label, accounts[1].address, 0, zeroAddress], { + account: accounts[1], + }) + .toBeRevertedWithCustomError('Unauthorised') + .withArgs(namehash(name), getAddress(accounts[1].address)) + }) + + it('Does not allows fuse to be burned if CANNOT_UNWRAP has not been burned.', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + + await expect(nameWrapper) + .write('wrapETH2LD', [ + label, + accounts[0].address, + CANNOT_SET_RESOLVER, + zeroAddress, + ]) + .toBeRevertedWithCustomError('OperationProhibited') + .withArgs(namehash(name)) + }) + + it('cannot burn any parent controlled fuse', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + + for (let i = 0; i < 7; i++) { + await expect(nameWrapper) + .write('wrapETH2LD', [ + label, + accounts[0].address, + IS_DOT_ETH * 2 ** i, // next undefined fuse + zeroAddress, + ]) + .toBeRevertedWithoutReason() + } + }) + + it('Allows fuse to be burned if CANNOT_UNWRAP has been burned', async () => { + const { nameWrapper, accounts, actions } = await loadFixture(fixture) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + + const initialFuses = CANNOT_UNWRAP | CANNOT_SET_RESOLVER + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: initialFuses, + resolver: zeroAddress, + }) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual(initialFuses | PARENT_CANNOT_CONTROL | IS_DOT_ETH) + }) + + it('Allows fuse to be burned if CANNOT_UNWRAP has been burned, but resets to 0 if expired', async () => { + const { nameWrapper, accounts, testClient, actions } = await loadFixture( + fixture, + ) + + await actions.register({ + label, + owner: accounts[0].address, + duration: 1n * DAY, + }) + await actions.setBaseRegistrarApprovalForWrapper() + + const initialFuses = CANNOT_UNWRAP | CANNOT_SET_RESOLVER + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: initialFuses, + resolver: zeroAddress, + }) + + await testClient.increaseTime({ + seconds: Number(DAY + 1n + GRACE_PERIOD), + }) + await testClient.mine({ blocks: 1 }) + + const [, fuses] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual(0) + }) + + it('Will not wrap an empty name', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + const emptyLabelhash = keccak256(new Uint8Array(0)) + + await baseRegistrar.write.register([ + toTokenId(emptyLabelhash), + accounts[0].address, + 1n * DAY, + ]) + await actions.setBaseRegistrarApprovalForWrapper() + + await expect(nameWrapper) + .write('wrapETH2LD', [ + '', + accounts[0].address, + CAN_DO_EVERYTHING, + zeroAddress, + ]) + .toBeRevertedWithCustomError('LabelTooShort') + }) + + it('Will not wrap a label greater than 255 characters', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + const longString = + 'yutaioxtcsbzrqhdjmltsdfkgomogohhcchjoslfhqgkuhduhxqsldnurwrrtoicvthwxytonpcidtnkbrhccaozdtoznedgkfkifsvjukxxpkcmgcjprankyzerzqpnuteuegtfhqgzcxqwttyfewbazhyilqhyffufxrookxrnjkmjniqpmntcbrowglgdpkslzechimsaonlcvjkhhvdvkvvuztihobmivifuqtvtwinljslusvhhbwhuhzty' + expect(longString.length).toEqual(256) + + await baseRegistrar.write.register([ + toTokenId(keccak256(stringToBytes(longString))), + accounts[0].address, + 1n * DAY, + ]) + await actions.setBaseRegistrarApprovalForWrapper() + + await expect(nameWrapper) + .write('wrapETH2LD', [ + longString, + accounts[0].address, + CAN_DO_EVERYTHING, + zeroAddress, + ]) + .toBeRevertedWithCustomError('LabelTooLong') + .withArgs(longString) + }) + + it('Rewrapping a previously wrapped unexpired name retains PCC and expiry', async () => { + const { baseRegistrar, nameWrapper, accounts, actions } = + await loadFixture(fixture) + + // register and wrap a name with PCC + await actions.registerSetupAndWrapName({ + label, + fuses: CAN_DO_EVERYTHING, + }) + const parentExpiry = await baseRegistrar.read.nameExpires([ + toLabelId(label), + ]) + + // unwrap it + await actions.unwrapEth2ld({ + label, + controller: accounts[0].address, + registrant: accounts[0].address, + }) + + // rewrap it without PCC being burned + await actions.wrapEth2ld({ + label, + owner: accounts[0].address, + fuses: CAN_DO_EVERYTHING, + resolver: zeroAddress, + }) + + // check that the PCC is still there + const [, fuses, expiry] = await nameWrapper.read.getData([toNameId(name)]) + expect(fuses).toEqual(PARENT_CANNOT_CONTROL | IS_DOT_ETH) + expect(expiry).toEqual(parentExpiry + GRACE_PERIOD) + }) + })