Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: When creating wrapped subnames, set the default expiry to 0 #180

Merged
merged 2 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading