// SPDX-License-Identifier: MIT pragma solidity 0.8.24; /// @notice Commit/reveal convenience names. A name conveys no protocol authority. contract VNSV3 { struct Record { address owner; address recovery; uint64 expiresAt; uint64 recoverySetAt; } uint64 public immutable maximumRegistrationDuration; uint64 public immutable recoveryDelay; uint32 public immutable minimumCommitBlocks; uint32 public immutable maximumCommitBlocks; /// @dev Indexed by commitmentKey(rawCommitment, committer), not by the copyable raw commitment. mapping(bytes32 => uint256) public commitmentBlock; mapping(bytes32 => Record) private _records; error BadConfiguration(); error InvalidCommitment(); error NameUnavailable(); error Unauthorized(); error BadDuration(); error RecoveryNotReady(); event NameCommittedV3(bytes32 indexed commitment, uint256 indexed blockNumber); event NameCommitterBoundV3(bytes32 indexed commitment, address indexed committer, uint256 indexed blockNumber); event NameRegisteredV3(bytes32 indexed labelHash, address indexed owner, uint64 expiresAt); event NameTransferredV3(bytes32 indexed labelHash, address indexed previousOwner, address indexed newOwner); event RecoverySetV3(bytes32 indexed labelHash, address indexed recovery); event NameRecoveredV3(bytes32 indexed labelHash, address indexed previousOwner, address indexed newOwner); event NameRenewedV3(bytes32 indexed labelHash, uint64 expiresAt); constructor(uint64 maximumRegistrationDuration_, uint64 recoveryDelay_, uint32 minBlocks_, uint32 maxBlocks_) { if (maximumRegistrationDuration_ == 0 || recoveryDelay_ == 0 || minBlocks_ == 0 || minBlocks_ > maxBlocks_) { revert BadConfiguration(); } maximumRegistrationDuration = maximumRegistrationDuration_; recoveryDelay = recoveryDelay_; minimumCommitBlocks = minBlocks_; maximumCommitBlocks = maxBlocks_; } function makeCommitment(bytes32 labelHash, address owner, bytes32 secret, uint64 duration) public pure returns (bytes32) { return keccak256(abi.encode(labelHash, owner, secret, duration)); } function commit(bytes32 commitment) external { bytes32 key = commitmentKey(commitment, msg.sender); if (commitment == bytes32(0) || commitmentBlock[key] != 0) revert InvalidCommitment(); commitmentBlock[key] = block.number; emit NameCommittedV3(commitment, block.number); emit NameCommitterBoundV3(commitment, msg.sender, block.number); } function register(bytes32 labelHash, bytes32 secret, uint64 duration) external { if (labelHash == bytes32(0)) revert InvalidCommitment(); if (duration == 0 || duration > maximumRegistrationDuration) revert BadDuration(); bytes32 commitment = makeCommitment(labelHash, msg.sender, secret, duration); bytes32 key = commitmentKey(commitment, msg.sender); uint256 committedAt = commitmentBlock[key]; if ( committedAt == 0 || block.number < committedAt + minimumCommitBlocks || block.number > committedAt + maximumCommitBlocks ) revert InvalidCommitment(); Record storage current = _records[labelHash]; if (current.owner != address(0) && block.timestamp <= current.expiresAt) revert NameUnavailable(); delete commitmentBlock[key]; uint64 expiry = uint64(block.timestamp) + duration; _records[labelHash] = Record(msg.sender, address(0), expiry, 0); emit NameRegisteredV3(labelHash, msg.sender, expiry); } function renew(bytes32 labelHash, uint64 duration) external { Record storage record = _records[labelHash]; if (msg.sender != record.owner || block.timestamp > record.expiresAt) revert Unauthorized(); if (duration == 0 || duration > maximumRegistrationDuration) revert BadDuration(); record.expiresAt = uint64(block.timestamp) + duration; emit NameRenewedV3(labelHash, record.expiresAt); } function transfer(bytes32 labelHash, address newOwner) external { Record storage record = _records[labelHash]; if (msg.sender != record.owner || block.timestamp > record.expiresAt || newOwner == address(0)) { revert Unauthorized(); } address previous = record.owner; record.owner = newOwner; record.recovery = address(0); record.recoverySetAt = 0; emit NameTransferredV3(labelHash, previous, newOwner); } function setRecovery(bytes32 labelHash, address recovery) external { Record storage record = _records[labelHash]; if (msg.sender != record.owner || block.timestamp > record.expiresAt || recovery == address(0)) { revert Unauthorized(); } record.recovery = recovery; record.recoverySetAt = uint64(block.timestamp); emit RecoverySetV3(labelHash, recovery); } function recover(bytes32 labelHash, address newOwner) external { Record storage record = _records[labelHash]; if (msg.sender != record.recovery || newOwner == address(0) || block.timestamp > record.expiresAt) { revert Unauthorized(); } if (block.timestamp < uint256(record.recoverySetAt) + recoveryDelay) revert RecoveryNotReady(); address previous = record.owner; record.owner = newOwner; record.recovery = address(0); record.recoverySetAt = 0; emit NameRecoveredV3(labelHash, previous, newOwner); } function resolve(bytes32 labelHash) external view returns (address) { Record storage record = _records[labelHash]; return block.timestamp <= record.expiresAt ? record.owner : address(0); } function recordOf(bytes32 labelHash) external view returns (Record memory) { return _records[labelHash]; } function commitmentKey(bytes32 commitment, address committer) public pure returns (bytes32) { return keccak256(abi.encode(commitment, committer)); } function authoritySemantics() external pure returns (string memory) { return "convenience naming only; never authorization, identity proof, reputation, or eligibility"; } }