Audit trail

The contract diff

This is every line that differs between the audited wei-names base and the deployed NameNFT and SubdomainRegistrar.
Nothing is summarised away: the sections below cover all changed lines, the NameCard renderer is a brand-new file, not a diff, it is shown in full.
Back to the audit overview

Sponsored registration: revealFor

New function alongside reveal. It binds the commitment to an explicit owner and mints the name to that owner while the caller pays the fee and receives any refund. It is a byte-for-byte copy of reveal with two substitutions (the commitment binds owner instead of msg.sender, and the name mints to owner), and reveal itself is unchanged. This is the relayer-safe reveal Cantina recommended (findings WNS-13 and WNS-10): because the commitment binds the recipient, a sponsor or relayer flow cannot be front-run to redirect the name, and refunds go to the payer. Verified by 8 Halmos symbolic proofs, a dedicated forge suite, mutation testing, and a full anvil deployment rehearsal.

+32 −0 across 1 hunk

src/NameNFT.sol+32 −0 · line 265
278265 }
279266 }
280267
268+ /// @dev Same as reveal, except the commitment must bind `owner` and the name mints to `owner`.
269+ /// msg.sender pays the fee; excess is refunded to msg.sender.
270+ function revealFor(string calldata label, address owner, bytes32 secret)
271+ external
272+ payable
273+ nonReentrant
274+ returns (uint256 tokenId)
275+ {
276+ uint256 fee = getFee(bytes(label).length);
277+ bytes memory normalized = _validateAndNormalize(bytes(label));
278+
279+ tokenId = uint256(keccak256(abi.encodePacked(ETHER_NODE, keccak256(normalized))));
280+ uint256 premium = getPremium(tokenId);
281+ uint256 total = fee + premium;
282+
283+ if (msg.value < total) revert InsufficientFee();
284+
285+ bytes32 commitment = keccak256(abi.encode(normalized, owner, secret));
286+ uint256 committedAt = commitments[commitment];
287+
288+ if (committedAt == 0) revert CommitmentNotFound();
289+ if (block.timestamp < committedAt + MIN_COMMITMENT_AGE) revert CommitmentTooNew();
290+ if (block.timestamp > committedAt + MAX_COMMITMENT_AGE) revert CommitmentTooOld();
291+
292+ delete commitments[commitment];
293+ _register(string(normalized), 0, owner);
294+
295+ if (msg.value > total) {
296+ SafeTransferLib.safeTransferETH(msg.sender, msg.value - total);
297+ }
298+ }
299+
281300 /*//////////////////////////////////////////////////////////////
282301 SUBDOMAIN REGISTRATION
283302 //////////////////////////////////////////////////////////////*/

Ownerless, no admin, fees burned

Removed Ownable (its inheritance and the constructor's _initializeOwner) and every owner-only function: the fee and premium setters and withdraw. The mutable fee storage becomes constants and getFee goes view → pure. With no withdraw and no owner, paid ETH is locked in the contract forever, effectively burned. No privileged role remains.

+28 −61 across 6 hunks

src/NameNFT.sol+2 −2 · line 3
33
44 import {Base64} from "solady/utils/Base64.sol";
55 import {ERC721} from "solady/tokens/ERC721.sol";
6-import {Ownable} from "solady/auth/Ownable.sol";
76 import {LibString} from "solady/utils/LibString.sol";
87 import {SafeTransferLib} from "solady/utils/SafeTransferLib.sol";
8+import {NameCard} from "./NameCard.sol";
99 import {ReentrancyGuard} from "soledge/utils/ReentrancyGuard.sol";
1010
1111 /// @title NameNFT
12-/// @notice ENS-style naming system for .wei TLD with ERC721 ownership
12+/// @notice ENS-style naming system for .ether TLD with ERC721 ownership
1313 /// @dev Token ID = uint256(namehash). ENS-compatible resolution.
1414 ///
1515 /// Unicode Support:
src/NameNFT.sol+1 −1 · line 18
1818 /// - For proper Unicode normalization, callers SHOULD pre-normalize using ENSIP-15
1919 /// - Off-chain: use adraffy/ens-normalize library or equivalent before calling
2020 /// - Example: normalize("RaFFY🚴‍♂️") => "raffy🚴‍♂" (do this off-chain, then call contract)
21-contract NameNFT is ERC721, Ownable, ReentrancyGuard {
21+contract NameNFT is ERC721, ReentrancyGuard {
2222 using LibString for uint256;
2323
2424 /*//////////////////////////////////////////////////////////////
src/NameNFT.sol+7 −3 · line 74
8174 uint256 constant GRACE_PERIOD = 90 days;
8275 uint256 constant MAX_SUBDOMAIN_DEPTH = 10;
8376 uint256 constant COIN_TYPE_ETH = 60;
84- uint256 constant MAX_PREMIUM_CAP = 10000 ether;
85- uint256 constant MAX_DECAY_PERIOD = 3650 days;
86- uint256 constant DEFAULT_FEE = 0.001 ether;
77+ uint256 constant FEE_LEN1 = 0.05 ether; // 1-byte labels
78+ uint256 constant FEE_LEN2 = 0.05 ether; // 2-byte labels
79+ uint256 constant FEE_LEN3 = 0.005 ether; // 3-byte labels
80+ uint256 constant FEE_LEN4 = 0.005 ether; // 4-byte labels
81+ uint256 constant DEFAULT_FEE = 0.0005 ether; // 5+ byte labels
82+ uint256 constant MAX_PREMIUM = 100 ether;
83+ uint256 constant PREMIUM_DECAY_PERIOD = 21 days;
8784
8885 /*//////////////////////////////////////////////////////////////
8986 STORAGE
src/NameNFT.sol+0 −6 · line 94
9794 uint64 parentEpoch;
9895 }
9996
100- uint256 public defaultFee;
101- uint256 public maxPremium;
102- uint256 public premiumDecayPeriod;
103-
104- mapping(uint256 => uint256) public lengthFees;
105- mapping(uint256 => bool) public lengthFeeSet;
10697 mapping(uint256 => NameRecord) public records;
10798 mapping(uint256 => uint256) public recordVersion;
10899 mapping(bytes32 => uint256) public commitments;
src/NameNFT.sol+10 −8 · line 105
114105 mapping(uint256 => mapping(uint256 => mapping(uint256 => bytes))) internal _coinAddr;
115106 mapping(uint256 => mapping(uint256 => mapping(string => string))) internal _text;
116107
117- /*//////////////////////////////////////////////////////////////
118- CONSTRUCTOR
119- //////////////////////////////////////////////////////////////*/
108+ /// @dev Stateless certificate renderer, pinned forever at deployment
109+ NameCard public immutable card;
120110
121- constructor() payable {
122- _initializeOwner(tx.origin);
123- defaultFee = DEFAULT_FEE;
124- maxPremium = 100 ether;
125- premiumDecayPeriod = 21 days;
111+ constructor(NameCard nameCard) {
112+ if (address(nameCard) == address(0)) revert InvalidRenderer();
113+ // probe the renderer: a wrong address (an EOA, a wrong contract) must
114+ // fail the deployment here, not every tokenURI call forever
115+ if (bytes(nameCard.render("a.ether", bytes32(0), NameCard.Status.Active)).length < 100) {
116+ revert InvalidRenderer();
117+ }
118+ card = nameCard;
126119 }
127120
128121 /*//////////////////////////////////////////////////////////////
src/NameNFT.sol+8 −41 · line 648
608648 FEE MANAGEMENT
609649 //////////////////////////////////////////////////////////////*/
610650
611- function getFee(uint256 length) public view returns (uint256) {
612- return lengthFeeSet[length] ? lengthFees[length] : defaultFee;
651+ function getFee(uint256 length) public pure returns (uint256) {
652+ if (length == 1) return FEE_LEN1;
653+ if (length == 2) return FEE_LEN2;
654+ if (length == 3) return FEE_LEN3;
655+ if (length == 4) return FEE_LEN4;
656+ return DEFAULT_FEE;
613657 }
614658
615659 function getPremium(uint256 tokenId) public view returns (uint256) {
616660 NameRecord storage record = records[tokenId];
617661 if (bytes(record.label).length == 0 || record.parent != 0) return 0;
618- if (maxPremium == 0 || premiumDecayPeriod == 0) return 0;
619662
620663 uint256 gracePeriodEnd = record.expiresAt + GRACE_PERIOD;
621664 if (block.timestamp <= gracePeriodEnd) return 0;
622665
623666 uint256 elapsed = block.timestamp - gracePeriodEnd;
624- if (elapsed >= premiumDecayPeriod) return 0;
667+ if (elapsed >= PREMIUM_DECAY_PERIOD) return 0;
625668
626- return maxPremium * (premiumDecayPeriod - elapsed) / premiumDecayPeriod;
627- }
628-
629- /*//////////////////////////////////////////////////////////////
630- ADMIN FUNCTIONS
631- //////////////////////////////////////////////////////////////*/
632-
633- function setDefaultFee(uint256 fee) public onlyOwner {
634- defaultFee = fee;
635- emit DefaultFeeChanged(fee);
636- }
637-
638- function setLengthFees(uint256[] calldata lengths, uint256[] calldata fees) public onlyOwner {
639- if (lengths.length != fees.length) revert LengthMismatch();
640- for (uint256 i; i < lengths.length; ++i) {
641- lengthFees[lengths[i]] = fees[i];
642- lengthFeeSet[lengths[i]] = true;
643- emit LengthFeeChanged(lengths[i], fees[i]);
644- }
645- }
646-
647- function clearLengthFee(uint256 length) public onlyOwner {
648- delete lengthFees[length];
649- delete lengthFeeSet[length];
650- emit LengthFeeCleared(length);
651- }
652-
653- function setPremiumSettings(uint256 _maxPremium, uint256 _decayPeriod) public onlyOwner {
654- if (_maxPremium > MAX_PREMIUM_CAP) revert PremiumTooHigh();
655- if (_decayPeriod > MAX_DECAY_PERIOD) revert DecayPeriodTooLong();
656- maxPremium = _maxPremium;
657- premiumDecayPeriod = _decayPeriod;
658- emit PremiumSettingsChanged(_maxPremium, _decayPeriod);
659- }
660-
661- function withdraw() public onlyOwner nonReentrant {
662- SafeTransferLib.safeTransferAllETH(msg.sender);
669+ return MAX_PREMIUM * (PREMIUM_DECAY_PERIOD - elapsed) / PREMIUM_DECAY_PERIOD;
663670 }
664671
665672 /*//////////////////////////////////////////////////////////////

On-chain certificate renderer

tokenURI renders a real certificate SVG (name, hash, status chip) via the NameCard contract in every state, instead of the old plain placeholder, and the JSON carries a Status attribute. The constructor now takes the renderer, probes it, and pins its address as immutable, so a wrong renderer fails at deploy time rather than on every call forever. NameCard is a separate, stateless contract shown in full below.

+44 −151 across 7 hunks

src/NameNFT.sol+2 −3 · line 29
2929 error TooDeep();
3030 error EmptyLabel();
3131 error InvalidName();
32+ error Unauthorized();
3233 error InvalidLength();
33- error LengthMismatch();
3434 error NotParentOwner();
35- error PremiumTooHigh();
3635 error InsufficientFee();
3736 error AlreadyCommitted();
3837 error CommitmentTooNew();
3938 error CommitmentTooOld();
4039 error AlreadyRegistered();
4140 error CommitmentNotFound();
42- error DecayPeriodTooLong();
41+ error InvalidRenderer();
4342
4443 /*//////////////////////////////////////////////////////////////
4544 EVENTS
src/NameNFT.sol+20 −46 · line 147
154147 if (!_recordExists(tokenId)) revert TokenDoesNotExist();
155148
156149 NameRecord storage record = records[tokenId];
157-
158- // Check for stale subdomain FIRST (parent epoch mismatch)
159- if (record.parent != 0) {
160- NameRecord storage parentRecord = records[record.parent];
161- if (record.parentEpoch != parentRecord.epoch) {
162- return string.concat(
163- "data:application/json;base64,",
164- Base64.encode(
165- bytes(
166- '{"name":"[Invalid]","description":"This subdomain is no longer valid.","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgNDAwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0iIzk5OSIvPjx0ZXh0IHg9IjIwMCIgeT0iMjAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZiI+W0ludmFsaWRdPC90ZXh0Pjwvc3ZnPg=="}'
167- )
168- )
169- );
170- }
171- }
172-
173- // Check for expired (top-level or parent chain expired)
174- if (!_isActive(tokenId)) {
175- return string.concat(
176- "data:application/json;base64,",
177- Base64.encode(
178- bytes(
179- '{"name":"[Expired]","description":"This name has expired.","image":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MDAgNDAwIj48cmVjdCB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0iIzk5OSIvPjx0ZXh0IHg9IjIwMCIgeT0iMjAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIyNCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iI2ZmZiI+W0V4cGlyZWRdPC90ZXh0Pjwvc3ZnPg=="}'
180- )
150+ NameCard.Status status = _cardStatus(tokenId);
151+
152+ // a stale chain cannot be rebuilt, so fall back to the token's own label
153+ string memory built = _buildFullName(tokenId);
154+ string memory fullName =
155+ string.concat(bytes(built).length == 0 ? record.label : built, ".ether");
156+
157+ string[5] memory statusNames = ["Active", "Subdomain", "Grace", "Expired", "Invalid"];
158+ string memory attributes = string.concat(
159+ ',"attributes":[{"trait_type":"Status","value":"',
160+ statusNames[uint256(status)],
161+ '"}',
162+ record.parent == 0
163+ ? string.concat(
164+ ',{"trait_type":"Expires","display_type":"date","value":',
165+ uint256(record.expiresAt).toString(),
166+ "}"
181167 )
182- );
183- }
184-
185- string memory fullName = _buildFullName(tokenId);
186- fullName = string.concat(fullName, ".wei");
187- string memory displayName = bytes(fullName).length <= 20
188- ? fullName
189- : string.concat(_truncateUTF8(fullName, 17), "...");
190-
191- // Build attributes with expiry info for marketplace compatibility
192- string memory attributes;
193- if (record.parent == 0) {
194- // Top-level name: show expiry
195- attributes = string.concat(
196- ',"attributes":[{"trait_type":"Expires","display_type":"date","value":',
197- uint256(record.expiresAt).toString(),
198- "}]"
199- );
200- } else {
201- // Subdomain: no direct expiry
202- attributes = ',"attributes":[{"trait_type":"Type","value":"Subdomain"}]';
203- }
168+ : "",
169+ "]"
170+ );
204171
205172 string memory escapedName = _escapeJSON(fullName);
206173
src/NameNFT.sol+2 −2 · line 178
211178 string.concat(
212179 '{"name":"',
213180 escapedName,
214- '","description":"Wei Name Service: ',
181+ '","description":"EtherNames: ',
215182 escapedName,
216183 '","image":"data:image/svg+xml;base64,',
217- Base64.encode(bytes(_generateSVG(displayName))),
184+ Base64.encode(bytes(card.render(fullName, bytes32(tokenId), status))),
218185 '"',
219186 attributes,
220187 "}"
src/NameNFT.sol+20 −0 · line 191
224191 );
225192 }
226193
194+ /// @dev Card status for tokenURI. isExpired and inGracePeriod use the
195+ /// expiresAt sentinel and lie for subdomains, so a subdomain walks
196+ /// its parent chain: an epoch mismatch anywhere is Invalid forever,
197+ /// otherwise the top ancestor's clock decides.
198+ function _cardStatus(uint256 tokenId) internal view returns (NameCard.Status) {
199+ uint256 cur = tokenId;
200+ bool sub = records[cur].parent != 0;
201+ for (uint256 depth = 0; depth <= MAX_SUBDOMAIN_DEPTH; ++depth) {
202+ NameRecord storage r = records[cur];
203+ if (r.parent == 0) {
204+ if (block.timestamp > r.expiresAt + GRACE_PERIOD) return NameCard.Status.Expired;
205+ if (block.timestamp > r.expiresAt) return NameCard.Status.Grace;
206+ return sub ? NameCard.Status.Subdomain : NameCard.Status.Active;
207+ }
208+ if (r.parentEpoch != records[r.parent].epoch) return NameCard.Status.Invalid;
209+ cur = r.parent;
210+ }
211+ return NameCard.Status.Invalid;
212+ }
213+
227214 /*//////////////////////////////////////////////////////////////
228215 COMMIT-REVEAL
229216 //////////////////////////////////////////////////////////////*/
src/NameNFT.sol+0 −28 · line 816
809816 }
810817 }
811818
812- /// @dev Truncate string to maxBytes, ensuring we don't cut in the middle of a UTF-8 character
813- function _truncateUTF8(string memory str, uint256 maxBytes)
814- internal
815- pure
816- returns (string memory)
817- {
818- bytes memory b = bytes(str);
819- if (b.length <= maxBytes) return str;
820-
821- // Find safe cut point - step back over UTF-8 continuation bytes (0x80-0xBF)
822- // This ensures we don't cut in the middle of a multi-byte character
823- uint256 cutPoint = maxBytes;
824- while (cutPoint > 0 && uint8(b[cutPoint]) >= 0x80 && uint8(b[cutPoint]) <= 0xBF) {
825- unchecked {
826- --cutPoint;
827- }
828- }
829- // cutPoint is now at either:
830- // - An ASCII byte (will be included as it's a complete character)
831- // - A multi-byte start byte (won't be included since we copy 0..cutPoint-1)
832-
833- bytes memory result = new bytes(cutPoint);
834- for (uint256 i; i < cutPoint; ++i) {
835- result[i] = b[i];
836- }
837- return string(result);
838- }
839-
840819 function _recordExists(uint256 tokenId) internal view returns (bool) {
841820 return bytes(records[tokenId].label).length > 0;
842821 }
src/NameNFT.sol+0 −8 · line 870
891870 return string.concat(record.label, ".", parentName);
892871 }
893872
894- function _generateSVG(string memory displayName) internal pure returns (string memory) {
895- return string.concat(
896- '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400"><rect width="400" height="400" fill="#fff"/><text x="200" y="200" font-family="sans-serif" font-size="24" text-anchor="middle" dominant-baseline="middle">',
897- _escapeXML(displayName),
898- "</text></svg>"
899- );
900- }
901-
902873 /// @dev Escape JSON special characters for safe metadata embedding
903874 function _escapeJSON(string memory input) internal pure returns (string memory) {
904875 bytes memory b = bytes(input);
src/NameNFT.sol+0 −64 · line 920
949920
950921 return string(result);
951922 }
952-
953- /// @dev Escape XML special characters for safe SVG embedding
954- function _escapeXML(string memory input) internal pure returns (string memory) {
955- bytes memory b = bytes(input);
956-
957- // Count how much extra space we need
958- uint256 extraLen;
959- unchecked {
960- for (uint256 i; i < b.length; ++i) {
961- bytes1 c = b[i];
962- if (c == 0x26) extraLen += 4;
963- else if (c == 0x3c) extraLen += 3;
964- else if (c == 0x3e) extraLen += 3;
965- else if (c == 0x22) extraLen += 5;
966- else if (c == 0x27) extraLen += 5;
967- }
968- }
969-
970- if (extraLen == 0) return input;
971-
972- bytes memory result = new bytes(b.length + extraLen);
973- uint256 j;
974-
975- unchecked {
976- for (uint256 i; i < b.length; ++i) {
977- bytes1 c = b[i];
978- if (c == 0x26) {
979- result[j++] = "&";
980- result[j++] = "a";
981- result[j++] = "m";
982- result[j++] = "p";
983- result[j++] = ";";
984- } else if (c == 0x3c) {
985- result[j++] = "&";
986- result[j++] = "l";
987- result[j++] = "t";
988- result[j++] = ";";
989- } else if (c == 0x3e) {
990- result[j++] = "&";
991- result[j++] = "g";
992- result[j++] = "t";
993- result[j++] = ";";
994- } else if (c == 0x22) {
995- result[j++] = "&";
996- result[j++] = "q";
997- result[j++] = "u";
998- result[j++] = "o";
999- result[j++] = "t";
1000- result[j++] = ";";
1001- } else if (c == 0x27) {
1002- result[j++] = "&";
1003- result[j++] = "a";
1004- result[j++] = "p";
1005- result[j++] = "o";
1006- result[j++] = "s";
1007- result[j++] = ";";
1008- } else {
1009- result[j++] = c;
1010- }
1011- }
1012- }
1013-
1014- return string(result);
1015- }
1016923 }

New file: src/NameCard.sol

The certificate renderer is entirely new code, not derived from the audited base. It is 369 lines of pure, stateless, string-building Solidity with no storage, no owner, and nothing to administer. It only ever returns an SVG string; the EVM's STATICCALL semantics make it incapable of mutating state or moving funds. Its output is pinned byte-for-byte to a differential fixture suite and re-proven against the site renderer. NameCard on Etherscan

One canonical ETH address

setAddr and setAddrForCoin(id, 60, …) now write the legacy and the ENSIP-9 multicoin ETH record together, so the two getters can never disagree. Verified by a Halmos symbolic proof that the two records stay in lockstep for every input.

+21 −2 across 2 hunks

src/NameNFT.sol+7 −1 · line 366
347366 function setAddr(uint256 tokenId, address addr) public {
348367 if (!_isActive(tokenId)) revert Expired();
349368 if (ownerOf(tokenId) != msg.sender) revert Unauthorized();
350- _resolvedAddress[tokenId][recordVersion[tokenId]] = addr;
369+ uint256 v = recordVersion[tokenId];
370+ _resolvedAddress[tokenId][v] = addr;
371+ // keep the ENSIP-9 coin-60 record in lockstep so addr(node) and addr(node,60)
372+ // can never disagree; zero address canonicalizes to empty bytes in both slots
373+ bytes memory enc = addr == address(0) ? bytes("") : abi.encodePacked(addr);
374+ _coinAddr[tokenId][v][COIN_TYPE_ETH] = enc;
375+ emit AddressChanged(bytes32(tokenId), COIN_TYPE_ETH, enc);
351376 emit AddrChanged(bytes32(tokenId), addr);
352377 }
353378
src/NameNFT.sol+14 −1 · line 417
392417 function setAddrForCoin(uint256 tokenId, uint256 coinType, bytes calldata addr) public {
393418 if (!_isActive(tokenId)) revert Expired();
394419 if (ownerOf(tokenId) != msg.sender) revert Unauthorized();
395- _coinAddr[tokenId][recordVersion[tokenId]][coinType] = addr;
420+ uint256 v = recordVersion[tokenId];
421+ if (coinType == COIN_TYPE_ETH) {
422+ // ETH addresses must be 20 bytes (or empty to clear); mirror into the
423+ // legacy slot so both getters always denote the same address
424+ if (addr.length != 20 && addr.length != 0) revert InvalidLength();
425+ address dec = addr.length == 20 ? address(bytes20(addr)) : address(0);
426+ bytes memory enc = dec == address(0) ? bytes("") : bytes(addr);
427+ _resolvedAddress[tokenId][v] = dec;
428+ _coinAddr[tokenId][v][COIN_TYPE_ETH] = enc;
429+ emit AddressChanged(bytes32(tokenId), COIN_TYPE_ETH, enc);
430+ emit AddrChanged(bytes32(tokenId), dec);
431+ return;
432+ }
433+ _coinAddr[tokenId][v][coinType] = addr;
396434 emit AddressChanged(bytes32(tokenId), coinType, addr);
397435 }
398436

Renamed TLD: .wei.ether

TLD namehash WEI_NODE → ETHER_NODE (= namehash('ether')), name()/symbol()"EtherNames"/"ETHERNAME", and the suffix strings and suffix-stripping (4-byte .wei → 6-byte .ether).

+20 −24 across 8 hunks

src/NameNFT.sol+3 −9 · line 58
5958 event AddressChanged(bytes32 indexed node, uint256 coinType, bytes addr);
6059 event TextChanged(bytes32 indexed node, string indexed key, string value);
6160
62- // Admin events
63- event DefaultFeeChanged(uint256 fee);
64- event LengthFeeChanged(uint256 indexed length, uint256 fee);
65- event LengthFeeCleared(uint256 indexed length);
66- event PremiumSettingsChanged(uint256 maxPremium, uint256 decayPeriod);
67-
6861 /*//////////////////////////////////////////////////////////////
6962 CONSTANTS
7063 //////////////////////////////////////////////////////////////*/
7164
72- /// @dev Namehash of "wei" TLD - kept public for off-chain tooling
73- bytes32 public constant WEI_NODE =
74- 0xa82820059d5df798546bcc2985157a77c3eef25eba9ba01899927333efacbd6f;
65+ /// @dev Namehash of "ether" TLD - kept public for off-chain tooling
66+ bytes32 public constant ETHER_NODE =
67+ 0xb7f8c26395211ac249dcf196aa8bf23249a87186996f42c47bc4d55dfe608eee;
7568
7669 uint256 constant MAX_LABEL_LENGTH = 255;
7770 uint256 constant MIN_LABEL_LENGTH = 1;
src/NameNFT.sol+2 −2 · line 123
130123 //////////////////////////////////////////////////////////////*/
131124
132125 function name() public pure override(ERC721) returns (string memory) {
133- return "Wei Name Service";
126+ return "EtherNames";
134127 }
135128
136129 function symbol() public pure override(ERC721) returns (string memory) {
137- return "WEI";
130+ return "ETHERNAME";
138131 }
139132
140133 /// @dev Blocks transfers of inactive tokens, but allows mint (from==0) and burn (to==0)
src/NameNFT.sol+1 −1 · line 244
257244 uint256 fee = getFee(bytes(label).length);
258245 bytes memory normalized = _validateAndNormalize(bytes(label));
259246
260- tokenId = uint256(keccak256(abi.encodePacked(WEI_NODE, keccak256(normalized))));
247+ tokenId = uint256(keccak256(abi.encodePacked(ETHER_NODE, keccak256(normalized))));
261248 uint256 premium = getPremium(tokenId);
262249 uint256 total = fee + premium;
263250
src/NameNFT.sol+1 −1 · line 395
370395 function reverseResolve(address addr) public view returns (string memory) {
371396 uint256 tokenId = primaryName[addr];
372397 if (tokenId == 0 || !_isActive(tokenId) || resolve(tokenId) != addr) return "";
373- return string.concat(_buildFullName(tokenId), ".wei");
398+ return string.concat(_buildFullName(tokenId), ".ether");
374399 }
375400
376401 /*//////////////////////////////////////////////////////////////
src/NameNFT.sol+10 −8 · line 501
463501 return uint256(computeNamehash(fullName));
464502 }
465503
466- /// @notice Compute namehash for a full name (e.g. "sub.name.wei" or "name")
504+ /// @notice Compute namehash for a full name (e.g. "sub.name.ether" or "name")
467505 /// @dev This function is intentionally permissive - it lowercases and hashes any input.
468506 /// Registration enforces validation: valid UTF-8, no space/control chars/dot.
469507 /// Use normalize() to check if a label is valid for registration.
470508 function computeNamehash(string calldata fullName) public pure returns (bytes32 node) {
471509 bytes memory b = bytes(fullName);
472- if (b.length == 0) return WEI_NODE;
510+ if (b.length == 0) return ETHER_NODE;
473511
474512 uint256 len = b.length;
475513
476- // Strip .wei suffix if present
514+ // Strip .ether suffix if present
477515 if (
478- len >= 4 && b[len - 4] == 0x2e && (b[len - 3] == 0x77 || b[len - 3] == 0x57)
516+ len >= 6 && b[len - 6] == 0x2e && (b[len - 5] == 0x65 || b[len - 5] == 0x45)
517+ && (b[len - 4] == 0x74 || b[len - 4] == 0x54)
518+ && (b[len - 3] == 0x68 || b[len - 3] == 0x48)
479519 && (b[len - 2] == 0x65 || b[len - 2] == 0x45)
480- && (b[len - 1] == 0x69 || b[len - 1] == 0x49)
520+ && (b[len - 1] == 0x72 || b[len - 1] == 0x52)
481521 ) {
482- len -= 4;
522+ len -= 6;
483523 }
484524
485- if (len == 0) return WEI_NODE;
525+ if (len == 0) return ETHER_NODE;
486526 if (b[0] == 0x2e || b[len - 1] == 0x2e) revert EmptyLabel();
487527
488- node = WEI_NODE;
528+ node = ETHER_NODE;
489529 uint256 labelEnd = len;
490530
491531 for (uint256 i = len; i > 0; --i) {
src/NameNFT.sol+1 −1 · line 606
566606 // Hyphen rules
567607 if (normalized[0] == 0x2d || normalized[b.length - 1] == 0x2d) return false;
568608
569- bytes32 parentNode = parentId == 0 ? WEI_NODE : bytes32(parentId);
609+ bytes32 parentNode = parentId == 0 ? ETHER_NODE : bytes32(parentId);
570610 uint256 tokenId = uint256(keccak256(abi.encodePacked(parentNode, keccak256(normalized))));
571611
572612 if (parentId != 0 && !_isActive(parentId)) return false;
src/NameNFT.sol+1 −1 · line 625
585625 function getFullName(uint256 tokenId) public view returns (string memory) {
586626 string memory baseName = _buildFullName(tokenId);
587627 if (bytes(baseName).length == 0) return "";
588- return string.concat(baseName, ".wei");
628+ return string.concat(baseName, ".ether");
589629 }
590630
591631 /// @notice On-chain normalization (lowercases ASCII only)
src/NameNFT.sol+1 −1 · line 678
671678 returns (uint256 tokenId)
672679 {
673680 bytes memory normalized = _validateAndNormalize(bytes(label));
674- bytes32 parentNode = parentId == 0 ? WEI_NODE : bytes32(parentId);
681+ bytes32 parentNode = parentId == 0 ? ETHER_NODE : bytes32(parentId);
675682 tokenId = uint256(keccak256(abi.encodePacked(parentNode, keccak256(normalized))));
676683
677684 // Invariant: subdomain registration requires parent ownership

SubdomainRegistrar: immutable name, non-payable constructor

The hardcoded mainnet name address becomes immutable, injected via the constructor; the constructor also drops payable so a deploy that accidentally carries ETH reverts.

+4 −2 across 2 hunks

src/SubdomainRegistrar.sol+1 −1 · line 92
9292 address payout; // receives ERC20 directly; ETH via ethBalance
9393 }
9494
95- INameNFT public constant name = INameNFT(0x0000000000696760E15f265e828DB644A0c242EB);
95+ INameNFT public immutable name;
9696
9797 mapping(uint256 => Config) public config;
9898 mapping(uint256 => address) public escrowedController; // nonzero => escrowed, controller recorded
src/SubdomainRegistrar.sol+3 −1 · line 101
101101
102102 uint256 constant _REENTRANCY_GUARD_SLOT = 0x929eee149b4bd21268;
103103
104- constructor() payable {}
104+ constructor(INameNFT nameNFT) {
105+ name = nameNFT;
106+ }
105107
106108 modifier nonReentrant() virtual {
107109 assembly ("memory-safe") {