Skip to content

Commit 2589194

Browse files
bonustrackclaude
andcommitted
fix: hash .gwei names like the contract instead of ethers namehash
ethers' namehash applies Unicode NFC normalization, but the GNS contract only lowercases ASCII and hashes raw UTF-8 bytes. Composed/decomposed twins of a name are distinct, separately-registerable tokens on-chain, yet namehash folds them to one id, so a lookup of the decomposed form would silently resolve to the composed form's owner. Reproduce the contract's namehash exactly (keccak label chain, ASCII-lowercase only, no NFC); verified against the deployed contract's computeId for ASCII, subdomain, CJK, and both café twins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dd36502 commit 2589194

2 files changed

Lines changed: 20 additions & 3 deletions

File tree

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,12 @@
2323
"@aws-sdk/client-s3": "^3.352.0",
2424
"@ethersproject/address": "^5.7.0",
2525
"@ethersproject/bignumber": "^5.7.0",
26+
"@ethersproject/bytes": "^5.7.0",
2627
"@ethersproject/contracts": "^5.7.0",
2728
"@ethersproject/hash": "^5.8.0",
29+
"@ethersproject/keccak256": "^5.7.0",
2830
"@ethersproject/providers": "^5.7.2",
31+
"@ethersproject/strings": "^5.7.0",
2932
"@metamask/jazzicon": "^2.0.0",
3033
"@self.id/core": "^0.3.0",
3134
"@snapshot-labs/snapshot-metrics": "^1.4.3",

src/addressResolvers/gwei.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { getAddress } from '@ethersproject/address';
2-
import { namehash } from '@ethersproject/hash';
2+
import { concat } from '@ethersproject/bytes';
3+
import { keccak256 } from '@ethersproject/keccak256';
4+
import { toUtf8Bytes } from '@ethersproject/strings';
35
import { capture } from '@snapshot-labs/snapshot-sentry';
46
import { Address, batchContractCalls, EMPTY_ADDRESS, Handle } from '../utils';
57
import { FetchError, provider as getProvider, isEvmAddress, isSilencedError } from './utils';
@@ -25,10 +27,22 @@ function normalizeHandles(handles: Handle[]): Handle[] {
2527
return handles.filter(h => h.endsWith(TLD));
2628
}
2729

28-
// Token id is the ENS namehash of the name; '' when it can't be normalized.
30+
// Token id is the namehash of the name. Unlike ethers' namehash, the GNS
31+
// contract only lowercases ASCII and hashes raw UTF-8 bytes (no Unicode NFC),
32+
// so we mirror that exactly — otherwise composed/decomposed twins of a name
33+
// would hash to the same id and resolve to the wrong owner.
2934
function tokenId(handle: Handle): string {
3035
try {
31-
return namehash(handle);
36+
const labels = handle
37+
.replace(/\.gwei$/i, '')
38+
.split('.')
39+
.concat('gwei');
40+
let node = `0x${'00'.repeat(32)}`;
41+
for (let i = labels.length - 1; i >= 0; i--) {
42+
const label = labels[i].replace(/[A-Z]/g, c => c.toLowerCase());
43+
node = keccak256(concat([node, keccak256(toUtf8Bytes(label))]));
44+
}
45+
return node;
3246
} catch {
3347
return '';
3448
}

0 commit comments

Comments
 (0)