Skip to content

Commit

Permalink
Merge pull request #180 from ensdomains/fix-namewrapper-create-subnames
Browse files Browse the repository at this point in the history
Fix: When creating wrapped subnames, set the default expiry to 0
  • Loading branch information
LeonmanRolls authored Apr 15, 2024
2 parents a0bd3b8 + 209e796 commit 38ebec7
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 5 deletions.
105 changes: 104 additions & 1 deletion packages/ensjs/src/functions/wallet/createSubname.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type { Address, Hex } from 'viem'
import { afterEach, beforeAll, beforeEach, expect, it } from 'vitest'
import { getChainContractAddress } from '../../contracts/getChainContractAddress.js'
import { nameWrapperOwnerOfSnippet } from '../../contracts/nameWrapper.js'
import {
nameWrapperGetDataSnippet,
nameWrapperOwnerOfSnippet,
} from '../../contracts/nameWrapper.js'
import { registryOwnerSnippet } from '../../contracts/registry.js'
import {
publicClient,
Expand All @@ -11,6 +14,8 @@ import {
} from '../../test/addTestContracts.js'
import { namehash } from '../../utils/normalise.js'
import createSubname from './createSubname.js'
import setFuses from './setFuses.js'
import getWrapperData from '../public/getWrapperData.js'

let snapshot: Hex
let accounts: Address[]
Expand Down Expand Up @@ -71,4 +76,102 @@ it('should allow creating a subname on the namewrapper', async () => {
args: [BigInt(namehash('test.wrapped.eth'))],
})
expect(owner).toBe(accounts[0])

const data = await publicClient.readContract({
abi: nameWrapperGetDataSnippet,
functionName: 'getData',
address: getChainContractAddress({
client: publicClient,
contract: 'ensNameWrapper',
}),
args: [BigInt(namehash('test.wrapped.eth'))],
})
expect(data[2]).toBe(0n)
})

it('should create a subname on the namewrapper with max expiry if pcc is burned and no expiry is set', async () => {
const setupTx = await setFuses(walletClient, {
name: 'wrapped.eth',
fuses: {
named: ['CANNOT_UNWRAP'],
},
account: accounts[1],
})
expect(setupTx).toBeTruthy()
const setUpReceipt = await waitForTransaction(setupTx)
expect(setUpReceipt.status).toBe('success')

const parentWrapperData = await getWrapperData(publicClient, {
name: 'wrapped.eth',
})

const tx = await createSubname(walletClient, {
name: 'test.wrapped.eth',
contract: 'nameWrapper',
owner: accounts[0],
account: accounts[1],
fuses: { parent: { named: ['PARENT_CANNOT_CONTROL'] } },
})
expect(tx).toBeTruthy()
const receipt = await waitForTransaction(tx)
expect(receipt.status).toBe('success')

const owner = await publicClient.readContract({
abi: nameWrapperOwnerOfSnippet,
functionName: 'ownerOf',
address: getChainContractAddress({
client: publicClient,
contract: 'ensNameWrapper',
}),
args: [BigInt(namehash('test.wrapped.eth'))],
})
expect(owner).toBe(accounts[0])

const data = await getWrapperData(publicClient, { name: 'test.wrapped.eth' })
expect(data?.expiry?.value).toBeTruthy()
expect(data!.expiry!.value).toBe(parentWrapperData?.expiry?.value)
})

it('should throw an error when creating a wrapped subname with PCC burned with a parent name that has not burned CANNOT_UNWRAP', async () => {
await expect(
createSubname(walletClient, {
name: 'test.wrapped.eth',
contract: 'nameWrapper',
owner: accounts[0],
account: accounts[1],
fuses: { parent: { named: ['PARENT_CANNOT_CONTROL'] } },
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`
[CreateSubnameParentNotLockedError: Create subname error: Cannot burn PARENT_CANNOT_CONTROL when wrapped.eth has not burned CANNOT_UNWRAP fuse
Version: @ensdomains/[email protected]]
`,
)
})

it('should throw an error when creating a wrapped subname with a parent name that has burned CANNOT_CREATE_SUBDOMAINS', async () => {
const parentTx = await setFuses(walletClient, {
name: 'wrapped.eth',
fuses: { named: ['CANNOT_UNWRAP', 'CANNOT_CREATE_SUBDOMAIN'] },
account: accounts[1],
})
expect(parentTx).toBeTruthy()
const setUpReceipt = await waitForTransaction(parentTx)
expect(setUpReceipt.status).toBe('success')
await expect(
createSubname(walletClient, {
name: 'test.wrapped.eth',
contract: 'nameWrapper',
owner: accounts[0],
account: accounts[1],
fuses: { parent: { named: ['PARENT_CANNOT_CONTROL'] } },
}),
).rejects.toThrowErrorMatchingInlineSnapshot(
`
[CreateSubnamePermissionDeniedError: Create subname error: wrapped.eth as burned CANNOT_CREATE_SUBDOMAIN fuse
Version: @ensdomains/[email protected]]
`,
)
})
75 changes: 71 additions & 4 deletions packages/ensjs/src/functions/wallet/createSubname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import {
type Transport,
} from 'viem'
import { sendTransaction } from 'viem/actions'
import type { ChainWithEns, ClientWithAccount } from '../../contracts/consts.js'
import type {
ChainWithEns,
ClientWithAccount,
ClientWithEns,
} from '../../contracts/consts.js'
import { getChainContractAddress } from '../../contracts/getChainContractAddress.js'
import { nameWrapperSetSubnodeRecordSnippet } from '../../contracts/nameWrapper.js'
import { registrySetSubnodeRecordSnippet } from '../../contracts/registry.js'
Expand All @@ -21,14 +25,20 @@ import type {
SimpleTransactionRequest,
WriteTransactionParameters,
} from '../../types.js'
import { encodeFuses, type EncodeFusesInputObject } from '../../utils/fuses.js'
import {
encodeFuses,
ParentFuses,
type EncodeFusesInputObject,
} from '../../utils/fuses.js'
import { getNameType } from '../../utils/getNameType.js'
import { makeLabelNodeAndParent } from '../../utils/makeLabelNodeAndParent.js'
import {
MAX_EXPIRY,
expiryToBigInt,
wrappedLabelLengthCheck,
makeDefaultExpiry,
} from '../../utils/wrapper.js'
import getWrapperData from '../public/getWrapperData.js'
import { BaseError } from '../../errors/base.js'

type BaseCreateSubnameDataParameters = {
/** Subname to create */
Expand Down Expand Up @@ -121,7 +131,9 @@ export const makeFunctionData = <
case 'nameWrapper': {
wrappedLabelLengthCheck(label)
const generatedFuses = fuses ? encodeFuses({ input: fuses }) : 0
const generatedExpiry = expiry ? expiryToBigInt(expiry) : MAX_EXPIRY
const generatedExpiry = expiry
? expiryToBigInt(expiry)
: makeDefaultExpiry(generatedFuses)
return {
to: getChainContractAddress({
client: wallet,
Expand Down Expand Up @@ -150,6 +162,58 @@ export const makeFunctionData = <
}
}

class CreateSubnamePermissionDeniedError extends BaseError {
parentName: string

override name = 'CreateSubnamePermissionDeniedError'

constructor({ parentName }: { parentName: string }) {
super(
`Create subname error: ${parentName} as burned CANNOT_CREATE_SUBDOMAIN fuse`,
)
this.parentName = parentName
}
}

class CreateSubnameParentNotLockedError extends BaseError {
parentName: string

override name = 'CreateSubnameParentNotLockedError'

constructor({ parentName }: { parentName: string }) {
super(
`Create subname error: Cannot burn PARENT_CANNOT_CONTROL when ${parentName} has not burned CANNOT_UNWRAP fuse`,
)
this.parentName = parentName
}
}

const checkCanCreateSubname = async (
wallet: ClientWithEns,
{
name,
fuses,
contract,
}: Pick<BaseCreateSubnameDataParameters, 'name' | 'contract' | 'fuses'>,
): Promise<void> => {
if (contract !== 'nameWrapper') return

const parentName = name.split('.').slice(1).join('.')
if (parentName === 'eth') return

const parentWrapperData = await getWrapperData(wallet, { name: parentName })
if (parentWrapperData?.fuses?.child?.CANNOT_CREATE_SUBDOMAIN)
throw new CreateSubnamePermissionDeniedError({ parentName })

const generatedFuses = fuses ? encodeFuses({ input: fuses }) : 0
const isBurningPCC =
fuses && BigInt(generatedFuses) & ParentFuses.PARENT_CANNOT_CONTROL
const isParentCannotUnwrapBurned =
parentWrapperData?.fuses?.child?.CANNOT_UNWRAP
if (isBurningPCC && !isParentCannotUnwrapBurned)
throw new CreateSubnameParentNotLockedError({ parentName })
}

/**
* Creates a subname
* @param wallet - {@link ClientWithAccount}
Expand Down Expand Up @@ -189,6 +253,8 @@ async function createSubname<
...txArgs
}: CreateSubnameParameters<TChain, TAccount, TChainOverride>,
): Promise<CreateSubnameReturnType> {
await checkCanCreateSubname(wallet, { name, fuses, contract })

const data = makeFunctionData(wallet, {
name,
contract,
Expand All @@ -201,6 +267,7 @@ async function createSubname<
...data,
...txArgs,
} as SendTransactionParameters<TChain, TAccount, TChainOverride>

return sendTransaction(wallet, writeArgs)
}

Expand Down
7 changes: 7 additions & 0 deletions packages/ensjs/src/utils/wrapper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { stringToBytes } from 'viem'
import { WrappedLabelTooLargeError } from '../errors/utils.js'
import type { AnyDate } from '../types.js'
import { ParentFuses } from './fuses.js'

export const MAX_EXPIRY = 2n ** 64n - 1n

Expand All @@ -18,3 +19,9 @@ export const wrappedLabelLengthCheck = (label: string) => {
if (bytes.byteLength > 255)
throw new WrappedLabelTooLargeError({ label, byteLength: bytes.byteLength })
}

export const makeDefaultExpiry = (fuses?: number): bigint => {
if (fuses && BigInt(fuses) & ParentFuses.PARENT_CANNOT_CONTROL)
return MAX_EXPIRY
return 0n
}

0 comments on commit 38ebec7

Please sign in to comment.