Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/SemverResolver.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// Using IERC165 from forge-std to avoid OpenZeppelin version conflicts
import {IERC165} from "forge-std/interfaces/IERC165.sol";
import {ENS} from "ens-contracts/registry/ENS.sol";
import {IExtendedResolver} from "ens-contracts/resolvers/profiles/IExtendedResolver.sol";
import {IContentHashResolver} from "ens-contracts/resolvers/profiles/IContentHashResolver.sol";
import {ITextResolver} from "ens-contracts/resolvers/profiles/ITextResolver.sol";
import {INameWrapper} from "ens-contracts/wrapper/INameWrapper.sol";
import {NameCoder} from "ens-contracts/utils/NameCoder.sol";
import {BytesUtils} from "ens-contracts/utils/BytesUtils.sol";
import {VersionRegistry} from "./VersionRegistry.sol";
/// @title SemverResolver
/// @notice ENS resolver with semantic versioning support and wildcard resolution
/// @dev Implements IExtendedResolver for wildcard queries (e.g., "1-2.myapp.eth" resolves to highest 1.2.x version)
/// @dev Supports contenthash and text("version") resolution for versioned content
contract SemverResolver is VersionRegistry, IExtendedResolver, IContentHashResolver, ITextResolver, IERC165 {
// ABI encoding constants
uint256 private constant SELECTOR_SIZE = 4;
// DNS encoding constants
uint256 private constant DNS_LABEL_LENGTH_OFFSET = 0; // Position of length byte in DNS label
uint256 private constant DNS_LABEL_DATA_OFFSET = 1; // Position where label data starts
// Array indexing constants
uint256 private constant FIRST_ELEMENT_INDEX = 0;
// Precomputed hash for "version" key to save gas
bytes32 private constant VERSION_KEY_HASH = keccak256("version");
// IPFS CIDv1 dag-pb contenthash with multihash prefix for ENS (EIP-1577)
// Format: <protocol><cid-version><multicodec><hash-function><hash-length>
// 0xe3 = IPFS protocol, 0x01 = CIDv1, 0x01 = raw, 0x70 = dag-pb, 0x12 = sha2-256, 0x20 = 32 bytes
bytes6 private constant IPFS_CONTENTHASH_PREFIX = hex"e30101701220";
ENS public immutable ENS_REGISTRY;
INameWrapper public immutable NAME_WRAPPER;
// Standard ENS errors (same signatures as defined in ENS ecosystem)
// These match the errors defined in:
// - Unauthorised: ens-contracts/wrapper/NameWrapper.sol:19
// - UnsupportedResolverProfile: ens-contracts/universalResolver/IUniversalResolver.sol:17
error Unauthorised(bytes32 node, address addr);
error UnsupportedResolverProfile(bytes4 selector);
/// @dev Gets the actual owner of an ENS name, handling wrapped names
/// @param node The namehash of the ENS name
/// @return The actual owner address (unwrapped if necessary)
function _getActualOwner(bytes32 node) internal view returns (address) {
address owner = ENS_REGISTRY.owner(node);
// If the owner is the NameWrapper contract, get the actual owner from the wrapper
if (owner == address(NAME_WRAPPER)) {
try NAME_WRAPPER.ownerOf(uint256(node)) returns (address actualOwner) {
return actualOwner;
} catch {
// If the call fails, fall back to the registry owner
return owner;
}
}
return owner;
}
/// @dev Checks if the caller is authorized for the given node
/// @param node The namehash of the ENS name
/// @param caller The address to check authorization for
/// @return True if authorized, false otherwise
function _isAuthorised(bytes32 node, address caller) internal view returns (bool) {
address actualOwner = _getActualOwner(node);
// Check if caller is the owner or approved by the owner
return caller == actualOwner || ENS_REGISTRY.isApprovedForAll(actualOwner, caller);
}
/// @dev Restricts access to ENS name owner or approved operators
/// @dev Now properly handles wrapped ENS names via NameWrapper contract
modifier authorised(bytes32 node) {
if (!_isAuthorised(node, msg.sender)) {
revert Unauthorised(node, msg.sender);
}
_;
}
/// @notice Creates a new SemverResolver that enables version-aware ENS resolution
/// @param _ens The ENS registry contract address
/// @param _nameWrapper The NameWrapper contract address
constructor(ENS _ens, INameWrapper _nameWrapper) {
ENS_REGISTRY = _ens;
NAME_WRAPPER = _nameWrapper;
}
/// @dev Encodes a raw IPFS hash for ENS contenthash (EIP-1577 compliance)
/// @param rawHash Raw 32-byte IPFS hash (sha256 digest only, not full CID)
/// @return Properly encoded contenthash with IPFS CIDv1 dag-pb multihash prefix
/// @dev Encoding format: 0xe3 (IPFS) + 0x01 (CIDv1) + 0x70 (dag-pb) + 0x12 (sha2-256) + 0x20 (32 bytes)
/// @dev Null safety: Returns empty bytes for zero hash (indicates no content)
/// @dev Examples:
/// - _encodeIpfsContenthash(0x0) → "" (empty)
/// - _encodeIpfsContenthash(sha256("content")) → 0xe30101701220{32-byte-hash}
function _encodeIpfsContenthash(bytes32 rawHash) internal pure returns (bytes memory) {
if (rawHash == bytes32(0)) {
return "";
}
// Encode IPFS hash with proper multihash prefix for ENS contenthash (EIP-1577)
return abi.encodePacked(IPFS_CONTENTHASH_PREFIX, rawHash);
}
/// @notice Checks if this resolver supports a specific interface like contenthash or text resolution
/// @param interfaceId The interface identifier to check (ERC-165)
/// @return True if the interface is supported, false otherwise
/// @dev Supports IExtendedResolver, IContentHashResolver, ITextResolver, and ERC165
function supportsInterface(bytes4 interfaceId) public pure override returns (bool) {
return interfaceId == type(IExtendedResolver).interfaceId
|| interfaceId == type(IContentHashResolver).interfaceId || interfaceId == 0xbc1c58d1 // ENSIP-7 contenthash
|| interfaceId == type(ITextResolver).interfaceId || interfaceId == 0x01ffc9a7; // ERC165
}
/// @notice Resolves version-aware ENS queries like "1-2.myapp.eth" to find the highest matching 1.2.x version
/// @param name DNS-encoded name (e.g., "\x031-2\x06myapp\x03eth\x00" for "1-2.myapp.eth")
/// @param data ABI-encoded function call (selector + arguments)
/// @return ABI-encoded return value from the resolved function
/// @dev Supports two resolution profiles:
/// 1. IContentHashResolver.contenthash → returns IPFS content hash for version
/// 2. ITextResolver.text → returns version string for key="version"
/// @dev Resolution strategy:
/// - First attempts direct resolution (exact name match)
/// - Falls back to wildcard resolution if no direct match found
/// @dev Complexity: O(log n) where n is number of versions for the base name
/// @dev Examples:
/// - resolve("1-2.myapp.eth", contenthash.selector) → content hash for highest 1.2.x version
/// - resolve("1.myapp.eth", text.selector + "version") → version string for highest 1.x.x
function resolve(bytes memory name, bytes memory data) external view override returns (bytes memory) {
require(data.length >= SELECTOR_SIZE, "Invalid data length");
bytes4 selector = bytes4(data);
if (selector == IContentHashResolver.contenthash.selector) {
bytes32 node = NameCoder.namehash(name, 0);
bytes memory hash = this.contenthash(node);
// If no direct match, try wildcard resolution
if (hash.length == 0) {
hash = _resolveWildcardContenthash(name, data);
}
return abi.encode(hash);
}
if (selector == ITextResolver.text.selector) {
// Strip the selector to get the arguments
assert(data.length >= SELECTOR_SIZE); // SMTChecker: ensure valid data length
(, string memory key) =
abi.decode(BytesUtils.substring(data, SELECTOR_SIZE, data.length - SELECTOR_SIZE), (bytes32, string));
string memory value = _resolveWildcardText(name, key);
return abi.encode(value);
}
revert UnsupportedResolverProfile(selector);
}
/// @notice Gets the IPFS content hash for the latest version of an ENS name
/// @param node The ENS namehash to query
/// @return The content hash of the latest version as bytes, or empty if no versions exist
/// @dev Implements IContentHashResolver interface for direct (non-wildcard) queries
function contenthash(bytes32 node) external view override returns (bytes memory) {
bytes32 hash = getLatestContentHash(node);
return _encodeIpfsContenthash(hash);
}
/// @notice Gets text data for an ENS name, currently only supports the "version" key
/// @param node The ENS namehash to query
/// @param key The text record key (only "version" is supported)
/// @return The text value for the key, or empty string if not found or unsupported
/// @dev Only supports key "version" which returns the latest version as a string (e.g., "1.2.3")
/// @dev All other keys return empty string as manual text records are not supported
function text(bytes32 node, string calldata key) external view override returns (string memory) {
// Special handling for "version" key - return latest version as string
if (keccak256(bytes(key)) == VERSION_KEY_HASH) {
Version memory latestVersion = getLatestVersion(node);
// If no versions exist, return empty string
if (latestVersion.major == 0 && latestVersion.minor == 0 && latestVersion.patch == 0) {
return "";
}
return _versionToString(latestVersion);
}
// For all other keys, return empty string (no manual text setting allowed)
return "";
}
/// @dev Core wildcard version resolution logic
/// @param name DNS-encoded name where first label is version (e.g., "\x031-2\x06myapp\x03eth\x00")
/// @return Version record with the highest matching version, or zero version if not found
/// @notice Supports three query types:
/// - Major-only: "1" → finds highest 1.x.x version
/// - Major.minor: "1-2" → finds highest 1.2.x version
/// - Exact: "1-2-3" → finds exact 1.2.3 version
/// @notice Uses hyphen separators instead of dots to avoid DNS label conflicts
/// @notice Returns zero version (0.0.0) if no matching version exists
/// @dev Complexity: Refactored into smaller functions for better readability
function _resolveWildcardVersion(bytes memory name) internal view returns (VersionRecord memory) {
// Parse the DNS-encoded name to extract version and base components
(string memory versionLabel, bytes32 baseNode) = _extractVersionAndBaseName(name);
// Parse the version label to determine query parameters
ParsedVersion memory parsedVersion = _parseVersionFromLabel(versionLabel);
// Execute the appropriate version query based on parsed components
return _executeVersionQuery(baseNode, parsedVersion);
}
/// @dev Extracts version label and base name from DNS-encoded name
/// @param name DNS-encoded name (e.g., "\x031-2\x06myapp\x03eth\x00")
/// @return versionLabel The version string from first label (e.g., "1-2")
/// @return baseNode The namehash of the base name (e.g., namehash("myapp.eth"))
/// @dev Example: "\x031-2\x06myapp\x03eth\x00" → ("1-2", namehash("myapp.eth"))
function _extractVersionAndBaseName(bytes memory name)
private
pure
returns (string memory versionLabel, bytes32 baseNode)
{
// Extract the first label length from DNS encoding
// Note: DNS name validation is handled upstream by NameCoder.namehash()
uint256 labelLength = uint256(uint8(name[DNS_LABEL_LENGTH_OFFSET]));
// Extract version label (first label after length byte)
bytes memory versionBytes = BytesUtils.substring(name, DNS_LABEL_DATA_OFFSET, labelLength);
versionLabel = string(versionBytes);
// Extract base name (remainder after version label)
bytes memory baseName = BytesUtils.substring(
name, labelLength + DNS_LABEL_DATA_OFFSET, name.length - labelLength - DNS_LABEL_DATA_OFFSET
);
baseNode = NameCoder.namehash(baseName, FIRST_ELEMENT_INDEX);
return (versionLabel, baseNode);
}
/// @dev Executes the appropriate version query based on parsed version components
/// @param baseNode The namehash of the base ENS name
/// @param parsedVersion The parsed version with component flags
/// @return The matching version record or zero version if not found
/// @dev Query types:
/// - Major only: hasMinor=false → getHighestVersionForMajor()
/// - Major.minor: hasMinor=true, hasPatch=false → getHighestVersionForMajorMinor()
/// - Exact: hasPatch=true → getExactVersion()
function _executeVersionQuery(bytes32 baseNode, ParsedVersion memory parsedVersion)
private
view
returns (VersionRecord memory)
{
Version memory version = parsedVersion.version;
if (!parsedVersion.hasMinor) {
// Major-only query: find highest version with matching major (e.g., "1" matches 1.x.x)
return getHighestVersionForMajor(baseNode, version.major);
} else if (!parsedVersion.hasPatch) {
// Major.minor query: find highest version with matching major.minor (e.g., "1-2" matches 1.2.x)
return getHighestVersionForMajorMinor(baseNode, version.major, version.minor);
} else {
// Exact version query: find exact match (e.g., "1-2-3" matches 1.2.3 only)
return getExactVersion(baseNode, version.major, version.minor, version.patch);
}
}
/// @dev Resolves contenthash for wildcard version queries
/// @param name DNS-encoded name with version prefix (e.g., "\x031-2\x06myapp\x03eth\x00")
/// @return ABI-encoded content hash of the matched version, or empty bytes if no match
/// @notice This function is called by resolve() when direct contenthash lookup fails
function _resolveWildcardContenthash(bytes memory name, bytes memory /* data */ )
internal
view
returns (bytes memory)
{
VersionRecord memory result = _resolveWildcardVersion(name);
// If no matching version found, return empty
if (result.contentHash == bytes32(0)) {
return "";
}
return _encodeIpfsContenthash(result.contentHash);
}
/// @dev Resolves text record (version string) for wildcard version queries
/// @param name DNS-encoded name with version prefix (e.g., "\x031-2\x06myapp\x03eth\x00")
/// @return Version string of the matched version (e.g., "1.2.3"), or empty if no match
/// @notice This function is called by resolve() for text("version") wildcard queries
function _resolveWildcardText(bytes memory name, string memory /* key */ ) internal view returns (string memory) {
VersionRecord memory result = _resolveWildcardVersion(name);
// If no matching version found, return empty
if (result.contentHash == bytes32(0)) {
return "";
}
// Return the version as a string
return _versionToString(result.version);
}
/// @notice Publishes a new version of content for your ENS name (e.g., version `major`.`minor`.`patch of your hash `contentHash`).
/// @param namehash The ENS namehash to publish content for
/// @param major The major version number (0-255)
/// @param minor The minor version number (0-255)
/// @param patch The patch version number (0-65535)
/// @param contentHash Raw IPFS hash (32 bytes, sha256 digest only)
/// @dev contentHash should be the raw sha256 hash from IPFS CID, not the full CID
/// @dev For JavaScript: use `ipfs.add()` then extract hash from CID using libraries like:
/// @dev - multiformats: `CID.parse(cid).multihash.digest`
/// @dev - ipfs-http-client: built-in hash extraction utilities
/// @dev The resolver automatically encodes this as EIP-1577 contenthash for ENS compatibility
/// @dev Only callable by the ENS name owner or approved operators
/// @dev Version must be strictly greater than all existing versions (enforced by addVersion)
/// @dev Emits ContenthashChanged and TextChanged events
function publishContent(bytes32 namehash, uint8 major, uint8 minor, uint16 patch, bytes32 contentHash)
external
authorised(namehash)
{
addVersion(namehash, major, minor, patch, contentHash);
// Emit ContenthashChanged event for the new content hash
emit ContenthashChanged(namehash, _encodeIpfsContenthash(contentHash));
// Emit TextChanged event for the "version" key since it will now return the new version
string memory newVersion = _versionToString(_createVersion(major, minor, patch));
emit TextChanged(namehash, "version", "version", newVersion);
}
}
"
},
"lib/forge-std/src/interfaces/IERC165.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.6.2;
interface IERC165 {
/// @notice Query if a contract implements an interface
/// @param interfaceID The interface identifier, as specified in ERC-165
/// @dev Interface identification is specified in ERC-165. This function
/// uses less than 30,000 gas.
/// @return `true` if the contract implements `interfaceID` and
/// `interfaceID` is not 0xffffffff, `false` otherwise
function supportsInterface(bytes4 interfaceID) external view returns (bool);
}
"
},
"lib/ens-contracts/contracts/registry/ENS.sol": {
"content": "//SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface ENS {
// Logged when the owner of a node assigns a new owner to a subnode.
event NewOwner(bytes32 indexed node, bytes32 indexed label, address owner);
// Logged when the owner of a node transfers ownership to a new account.
event Transfer(bytes32 indexed node, address owner);
// Logged when the resolver for a node changes.
event NewResolver(bytes32 indexed node, address resolver);
// Logged when the TTL of a node changes
event NewTTL(bytes32 indexed node, uint64 ttl);
// Logged when an operator is added or removed.
event ApprovalForAll(
address indexed owner,
address indexed operator,
bool approved
);
function setRecord(
bytes32 node,
address owner,
address resolver,
uint64 ttl
) external;
function setSubnodeRecord(
bytes32 node,
bytes32 label,
address owner,
address resolver,
uint64 ttl
) external;
function setSubnodeOwner(
bytes32 node,
bytes32 label,
address owner
) external returns (bytes32);
function setResolver(bytes32 node, address resolver) external;
function setOwner(bytes32 node, address owner) external;
function setTTL(bytes32 node, uint64 ttl) external;
function setApprovalForAll(address operator, bool approved) external;
function owner(bytes32 node) external view returns (address);
function resolver(bytes32 node) external view returns (address);
function ttl(bytes32 node) external view returns (uint64);
function recordExists(bytes32 node) external view returns (bool);
function isApprovedForAll(
address owner,
address operator
) external view returns (bool);
}
"
},
"lib/ens-contracts/contracts/resolvers/profiles/IExtendedResolver.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
interface IExtendedResolver {
function resolve(
bytes memory name,
bytes memory data
) external view returns (bytes memory);
}
"
},
"lib/ens-contracts/contracts/resolvers/profiles/IContentHashResolver.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface IContentHashResolver {
event ContenthashChanged(bytes32 indexed node, bytes hash);
/// Returns the contenthash associated with an ENS node.
/// @param node The ENS node to query.
/// @return The associated contenthash.
function contenthash(bytes32 node) external view returns (bytes memory);
}
"
},
"lib/ens-contracts/contracts/resolvers/profiles/ITextResolver.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity >=0.8.4;
interface ITextResolver {
event TextChanged(
bytes32 indexed node,
string indexed indexedKey,
string key,
string value
);
/// Returns the text data associated with an ENS node and key.
/// @param node The ENS node to query.
/// @param key The text data key to query.
/// @return The associated text data.
function text(
bytes32 node,
string calldata key
) external view returns (string memory);
}
"
},
"lib/ens-contracts/contracts/wrapper/INameWrapper.sol": {
"content": "//SPDX-License-Identifier: MIT
pragma solidity ~0.8.17;
import "../registry/ENS.sol";
import "../ethregistrar/IBaseRegistrar.sol";
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
import "./IMetadataService.sol";
import "./INameWrapperUpgrade.sol";
uint32 constant CANNOT_UNWRAP = 1;
uint32 constant CANNOT_BURN_FUSES = 2;
uint32 constant CANNOT_TRANSFER = 4;
uint32 constant CANNOT_SET_RESOLVER = 8;
uint32 constant CANNOT_SET_TTL = 16;
uint32 constant CANNOT_CREATE_SUBDOMAIN = 32;
uint32 constant CANNOT_APPROVE = 64;
//uint16 reserved for parent controlled fuses from bit 17 to bit 32
uint32 constant PARENT_CANNOT_CONTROL = 1 << 16;
uint32 constant IS_DOT_ETH = 1 << 17;
uint32 constant CAN_EXTEND_EXPIRY = 1 << 18;
uint32 constant CAN_DO_EVERYTHING = 0;
uint32 constant PARENT_CONTROLLED_FUSES = 0xFFFF0000;
// all fuses apart from IS_DOT_ETH
uint32 constant USER_SETTABLE_FUSES = 0xFFFDFFFF;
interface INameWrapper is IERC1155 {
event NameWrapped(
bytes32 indexed node,
bytes name,
address owner,
uint32 fuses,
uint64 expiry
);
event NameUnwrapped(bytes32 indexed node, address owner);
event FusesSet(bytes32 indexed node, uint32 fuses);
event ExpiryExtended(bytes32 indexed node, uint64 expiry);
function ens() external view returns (ENS);
function registrar() external view returns (IBaseRegistrar);
function metadataService() external view returns (IMetadataService);
function names(bytes32) external view returns (bytes memory);
function name() external view returns (string memory);
function upgradeContract() external view returns (INameWrapperUpgrade);
function supportsInterface(bytes4 interfaceID) external view returns (bool);
function wrap(
bytes calldata name,
address wrappedOwner,
address resolver
) external;
function wrapETH2LD(
string calldata label,
address wrappedOwner,
uint16 ownerControlledFuses,
address resolver
) external returns (uint64 expires);
function registerAndWrapETH2LD(
string calldata label,
address wrappedOwner,
uint256 duration,
address resolver,
uint16 ownerControlledFuses
) external returns (uint256 registrarExpiry);
function renew(
uint256 labelHash,
uint256 duration
) external returns (uint256 expires);
function unwrap(bytes32 node, bytes32 label, address owner) external;
function unwrapETH2LD(
bytes32 label,
address newRegistrant,
address newController
) external;
function upgrade(bytes calldata name, bytes calldata extraData) external;
function setFuses(
bytes32 node,
uint16 ownerControlledFuses
) external returns (uint32 newFuses);
function setChildFuses(
bytes32 parentNode,
bytes32 labelhash,
uint32 fuses,
uint64 expiry
) external;
function setSubnodeRecord(
bytes32 node,
string calldata label,
address owner,
address resolver,
uint64 ttl,
uint32 fuses,
uint64 expiry
) external returns (bytes32);
function setRecord(
bytes32 node,
address owner,
address resolver,
uint64 ttl
) external;
function setSubnodeOwner(
bytes32 node,
string calldata label,
address newOwner,
uint32 fuses,
uint64 expiry
) external returns (bytes32);
function extendExpiry(
bytes32 node,
bytes32 labelhash,
uint64 expiry
) external returns (uint64);
function canModifyName(
bytes32 node,
address addr
) external view returns (bool);
function setResolver(bytes32 node, address resolver) external;
function setTTL(bytes32 node, uint64 ttl) external;
function ownerOf(uint256 id) external view returns (address owner);
function approve(address to, uint256 tokenId) external;
function getApproved(uint256 tokenId) external view returns (address);
function getData(
uint256 id
) external view returns (address, uint32, uint64);
function setMetadataService(IMetadataService _metadataService) external;
function uri(uint256 tokenId) external view returns (string memory);
function setUpgradeContract(INameWrapperUpgrade _upgradeAddress) external;
function allFusesBurned(
bytes32 node,
uint32 fuseMask
) external view returns (bool);
function isWrapped(bytes32) external view returns (bool);
function isWrapped(bytes32, bytes32) external view returns (bool);
}
"
},
"lib/ens-contracts/contracts/utils/NameCoder.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {HexUtils} from "../utils/HexUtils.sol";
/// @dev Library for encoding/decoding names.
///
/// An ENS name is stop-separated labels, eg. "aaa.bb.c".
///
/// A DNS-encoded name is composed of byte length-prefixed labels with a terminator byte.
/// eg. "\x03aaa\x02bb\x01c\x00".
/// - maximum label length is 255 bytes.
/// - length = 0 is reserved for the terminator (root).
///
/// To encode a label larger than 255 bytes, use a hashed label.
/// A label of any length can be converted to a hashed label.
///
/// A hashed label is encoded as "[" + toHex(keccak256(label)) + "]".
/// eg. [af2caa1c2ca1d027f1ac823b529d0a67cd144264b2789fa2ea4d63a67c7103cc] = "vitalik".
/// - always 66 bytes.
/// - matches: `/^\[[0-9a-f]{64}\]$/`.
///
/// w/o hashed labels: `dns.length == 2 + ens.length` and the mapping is injective.
/// w/ hashed labels: `dns.length == 2 + ens.split('.').map(x => x.utf8Length).sum(n => n > 255 ? 66 : n)`.
///
library NameCoder {
/// @dev The DNS-encoded name is malformed.
/// Error selector: `0xba4adc23`
error DNSDecodingFailed(bytes dns);
/// @dev A label of the ENS name has an invalid size.
/// Error selector: `0x9a4c3e3b`
error DNSEncodingFailed(string ens);
/// @dev Read the `size` of the label at `offset`.
/// If `size = 0`, it must be the end of `name` (no junk at end).
/// Reverts `DNSDecodingFailed`.
/// @param name The DNS-encoded name.
/// @param offset The offset into `name` to start reading.
/// @return size The size of the label in bytes.
/// @return nextOffset The offset into `name` of the next label.
function nextLabel(
bytes memory name,
uint256 offset
) internal pure returns (uint8 size, uint256 nextOffset) {
assembly {
size := byte(0, mload(add(add(name, 32), offset))) // uint8(name[offset])
nextOffset := add(offset, add(1, size)) // offset + 1 + size
}
if (size > 0 ? nextOffset >= name.length : nextOffset != name.length) {
revert DNSDecodingFailed(name);
}
}
/// @dev Find the offset of the label before `offset` in `name`.
/// * `prevOffset(name, 0)` reverts.
/// * `prevOffset(name, name.length + 1)` reverts.
/// * `prevOffset(name, name.length) = name.length - 1`.
/// * `prevOffset(name, name.length - 1) = <tld>`.
/// Reverts `DNSDecodingFailed`.
/// @param name The DNS-encoded name.
/// @param offset The offset into `name` to start reading backwards.
/// @return prevOffset The offset into `name` of the previous label.
function prevLabel(
bytes memory name,
uint256 offset
) internal pure returns (uint256 prevOffset) {
while (true) {
(, uint256 nextOffset) = nextLabel(name, prevOffset);
if (nextOffset == offset) break;
if (nextOffset > offset) {
revert DNSDecodingFailed(name);
}
prevOffset = nextOffset;
}
}
/// @dev Compute the ENS labelhash of the label at `offset` and the offset for the next label.
/// Disallows hashed label of zero (eg. `[0..0]`) to prevent confusion with terminator.
/// Reverts `DNSDecodingFailed`.
/// @param name The DNS-encoded name.
/// @param offset The offset into `name` to start reading.
/// @param parseHashed If true, supports hashed labels.
/// @return labelHash The resulting labelhash.
/// @return nextOffset The offset into `name` of the next label.
/// @return size The size of the label in bytes.
/// @return wasHashed If true, the label was interpreted as a hashed label.
function readLabel(
bytes memory name,
uint256 offset,
bool parseHashed
)
internal
pure
returns (
bytes32 labelHash,
uint256 nextOffset,
uint8 size,
bool wasHashed
)
{
(size, nextOffset) = nextLabel(name, offset);
if (
parseHashed &&
size == 66 &&
name[offset + 1] == "[" &&
name[nextOffset - 1] == "]"
) {
(labelHash, wasHashed) = HexUtils.hexStringToBytes32(
name,
offset + 2,
nextOffset - 1
); // will not revert
if (!wasHashed || labelHash == bytes32(0)) {
revert DNSDecodingFailed(name); // "readLabel: malformed" or null literal
}
} else if (size > 0) {
assembly {
labelHash := keccak256(add(add(name, offset), 33), size)
}
}
}
/// @dev Same as `BytesUtils.namehash()` but supports hashed labels.
function readLabel(
bytes memory name,
uint256 offset
) internal pure returns (bytes32 labelHash, uint256 nextOffset) {
(labelHash, nextOffset, , ) = readLabel(name, offset, true);
}
/// @dev Compute the ENS namehash of `name[:offset]`.
/// Supports hashed labels.
/// Reverts `DNSDecodingFailed`.
/// @param name The DNS-encoded name.
/// @param offset The offset into name start hashing.
/// @return hash The namehash of `name[:offset]`.
function namehash(
bytes memory name,
uint256 offset
) internal pure returns (bytes32 hash) {
(hash, offset) = readLabel(name, offset);
if (hash != bytes32(0)) {
hash = namehash(namehash(name, offset), hash);
}
}
/// @dev Compute a child namehash from a parent namehash.
/// @param parentNode The namehash of the parent.
/// @param labelHash The labelhash of the child.
/// @return node The namehash of the child.
function namehash(
bytes32 parentNode,
bytes32 labelHash
) internal pure returns (bytes32 node) {
// ~100 gas less than: keccak256(abi.encode(parentNode, labelHash))
assembly {
mstore(0, parentNode)
mstore(32, labelHash)
node := keccak256(0, 64)
}
}
/// @dev Convert DNS-encoded name to ENS name.
/// Reverts `DNSDecodingFailed`.
/// @param dns The DNS-encoded name to convert, eg. `\x03aaa\x02bb\x01c\x00`.
/// @return ens The equivalent ENS name, eg. `aaa.bb.c`.
function decode(
bytes memory dns
) internal pure returns (string memory ens) {
unchecked {
uint256 n = dns.length;
if (n == 1 && dns[0] == 0) return ""; // only valid answer is root
if (n < 3) revert DNSDecodingFailed(dns);
bytes memory v = new bytes(n - 2); // always 2-shorter
uint256 src;
uint256 dst;
while (src < n) {
uint8 len = uint8(dns[src++]);
if (len == 0) break;
uint256 end = src + len;
if (end > dns.length) revert DNSDecodingFailed(dns); // overflow
if (dst > 0) v[dst++] = "."; // skip first stop
while (src < end) {
bytes1 x = dns[src++]; // read byte
if (x == ".") revert DNSDecodingFailed(dns); // malicious label
v[dst++] = x; // write byte
}
}
if (src != dns.length) revert DNSDecodingFailed(dns); // junk at end
return string(v);
}
}
/// @dev Convert ENS name to DNS-encoded name.
/// Hashes labels longer than 255 bytes.
/// Reverts `DNSEncodingFailed`.
/// @param ens The ENS name to convert, eg. `aaa.bb.c`.
/// @return dns The corresponding DNS-encoded name, eg. `\x03aaa\x02bb\x01c\x00`.
function encode(
string memory ens
) internal pure returns (bytes memory dns) {
unchecked {
uint256 n = bytes(ens).length;
if (n == 0) return hex"00"; // root
dns = new bytes(n + 2);
uint256 start;
assembly {
start := add(dns, 32) // first byte of output
}
uint256 end = start; // remember position to write length
for (uint256 i; i < n; i++) {
bytes1 x = bytes(ens)[i]; // read byte
if (x == ".") {
start = _createHashedLabel(start, end);
if (start == 0) revert DNSEncodingFailed(ens);
end = start; // jump to next position
} else {
assembly {
end := add(end, 1) // increase length
mstore(end, x) // write byte
}
}
}
start = _createHashedLabel(start, end);
if (start == 0) revert DNSEncodingFailed(ens);
assembly {
mstore8(start, 0) // terminal byte
mstore(dns, sub(start, add(dns, 31))) // truncate length
}
}
}
/// @dev Write the label length.
/// If longer than 255, writes a hashed label instead.
/// @param start The memory offset of the length-prefixed label.
/// @param end The memory offset at the end of the label.
/// @return next The memory offset for the next label.
/// Returns 0 if label is empty (handled by caller).
function _createHashedLabel(
uint256 start,
uint256 end
) internal pure returns (uint256 next) {
uint256 size = end - start; // length of label
if (size > 255) {
assembly {
mstore(0, keccak256(add(start, 1), size)) // compute hash of label
}
HexUtils.unsafeHex(0, start + 2, 64); // override label with hex(hash)
assembly {
mstore8(add(start, 1), 0x5B) // "["
mstore8(add(start, 66), 0x5D) // "]"
}
size = 66;
}
if (size > 0) {
assembly {
mstore8(start, size) // update length
}
next = start + 1 + size; // advance
}
}
/// @dev Find the offset of `name` that namehashes to `nodeSuffix`.
/// @param name The name to search.
/// @param nodeSuffix The node to match.
/// @return matched True if `name` ends with the suffix.
/// @return node The namehash of `name[offset:]`.
/// @return prevOffset The offset into `name` of the label before the suffix, or `matchOffset` if no match or prior label.
/// @return matchOffset The offset into `name` that namehashes to the `nodeSuffix`, or 0 if no match.
function matchSuffix(
bytes memory name,
uint256 offset,
bytes32 nodeSuffix
)
internal
pure
returns (
bool matched,
bytes32 node,
uint256 prevOffset,
uint256 matchOffset
)
{
(bytes32 labelHash, uint256 next) = readLabel(name, offset);
if (labelHash != bytes32(0)) {
(matched, node, prevOffset, matchOffset) = matchSuffix(
name,
next,
nodeSuffix
);
if (node == nodeSuffix) {
matched = true;
prevOffset = offset;
matchOffset = next;
}
node = namehash(node, labelHash);
}
if (node == nodeSuffix) {
matched = true;
prevOffset = matchOffset = offset;
}
}
}
"
},
"lib/ens-contracts/contracts/utils/BytesUtils.sol": {
"content": "//SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
library BytesUtils {
/// @dev `offset` was beyond `length`.
/// Error selector: `0x8a3c1cfb`
error OffsetOutOfBoundsError(uint256 offset, uint256 length);
/// @dev Assert `end` is not beyond the length of `v`.
function _checkBound(bytes memory v, uint256 end) internal pure {
if (end > v.length) {
revert OffsetOutOfBoundsError(end, v.length);
}
}
/// @dev Compute `keccak256(v[off:off+len])`.
/// @param v The source bytes.
/// @param off The offset into the source.
/// @param len The number of bytes to hash.
/// @return ret The corresponding hash.
function keccak(
bytes memory v,
uint256 off,
uint256 len
) internal pure returns (bytes32 ret) {
_checkBound(v, off + len);
assembly {
ret := keccak256(add(add(v, 32), off), len)
}
}
/// @dev Lexicographically compare two byte strings.
/// @param vA The first bytes to compare.
/// @param vB The second bytes to compare.
/// @return Positive number if `A > B`, negative number if `A < B`, or zero if `A == B`.
function compare(
bytes memory vA,
bytes memory vB
) internal pure returns (int256) {
return compare(vA, 0, vA.length, vB, 0, vB.length);
}
/// @dev Lexicographically compare two byte ranges: `A = vA[offA:offA+lenA]` and `B = vB[offB:offB+lenB]`.
/// @param vA The first bytes.
/// @param offA The offset of the first bytes.
/// @param lenA The length of the first bytes.
/// @param vB The second bytes.
/// @param offB The offset of the second bytes.
/// @param lenB The length of the second bytes.
/// @return Positive number if `A > B`, negative number if `A < B`, or zero if `A == B`.
function compare(
bytes memory vA,
uint256 offA,
uint256 lenA,
bytes memory vB,
uint256 offB,
uint256 lenB
) internal pure returns (int256) {
_checkBound(vA, offA + lenA);
_checkBound(vB, offB + lenB);
uint256 ptrA;
uint256 ptrB;
assembly {
ptrA := add(vA, offA)
ptrB := add(vB, offB)
}
uint256 shortest = lenA < lenB ? lenA : lenB;
for (uint256 i; i < shortest; i += 32) {
uint256 a;
uint256 b;
assembly {
ptrA := add(ptrA, 32)
ptrB := add(ptrB, 32)
a := mload(ptrA)
b := mload(ptrB)
}
if (a != b) {
uint256 rest = shortest - i;
if (rest < 32) {
rest = (32 - rest) << 3; // bits to drop
a >>= rest; // shift out the
b >>= rest; // irrelevant bits
}
if (a < b) {
return -1;
} else if (a > b) {
return 1;
}
}
}
return int256(lenA) - int256(lenB);
}
/// @dev Determine if `a[offA:offA+len] == b[offB:offB+len]`.
/// @param vA The first bytes.
/// @param offA The offset into the first bytes.
/// @param vB The second bytes.
/// @param offB The offset into the second bytes.
/// @param len The number of bytes to compare.
/// @return True if the byte ranges are equal.
function equals(
bytes memory vA,
uint256 offA,
bytes memory vB,
uint256 offB,
uint256 len
) internal pure returns (bool) {
return keccak(vA, offA, len) == keccak(vB, offB, len);
}
/// @dev Determine if `a[offA:] == b[offB:]`.
/// @param vA The first bytes.
/// @param offA The offset into the first bytes.
/// @param vB The second bytes.
/// @param offB The offset into the second bytes.
/// @return True if the byte ranges are equal.
function equals(
bytes memory vA,
uint256 offA,
bytes memory vB,
uint256 offB
) internal pure returns (bool) {
_checkBound(vA, offA);
_checkBound(vB, offB);
return
keccak(vA, offA, vA.length - offA) ==
keccak(vB, offB, vB.length - offB);
}
/// @dev Determine if `a[offA:] == b`.
/// @param vA The first bytes.
/// @param offA The offset into the first bytes.
/// @param vB The second bytes.
/// @return True if the byte ranges are equal.
function equals(
bytes memory vA,
uint256 offA,
bytes memory vB
) internal pure returns (bool) {
return
vA.length == offA + vB.length &&
keccak(vA, offA, vB.length) == keccak256(vB);
}
/// @dev Determine if `a == b`.
/// @param vA The first bytes.
/// @param vB The second bytes.
/// @return True if the bytes are equal.
function equals(
bytes memory vA,
bytes memory vB
) internal pure returns (bool) {
return vA.length == vB.length && keccak256(vA) == keccak256(vB);
}
/// @dev Returns `uint8(v[off])`.
/// @param v The source bytes.
/// @param off The offset into the source.
/// @return The corresponding `uint8`.
function readUint8(
bytes memory v,
uint256 off
) internal pure returns (uint8) {
_checkBound(v, off + 1);
return uint8(v[off]);
}
/// @dev Returns `uint16(bytes2(v[off:off+2]))`.
/// @param v The source bytes.
/// @param off The offset into the source.
/// @return ret The corresponding `uint16`.
function readUint16(
bytes memory v,
uint256 off
) internal pure returns (uint16 ret) {
_checkBound(v, off + 2);
assembly {
ret := shr(240, mload(add(add(v, 32), off)))
}
}
/// @dev Returns `uint32(bytes4(v[off:off+4]))`.
/// @param v The source bytes.
/// @param off The offset into the source.
/// @return ret The corresponding `uint32`.
function readUint32(
bytes memory v,
uint256 off
) internal pure returns (uint32 ret) {
_checkBound(v, off + 4);
assembly {
ret := shr(224, mload(add(add(v, 32), off)))
}
}
/// @dev Returns `bytes20(v[off:off+20])`.
/// @param v The source bytes.
/// @param off The offset into the source.
/// @return ret The corresponding `bytes20`.
function readBytes20(
bytes memory v,
uint256 off
) internal pure returns (bytes20 ret) {
_checkBound(v, off + 20);
assembly {
ret := shl(96, mload(add(add(v, 20), off)))
}
}
/// @dev Returns `bytes32(v[off:off+32])`.
/// @param v The source bytes.
/// @param off The offset into the source.
/// @return ret The corresponding `bytes32`.
function readBytes32(
bytes memory v,
uint256 off
) internal pure returns (bytes32 ret) {
_checkBound(v, off + 32);
assembly {
ret := mload(add(add(v, 32), off))
}
}
/// @dev Returns `bytes32(bytesN(v[off:off+len]))`.
/// Accepts 0-32 bytes or reverts.
/// @param v The source bytes.
/// @param off The offset into the source.
/// @param len The number of bytes.
/// @return ret The corresponding N-bytes left-aligned in a `bytes32`.
function readBytesN(
bytes memory v,
uint256 off,
uint256 len
) internal pure returns (bytes32 ret) {
assert(len <= 32);
_checkBound(v, off + len);
assembly {
let mask := sub(shl(shl(3, sub(32, len)), 1), 1) // <(32-N)x00><NxFF>
ret := and(mload(add(add(v, 32), off)), not(mask))
}
}
/// @dev Copy `mem[src:src+len]` to `mem[dst:dst+len]`.
/// @param src The source memory offset.
/// @param dst The destination memory offset.
/// @param len The number of bytes to copy.
function unsafeMemcpy(uint256 dst, uint256 src, uint256 len) internal pure {
assembly {
// Copy word-length chunks while offsible
// prettier-ignore
for {} gt(len, 31) {} {
mstore(dst, mload(src))
dst := add(dst, 32)
src := add(src, 32)
len := sub(len, 32)
}
// Copy remaining bytes
if len {
let mask := sub(shl(shl(3, sub(32, len)), 1), 1) // see above
let wSrc := and(mload(src), not(mask))
let wDst := and(mload(dst), mask)
mstore(dst, or(wSrc, wDst))
}
}
}
/// @dev Copy `vSrc[offSrc:offSrc+len]` to `vDst[offDst:offDst:len]`.
/// @param vSrc The source bytes.
/// @param offSrc The offset into the source to begin the copy.
/// @param vDst The destination bytes.
/// @param offDst The offset into the destination to place the copy.
/// @param len The number of bytes to copy.
function copyBytes(
bytes memory vSrc,
uint256 offSrc,
bytes memory vDst,
uint256 offDst,
uint256 len
) internal pure {
_checkBound(vSrc, offSrc + len);
_checkBound(vDst, offDst + len);
uint256 src;
uint256 dst;
assembly {
src := add(add(vSrc, 32), offSrc)
dst := add(add(vDst, 32), offDst)
}
unsafeMemcpy(dst, src, len);
}
/// @dev Copies a substring into a new byte string.
/// @param vSrc The byte string to copy from.
/// @param off The offset to start copying at.
/// @param len The number of bytes to copy.
/// @return vDst The copied substring.
function substring(
bytes memory vSrc,
uint256 off,
uint256 len
) internal pure returns (bytes memory vDst) {
vDst = new bytes(len);
copyBytes(vSrc, off, vDst, 0, len);
}
/// @dev Find the first occurrence of `needle`.
/// @param v The bytes to search.
/// @param off The offset to start searching.
/// @param len The number of bytes to search.
/// @param needle The byte to search for.
/// @return The offset of `needle`, or `type(uint256).max` if not found.
function find(
bytes memory v,
uint256 off,
uint256 len,
bytes1 needle
) internal pure returns (uint256) {
for (uint256 end = off + len; off < end; off++) {
if (v[off] == needle) {
return off;
}
}
return type(uint256).max;
}
}
"
},
"src/VersionRegistry.sol": {
"content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {SemverLib} from "./SemverLib.sol";
/// @title VersionRegistry - Storage and retrieval constants
contract VersionRegistryConstants {
// Version validation constants
uint8 internal constant ZERO_VERSION_MAJOR = 0;
uint8 internal constant ZERO_VERSION_MINOR = 0;
uint16 internal constant ZERO_VERSION_PATCH = 0;
// Binary search algorithm constants
uint256 internal constant SEARCH_NOT_FOUND = type(uint256).max;
uint256 internal constant ARRAY_START_INDEX = 0;
// Version comparison result constants
int8 internal constant COMPARISON_LESS = -1;
int8 internal constant COMPARISON_EQUAL = 0;
int8 internal constant COMPARISON_GREATER = 1;
// Patch version sequencing constants
uint16 internal constant PATCH_INCREMENT = 1;
}
/// @title VersionRegistry
/// @notice Abstract contract for storing and querying versioned content by ENS namehash
/// @dev Uses component-wise version ordering: major/minor can be added out of order,
/// but patch versions must be strictly sequential within each major.minor
/// @dev Uses binary search for O(log n) version lookups
/// @dev Example valid sequence: 1.1.4 → 2.0.0 → 1.1.5 → 1.2.0 → 2.0.1
abstract contract VersionRegistry is SemverLib, VersionRegistryConstants {
/// @notice Represents a versioned content record
/// @param version The semantic version (major.minor.patch)
/// @param contentHash The IPFS or other content hash for this version
struct VersionRecord {
Version version;
bytes32 contentHash;
}
/// @dev Maps ENS namehash to array of version records, stored in ascending order
/// @dev Array is sorted to enable binary search for efficient lookups
mapping(bytes32 => VersionRecord[]) private versionRegistry;
error ZeroVersionNotAllowed();
error PatchVersionNotSequential();
/// @notice Validates that a new version follows the required ordering rules
/// @param versions Array of existing version records (sorted)
/// @param newVersion The new version to validate
/// @dev Enforces rules:
/// - Major and minor versions can be added out of chronological order (gaps allowed)
/// - Patch versions must be strictly sequential within same major.minor (no gaps)
/// - Example: 1.0.0 → 1.0.1 → 1.0.2 (valid), 1.0.0 → 1.0.2 (invalid)
/// @dev Validation rules:
/// - Major and minor versions can be added out of chronological order (gaps allowed)
/// - Patch versions must be strictly sequential within same major.minor (no gaps)
/// - Duplicate versions are implicitly rejected by sequential check
/// @dev Complexity: O(n) where n is the number of existing versions
/// @dev Algorithm: Single pass to find highest patch for matching major.minor
/// then validates new patch is exactly highest + 1 (for existing major.minor)
/// or allows any patch value for new major.minor combinations
/// @dev Examples:
/// - Existing: [1.0.0, 1.0.1] + New: 1.0.2 → valid (sequential patch)
/// - Existing: [1.0.0, 1.0.1] + New: 1.0.0 → invalid (duplicate/backward)
/// - Existing: [1.0.0, 1.0.1] + New: 2.0.5 → valid (new major.minor)
function _validateComponentWiseOrder(VersionRecord[] storage versions, Version memory newVersion) private view {
uint16 highestPatch = 0;
bool foundMajorMinor = false;
// Single pass: find highest patch for this major.minor AND check for duplicates
for (uint256 i = 0; i < versions.length; i++) {
Version memory existing = versions[i].version;
if (existing.major == newVersion.major && existing.minor == newVersion.minor) {
foundMajorMinor = true;
// Duplicate check is implicit: if patch matches highestPatch,
// the sequential check below will catch it
if (existing.patch > highestPatch) {
highestPatch = existing.patch;
}
}
}
// For existing major.minor: patch must be exactly highestPatch + 1
// For new major.minor: any patch value is allowed as the starting patch
if (foundMajorMinor && newVersion.patch != highestPatch + PATCH_INCREMENT) {
revert PatchVersionNotSequential();
}
}
/// @notice Finds where to insert a new version in the sorted array to maintain order
/// @param versions Array of existing version records (sorted)
/// @param newVersion The version to find insertion point for
/// @return The index where the new version should be inserted
/// @dev Inspired by OpenZeppelin Arrays.sol lowerBound implementation
function _findInsertionPoint(VersionRecord[] storage versions, Version memory newVersion)
private
view
returns (uint256)
{
uint256 low = 0;
uint256 high = versions.length;
while (low < high) {
uint256 mid = (low + high) / 2;
if (_isGreater(newVersion, versions[mid].version)) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
/// @notice Adds a new version of content to the registry for an ENS name
/// @param namehash The ENS namehash to add the version for
/// @param major The major version number (0-255)
/// @param minor The minor version number (0-255)
/// @param patch The patch version number (0-65535)
/// @param contentHash The content hash for this version
/// @dev Component-wise ordering rules:
/// - Major and minor versions can be added out of chronological order
/// - Patch versions must be strictly sequential within same major.minor
/// - Cannot add duplicate versions; no patch gaps allowed
/// @dev Reverts if version is 0.0.0 (reserved as sentinel value)
/// @dev Examples: 1.1.4 → 2.0.0 → 1.1.5 (valid), 1.1.4 → 1.1.3 (invalid)
/// @dev Component-wise ordering rules enforced:
/// - Major and minor versions can be added out of chronological order (gaps allowed)
/// - Patch versions must be strictly sequential within same major.minor (no gaps)
/// - Cannot add duplicate versions; ensures patch continuity
/// @dev Complexity: O(n) for validation + O(log n) for insertion + O(n) for array shifting
/// Total: O(n) where n is number of existing versions
/// @dev Version 0.0.0 is reserved as sentinel value and rejected
/// @dev Examples of valid sequences:
/// - 1.1.0 → 1.1.1 → 2.0.0 → 1.1.2 (component-wise valid)
/// - 1.0.0 → 1.0.2 (invalid: missing 1.0.1)
/// - 2.0.0 → 1.0.0 (valid: different major versions)
function addVersion(bytes32 namehash, uint8 major, uint8 minor, uint16 patch, bytes32 contentHash) internal {
Version memory newVersion = _createVersion(major, minor, patch);
// Reject version 0.0.0 as it's reserved for "no version" sentinel value
// However, allow versions like 0.0.1, 0.1.0, etc. for pre-release/development
if (major == ZERO_VERSION_MAJOR && minor == ZERO_VERSION_MINOR && patch == ZERO_VERSION_PATCH) {
revert ZeroVersionNotAllowed();
}
VersionRecord[] storage versions = versionRegistry[namehash];
// Validate component-wise ordering rules
_validateComponentWiseOrder(versions, newVersion);
// Add the new version using binary search insertion (inspired by OpenZeppelin Arrays.sol)
VersionRecord memory newRecord = VersionRecord({version: newVersion, contentHash: contentHash});
// Find insertion point using binary search (O(log n))
uint256 insertIndex = _findInsertionPoint(versions, newVersion);
// Insert at correct position
versions.push(newRecord);
for (uint256 i = versions.length - 1; i > insertIndex; i--) {
versions[i] = versions[i - 1];
}
versions[insertIndex] = newRecord;
}
/// @notice Gets the content hash of the most recent version for an ENS name
/// @param namehash The ENS namehash to query
/// @return The content hash of the latest version, or bytes32(0) if no versions exist
function getLatestContentHash(bytes32 namehash) internal view returns (bytes32) {
VersionRecord[] storage versions = versionRegistry[namehash];
if (versions.length == 0) {
return bytes32(0);
}
return versions[versions.length - 1].contentHash;
}
/// @notice Gets the most recent version number for an ENS name
/// @param namehash The ENS namehash to query
/// @return The latest version, or Version(0,0,0) if no versions exist
function getLatestVersion(bytes32 namehash) internal view returns (Version memory) {
VersionRecord[] storage versions = versionRegistry[namehash];
if (versions.length == 0) {
return _createVersion(0, 0, 0);
}
return versions[versions.length - 1].version;
}
/// @notice Finds the highest version with a specific major version number (e.g., highest 1.x.x)
/// @param namehash The ENS namehash to search
/// @param targetMajor The major version to match
/// @return The highest version matching the major version, or zero version if not found
/// @dev Example: targetMajor=1 finds highest version like 1.x.x
function getHighestVersionForMajor(bytes32 namehash, uint8 targetMajor)
internal
view
returns (VersionRecord memory)
{
return _getHighestVersionMatching(namehash, targetMajor, 0, false);
}
/// @notice Finds the highest version with specific major.minor numbers (e.g., highest 1.2.x)
/// @param namehash The ENS namehash to search
/// @param targetMajor The major version to match
/// @param targetMinor The minor version to match
/// @return The highest version matching the major.minor version, or zero version if not found
/// @dev Example: targetMajor=1, targetMinor=2 finds highest version like 1.2.x
function getHighestVersionForMajorMinor(bytes32 namehash, uint8 targetMajor, uint8 targetMinor)
internal
view
returns (VersionRecord memory)
{
return _getHighestVersionMatching(namehash, targetMajor, targetMinor, true);
}
/// @notice Finds an exact version match (e.g., finds 1.2.3 exactly, not 1.2.4)
/// @param namehash The ENS namehash to search
/// @param targetMajor The major version to match
/// @param targetMinor The minor version to match
/// @param targetPatch The patch version to match
/// @return The exact version record if found, or zero version if not found
/// @dev Example: targetMajor=1, targetMinor=2, targetPatch=3 finds version 1.2.3 only
/// @dev Uses binary search for O(log n) lookup
function getExactVersion(bytes32 namehash, uint8 targetMajor, uint8 targetMinor, uint16 targetPatch)
internal
view
returns (VersionRecord memory)
{
VersionRecord[] storage versions = versionRegistry[namehash];
if (versions.length == 0) {
return _createZeroVersionRecord();
}
// Binary search for exact version match
uint256 left = 0;
uint256 right = versions.length;
while (left < right) {
uint256 mid = left + (right - left) / 2;
// Invariant: Ensures calculated midpoint is always within search range to prevent algorithm errors.
// Note: Array access safety (mid < versions.length) is automatically verified by outOfBounds target.
assert(mid >= left && mid < right);
int8 comparison = _compareVersionExact(versions[mid].version, targetMajor, targetMinor, targetPatch);
// Invariant: Documents that comparison function contract returns only valid values (-1, 0, or 1)
assert(comparison >= -1 && comparison <= 1);
if (comparison < 0) {
left = mid + 1;
} else if (comparison > 0) {
right = mid;
} else {
// Exact match found
return versions[mid];
}
}
// No exact match found
return _createZeroVersionRecord();
}
/// @dev Finds the highest version matching a given major (and optionally minor) version prefix
/// @param namehash The ENS namehash to search
/// @param targetMajor The major version to match
/// @param targetMinor The minor version to match (only used if includeMinor is true)
/// @param includeMinor If true, match both major and minor; if false, match only major
/// @return The highest matching version record, or zero version if no match found
/// @notice Uses optimized binary search that finds the rightmost (highest) match directly in O(log n)
/// @dev Complexity: Extracted binary search algorithm for better maintainability
function _getHighestVersionMatching(bytes32 namehash, uint8 targetMajor, uint8 targetMinor, bool includeMinor)
private
view
returns (VersionRecord memory)
{
VersionRecord[] storage versions = versionRegistry[namehash];
if (versions.length == 0) {
return _createZeroVersionRecord();
}
// Use specialized binary search to find the rightmost (highest) matching version
uint256 matchIndex = _binarySearchRightmostMatch(versions, targetMajor, targetMinor, includeMinor);
if (matchIndex == SEARCH_NOT_FOUND) {
return _createZeroVersionRecord();
}
return versions[matchIndex];
}
/// @dev Creates a zero version record (sentinel value for "no version found")
/// @return A version record with version 0.0.0 and empty content hash
function _createZeroVersionRecord() private pure returns (VersionRecord memory) {
return VersionRecord({
version: _createVersion(ZERO_VERSION_MAJOR, ZERO_VERSION_MINOR, ZERO_VERSION_PATCH),
contentHash: bytes32(0)
});
}
/// @dev Binary search algorithm that finds the rightmost (highest) matching version
/// @param versions Array of version records to search (must be sorted)
/// @param targetMajor The major version to match
/// @param targetMinor The minor version to match (only used if includeMinor is true)
/// @param includeMinor If true, match both major and minor; if false, match only major
/// @return Index of the rightmost matching version, or SEARCH_NOT_FOUND if no match
/// @dev Time Complexity: O(log n) where n is the number of versions
/// @dev Algorithm: Modified binary search that continues searching right after finding matches
/// to ensure we get the highest patch version within the matching major[.minor] range
function _binarySearchRightmostMatch(
VersionRecord[] storage versions,
uint8 targetMajor,
uint8 targetMinor,
bool includeMinor
) private view returns (uint256) {
uint256 left = ARRAY_START_INDEX;
uint256 right = versions.length;
uint256 bestIndex = SEARCH_NOT_FOUND;
while (left < right) {
uint
Submitted on: 2025-10-08 08:51:58
Comments
Log in to comment.
No comments yet.