diff --git a/src/registrar/Registrar.sol b/src/registrar/Registrar.sol index 6855801..9251e03 100644 --- a/src/registrar/Registrar.sol +++ b/src/registrar/Registrar.sol @@ -66,6 +66,9 @@ contract RegistrarController is Ownable { /// @notice Thrown when the name is not reserved but you try to mint via reserved minting flow error NameNotReserved(); + /// @notice Thrown when a free mint signature has already been used. + error FreeMintSignatureAlreadyUsed(); + /// Events ----------------------------------------------------------- /// @notice Emitted when an ETH payment was processed successfully. @@ -185,6 +188,9 @@ contract RegistrarController is Ownable { /// @notice The mapping of used signatures. mapping(bytes32 => bool) public usedSignatures; + /// @notice The mapping of used free mints signatures. + mapping(bytes32 => bool) public usedFreeMintsSignatures; + /// @notice The mapping of mints count by round by address. /// example: 0x123 => { 1st Round => 3 mints, 2nd Round => 1 mint } mapping(address => mapping(uint256 => uint256)) public mintsCountByRoundByAddress; @@ -382,13 +388,35 @@ contract RegistrarController is Ownable { _register(request.registerRequest); } - function reservedRegister(RegisterRequest calldata request) public { + /// @notice Allows a whitelisted address to register a name for free + /// + /// @param request The `RegisterRequest` struct containing the details for the registration. + /// @param signature The signature of the whitelisted address. + function whitelistFreeRegister(RegisterRequest calldata request, bytes calldata signature) + public + validRegistration(request) + { + _validateFreeWhitelist(request.owner, signature); + + uint256 strlen = request.name.strlen(); + if (strlen < 3) revert NameNotAvailable(request.name); + + _validateRegistration(request); + _registerRequest(request); + } + + /// @notice Allows the reserved names minter to register a reserved name. + /// + /// @dev Skips the _validateRegistration because it's callable only by reservedNamesMinter + /// @dev Calls the _registerRequest directly because it's not payable, so we don't need to validate payment + /// + /// @param request The `RegisterRequest` struct containing the details for the registration. + function reservedRegister(RegisterRequest calldata request) public validRegistration(request) { if (msg.sender != reservedNamesMinter) { revert NotAuthorisedToMintReservedNames(); } if (!reservedRegistry.isReservedName(request.name)) revert NameNotReserved(); - // Skip the _register because this mint is not payable, so no money sent _registerRequest(request); } @@ -447,16 +475,7 @@ contract RegistrarController is Ownable { } function _validateWhitelist(WhitelistRegisterRequest calldata request, bytes calldata signature) internal { - // Break signature into r, s, v - bytes32 r; - bytes32 s; - uint8 v; - // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol#L66-L70 - assembly { - r := calldataload(signature.offset) - s := calldataload(add(signature.offset, 32)) - v := byte(0, calldataload(add(signature.offset, 64))) - } + (bytes32 r, bytes32 s, uint8 v) = unpackSignature(signature); // Encode payload - signature format: (address owner, address referrer, uint256 duration, string name) bytes memory payload = abi.encode( @@ -481,6 +500,28 @@ contract RegistrarController is Ownable { mintsCountByRoundByAddress[msg.sender][request.round_id]++; } + function _validateFreeWhitelist(address owner, bytes calldata signature) internal { + (bytes32 r, bytes32 s, uint8 v) = unpackSignature(signature); + + bytes memory payload = abi.encode(owner); + if (usedFreeMintsSignatures[keccak256(payload)]) revert FreeMintSignatureAlreadyUsed(); + + whitelistValidator.validateSignature(payload, v, r, s); + + usedFreeMintsSignatures[keccak256(payload)] = true; + } + + function unpackSignature(bytes calldata signature) internal pure returns (bytes32 r, bytes32 s, uint8 v) { + // https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol#L66-L70 + assembly { + r := calldataload(signature.offset) + s := calldataload(add(signature.offset, 32)) + v := byte(0, calldataload(add(signature.offset, 64))) + } + + return (r, s, v); + } + /// @notice Helper for deciding whether to include a launch-premium. /// /// @dev If the token returns a `0` expiry time, it hasn't been registered before. On launch, this will be true for all diff --git a/test/registrar/FreeWhitelistRegistrar.t.sol b/test/registrar/FreeWhitelistRegistrar.t.sol new file mode 100644 index 0000000..1e2fd47 --- /dev/null +++ b/test/registrar/FreeWhitelistRegistrar.t.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +import {SystemTest} from "../System.t.sol"; +import {RegistrarController} from "src/registrar/Registrar.sol"; + +/// @notice this contract tests the RegistrarController, but only whitelisting tests +/// separated for clarity and organisation +contract FreeWhitelistRegistrarTest is SystemTest { + function test_whitelist_free_register() public { + // set launch time in 10 days + vm.prank(registrarAdmin); + registrar.setLaunchTime(block.timestamp + 10 days); + vm.stopPrank(); + + // mint with success + vm.startPrank(alice); + deal(address(alice), 1000 ether); + + string memory nameToMint = "s"; // short name + RegistrarController.RegisterRequest memory request = RegistrarController.RegisterRequest({ + name: nameToMint, + owner: alice, + duration: 365 days, + resolver: address(resolver), + data: new bytes[](0), + reverseRecord: true, + referrer: address(0) + }); + + bytes memory payload = abi.encode(request.owner); + bytes32 payloadHash = keccak256(payload); + bytes memory prefix = "\x19Ethereum Signed Message:\n32"; + bytes32 prefixedHash = keccak256(abi.encodePacked(prefix, payloadHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPk, prefixedHash); + + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(abi.encodeWithSelector(RegistrarController.NameNotAvailable.selector, nameToMint)); + registrar.whitelistFreeRegister(request, signature); + + request.name = unicode"alice🐻‍❄️-free-whitelisted"; + registrar.whitelistFreeRegister(request, signature); + assertEq(baseRegistrar.ownerOf(uint256(keccak256(bytes(request.name)))), alice); + + // second time fails because the signature has already been used + request.name = unicode"alice🐻‍❄️-free-whitelisted2"; + vm.expectRevert(abi.encodeWithSelector(RegistrarController.FreeMintSignatureAlreadyUsed.selector)); + registrar.whitelistFreeRegister(request, signature); + + // also if you change the name, it fails, because signature is used + request.name = "foooooobar"; + vm.expectRevert(abi.encodeWithSelector(RegistrarController.FreeMintSignatureAlreadyUsed.selector)); + registrar.whitelistFreeRegister(request, signature); + } +}