diff --git a/.env.sample b/.env.sample index 085892c8..412f3aea 100644 --- a/.env.sample +++ b/.env.sample @@ -25,7 +25,8 @@ MONGO_DB_VERSION= ARCHIVE_PREVIOUS_DB_VERSION="true" | "false" # ENV vars for Logger -LOG_LEVEL="debug" | "info" | "warn" | "error +LOG_LEVEL="debug" | "info" | "warn" | "error" + # Removes logger output and does not write to file as well SILENT_LOGGER="false" | "true" @@ -65,6 +66,10 @@ ADMIN_ADDRESSES= MONITOR_CONTRACTS="false" VERIFY_CONTRACTS="false" +# The domain name and version for using EIP712 +EIP712_NAME= +EIP712_VERSION= + DEFENDER_KEY= DEFENDER_SECRET= RELAYER_KEY= diff --git a/contracts/registrar/EIP712Helper.sol b/contracts/registrar/EIP712Helper.sol new file mode 100644 index 00000000..c95d2cb0 --- /dev/null +++ b/contracts/registrar/EIP712Helper.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + +import { IEIP712Helper } from "./IEIP712Helper.sol"; +import { EIP712 } from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + + +contract EIP712Helper is EIP712, IEIP712Helper { + using ECDSA for bytes32; + + // TODO make this real, not the HH rootOwner + // idea around creating signer in `hashCoupon` or similar + // then storing that data, and in recreation we have to get the address that signed? + // how do we bulk sign? + address private constant COUPON_SIGNER = 0x15d34AAf54267DB7D7c367839AAf71A00a2C6A65; + + bytes32 private constant COUPON_TYPEHASH = keccak256( + "Coupon(bytes32 parentHash,address registrantAddress,string domainLabel)" + ); + + constructor( + string memory name, + string memory version + ) EIP712(name, version) {} + + function hashCoupon(Coupon memory coupon) public view override returns (bytes32) { + return + _hashTypedDataV4( + keccak256( + abi.encode( + COUPON_TYPEHASH, + coupon.parentHash, + coupon.registrantAddress, + keccak256(bytes(coupon.domainLabel)) + ) + ) + ); + } + + /** + * @notice Recovers the account that signed a message using openzeppelin's ECDSA library. + * @param coupon The unsigned coupon data + * @param signature The signed message + */ + function recoverSigner(Coupon memory coupon, bytes memory signature) public view override returns (address) { + return _recoverSigner(coupon, signature); + } + + function isCouponSigner(Coupon memory coupon, bytes memory signature) public view override returns (bool) { + address signer = _recoverSigner(coupon, signature); + return signer == COUPON_SIGNER; + } + + function _recoverSigner(Coupon memory coupon, bytes memory signature) internal view returns (address) { + bytes32 hash = hashCoupon(coupon); + return hash.recover(signature); + } +} \ No newline at end of file diff --git a/contracts/registrar/IEIP712Helper.sol b/contracts/registrar/IEIP712Helper.sol new file mode 100644 index 00000000..4556d384 --- /dev/null +++ b/contracts/registrar/IEIP712Helper.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.18; + + +interface IEIP712Helper { + struct Coupon { + bytes32 parentHash; + address registrantAddress; + string domainLabel; + } + + function hashCoupon( + Coupon memory coupon + ) external view returns (bytes32); + + function recoverSigner( + Coupon memory coupon, + bytes memory signature + ) external view returns (address); + + function isCouponSigner( + Coupon memory coupon, + bytes memory signature + ) external view returns (bool); +} \ No newline at end of file diff --git a/contracts/registrar/IZNSSubRegistrar.sol b/contracts/registrar/IZNSSubRegistrar.sol index 1c6574bd..7fea5bc0 100644 --- a/contracts/registrar/IZNSSubRegistrar.sol +++ b/contracts/registrar/IZNSSubRegistrar.sol @@ -4,13 +4,20 @@ pragma solidity 0.8.18; import { IDistributionConfig } from "../types/IDistributionConfig.sol"; import { PaymentConfig } from "../treasury/IZNSTreasury.sol"; import { IZNSPricer } from "../types/IZNSPricer.sol"; - +import { IEIP712Helper } from "./IEIP712Helper.sol"; /** * @title IZNSSubRegistrar.sol - Interface for the ZNSSubRegistrar contract responsible for registering subdomains. */ interface IZNSSubRegistrar is IDistributionConfig { + struct RegistrationArgs { + bytes32 parentHash; + string label; + string tokenURI; + address domainAddress; + } + /** * @notice Emitted when a new `DistributionConfig.pricerContract` is set for a domain. */ @@ -65,24 +72,18 @@ interface IZNSSubRegistrar is IDistributionConfig { AccessType accessType ); - function isMintlistedForDomain( - bytes32 domainHash, - address candidate - ) external view returns (bool); - function initialize( - address _accessController, - address _registry, - address _rootRegistrar + address accessController, + address registry, + address rootRegistrar, + address eip712Helper ) external; function registerSubdomain( - bytes32 parentHash, - string calldata label, - address domainAddress, - string calldata tokenURI, - DistributionConfig calldata configForSubdomains, - PaymentConfig calldata paymentConfig + RegistrationArgs calldata args, + DistributionConfig calldata distrConfig, + PaymentConfig calldata paymentConfig, + bytes calldata signature ) external returns (bytes32); function hashWithParent( @@ -90,6 +91,11 @@ interface IZNSSubRegistrar is IDistributionConfig { string calldata label ) external pure returns (bytes32); + function recoverSigner( + IEIP712Helper.Coupon memory coupon, + bytes memory signature + ) external view returns (address); + function setDistributionConfigForDomain( bytes32 parentHash, DistributionConfig calldata config @@ -110,17 +116,9 @@ interface IZNSSubRegistrar is IDistributionConfig { AccessType accessType ) external; - function updateMintlistForDomain( - bytes32 domainHash, - address[] calldata candidates, - bool[] calldata allowed - ) external; - - function clearMintlistForDomain(bytes32 domainHash) external; - - function clearMintlistAndLock(bytes32 domainHash) external; + function setRegistry(address registry) external; - function setRegistry(address registry_) external; + function setEIP712Helper(address helper) external; - function setRootRegistrar(address registrar_) external; + function setRootRegistrar(address registrar) external; } diff --git a/contracts/registrar/ZNSRootRegistrar.sol b/contracts/registrar/ZNSRootRegistrar.sol index fc744202..9183a275 100644 --- a/contracts/registrar/ZNSRootRegistrar.sol +++ b/contracts/registrar/ZNSRootRegistrar.sol @@ -261,7 +261,8 @@ contract ZNSRootRegistrar is "ZNSRootRegistrar: Not the owner of both Name and Token" ); - subRegistrar.clearMintlistAndLock(domainHash); + // subRegistrar.clearMintlistAndLock(domainHash); + subRegistrar.setAccessTypeForDomain(domainHash, AccessType.LOCKED); _coreRevoke(domainHash, msg.sender); } diff --git a/contracts/registrar/ZNSSubRegistrar.sol b/contracts/registrar/ZNSSubRegistrar.sol index cd01d86a..2cb2be4f 100644 --- a/contracts/registrar/ZNSSubRegistrar.sol +++ b/contracts/registrar/ZNSSubRegistrar.sol @@ -9,7 +9,7 @@ import { ARegistryWired } from "../registry/ARegistryWired.sol"; import { StringUtils } from "../utils/StringUtils.sol"; import { PaymentConfig } from "../treasury/IZNSTreasury.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; - +import { IEIP712Helper } from "./IEIP712Helper.sol"; /** * @title ZNSSubRegistrar.sol - The contract for registering and revoking subdomains of zNS. @@ -32,17 +32,10 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, */ mapping(bytes32 domainHash => DistributionConfig config) public override distrConfigs; - struct Mintlist { - mapping(uint256 idx => mapping(address candidate => bool allowed)) list; - uint256 ownerIndex; - } - /** - * @notice Mapping of domainHash to mintlist set by the domain owner/operator. - * These configs are used to determine who can register subdomains for every parent - * in the case where parent's DistributionConfig.AccessType is set to AccessType.MINTLIST. - */ - mapping(bytes32 domainHash => Mintlist mintStruct) public mintlist; + * @notice Helper for mintlist coupon creation + */ + IEIP712Helper public eip712Helper; modifier onlyOwnerOperatorOrRegistrar(bytes32 domainHash) { require( @@ -61,11 +54,13 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, function initialize( address _accessController, address _registry, - address _rootRegistrar + address _rootRegistrar, + address _eip712Helper ) external override initializer { _setAccessController(_accessController); setRegistry(_registry); setRootRegistrar(_rootRegistrar); + setEIP712Helper(_eip712Helper); } /** @@ -74,59 +69,67 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, * checks if the sender is allowed to register, check if subdomain is available, * acquires the price and other data needed to finalize the registration * and calls the `ZNSRootRegistrar.coreRegister()` to finalize. - * @param parentHash The hash of the parent domain to register the subdomain under - * @param label The label of the subdomain to register (e.g. in 0://zero.child the label would be "child"). - * @param domainAddress (optional) The address to which the subdomain will be resolved to - * @param tokenURI (required) The tokenURI for the subdomain to be registered + * @ args parentHash The hash of the parent domain to register the subdomain under + * @ args label The label of the subdomain to register (e.g. in 0://zero.child the label would be "child"). + * @ args domainAddress (optional) The address to which the subdomain will be resolved to + * @ args tokenURI (required) The tokenURI for the subdomain to be registered + * @param args The above args packed into a struct * @param distrConfig (optional) The distribution config to be set for the subdomain to set rules for children * @param paymentConfig (optional) Payment config for the domain to set on ZNSTreasury in the same tx + * @param signature (optional) The signed message to validate the mintlist claim, if needed * > `paymentConfig` has to be fully filled or all zeros. It is optional as a whole, * but all the parameters inside are required. */ function registerSubdomain( - bytes32 parentHash, - string calldata label, - address domainAddress, - string calldata tokenURI, + RegistrationArgs calldata args, DistributionConfig calldata distrConfig, - PaymentConfig calldata paymentConfig + PaymentConfig calldata paymentConfig, + bytes memory signature ) external override returns (bytes32) { // Confirms string values are only [a-z0-9-] - label.validate(); + args.label.validate(); - bytes32 domainHash = hashWithParent(parentHash, label); + bytes32 domainHash = hashWithParent(args.parentHash, args.label); require( !registry.exists(domainHash), "ZNSSubRegistrar: Subdomain already exists" ); - DistributionConfig memory parentConfig = distrConfigs[parentHash]; + DistributionConfig memory parentConfig = distrConfigs[args.parentHash]; - bool isOwnerOrOperator = registry.isOwnerOrOperator(parentHash, msg.sender); + bool isOwnerOrOperator = registry.isOwnerOrOperator(args.parentHash, msg.sender); require( parentConfig.accessType != AccessType.LOCKED || isOwnerOrOperator, "ZNSSubRegistrar: Parent domain's distribution is locked or parent does not exist" ); + // Not possible to spoof coupons meant for other users if we form data here with msg.sender if (parentConfig.accessType == AccessType.MINTLIST) { + IEIP712Helper.Coupon memory coupon = IEIP712Helper.Coupon({ + parentHash: args.parentHash, + registrantAddress: msg.sender, + domainLabel: args.label + }); + + // If the generated coupon data is incorrect in any way, the wrong address is recovered + // and this will fail the registration here. require( - mintlist[parentHash] - .list - [mintlist[parentHash].ownerIndex] - [msg.sender], - "ZNSSubRegistrar: Sender is not approved for purchase" + rootRegistrar.isOwnerOf(args.parentHash, msg.sender, IZNSRootRegistrar.OwnerOf.BOTH) + || + eip712Helper.isCouponSigner(coupon, signature), + "ZNSSubRegistrar: Invalid claim for mintlist" ); } CoreRegisterArgs memory coreRegisterArgs = CoreRegisterArgs({ - parentHash: parentHash, + parentHash: args.parentHash, domainHash: domainHash, - label: label, + label: args.label, registrant: msg.sender, price: 0, stakeFee: 0, - domainAddress: domainAddress, - tokenURI: tokenURI, + domainAddress: args.domainAddress, + tokenURI: args.tokenURI, isStakePayment: parentConfig.paymentType == PaymentType.STAKE, paymentConfig: paymentConfig }); @@ -135,15 +138,15 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, if (coreRegisterArgs.isStakePayment) { (coreRegisterArgs.price, coreRegisterArgs.stakeFee) = IZNSPricer(address(parentConfig.pricerContract)) .getPriceAndFee( - parentHash, - label, + args.parentHash, + args.label, true ); } else { coreRegisterArgs.price = IZNSPricer(address(parentConfig.pricerContract)) .getPrice( - parentHash, - label, + args.parentHash, + args.label, true ); } @@ -175,6 +178,14 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, ); } + // Receive the coupon already formed + function recoverSigner( + IEIP712Helper.Coupon memory coupon, + bytes memory signature + ) public view override returns (address) { + return eip712Helper.recoverSigner(coupon, signature); + } + /** * @notice Setter for `distrConfigs[domainHash]`. * Only domain owner/operator or ZNSRootRegistrar can call this function. @@ -269,85 +280,32 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, } /** - * @notice Setter for `mintlist[domainHash][candidate]`. Only domain owner/operator can call this function. - * Adds or removes candidates from the mintlist for a domain. Should only be used when the domain's owner - * wants to limit subdomain registration to a specific set of addresses. - * Can be used to add/remove multiple candidates at once. Can only be called by the domain owner/operator. - * Fires `MintlistUpdated` event. - * @param domainHash The domain hash to set the mintlist for - * @param candidates The array of candidates to add/remove - * @param allowed The array of booleans indicating whether to add or remove the candidate + * @notice Sets the registry address in state. + * @dev This function is required for all contracts inheriting `ARegistryWired`. */ - function updateMintlistForDomain( - bytes32 domainHash, - address[] calldata candidates, - bool[] calldata allowed - ) external override { - require( - registry.isOwnerOrOperator(domainHash, msg.sender), - "ZNSSubRegistrar: Not authorized" - ); - - Mintlist storage mintlistForDomain = mintlist[domainHash]; - uint256 ownerIndex = mintlistForDomain.ownerIndex; - - for (uint256 i; i < candidates.length; i++) { - mintlistForDomain.list[ownerIndex][candidates[i]] = allowed[i]; - } - - emit MintlistUpdated(domainHash, ownerIndex, candidates, allowed); - } - - function isMintlistedForDomain( - bytes32 domainHash, - address candidate - ) external view override returns (bool) { - uint256 ownerIndex = mintlist[domainHash].ownerIndex; - return mintlist[domainHash].list[ownerIndex][candidate]; - } - - /* - * @notice Function to completely clear/remove the whole mintlist set for a given domain. - * Can only be called by the owner/operator of the domain or by `ZNSRootRegistrar` as a part of the - * `revokeDomain()` flow. - * Emits `MintlistCleared` event. - * @param domainHash The domain hash to clear the mintlist for - */ - function clearMintlistForDomain(bytes32 domainHash) - public - override - onlyOwnerOperatorOrRegistrar(domainHash) { - mintlist[domainHash].ownerIndex = mintlist[domainHash].ownerIndex + 1; - - emit MintlistCleared(domainHash); - } - - function clearMintlistAndLock(bytes32 domainHash) - external - override - onlyOwnerOperatorOrRegistrar(domainHash) { - setAccessTypeForDomain(domainHash, AccessType.LOCKED); - clearMintlistForDomain(domainHash); + function setRegistry(address registry) public override(ARegistryWired, IZNSSubRegistrar) onlyAdmin { + _setRegistry(registry); } /** - * @notice Sets the registry address in state. - * @dev This function is required for all contracts inheriting `ARegistryWired`. - */ - function setRegistry(address registry_) public override(ARegistryWired, IZNSSubRegistrar) onlyAdmin { - _setRegistry(registry_); + * @notice Set the helper used in cryptographic signing of mintlist data + * @param helper The address of the EIP712 helper to set + */ + function setEIP712Helper(address helper) public override onlyAdmin { + require(helper != address(0), "ZNSSubRegistrar: EIP712Helper can not be 0x0 address"); + eip712Helper = IEIP712Helper(helper); } /** * @notice Setter for `rootRegistrar`. Only admin can call this function. * Fires `RootRegistrarSet` event. - * @param registrar_ The new address of the ZNSRootRegistrar contract + * @param registrar The new address of the ZNSRootRegistrar contract */ - function setRootRegistrar(address registrar_) public override onlyAdmin { - require(registrar_ != address(0), "ZNSSubRegistrar: _registrar can not be 0x0 address"); - rootRegistrar = IZNSRootRegistrar(registrar_); + function setRootRegistrar(address registrar) public override onlyAdmin { + require(registrar != address(0), "ZNSSubRegistrar: _registrar can not be 0x0 address"); + rootRegistrar = IZNSRootRegistrar(registrar); - emit RootRegistrarSet(registrar_); + emit RootRegistrarSet(registrar); } /** diff --git a/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol b/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol index 95269a9f..323ca5e8 100644 --- a/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol +++ b/contracts/upgrade-test-mocks/distribution/ZNSSubRegistrarMock.sol @@ -12,6 +12,8 @@ import { ARegistryWired } from "../../registry/ARegistryWired.sol"; import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; import { StringUtils } from "../../utils/StringUtils.sol"; import { PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { IEIP712Helper } from "../../registrar/IEIP712Helper.sol"; +import { EIP712Helper } from "../../registrar/EIP712Helper.sol"; enum AccessType { @@ -25,6 +27,13 @@ enum PaymentType { STAKE } +struct RegistrationArgs { + bytes32 parentHash; + string label; + string tokenURI; + address domainAddress; +} + struct DistributionConfig { IZNSPricer pricerContract; PaymentType paymentType; @@ -35,11 +44,12 @@ struct DistributionConfig { contract ZNSSubRegistrarMainState { + IZNSRootRegistrar public rootRegistrar; mapping(bytes32 domainHash => DistributionConfig config) public distrConfigs; - mapping(bytes32 domainHash => mapping(address candidate => bool allowed)) public mintlist; + IEIP712Helper public eip712Helper; } @@ -64,81 +74,96 @@ contract ZNSSubRegistrarUpgradeMock is function initialize( address _accessController, address _registry, - address _rootRegistrar + address _rootRegistrar, + address _eip712Helper ) external initializer { - _setAccessController(_accessController); + _setAccessController(_accessController); setRegistry(_registry); setRootRegistrar(_rootRegistrar); + setEIP712Helper(_eip712Helper); } function registerSubdomain( - bytes32 parentHash, - string calldata label, - address domainAddress, - string memory tokenURI, + RegistrationArgs calldata args, DistributionConfig calldata distrConfig, - PaymentConfig calldata paymentConfig - ) external returns (bytes32) { - label.validate(); + PaymentConfig calldata paymentConfig, + bytes memory signature + ) external returns (bytes32) { // TODO replace override again + // Confirms string values are only [a-z0-9-] + args.label.validate(); + + bytes32 domainHash = hashWithParent(args.parentHash, args.label); + require( + !registry.exists(domainHash), + "ZNSSubRegistrar: Subdomain already exists" + ); - DistributionConfig memory parentConfig = distrConfigs[parentHash]; + DistributionConfig memory parentConfig = distrConfigs[args.parentHash]; - bool isOwnerOrOperator = registry.isOwnerOrOperator(parentHash, msg.sender); + bool isOwnerOrOperator = registry.isOwnerOrOperator(args.parentHash, msg.sender); require( parentConfig.accessType != AccessType.LOCKED || isOwnerOrOperator, "ZNSSubRegistrar: Parent domain's distribution is locked or parent does not exist" ); + // Not possible to spoof coupons meant for other users if we form data here with msg.sender if (parentConfig.accessType == AccessType.MINTLIST) { + IEIP712Helper.Coupon memory coupon = IEIP712Helper.Coupon({ + parentHash: args.parentHash, + registrantAddress: msg.sender, + domainLabel: args.label + }); + + // If the generated coupon data is incorrect in any way, the wrong address is recovered + // and this will fail the registration here. require( - mintlist[parentHash][msg.sender], - "ZNSSubRegistrar: Sender is not approved for purchase" + rootRegistrar.isOwnerOf(args.parentHash, msg.sender, IZNSRootRegistrar.OwnerOf.BOTH) + || + eip712Helper.isCouponSigner(coupon, signature), + "ZNSSubRegistrar: Invalid claim for mintlist" ); } CoreRegisterArgs memory coreRegisterArgs = CoreRegisterArgs({ - parentHash: parentHash, - domainHash: hashWithParent(parentHash, label), - label: label, + parentHash: args.parentHash, + domainHash: domainHash, + label: args.label, registrant: msg.sender, price: 0, stakeFee: 0, - domainAddress: domainAddress, - tokenURI: tokenURI, + domainAddress: args.domainAddress, + tokenURI: args.tokenURI, isStakePayment: parentConfig.paymentType == PaymentType.STAKE, paymentConfig: paymentConfig }); - require( - !registry.exists(coreRegisterArgs.domainHash), - "ZNSSubRegistrar: Subdomain already exists" - ); - if (!isOwnerOrOperator) { if (coreRegisterArgs.isStakePayment) { (coreRegisterArgs.price, coreRegisterArgs.stakeFee) = IZNSPricer(address(parentConfig.pricerContract)) - .getPriceAndFee( - parentHash, - label, - true - ); + .getPriceAndFee( + args.parentHash, + args.label, + true + ); } else { coreRegisterArgs.price = IZNSPricer(address(parentConfig.pricerContract)) .getPrice( - parentHash, - label, - true - ); + args.parentHash, + args.label, + true + ); } } rootRegistrar.coreRegister(coreRegisterArgs); + // ! note that the config is set ONLY if ALL values in it are set, specifically, + // without pricerContract being specified, the config will NOT be set if (address(distrConfig.pricerContract) != address(0)) { setDistributionConfigForDomain(coreRegisterArgs.domainHash, distrConfig); } - return coreRegisterArgs.domainHash; + return domainHash; } function hashWithParent( @@ -153,6 +178,19 @@ contract ZNSSubRegistrarUpgradeMock is ); } + // Receive the coupon already formed + function recoverSigner( + IEIP712Helper.Coupon memory coupon, + bytes memory signature + ) public view returns (address) { + return eip712Helper.recoverSigner(coupon, signature); + } + + // TODO temporary while the fixes for zdc haven't been added + function getEIP712AHelperAddress() public view returns (address) { + return address(eip712Helper); + } + function setDistributionConfigForDomain( bytes32 domainHash, DistributionConfig calldata config @@ -208,25 +246,15 @@ contract ZNSSubRegistrarUpgradeMock is _setAccessTypeForDomain(domainHash, accessType); } - function updateMintlistForDomain( - bytes32 domainHash, - address[] calldata candidates, - bool[] calldata allowed - ) external { - require( - registry.isOwnerOrOperator(domainHash, msg.sender), - "ZNSSubRegistrar: Not authorized" - ); - - for (uint256 i; i < candidates.length; i++) { - mintlist[domainHash][candidates[i]] = allowed[i]; - } - } - function setRegistry(address registry_) public override onlyAdmin { _setRegistry(registry_); } + function setEIP712Helper(address helper) public onlyAdmin { + require(helper != address(0), "ZNSSubRegistrar: EIP712Helper can not be 0x0 address"); + eip712Helper = IEIP712Helper(helper); + } + function setRootRegistrar(address registrar_) public onlyAdmin { require(registrar_ != address(0), "ZNSSubRegistrar: _registrar can not be 0x0 address"); rootRegistrar = IZNSRootRegistrar(registrar_); diff --git a/src/deploy/campaign/environments.ts b/src/deploy/campaign/environments.ts index 2dedf03e..e8dbb69a 100644 --- a/src/deploy/campaign/environments.ts +++ b/src/deploy/campaign/environments.ts @@ -1,4 +1,4 @@ -import { HardhatEthersSigner, SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { IZNSCampaignConfig } from "./types"; import { @@ -19,7 +19,6 @@ import { import { ethers } from "ethers"; import { ICurvePriceConfig } from "../missions/types"; import { MeowMainnet } from "../missions/contracts/meow-token/mainnet-data"; -import { DefenderRelaySigner } from "@openzeppelin/defender-sdk-relay-signer-client/lib/ethers"; const getCustomAddresses = ( @@ -114,6 +113,10 @@ export const getConfig = async ({ zeroVaultAddress: zeroVaultAddressConf, mockMeowToken: process.env.MOCK_MEOW_TOKEN === "true", stakingTokenAddress: process.env.STAKING_TOKEN_ADDRESS!, + eip712Config: { + name: process.env.EIP712_NAME ?? "ZNS", + version: process.env.EIP712_VERSION ?? "1.0", + }, postDeploy: { tenderlyProjectSlug: process.env.TENDERLY_PROJECT_SLUG!, monitorContracts: process.env.MONITOR_CONTRACTS === "true", diff --git a/src/deploy/campaign/types.ts b/src/deploy/campaign/types.ts index f3209634..5dda5220 100644 --- a/src/deploy/campaign/types.ts +++ b/src/deploy/campaign/types.ts @@ -3,6 +3,7 @@ import { DefenderRelaySigner } from "@openzeppelin/defender-sdk-relay-signer-cli import { ICurvePriceConfig } from "../missions/types"; import { IContractState, IDeployCampaignConfig } from "@zero-tech/zdc"; import { + EIP712Helper, MeowTokenMock, ZNSAccessController, ZNSAddressResolver, @@ -18,7 +19,7 @@ import { export type IZNSSigner = HardhatEthersSigner | DefenderRelaySigner | SignerWithAddress; -export interface IZNSCampaignConfig extends IDeployCampaignConfig { +export interface IZNSCampaignConfig extends IDeployCampaignConfig { env : string; deployAdmin : Signer; governorAddresses : Array; @@ -33,6 +34,10 @@ export interface IZNSCampaignConfig extends IDeployCampaignConfig extends IDeployCampaignConfig { + eip712Helper : EIP712Helper; accessController : ZNSAccessController; registry : ZNSRegistry; domainToken : ZNSDomainToken; diff --git a/src/deploy/missions/contracts/address-resolver.ts b/src/deploy/missions/contracts/address-resolver.ts index a81e0f0b..a3195065 100644 --- a/src/deploy/missions/contracts/address-resolver.ts +++ b/src/deploy/missions/contracts/address-resolver.ts @@ -1,9 +1,6 @@ import { BaseDeployMission, TDeployArgs, - IHardhatBase, - IProviderBase, - ISignerBase, IContractState, } from "@zero-tech/zdc"; import { ProxyKinds, ResolverTypes } from "../../constants"; import { znsNames } from "./names"; diff --git a/src/deploy/missions/contracts/eip712-helper.ts b/src/deploy/missions/contracts/eip712-helper.ts new file mode 100644 index 00000000..9811694b --- /dev/null +++ b/src/deploy/missions/contracts/eip712-helper.ts @@ -0,0 +1,38 @@ +import { + BaseDeployMission, + TDeployArgs, +} from "@zero-tech/zdc"; +import { znsNames } from "./names"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DefenderRelayProvider } from "@openzeppelin/defender-sdk-relay-signer-client/lib/ethers"; +import { IZNSCampaignConfig, IZNSContracts } from "../../campaign/types"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; + + +export class EIP712HelperDM extends BaseDeployMission< +HardhatRuntimeEnvironment, +SignerWithAddress, +DefenderRelayProvider, +IZNSContracts +> { + proxyData = { + isProxy: false, + }; + + contractName = znsNames.eip712Helper.contract; + instanceName = znsNames.eip712Helper.instance; + + async deployArgs () : Promise { + const { + eip712Config : { + name, + version, + }, + } = this.config as IZNSCampaignConfig; + + return [ + name, + version, + ]; + } +} diff --git a/src/deploy/missions/contracts/index.ts b/src/deploy/missions/contracts/index.ts index 9d86beae..c4c82e8b 100644 --- a/src/deploy/missions/contracts/index.ts +++ b/src/deploy/missions/contracts/index.ts @@ -1,3 +1,4 @@ +export * from "./eip712-helper"; export * from "./address-resolver"; export * from "./registry"; export * from "./root-registrar"; diff --git a/src/deploy/missions/contracts/names.ts b/src/deploy/missions/contracts/names.ts index d9272765..f8fa3fbf 100644 --- a/src/deploy/missions/contracts/names.ts +++ b/src/deploy/missions/contracts/names.ts @@ -47,4 +47,8 @@ export const znsNames = { contract: erc1967ProxyName, instance: "erc1967Proxy", }, + eip712Helper: { + contract: "EIP712Helper", + instance: "eip712Helper", + }, }; diff --git a/src/deploy/missions/contracts/sub-registrar.ts b/src/deploy/missions/contracts/sub-registrar.ts index 88f7a08a..1db4d783 100644 --- a/src/deploy/missions/contracts/sub-registrar.ts +++ b/src/deploy/missions/contracts/sub-registrar.ts @@ -1,5 +1,5 @@ import { - BaseDeployMission, IContractState, IHardhatBase, IProviderBase, ISignerBase, + BaseDeployMission, TDeployArgs, } from "@zero-tech/zdc"; import { ProxyKinds, REGISTRAR_ROLE } from "../../constants"; @@ -32,9 +32,15 @@ IZNSContracts accessController, registry, rootRegistrar, + eip712Helper, } = this.campaign; - return [await accessController.getAddress(), await registry.getAddress(), await rootRegistrar.getAddress()]; + return [ + await accessController.getAddress(), + await registry.getAddress(), + await rootRegistrar.getAddress(), + await eip712Helper.getAddress(), + ]; } async needsPostDeploy () { diff --git a/src/deploy/zns-campaign.ts b/src/deploy/zns-campaign.ts index d3bebab4..557e89b0 100644 --- a/src/deploy/zns-campaign.ts +++ b/src/deploy/zns-campaign.ts @@ -8,10 +8,16 @@ import { } from "@zero-tech/zdc"; import { MeowTokenDM, + EIP712HelperDM, ZNSAccessControllerDM, ZNSAddressResolverDM, - ZNSDomainTokenDM, ZNSCurvePricerDM, ZNSRootRegistrarDM, - ZNSRegistryDM, ZNSTreasuryDM, ZNSFixedPricerDM, ZNSSubRegistrarDM, + ZNSDomainTokenDM, + ZNSCurvePricerDM, + ZNSRootRegistrarDM, + ZNSRegistryDM, + ZNSTreasuryDM, + ZNSFixedPricerDM, + ZNSSubRegistrarDM, } from "./missions/contracts"; import { IZNSCampaignConfig, IZNSContracts } from "./campaign/types"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; @@ -52,6 +58,7 @@ export const runZnsCampaign = async ({ >({ missions: [ ZNSAccessControllerDM, + EIP712HelperDM, ZNSRegistryDM, ZNSDomainTokenDM, MeowTokenDM, diff --git a/test/DeployCampaign.integration.test.ts b/test/DeployCampaign.integration.test.ts index 55ec9684..61b56ed9 100644 --- a/test/DeployCampaign.integration.test.ts +++ b/test/DeployCampaign.integration.test.ts @@ -104,15 +104,8 @@ describe("zNS + zDC Single Integration Test", () => { // Then run this test. The campaign won't be run, but those addresses will be picked up from the DB const campaign = await runZnsCampaign({ config }); - // TODO the zns.zeroVaultAddress is not set internally by the treasury, fix this - // because not new deployment? - // Using config.zeroVaultAddress in funcs for now, which is set properly zns = campaign.state.contracts; - // Surprised this typing works for signer of tx - // await zns.treasury.connect(deployer as unknown as Signer) - // .setBeneficiary(ethers.ZeroHash, config.zeroVaultAddress); - // CurvePricer, stake, open distConfig = { pricerContract: await zns.curvePricer.getAddress(), diff --git a/test/DeployCampaignInt.test.ts b/test/DeployCampaignInt.test.ts index 9b9e570f..d3b9ba24 100644 --- a/test/DeployCampaignInt.test.ts +++ b/test/DeployCampaignInt.test.ts @@ -11,7 +11,7 @@ import { MongoDBAdapter, ITenderlyContractData, TDeployArgs, - VERSION_TYPES, IHardhatBase, ISignerBase, IProviderBase, + VERSION_TYPES, } from "@zero-tech/zdc"; import { DEFAULT_ROYALTY_FRACTION, @@ -25,6 +25,7 @@ import { MONGO_URI_ERR, } from "./helpers"; import { + EIP712HelperDM, MeowTokenDM, meowTokenName, meowTokenSymbol, @@ -46,7 +47,6 @@ import { saveTag } from "../src/utils/git-tag/save-tag"; import { IZNSCampaignConfig, IZNSContracts } from "../src/deploy/campaign/types"; import { HardhatRuntimeEnvironment } from "hardhat/types"; import { DefenderRelayProvider } from "@openzeppelin/defender-sdk-relay-signer-client/lib/ethers"; -import { IZNSContractsLocal } from "./helpers/types"; import { getZnsMongoAdapter } from "../src/deploy/mongo"; @@ -86,6 +86,10 @@ describe("Deploy Campaign Test", () => { }, rootPriceConfig: DEFAULT_PRICE_CONFIG, zeroVaultAddress: zeroVault.address, + eip712Config: { + name: "ZNS", + version: "1.0", + }, stakingTokenAddress: MeowMainnet.address, mockMeowToken: true, postDeploy: { @@ -316,9 +320,9 @@ describe("Deploy Campaign Test", () => { // state should have 10 contracts in it const { state } = nextCampaign; - expect(Object.keys(state.contracts).length).to.equal(10); - expect(Object.keys(state.instances).length).to.equal(10); - expect(state.missions.length).to.equal(10); + expect(Object.keys(state.contracts).length).to.equal(11); + expect(Object.keys(state.instances).length).to.equal(11); + expect(state.missions.length).to.equal(11); // it should deploy AddressResolver expect(await state.contracts.addressResolver.getAddress()).to.be.properAddress; @@ -373,6 +377,10 @@ describe("Deploy Campaign Test", () => { }, rootPriceConfig: DEFAULT_PRICE_CONFIG, zeroVaultAddress: zeroVault.address, + eip712Config: { + name: "ZNS", + version: "1.0", + }, // TODO dep: what do we pass here for test flow? we don't have a deployed MeowToken contract stakingTokenAddress: "", mockMeowToken: true, // 1700083028872 @@ -404,6 +412,7 @@ describe("Deploy Campaign Test", () => { const deployedNames = [ znsNames.accessController, + znsNames.eip712Helper, znsNames.registry, znsNames.domainToken, { @@ -425,6 +434,7 @@ describe("Deploy Campaign Test", () => { await runTest({ missionList: [ ZNSAccessControllerDM, + EIP712HelperDM, ZNSRegistryDM, ZNSDomainTokenDM, MeowTokenDM, @@ -452,6 +462,7 @@ describe("Deploy Campaign Test", () => { const deployedNames = [ znsNames.accessController, + znsNames.eip712Helper, znsNames.registry, znsNames.domainToken, { @@ -469,16 +480,14 @@ describe("Deploy Campaign Test", () => { znsNames.subRegistrar, ]; - const checkPostDeploy = async < - H extends IHardhatBase, - S extends ISignerBase, - P extends IProviderBase, - > (failingCampaign : DeployCampaign< - HardhatRuntimeEnvironment, - SignerWithAddress, - DefenderRelayProvider, - IZNSContracts - >) => { + const checkPostDeploy = async ( + failingCampaign : DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + DefenderRelayProvider, + IZNSContracts + > + ) => { const { // eslint-disable-next-line no-shadow registry, @@ -494,6 +503,7 @@ describe("Deploy Campaign Test", () => { } = await runTest({ missionList: [ ZNSAccessControllerDM, + EIP712HelperDM, ZNSRegistryDM, ZNSDomainTokenDM, MeowTokenDM, @@ -529,6 +539,7 @@ describe("Deploy Campaign Test", () => { const deployedNames = [ znsNames.accessController, + znsNames.eip712Helper, znsNames.registry, znsNames.domainToken, { @@ -550,6 +561,7 @@ describe("Deploy Campaign Test", () => { await runTest({ missionList: [ ZNSAccessControllerDM, + EIP712HelperDM, ZNSRegistryDM, ZNSDomainTokenDM, MeowTokenDM, @@ -577,6 +589,7 @@ describe("Deploy Campaign Test", () => { const deployedNames = [ znsNames.accessController, + znsNames.eip712Helper, znsNames.registry, znsNames.domainToken, { @@ -617,6 +630,7 @@ describe("Deploy Campaign Test", () => { } = await runTest({ missionList: [ ZNSAccessControllerDM, + EIP712HelperDM, ZNSRegistryDM, ZNSDomainTokenDM, MeowTokenDM, @@ -891,6 +905,10 @@ describe("Deploy Campaign Test", () => { rootPriceConfig: DEFAULT_PRICE_CONFIG, zeroVaultAddress: zeroVault.address, stakingTokenAddress: MeowMainnet.address, + eip712Config: { + name: "ZNS", + version: "1.0", + }, mockMeowToken: true, postDeploy: { tenderlyProjectSlug: "", @@ -1072,6 +1090,10 @@ describe("Deploy Campaign Test", () => { rootPriceConfig: DEFAULT_PRICE_CONFIG, zeroVaultAddress: zeroVault.address, stakingTokenAddress: MeowMainnet.address, + eip712Config: { + name: "ZNS", + version: "1.0", + }, mockMeowToken: true, postDeploy: { tenderlyProjectSlug: "", diff --git a/test/ZNSRootRegistrar.test.ts b/test/ZNSRootRegistrar.test.ts index 241fd9e3..0ce301a2 100644 --- a/test/ZNSRootRegistrar.test.ts +++ b/test/ZNSRootRegistrar.test.ts @@ -177,34 +177,15 @@ describe("ZNSRootRegistrar", () => { // Registering as deployer (owner of parent) and user is different gas values await zns.subRegistrar.connect(deployer).registerSubdomain( - domainHash, - "subdomain", - deployer.address, - tokenURI, + { + parentHash: domainHash, + label: "subdomain", + domainAddress: deployer.address, + tokenURI, + }, distrConfigEmpty, paymentConfigEmpty, - ); - - const candidates = [ - deployer.address, - user.address, - governor.address, - admin.address, - randomUser.address, - ]; - - const allowed = [ - true, - true, - true, - true, - true, - ]; - - await zns.subRegistrar.updateMintlistForDomain( - domainHash, - candidates, - allowed + ethers.ZeroHash ); }); @@ -1026,7 +1007,7 @@ describe("ZNSRootRegistrar", () => { expect(balanceAfter).to.eq(balanceBefore + price - protocolFee); }); - it("Revokes a Top level Domain, locks distribution and removes mintlist", async () => { + it("Revokes a Top level Domain, locks distribution", async () => { // Register Top level await defaultRootRegistration({ user, @@ -1044,13 +1025,6 @@ describe("ZNSRootRegistrar", () => { user, }); - // add mintlist to check revocation - await zns.subRegistrar.connect(user).updateMintlistForDomain( - domainHash, - [user.address, zeroVault.address], - [true, true] - ); - const ogPrice = BigInt(135); await zns.fixedPricer.connect(user).setPriceConfig( domainHash, @@ -1085,10 +1059,6 @@ describe("ZNSRootRegistrar", () => { // validate access type has been set to LOCKED const { accessType } = await zns.subRegistrar.distrConfigs(domainHash); expect(accessType).to.eq(AccessType.LOCKED); - - // validate mintlist has been removed - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, user.address)).to.be.false; - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, zeroVault.address)).to.be.false; }); it("Cannot revoke a domain that doesnt exist", async () => { diff --git a/test/ZNSSubRegistrar.test.ts b/test/ZNSSubRegistrar.test.ts index 89d13d79..9de5dc00 100644 --- a/test/ZNSSubRegistrar.test.ts +++ b/test/ZNSSubRegistrar.test.ts @@ -1,5 +1,5 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { IDomainConfigForTest, IFixedPriceConfig, IPathRegResult, IZNSContractsLocal } from "./helpers/types"; +import { Coupon, IDomainConfigForTest, IFixedPriceConfig, IPathRegResult, IZNSContractsLocal } from "./helpers/types"; import { AccessType, ADMIN_ROLE, @@ -20,6 +20,8 @@ import { DECAULT_PRECISION, DEFAULT_PRICE_CONFIG, validateUpgrade, + INVALID_MINTLIST_CLAIM_ERR, + createCouponSignature, } from "./helpers"; import * as hre from "hardhat"; import * as ethers from "ethers"; @@ -61,10 +63,14 @@ describe("ZNSSubRegistrar", () => { describe("Single Subdomain Registration", () => { let rootHash : string; + let rootWithMintlistHash : string; let rootPriceConfig : IFixedPriceConfig; const subTokenURI = "https://token-uri.com/8756a4b6f"; - before(async () => { + // Address of the EIP712 helper contract + let helperAddress : string; + + beforeEach(async () => { [ deployer, zeroVault, @@ -72,6 +78,7 @@ describe("ZNSSubRegistrar", () => { admin, rootOwner, lvl2SubOwner, + lvl3SubOwner, ] = await hre.ethers.getSigners(); // zeroVault address is used to hold the fee charged to the user when registering zns = await deployZNS({ @@ -86,10 +93,13 @@ describe("ZNSSubRegistrar", () => { [ rootOwner, lvl2SubOwner, + lvl3SubOwner, ].map(async ({ address }) => zns.meowToken.mint(address, ethers.parseEther("100000000000"))) ); await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + await zns.meowToken.connect(lvl2SubOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + await zns.meowToken.connect(lvl3SubOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); rootPriceConfig = { price: ethers.parseEther("1375.612"), @@ -114,6 +124,267 @@ describe("ZNSSubRegistrar", () => { priceConfig: rootPriceConfig, }, }); + + rootWithMintlistHash = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "root-mint", + fullConfig: { + distrConfig: { + accessType: AccessType.MINTLIST, + pricerContract: await zns.fixedPricer.getAddress(), + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: rootOwner.address, + }, + priceConfig: rootPriceConfig, + }, + }); + + helperAddress = await zns.eip712Helper.getAddress(); + }); + + it("Recovers the correct address with valid data", async () => { + const coupon : Coupon = { + parentHash: rootWithMintlistHash, + registrantAddress: lvl2SubOwner.address, + domainLabel: "label", + }; + + const signed = await createCouponSignature( + coupon.parentHash, + coupon.registrantAddress, + coupon.domainLabel, + helperAddress, + rootOwner + ); + + const address = await zns.subRegistrar.recoverSigner(coupon, signed); + expect(address).to.eq(rootOwner.address); + }); + + it("Registers a subdomain in a mintlist", async () => { + const sub = "coupon-mintlist-label"; + const signed = await createCouponSignature( + rootWithMintlistHash, + lvl2SubOwner.address, + sub, + helperAddress, + rootOwner + ); + + const tx = zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label: sub, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + signed + ); + + await expect(tx).to.not.be.reverted; + + const hash = await getDomainHashFromEvent({ + zns, + user: lvl2SubOwner, + }); + + expect(await zns.registry.exists(hash)).to.be.true; + }); + + it("Fails to register when caller is not in signed typed data", async () => { + const label = "failing-mintlist"; + + const signed = await createCouponSignature( + rootWithMintlistHash, + lvl2SubOwner.address, + label, + helperAddress, + rootOwner + ); + + // Users cannot use coupons that weren't signed for them + const tx = zns.subRegistrar.connect(lvl3SubOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + signed, + ); + + await expect(tx).to.be.revertedWith(INVALID_MINTLIST_CLAIM_ERR); + }); + + it("Fails to register when using a coupon that's already been used", async () => { + const label = "my-mint-label"; + + const signed = await createCouponSignature( + rootWithMintlistHash, + lvl2SubOwner.address, + label, + helperAddress, + rootOwner, + ); + + const tx = zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + signed + ); + + await expect(tx).to.not.be.reverted; + + // Try to register again with the coupon we just used for a new domain + // The same domain label will fail because of the `registry.exists` check + // already in place + const txReuse = zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label: "diff-new-label", + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + signed + ); + + await expect(txReuse).to.be.revertedWith(INVALID_MINTLIST_CLAIM_ERR); + }); + + it("Fails to register when using the wrong domain hash", async () => { + const label = "uniquelabel"; + const signed = await createCouponSignature( + rootHash, + lvl2SubOwner.address, + label, + helperAddress, + rootOwner, + ); + + const tx = zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + signed + ); + + await expect(tx).to.be.revertedWith(INVALID_MINTLIST_CLAIM_ERR); + }); + + it("Fails to register when the parent domain is locked", async () => { + + await zns.subRegistrar.connect(rootOwner).setAccessTypeForDomain(rootWithMintlistHash, AccessType.LOCKED); + + const label = "moreuniquelabel"; + const signed = await createCouponSignature( + rootWithMintlistHash, + lvl2SubOwner.address, + label, + helperAddress, + rootOwner, + ); + + const tx = zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + signed + ); + + await expect(tx).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); + + // Reset + await zns.subRegistrar.connect(rootOwner).setAccessTypeForDomain(rootWithMintlistHash, AccessType.MINTLIST); + }); + + it("Fails to register when the parent domain has been revoked", async () => { + await zns.rootRegistrar.connect(rootOwner).revokeDomain(rootWithMintlistHash); + + const label = "moreuniquelabel"; + const signed = await createCouponSignature( + rootWithMintlistHash, + lvl2SubOwner.address, + label, + helperAddress, + rootOwner, + ); + + const tx = zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + signed + ); + + await expect(tx).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); + + // Reset by re-registering the domain + rootWithMintlistHash = await registrationWithSetup({ + zns, + user: rootOwner, + domainLabel: "root-mint", + fullConfig: { + distrConfig: { + accessType: AccessType.MINTLIST, + pricerContract: await zns.fixedPricer.getAddress(), + paymentType: PaymentType.STAKE, + }, + paymentConfig: { + token: await zns.meowToken.getAddress(), + beneficiary: rootOwner.address, + }, + priceConfig: rootPriceConfig, + }, + }); + }); + + it("Owners of both can register in a mintlist without a valid coupon", async () => { + const label = "owner-unique-sub"; + + const tx = zns.subRegistrar.connect(rootOwner).registerSubdomain( + { + parentHash: rootWithMintlistHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, + distrConfigEmpty, + paymentConfigEmpty, + ethers.ZeroHash + ); + + await expect(tx).to.not.be.reverted; }); it("Sets the payment config when given", async () => { @@ -122,15 +393,18 @@ describe("ZNSSubRegistrar", () => { await zns.meowToken.connect(lvl2SubOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); await zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - rootHash, - subdomain, - lvl2SubOwner.address, - subTokenURI, + { + parentHash: rootHash, + label: subdomain, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, { token: await zns.meowToken.getAddress(), beneficiary: lvl2SubOwner.address, }, + ethers.ZeroHash ); const subHash = await zns.subRegistrar.hashWithParent(rootHash, subdomain); @@ -143,12 +417,15 @@ describe("ZNSSubRegistrar", () => { const subdomain = "not-world-subdomain"; await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - rootHash, - subdomain, - lvl2SubOwner.address, - subTokenURI, + { + parentHash: rootHash, + label: subdomain, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, - paymentConfigEmpty + paymentConfigEmpty, + ethers.ZeroHash ) ); @@ -185,15 +462,18 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - newRootHash, - "subunset", - lvl2SubOwner.address, - subTokenURI, + { + parentHash: newRootHash, + label: "subunset", + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, { token: await zns.meowToken.getAddress(), beneficiary: rootOwner.address, }, + ethers.ZeroHash ) ).to.be.revertedWith( "ZNSFixedPricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" @@ -224,12 +504,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - newRootHash, - "subunset", - lvl2SubOwner.address, - subTokenURI, + { + parentHash: newRootHash, + label: "subunset", + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( "ZNSCurvePricer: parent's price config has not been set properly through IZNSPricer.setPriceConfig()" @@ -274,6 +557,15 @@ describe("ZNSSubRegistrar", () => { // While "to.not.be.reverted" isn't really a full "test" // we don't emit a custom event here, only in the `rootRegistrar.coreRegister` // call. So we can't use the `.to.emit` syntax + + const signed = await createCouponSignature( + rootHash, + lvl2SubOwner.address, + alphaNumeric, + helperAddress, + rootOwner + ); + await expect(defaultSubdomainRegistration( { user: lvl2SubOwner, @@ -283,6 +575,7 @@ describe("ZNSSubRegistrar", () => { domainContent: lvl2SubOwner.address, tokenURI: subTokenURI, distrConfig: distrConfigEmpty, + signature: signed, } )).to.not.be.reverted; }); @@ -293,6 +586,14 @@ describe("ZNSSubRegistrar", () => { const nameC = "!%$#^*?!#👍3^29"; const nameD = "wo.rld"; + let signature = await createCouponSignature( + rootHash, + lvl2SubOwner.address, + nameA, + helperAddress, + rootOwner + ); + await expect(defaultSubdomainRegistration( { user: lvl2SubOwner, @@ -302,9 +603,18 @@ describe("ZNSSubRegistrar", () => { domainContent: lvl2SubOwner.address, tokenURI: subTokenURI, distrConfig: distrConfigEmpty, + signature, } )).to.be.revertedWith(INVALID_NAME_ERR); + signature = await createCouponSignature( + rootHash, + lvl2SubOwner.address, + nameB, + helperAddress, + rootOwner + ); + await expect(defaultSubdomainRegistration( { user: lvl2SubOwner, @@ -314,9 +624,18 @@ describe("ZNSSubRegistrar", () => { domainContent: lvl2SubOwner.address, tokenURI: subTokenURI, distrConfig: distrConfigEmpty, + signature, } )).to.be.revertedWith(INVALID_NAME_ERR); + signature = await createCouponSignature( + rootHash, + lvl2SubOwner.address, + nameC, + helperAddress, + rootOwner + ); + await expect(defaultSubdomainRegistration( { user: lvl2SubOwner, @@ -326,9 +645,18 @@ describe("ZNSSubRegistrar", () => { domainContent: lvl2SubOwner.address, tokenURI: subTokenURI, distrConfig: distrConfigEmpty, + signature, } )).to.be.revertedWith(INVALID_NAME_ERR); + signature = await createCouponSignature( + rootHash, + lvl2SubOwner.address, + nameD, + helperAddress, + rootOwner + ); + await expect(defaultSubdomainRegistration( { user: lvl2SubOwner, @@ -338,6 +666,7 @@ describe("ZNSSubRegistrar", () => { domainContent: lvl2SubOwner.address, tokenURI: subTokenURI, distrConfig: distrConfigEmpty, + signature, } )).to.be.revertedWith(INVALID_NAME_ERR); }); @@ -346,12 +675,15 @@ describe("ZNSSubRegistrar", () => { // check that 0x0 hash can NOT be passed as parentHash await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - ethers.ZeroHash, - "sub", - lvl2SubOwner.address, - subTokenURI, + { + parentHash: ethers.ZeroHash, + label: "sub", + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( DISTRIBUTION_LOCKED_NOT_EXIST_ERR @@ -361,12 +693,15 @@ describe("ZNSSubRegistrar", () => { const randomHash = ethers.keccak256(ethers.toUtf8Bytes("random")); await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - randomHash, - "sub", - lvl2SubOwner.address, - subTokenURI, + { + parentHash: randomHash, + label: "sub", + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( DISTRIBUTION_LOCKED_NOT_EXIST_ERR @@ -440,12 +775,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - rootHash, - label, - lvl2SubOwner.address, - subTokenURI, + { + parentHash: rootHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( "ERC20: transfer amount exceeds balance" @@ -464,12 +802,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - rootHash, - label, - lvl2SubOwner.address, - subTokenURI, + { + parentHash: rootHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: subTokenURI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroAddress ) ).to.be.revertedWith( "ERC20: insufficient allowance" @@ -555,6 +896,7 @@ describe("ZNSSubRegistrar", () => { describe("Operations within domain paths", () => { let domainConfigs : Array; let regResults : Array; + let helperAddress : string; const fixedPrice = ethers.parseEther("1375.612"); const fixedFeePercentage = BigInt(200); @@ -601,6 +943,8 @@ describe("ZNSSubRegistrar", () => { ); await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + helperAddress = await zns.eip712Helper.getAddress(); + domainConfigs = [ { user: rootOwner, @@ -901,16 +1245,9 @@ describe("ZNSSubRegistrar", () => { ); }); - it("should revoke lvl 6 domain without refund, lock registration and remove mintlist", async () => { + it("should revoke lvl 6 domain without refund and lock registration", async () => { const domainHash = regResults[5].domainHash; - // add to mintlist - await zns.subRegistrar.connect(lvl6SubOwner).updateMintlistForDomain( - domainHash, - [lvl6SubOwner.address, lvl2SubOwner.address], - [true, true] - ); - const userBalBefore = await zns.meowToken.balanceOf(lvl6SubOwner.address); await zns.rootRegistrar.connect(lvl6SubOwner).revokeDomain( @@ -926,18 +1263,17 @@ describe("ZNSSubRegistrar", () => { const { accessType: accessTypeFromSC } = await zns.subRegistrar.distrConfigs(domainHash); expect(accessTypeFromSC).to.eq(AccessType.LOCKED); - // make sure that mintlist has been removed - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, lvl6SubOwner.address)).to.eq(false); - expect(await zns.subRegistrar.isMintlistedForDomain(domainHash, lvl2SubOwner.address)).to.eq(false); - await expect( zns.subRegistrar.connect(lvl6SubOwner).registerSubdomain( - domainHash, - "newsubdomain", - lvl6SubOwner.address, - DEFAULT_TOKEN_URI, + { + parentHash: domainHash, + label: "newsubdomain", + domainAddress: lvl6SubOwner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( DISTRIBUTION_LOCKED_NOT_EXIST_ERR @@ -1002,12 +1338,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(lvl6SubOwner).registerSubdomain( - domainHash, - "newsubdomain", - lvl6SubOwner.address, - DEFAULT_TOKEN_URI, + { + parentHash: domainHash, + label: "newsubdomain", + domainAddress: lvl6SubOwner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( DISTRIBUTION_LOCKED_NOT_EXIST_ERR @@ -1240,12 +1579,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(branchLvl1Owner).registerSubdomain( - lvl1Hash, - "newsubdomain", - branchLvl1Owner.address, - DEFAULT_TOKEN_URI, + { + parentHash: lvl1Hash, + label: "newsubdomain", + domainAddress: branchLvl1Owner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); @@ -1272,12 +1614,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(branchLvl2Owner).registerSubdomain( - lvl4Hash, - "newsubdomain", - branchLvl2Owner.address, - DEFAULT_TOKEN_URI, + { + parentHash: lvl4Hash, + label: "newsubdomain", + domainAddress: branchLvl2Owner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith(DISTRIBUTION_LOCKED_NOT_EXIST_ERR); }); @@ -1321,25 +1666,29 @@ describe("ZNSSubRegistrar", () => { expect(newHash).to.eq(regResults[1].domainHash); - // add new child owner to mintlist - await zns.subRegistrar.connect(branchLvl1Owner).updateMintlistForDomain( - newHash, - [ branchLvl2Owner.address ], - [ true ], - ); const parentOwnerFromReg = await zns.registry.getDomainOwner(newHash); expect(parentOwnerFromReg).to.eq(branchLvl1Owner.address); const childBalBefore = await zns.meowToken.balanceOf(branchLvl2Owner.address); + const label = "newchildddd"; + + const signed = await createCouponSignature( + newHash, + branchLvl2Owner.address, + label, + helperAddress, + rootOwner + ); // try register a new child under the new parent - const newChildHash = await registrationWithSetup({ + const newChildHash = await registrationWithSetup({ // TODO needs to give coupon data when fixed zns, user: branchLvl2Owner, parentHash: newHash, - domainLabel: "newchildddd", + domainLabel: label, fullConfig: fullDistrConfigEmpty, + signature: signed, }); const childBalAfter = await zns.meowToken.balanceOf(branchLvl2Owner.address); @@ -2396,12 +2745,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.registerSubdomain( - subdomainParentHash, - label, - lvl3SubOwner.address, - DEFAULT_TOKEN_URI, + { + parentHash: subdomainParentHash, + label, + domainAddress: lvl3SubOwner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith("ERC20: insufficient allowance"); @@ -2429,6 +2781,7 @@ describe("ZNSSubRegistrar", () => { let domainConfigs : Array; let regResults : Array; let fixedFeePercentage : bigint; + let helperAddress : string; before(async () => { [ @@ -2436,8 +2789,8 @@ describe("ZNSSubRegistrar", () => { zeroVault, governor, admin, - operator, rootOwner, + operator, lvl2SubOwner, lvl3SubOwner, lvl4SubOwner, @@ -2453,6 +2806,8 @@ describe("ZNSSubRegistrar", () => { zeroVaultAddress: zeroVault.address, }); + helperAddress = await zns.eip712Helper.getAddress(); + fixedPrice = ethers.parseEther("397"); fixedFeePercentage = BigInt(200); @@ -2577,12 +2932,15 @@ describe("ZNSSubRegistrar", () => { // try to register child await expect( zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - res[0].domainHash, - "tobedenied", - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, + { + parentHash: res[0].domainHash, + label: "tobedenied", + domainAddress: ethers.ZeroAddress, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( DISTRIBUTION_LOCKED_NOT_EXIST_ERR @@ -2608,12 +2966,15 @@ describe("ZNSSubRegistrar", () => { ); await zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - parentHash, - domainLabel, - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, + { + parentHash, + label: domainLabel, + domainAddress: ethers.ZeroAddress, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ); const hash = await getDomainHashFromEvent({ @@ -2656,11 +3017,13 @@ describe("ZNSSubRegistrar", () => { }, }); - // mintlist potential child user - await zns.subRegistrar.connect(lvl3SubOwner).updateMintlistForDomain( + const label = "mintlisted"; + const signature = await createCouponSignature( parentHash, - [lvl4SubOwner.address], - [true], + lvl4SubOwner.address, + label, + helperAddress, + rootOwner ); // register child @@ -2668,7 +3031,8 @@ describe("ZNSSubRegistrar", () => { zns, user: lvl4SubOwner, parentHash, - domainLabel: "mintlisted", + domainLabel: label, + signature, }); // check registry @@ -2684,129 +3048,57 @@ describe("ZNSSubRegistrar", () => { // try to register child with non-mintlisted user await expect( zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - parentHash, - "notmintlisted", - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, + { + parentHash, + label: "notmintlisted", + domainAddress: ethers.ZeroAddress, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + signature ) ).to.be.revertedWith( - "ZNSSubRegistrar: Sender is not approved for purchase" - ); - - // remove user from mintlist - await zns.subRegistrar.connect(lvl3SubOwner).updateMintlistForDomain( - parentHash, - [lvl4SubOwner.address], - [false], + INVALID_MINTLIST_CLAIM_ERR ); // try to register again await expect( zns.subRegistrar.connect(lvl4SubOwner).registerSubdomain( - parentHash, - "notmintlistednow", - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, + { + parentHash, + label: "notmintlistednow", + domainAddress: ethers.ZeroAddress, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + signature ) ).to.be.revertedWith( - "ZNSSubRegistrar: Sender is not approved for purchase" - ); - }); - - // eslint-disable-next-line max-len - it("#updateMintlistForDomain() should NOT allow setting if called by non-authorized account or registrar", async () => { - const { domainHash } = regResults[1]; - - // assign operator in registry - // to see that he CAN do it - await zns.registry.connect(lvl2SubOwner).setOwnersOperator( - operator.address, - true, - ); - - // try with operator - await zns.subRegistrar.connect(operator).updateMintlistForDomain( - domainHash, - [lvl5SubOwner.address], - [true], - ); - - const mintlisted = await zns.subRegistrar.isMintlistedForDomain( - domainHash, - lvl5SubOwner.address - ); - assert.ok(mintlisted, "User did NOT get mintlisted, but should've"); - - // try with non-authorized - await expect( - zns.subRegistrar.connect(lvl5SubOwner).updateMintlistForDomain( - domainHash, - [lvl5SubOwner.address], - [true], - ) - ).to.be.revertedWith( - "ZNSSubRegistrar: Not authorized" + INVALID_MINTLIST_CLAIM_ERR ); }); - it("#updateMintlistForDomain() should fire a #MintlistUpdated event with correct params", async () => { - const { domainHash } = regResults[1]; - - const candidatesArr = [ - lvl5SubOwner.address, - lvl6SubOwner.address, - lvl3SubOwner.address, - lvl4SubOwner.address, - ]; - - const allowedArr = [ - true, - true, - false, - true, - ]; - - await zns.subRegistrar.connect(lvl2SubOwner).updateMintlistForDomain( - domainHash, - candidatesArr, - allowedArr - ); - - const latestBlock = await time.latestBlock(); - const filter = zns.subRegistrar.filters.MintlistUpdated(domainHash); - const events = await zns.subRegistrar.queryFilter( - filter, - latestBlock - 3, - latestBlock - ); - const event = events[events.length - 1]; - - const ownerIndex = await zns.subRegistrar.mintlist(domainHash); - - expect(event.args?.domainHash).to.eq(domainHash); - expect(event.args?.ownerIndex).to.eq(ownerIndex); - expect(event.args?.candidates).to.deep.eq(candidatesArr); - expect(event.args?.allowed).to.deep.eq(allowedArr); - }); - it("should switch accessType for existing parent domain", async () => { await zns.subRegistrar.connect(lvl2SubOwner).setAccessTypeForDomain( regResults[1].domainHash, AccessType.LOCKED ); + const notAllowedLabel = "notallowed"; + await expect( zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( - regResults[1].domainHash, - "notallowed", - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, + { + parentHash: regResults[1].domainHash, + label: notAllowedLabel, + domainAddress: ethers.ZeroAddress, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( DISTRIBUTION_LOCKED_NOT_EXIST_ERR @@ -2818,13 +3110,6 @@ describe("ZNSSubRegistrar", () => { AccessType.MINTLIST ); - // add to mintlist - await zns.subRegistrar.connect(lvl2SubOwner).updateMintlistForDomain( - regResults[1].domainHash, - [lvl5SubOwner.address], - [true], - ); - const label = "alloweddddd"; // approve @@ -2832,7 +3117,7 @@ describe("ZNSSubRegistrar", () => { expectedPrice, stakeFee, } = getPriceObject( - label, + notAllowedLabel, domainConfigs[1].fullConfig.priceConfig ); const paymentToParent = domainConfigs[1].fullConfig.distrConfig.paymentType === PaymentType.STAKE @@ -2845,14 +3130,24 @@ describe("ZNSSubRegistrar", () => { paymentToParent + protocolFee ); - // register - await zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( + const signature = await createCouponSignature( regResults[1].domainHash, - "alloweddddd", - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, + lvl5SubOwner.address, + label, + helperAddress, + rootOwner + ); + // register, TODO fails here + await zns.subRegistrar.connect(lvl5SubOwner).registerSubdomain( + { + parentHash: regResults[1].domainHash, + label, + domainAddress: ethers.ZeroAddress, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + signature ); const hash = await getDomainHashFromEvent({ @@ -2883,12 +3178,15 @@ describe("ZNSSubRegistrar", () => { await expect( zns.subRegistrar.connect(lvl4SubOwner).registerSubdomain( - parentHash, - "notallowed", - ethers.ZeroAddress, - DEFAULT_TOKEN_URI, + { + parentHash, + label: "notallowed", + domainAddress: ethers.ZeroAddress, + tokenURI: DEFAULT_TOKEN_URI, + }, distrConfigEmpty, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( DISTRIBUTION_LOCKED_NOT_EXIST_ERR @@ -3004,12 +3302,15 @@ describe("ZNSSubRegistrar", () => { it("should NOT allow to register an existing subdomain that has not been revoked", async () => { await expect( zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - regResults[0].domainHash, - domainConfigs[1].domainLabel, - lvl2SubOwner.address, - DEFAULT_TOKEN_URI, + { + parentHash: regResults[0].domainHash, + label: domainConfigs[1].domainLabel, + domainAddress: lvl2SubOwner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, domainConfigs[1].fullConfig.distrConfig, paymentConfigEmpty, + ethers.ZeroHash ) ).to.be.revertedWith( "ZNSSubRegistrar: Subdomain already exists" @@ -3361,6 +3662,7 @@ describe("ZNSSubRegistrar", () => { deployer.address, deployer.address, deployer.address, + deployer.address, ) ).to.be.revertedWith(INITIALIZED_ERR); }); @@ -3497,12 +3799,16 @@ describe("ZNSSubRegistrar", () => { const tx = zns.subRegistrar.connect(deployer).upgradeTo(await newRegistrar.getAddress()); await expect(tx).to.not.be.reverted; - await expect( - zns.subRegistrar.connect(deployer).initialize( - await zns.accessController.getAddress(), - await zns.registry.getAddress(), - await zns.rootRegistrar.getAddress(), - ) + // The subregistrar updated successfully but internally it's type doesn't change to reflect the upgrade mock + // Force this by connecting to the same address using the factory mock + const newSubRegistrar = factory.attach(await zns.subRegistrar.getAddress()) as ZNSSubRegistrarUpgradeMock; + + await expect(newSubRegistrar.connect(deployer).initialize( + await zns.accessController.getAddress(), + await zns.registry.getAddress(), + await zns.rootRegistrar.getAddress(), + await zns.eip712Helper.getAddress(), + ) ).to.be.revertedWith(INITIALIZED_ERR); }); @@ -3576,25 +3882,43 @@ describe("ZNSSubRegistrar", () => { it("Allows to add more fields to the existing struct in a mapping", async () => { // SubRegistrar to upgrade to const factory = new ZNSSubRegistrarUpgradeMock__factory(deployer); - const newRegistrar = await factory.deploy(); - await newRegistrar.waitForDeployment(); - const tx = zns.subRegistrar.connect(deployer).upgradeTo(await newRegistrar.getAddress()); - await expect(tx).to.not.be.reverted; - - // create new proxy object - const newRegistrarProxy = factory.attach(await zns.subRegistrar.getAddress()) as ZNSSubRegistrarUpgradeMock; + const rootConfigBefore = await zns.subRegistrar.distrConfigs(rootHash); - // check values in storage - const rootConfigBefore = await newRegistrarProxy.distrConfigs(rootHash); expect(rootConfigBefore.accessType).to.eq(AccessType.OPEN); expect(rootConfigBefore.pricerContract).to.eq(await zns.fixedPricer.getAddress()); expect(rootConfigBefore.paymentType).to.eq(PaymentType.DIRECT); + const tx = await hre.upgrades.upgradeProxy( + await zns.subRegistrar.getAddress(), + factory, + { + kind: "uups", + } + ); + + // New instance from OZ helper above is still typed as the ZNSSubregistrar, not the new upgrade mock + const newSubRegistrar = await tx.waitForDeployment() as unknown as ZNSSubRegistrarUpgradeMock; + + // Verify change to `dist config` struct is not present even though we upgrade successfully + expect((await zns.subRegistrar.distrConfigs(rootHash)).length).to.eq(3); + expect((await newSubRegistrar.distrConfigs(rootHash)).length).to.eq(5); + + // Check values in storage + const rootConfigAfter = await newSubRegistrar.distrConfigs(rootHash); + + expect(rootConfigAfter.accessType).to.eq(AccessType.OPEN); + expect(rootConfigAfter.pricerContract).to.eq(await zns.fixedPricer.getAddress()); + expect(rootConfigAfter.paymentType).to.eq(PaymentType.DIRECT); + + // New values are present in the config type + expect(rootConfigAfter.newAddress).to.eq(ethers.ZeroAddress); + expect(rootConfigAfter.newUint).to.eq(BigInt(0)); + await zns.meowToken.mint(lvl2SubOwner.address, ethers.parseEther("1000000")); await zns.meowToken.connect(lvl2SubOwner).approve(await zns.treasury.getAddress(), ethers.parseEther("1000000")); - const subConfigToSet = { + const subDistConfig = { accessType: AccessType.MINTLIST, pricerContract: await zns.curvePricer.getAddress(), paymentType: PaymentType.STAKE, @@ -3602,14 +3926,19 @@ describe("ZNSSubRegistrar", () => { newUint: BigInt(1912171236), }; + const label = "subbb"; + // register a subdomain with new logic - await newRegistrarProxy.connect(lvl2SubOwner).registerSubdomain( - rootHash, - "subbb", - lvl2SubOwner.address, - DEFAULT_TOKEN_URI, - subConfigToSet, + await newSubRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootHash, + label, + domainAddress: lvl2SubOwner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, + subDistConfig, paymentConfigEmpty, + ethers.ZeroHash ); const subHash = await getDomainHashFromEvent({ @@ -3617,48 +3946,18 @@ describe("ZNSSubRegistrar", () => { user: lvl2SubOwner, }); - const rootConfigAfter = await zns.subRegistrar.distrConfigs(rootHash); - expect(rootConfigAfter.accessType).to.eq(rootConfigBefore.accessType); - expect(rootConfigAfter.pricerContract).to.eq(rootConfigBefore.pricerContract); - expect(rootConfigAfter.paymentType).to.eq(rootConfigBefore.paymentType); - expect(rootConfigAfter.length).to.eq(3); - - const updatedStructConfig = { - accessType: AccessType.OPEN, - pricerContract: await zns.fixedPricer.getAddress(), - paymentType: PaymentType.DIRECT, - newAddress: lvl2SubOwner.address, - newUint: BigInt(123), - }; - - // try setting new fields to the new struct - await newRegistrarProxy.connect(rootOwner).setDistributionConfigForDomain( - rootHash, - updatedStructConfig - ); - - // check what we got for new - const rootConfigFinal = await newRegistrarProxy.distrConfigs(rootHash); - const subConfigAfter = await newRegistrarProxy.distrConfigs(subHash); - - // validate the new config has been set correctly - expect(subConfigAfter.accessType).to.eq(subConfigToSet.accessType); - expect(subConfigAfter.pricerContract).to.eq(subConfigToSet.pricerContract); - expect(subConfigAfter.paymentType).to.eq(subConfigToSet.paymentType); - expect(subConfigAfter.newAddress).to.eq(subConfigToSet.newAddress); - expect(subConfigAfter.newUint).to.eq(subConfigToSet.newUint); + const subConfig = await newSubRegistrar.distrConfigs(subHash); - // validate the old values stayed the same and new values been added - expect(rootConfigFinal.accessType).to.eq(rootConfigBefore.accessType); - expect(rootConfigFinal.pricerContract).to.eq(rootConfigBefore.pricerContract); - expect(rootConfigFinal.paymentType).to.eq(rootConfigBefore.paymentType); - expect(rootConfigFinal.newAddress).to.eq(updatedStructConfig.newAddress); - expect(rootConfigFinal.newUint).to.eq(updatedStructConfig.newUint); + expect(subConfig.accessType).to.eq(subDistConfig.accessType); + expect(subConfig.pricerContract).to.eq(subDistConfig.pricerContract); + expect(subConfig.paymentType).to.eq(subDistConfig.paymentType); + expect(subConfig.newAddress).to.eq(subDistConfig.newAddress); + expect(subConfig.newUint).to.eq(subDistConfig.newUint); // check that crucial state vars stayed the same - expect(await newRegistrarProxy.getAccessController()).to.eq(await zns.accessController.getAddress()); - expect(await newRegistrarProxy.registry()).to.eq(await zns.registry.getAddress()); - expect(await newRegistrarProxy.rootRegistrar()).to.eq(await zns.rootRegistrar.getAddress()); + expect(await newSubRegistrar.getAccessController()).to.eq(await zns.accessController.getAddress()); + expect(await newSubRegistrar.registry()).to.eq(await zns.registry.getAddress()); + expect(await newSubRegistrar.rootRegistrar()).to.eq(await zns.rootRegistrar.getAddress()); }); }); }); diff --git a/test/gas/TransactionGasCosts.test.ts b/test/gas/TransactionGasCosts.test.ts index d27dd320..1a8a82fd 100644 --- a/test/gas/TransactionGasCosts.test.ts +++ b/test/gas/TransactionGasCosts.test.ts @@ -1,6 +1,12 @@ import { IDistributionConfig, IZNSContractsLocal } from "../helpers/types"; import * as hre from "hardhat"; -import { AccessType, DEFAULT_TOKEN_URI, deployZNS, PaymentType, DEFAULT_PRICE_CONFIG } from "../helpers"; +import { AccessType, + DEFAULT_TOKEN_URI, + deployZNS, + PaymentType, + DEFAULT_PRICE_CONFIG, + createCouponSignature, +} from "../helpers"; import * as ethers from "ethers"; import { registrationWithSetup } from "../helpers/register-setup"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; @@ -20,7 +26,7 @@ describe("Transaction Gas Costs Test", () => { let zns : IZNSContractsLocal; - let rootHashDirect : string; + let rootHash : string; // let rootHashStake : string; let config : IDistributionConfig; @@ -60,15 +66,15 @@ describe("Transaction Gas Costs Test", () => { ); await zns.meowToken.connect(rootOwner).approve(await zns.treasury.getAddress(), ethers.MaxUint256); - rootHashDirect = await registrationWithSetup({ + rootHash = await registrationWithSetup({ zns, user: rootOwner, - domainLabel: "rootdirect", + domainLabel: "rooty-roo", fullConfig: { distrConfig: { - accessType: AccessType.OPEN, + accessType: AccessType.MINTLIST, pricerContract: await zns.curvePricer.getAddress(), - paymentType: PaymentType.DIRECT, + paymentType: PaymentType.STAKE, }, paymentConfig: { token: await zns.meowToken.getAddress(), @@ -140,13 +146,26 @@ describe("Transaction Gas Costs Test", () => { beneficiary: rootOwner.address, }; - const tx = await zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( - rootHashDirect, - "subdomain", + const domainLabel = "label"; + + const signed = await createCouponSignature( + rootHash, lvl2SubOwner.address, - DEFAULT_TOKEN_URI, + domainLabel, + await zns.subRegistrar.eip712Helper(), + rootOwner + ); + + const tx = await zns.subRegistrar.connect(lvl2SubOwner).registerSubdomain( + { + parentHash: rootHash, + label: domainLabel, + domainAddress: lvl2SubOwner.address, + tokenURI: DEFAULT_TOKEN_URI, + }, config, - paymentConfig + paymentConfig, + signed ); const receipt = await tx.wait(); const gasUsed = receipt?.gasUsed as bigint; diff --git a/test/gas/gas-costs.json b/test/gas/gas-costs.json index 0e8224f1..1322ebfb 100644 --- a/test/gas/gas-costs.json +++ b/test/gas/gas-costs.json @@ -1,4 +1,4 @@ { "Root Domain Price": "475352", - "Subdomain Price": "469054" + "Subdomain Price": "539795" } \ No newline at end of file diff --git a/test/helpers/constants.ts b/test/helpers/constants.ts index 54886505..b4953bd9 100644 --- a/test/helpers/constants.ts +++ b/test/helpers/constants.ts @@ -74,6 +74,7 @@ export const fullDistrConfigEmpty = { export const implSlotErc1967 = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; // Contract names +export const eip712HelperName = "EIP712Helper"; export const accessControllerName = "ZNSAccessController"; export const registryName = "ZNSRegistry"; export const domainTokenName = "ZNSDomainToken"; diff --git a/test/helpers/deploy-helpers.ts b/test/helpers/deploy-helpers.ts index 01d1d721..56f67ceb 100644 --- a/test/helpers/deploy-helpers.ts +++ b/test/helpers/deploy-helpers.ts @@ -93,7 +93,7 @@ export const registerRootDomainBulk = async ( ) : Promise => { let index = 0; - for(const domain of domains) { + for (const domain of domains) { const balanceBefore = await zns.meowToken.balanceOf(signers[index].address); const tx = await zns.rootRegistrar.connect(signers[index]).registerRootDomain( domain, @@ -142,12 +142,15 @@ export const registerSubdomainBulk = async ( for (const subdomain of subdomains) { const balanceBefore = await zns.meowToken.balanceOf(signers[index].address); const tx = await zns.subRegistrar.connect(signers[index]).registerSubdomain( - parents[index], - subdomain, - domainAddress, - `${tokenUri}${index}`, + { + parentHash: parents[index], + label: subdomain, + domainAddress, + tokenURI: `${tokenUri}${index}`, + }, distConfig, - paymentConfigEmpty + paymentConfigEmpty, + ethers.ZeroHash ); logger.info("Deploy transaction submitted, waiting..."); diff --git a/test/helpers/deploy/deploy-zns.ts b/test/helpers/deploy/deploy-zns.ts index c2006a9d..7de91132 100644 --- a/test/helpers/deploy/deploy-zns.ts +++ b/test/helpers/deploy/deploy-zns.ts @@ -19,12 +19,15 @@ import { ZNSFixedPricer, ZNSSubRegistrar, MeowTokenMock, + EIP712Helper__factory, + EIP712Helper, } from "../../../typechain"; import { DeployZNSParams, RegistrarConfig, IZNSContractsLocal } from "../types"; import * as hre from "hardhat"; import { ethers, upgrades } from "hardhat"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { + eip712HelperName, accessControllerName, addressResolverName, domainTokenName, @@ -48,6 +51,29 @@ import { ICurvePriceConfig } from "../../../src/deploy/missions/types"; import { meowTokenName, meowTokenSymbol } from "../../../src/deploy/missions/contracts"; import { transparentProxyName } from "../../../src/deploy/missions/contracts/names"; +export const deployEIP712Helper = async ( + deployer : SignerWithAddress, + domainName = "ZNS", + domainVersion = "1.0", + isTenderlyRun = false, +) : Promise => { + const eip712HelperFactory = new EIP712Helper__factory(deployer); + const eip712Helper = await eip712HelperFactory.deploy(domainName, domainVersion); + + await eip712Helper.waitForDeployment(); + const address = await eip712Helper.getAddress(); + + if (isTenderlyRun) { + await hre.tenderly.verify({ + name: eip712HelperName, + address, + }); + + console.log(`AccessController deployed at: ${address}`); + } + + return eip712Helper; +}; export const deployAccessController = async ({ deployer, @@ -453,16 +479,18 @@ export const deployFixedPricer = async ({ export const deploySubRegistrar = async ({ deployer, - accessController, registry, rootRegistrar, + accessController, + eip712Helper, admin, isTenderlyRun = false, } : { deployer : SignerWithAddress; - accessController : ZNSAccessController; registry : ZNSRegistry; + accessController : ZNSAccessController; rootRegistrar : ZNSRootRegistrar; + eip712Helper : EIP712Helper; admin : SignerWithAddress; isTenderlyRun ?: boolean; }) => { @@ -473,6 +501,7 @@ export const deploySubRegistrar = async ({ await accessController.getAddress(), await registry.getAddress(), await rootRegistrar.getAddress(), + await eip712Helper.getAddress(), ], { kind: "uups", @@ -605,16 +634,25 @@ export const deployZNS = async ({ isTenderlyRun, }); + const eip712Helper = await deployEIP712Helper( + deployer, + "ZNS", + "1.0", + isTenderlyRun + ); + const subRegistrar = await deploySubRegistrar({ deployer, accessController, registry, rootRegistrar, + eip712Helper, admin: deployer, isTenderlyRun, }); const znsContracts : IZNSContractsLocal = { + eip712Helper, accessController, registry, domainToken, diff --git a/test/helpers/errors.ts b/test/helpers/errors.ts index 2d6cdf0e..25768e31 100644 --- a/test/helpers/errors.ts +++ b/test/helpers/errors.ts @@ -27,6 +27,9 @@ export const NOT_BOTH_OWNER_RAR_ERR = "ZNSRootRegistrar: Not the owner of both N // Subdomain Registrar // eslint-disable-next-line max-len export const DISTRIBUTION_LOCKED_NOT_EXIST_ERR = "ZNSSubRegistrar: Parent domain's distribution is locked or parent does not exist"; +export const USED_COUPON_ERR = "ZNSSubRegistrar: Coupon already used"; +export const INVALID_MINTLIST_CLAIM_ERR = "ZNSSubRegistrar: Invalid claim for mintlist"; + // StringUtils export const INVALID_NAME_ERR = "StringUtils: Invalid domain label"; diff --git a/test/helpers/hashing.ts b/test/helpers/hashing.ts index b3ea8170..4980a88f 100644 --- a/test/helpers/hashing.ts +++ b/test/helpers/hashing.ts @@ -1,3 +1,6 @@ +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { Coupon } from "./types"; + /* eslint-disable @typescript-eslint/no-var-requires */ const ensjs = require("@ensdomains/ensjs"); const namehash = require("eth-ens-namehash"); @@ -32,3 +35,40 @@ export const hashSubdomainName = (name : string) => { * Hashes last name label only. */ export const hashDomainLabel = (label : string) => ensjs.labelhash(label); + +export const createCouponSignature = async ( + parentHash : string, + registrantAddress : string, + label : string, + verifyingContract : string, // Address of the contract that verifies the coupon + signer : SignerWithAddress +) : Promise => { + const domain = { + name: "ZNS", + version: "1.0", + chainId: (await signer.provider.getNetwork()).chainId, + verifyingContract, + }; + + const types = { + Coupon: [ + { name: "parentHash", type: "bytes32" }, + { name: "registrantAddress", type: "address" }, + { name: "domainLabel", type: "string" }, + ], + }; + + const coupon : Coupon = { + parentHash, + registrantAddress, + domainLabel: label, + }; + + const signedCoupon = await signer.signTypedData( + domain, + types, + coupon + ); + + return signedCoupon; +}; diff --git a/test/helpers/register-setup.ts b/test/helpers/register-setup.ts index 879eb676..05be3440 100644 --- a/test/helpers/register-setup.ts +++ b/test/helpers/register-setup.ts @@ -87,27 +87,32 @@ export const defaultSubdomainRegistration = async ({ zns, parentHash, subdomainLabel, + distrConfig, domainContent = user.address, tokenURI = DEFAULT_TOKEN_URI, - distrConfig, + signature = ethers.ZeroHash, } : { user : SignerWithAddress; zns : IZNSContractsLocal; parentHash : string; subdomainLabel : string; - domainContent ?: string; - tokenURI ?: string; distrConfig : IDistributionConfig; + domainContent : string; + tokenURI : string; + signature : string; }) => { const supplyBefore = await zns.domainToken.totalSupply(); const tx = await zns.subRegistrar.connect(user).registerSubdomain( - parentHash, - subdomainLabel, - domainContent, // Arbitrary address value - tokenURI, + { + parentHash, + label: subdomainLabel, + domainAddress: domainContent, // Arbitrary address value + tokenURI, + }, distrConfig, - paymentConfigEmpty + paymentConfigEmpty, + signature ); const supplyAfter = await zns.domainToken.totalSupply(); @@ -125,15 +130,17 @@ export const registrationWithSetup = async ({ tokenURI = DEFAULT_TOKEN_URI, fullConfig = fullDistrConfigEmpty, setConfigs = true, + signature = ethers.ZeroHash, } : { zns : IZNSContractsLocal; user : SignerWithAddress; - parentHash ?: string; domainLabel : string; + parentHash ?: string; domainContent ?: string; tokenURI ?: string; fullConfig ?: IFullDistributionConfig; setConfigs ?: boolean; + signature ?: string; }) => { const hasConfig = !!fullConfig; const distrConfig = hasConfig @@ -166,6 +173,7 @@ export const registrationWithSetup = async ({ domainContent, tokenURI, distrConfig, + signature, }); } diff --git a/test/helpers/types.ts b/test/helpers/types.ts index 49b03016..f9443e36 100644 --- a/test/helpers/types.ts +++ b/test/helpers/types.ts @@ -1,4 +1,5 @@ import { + EIP712Helper, MeowTokenMock, ZNSAccessController, ZNSAddressResolver, @@ -78,6 +79,7 @@ export interface RegistrarConfig { } export interface IZNSContractsLocal { + eip712Helper : EIP712Helper; accessController : ZNSAccessController; registry : ZNSRegistry; domainToken : ZNSDomainToken; @@ -138,3 +140,9 @@ export interface IPathRegResult { zeroVaultBalanceBefore : bigint; zeroVaultBalanceAfter : bigint; } + +export interface Coupon { + parentHash : string; + registrantAddress : string; + domainLabel : string; +}