Skip to content

Commit

Permalink
add whitelistFreeRegister function
Browse files Browse the repository at this point in the history
  • Loading branch information
lethaale committed Nov 27, 2024
1 parent 6b3e9aa commit 6e13c82
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 11 deletions.
56 changes: 45 additions & 11 deletions src/registrar/Registrar.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -382,13 +388,28 @@ contract RegistrarController is Ownable {
_register(request.registerRequest);
}

/// @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 {
_validateFreeWhitelist(request.owner, signature);
_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 {
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);
}

Expand Down Expand Up @@ -447,16 +468,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(
Expand All @@ -481,6 +493,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
Expand Down
70 changes: 70 additions & 0 deletions test/registrar/FreeWhitelistRegistrar.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 = unicode"alice🐻‍❄️-free-whitelisted";
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);

registrar.whitelistFreeRegister(request, signature);

assertEq(baseRegistrar.ownerOf(uint256(keccak256(bytes(nameToMint)))), alice);

// second time fails because the signature has already been used
vm.expectRevert(abi.encodeWithSelector(RegistrarController.FreeMintSignatureAlreadyUsed.selector));
registrar.whitelistFreeRegister(request, signature);

// also if you change the name, it fails, because signature is used
request = RegistrarController.RegisterRequest({
name: "foooooobar",
owner: alice,
duration: 365 days,
resolver: address(resolver),
data: new bytes[](0),
reverseRecord: true,
referrer: address(0)
});
payload = abi.encode(request.owner);
payloadHash = keccak256(payload);
prefix = "\x19Ethereum Signed Message:\n32";
prefixedHash = keccak256(abi.encodePacked(prefix, payloadHash));

(v, r, s) = vm.sign(signerPk, prefixedHash);

signature = abi.encodePacked(r, s, v);

vm.expectRevert(abi.encodeWithSelector(RegistrarController.FreeMintSignatureAlreadyUsed.selector));
registrar.whitelistFreeRegister(request, signature);
}
}

0 comments on commit 6e13c82

Please sign in to comment.