diff --git a/contracts/registrar/ZNSRootRegistrar.sol b/contracts/registrar/ZNSRootRegistrar.sol index 5cb841e91..3fc9c5150 100644 --- a/contracts/registrar/ZNSRootRegistrar.sol +++ b/contracts/registrar/ZNSRootRegistrar.sol @@ -94,7 +94,7 @@ contract ZNSRootRegistrar is string calldata tokenURI, DistributionConfig calldata distributionConfig, PaymentConfig calldata paymentConfig - ) external override returns (bytes32) { + ) external override onlyAdmin returns (bytes32) { // Confirms string values are only [a-z0-9-] name.validate(); diff --git a/contracts/registrar/ZNSSubRegistrar.sol b/contracts/registrar/ZNSSubRegistrar.sol index b1b8a65a8..a2145f00c 100644 --- a/contracts/registrar/ZNSSubRegistrar.sol +++ b/contracts/registrar/ZNSSubRegistrar.sol @@ -90,7 +90,7 @@ contract ZNSSubRegistrar is AAccessControlled, ARegistryWired, UUPSUpgradeable, string calldata tokenURI, DistributionConfig calldata distrConfig, PaymentConfig calldata paymentConfig - ) external override returns (bytes32) { + ) external override onlyAdmin returns (bytes32) { // Confirms string values are only [a-z0-9-] label.validate(); diff --git a/contracts/upgrade-test-mocks/distribution/ZNSRootRegistrarPostMigrationMock.sol b/contracts/upgrade-test-mocks/distribution/ZNSRootRegistrarPostMigrationMock.sol new file mode 100644 index 000000000..17d59e619 --- /dev/null +++ b/contracts/upgrade-test-mocks/distribution/ZNSRootRegistrarPostMigrationMock.sol @@ -0,0 +1,403 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { AAccessControlled } from "../../access/AAccessControlled.sol"; +import { ARegistryWired } from "../../registry/ARegistryWired.sol"; +import { IZNSRootRegistrar, CoreRegisterArgs } from "../../registrar/IZNSRootRegistrar.sol"; +import { IZNSTreasury, PaymentConfig } from "../../treasury/IZNSTreasury.sol"; +import { IZNSDomainToken } from "../../token/IZNSDomainToken.sol"; +import { IZNSAddressResolver } from "../../resolver/IZNSAddressResolver.sol"; +import { IZNSSubRegistrar } from "../../registrar/IZNSSubRegistrar.sol"; +import { IZNSPricer } from "../../types/IZNSPricer.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { StringUtils } from "../../utils/StringUtils.sol"; +import { ZeroAddressPassed, DomainAlreadyExists } from "../../utils/CommonErrors.sol"; + + +/** + * @title Main entry point for the three main flows of ZNS - Register Root Domain, Reclaim and Revoke any domain. + * @notice This contract serves as the "umbrella" for many ZNS operations, it is given REGISTRAR_ROLE + * to combine multiple calls/operations between different modules to achieve atomic state changes + * and proper logic for the ZNS flows. You can see functions in other modules that are only allowed + * to be called by this contract to ensure proper management of ZNS data in multiple places. + * RRR - Register, Reclaim, Revoke start here and then call other modules to complete the flow. + * ZNSRootRegistrar.sol stores most of the other contract addresses and can communicate with other modules, + * but the relationship is one-sided, where other modules do not need to know about the ZNSRootRegistrar.sol, + * they only check REGISTRAR_ROLE that can, in theory, be assigned to any other address. + * @dev This contract is also called at the last stage of registering subdomains, since it has the common + * logic required to be performed for any level domains. + */ +contract ZNSRootRegistrarPostMigrationMock is + UUPSUpgradeable, + AAccessControlled, + ARegistryWired, + IZNSRootRegistrar { + using StringUtils for string; + + IZNSPricer public rootPricer; + IZNSTreasury public treasury; + IZNSDomainToken public domainToken; + IZNSSubRegistrar public subRegistrar; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Create an instance of the ZNSRootRegistrar.sol + * for registering, reclaiming and revoking ZNS domains + * @dev Instead of direct assignments, we are calling the setter functions + * to apply Access Control and ensure only the ADMIN can set the addresses. + * @param accessController_ Address of the ZNSAccessController contract + * @param registry_ Address of the ZNSRegistry contract + * @param rootPricer_ Address of the IZNSPricer type contract that Zero chose to use for the root domains + * @param treasury_ Address of the ZNSTreasury contract + * @param domainToken_ Address of the ZNSDomainToken contract + */ + function initialize( + address accessController_, + address registry_, + address rootPricer_, + address treasury_, + address domainToken_ + ) external override initializer { + _setAccessController(accessController_); + setRegistry(registry_); + setRootPricer(rootPricer_); + setTreasury(treasury_); + setDomainToken(domainToken_); + } + + /** + * @notice This function is the main entry point for the Register Root Domain flow. + * Registers a new root domain such as `0://wilder`. + * Gets domain hash as a keccak256 hash of the domain label string casted to bytes32, + * checks existence of the domain in the registry and reverts if it exists. + * Calls `ZNSTreasury` to do the staking part, gets `tokenId` for the new token to be minted + * as domain hash casted to uint256, mints the token and sets the domain data in the `ZNSRegistry` + * and, possibly, `ZNSAddressResolver`. Emits a `DomainRegistered` event. + * @param name Name (label) of the domain to register + * @param domainAddress (optional) Address for the `ZNSAddressResolver` to return when requested + * @param tokenURI URI to assign to the Domain Token issued for the domain + * @param distributionConfig (optional) Distribution config for the domain to set in the same tx + * > Please note that passing distribution config will add more gas to the tx and most importantly - + * - the distributionConfig HAS to be passed FULLY filled or all zeros. It is optional as a whole, + * but all the parameters inside are required. + * @param paymentConfig (optional) Payment config for the domain to set on ZNSTreasury in the same tx + * > `paymentConfig` has to be fully filled or all zeros. It is optional as a whole, + * but all the parameters inside are required. + */ + function registerRootDomain( + string calldata name, + address domainAddress, + string calldata tokenURI, + DistributionConfig calldata distributionConfig, + PaymentConfig calldata paymentConfig + ) external override returns (bytes32) { + // Confirms string values are only [a-z0-9-] + name.validate(); + + // Create hash for given domain name + bytes32 domainHash = keccak256(bytes(name)); + + if (registry.exists(domainHash)) + revert DomainAlreadyExists(domainHash); + + // Get price for the domain + uint256 domainPrice = rootPricer.getPrice(0x0, name, true); + + _coreRegister( + CoreRegisterArgs( + bytes32(0), + domainHash, + msg.sender, + domainAddress, + domainPrice, + 0, + name, + tokenURI, + true, + paymentConfig + ) + ); + + if (address(distributionConfig.pricerContract) != address(0)) { + // this adds additional gas to the register tx if passed + subRegistrar.setDistributionConfigForDomain(domainHash, distributionConfig); + } + + return domainHash; + } + + /** + * @notice External function used by `ZNSSubRegistrar` for the final stage of registering subdomains. + * @param args `CoreRegisterArgs`: Struct containing all the arguments required to register a domain + * with ZNSRootRegistrar.coreRegister(): + * + `parentHash`: The hash of the parent domain (0x0 for root domains) + * + `domainHash`: The hash of the domain to be registered + * + `label`: The label of the domain to be registered + * + `registrant`: The address of the user who is registering the domain + * + `price`: The determined price for the domain to be registered based on parent rules + * + `stakeFee`: The determined stake fee for the domain to be registered (only for PaymentType.STAKE!) + * + `domainAddress`: The address to which the domain will be resolved to + * + `tokenURI`: The tokenURI for the domain to be registered + * + `isStakePayment`: A flag for whether the payment is a stake payment or not + */ + function coreRegister( + CoreRegisterArgs memory args + ) external override onlyRegistrar { + _coreRegister( + args + ); + } + + /** + * @dev Internal function that is called by this contract to finalize the registration of a domain. + * This function as also called by the external `coreRegister()` function as a part of + * registration of subdomains. + * This function kicks off payment processing logic, mints the token, sets the domain data in the `ZNSRegistry` + * and fires a `DomainRegistered` event. + * For params see external `coreRegister()` docs. + */ + function _coreRegister( + CoreRegisterArgs memory args + ) internal { + // payment part of the logic + if (args.price > 0) { + _processPayment(args); + } + + // Get tokenId for the new token to be minted for the new domain + uint256 tokenId = uint256(args.domainHash); + // mint token + domainToken.register(args.registrant, tokenId, args.tokenURI); + + // set data on Registry (for all) + Resolver (optional) + // If no domain address is given, only the domain owner is set, otherwise + // `ZNSAddressResolver` is called to assign an address to the newly registered domain. + // If the `domainAddress` is not provided upon registration, a user can call `ZNSAddressResolver.setAddress` + // to set the address themselves. + if (args.domainAddress != address(0)) { + registry.createDomainRecord(args.domainHash, args.registrant, "address"); + + IZNSAddressResolver(registry.getDomainResolver(args.domainHash)) + .setAddress(args.domainHash, args.domainAddress); + } else { + // By passing an empty string we tell the registry to not add a resolver + registry.createDomainRecord(args.domainHash, args.registrant, ""); + } + + // Because we check in the web app for the existance of both values in a payment config, + // it's fine to just check for one here + if (args.paymentConfig.beneficiary != address(0)) { + treasury.setPaymentConfig(args.domainHash, args.paymentConfig); + } + + emit DomainRegistered( + args.parentHash, + args.domainHash, + args.label, + tokenId, + args.tokenURI, + args.registrant, + args.domainAddress + ); + } + + /** + * @dev Internal function that is called by this contract to finalize the payment for a domain. + * Once the specific case is determined and `protocolFee` calculated, it calls ZNSTreasury to perform transfers. + */ + function _processPayment(CoreRegisterArgs memory args) internal { + // args.stakeFee can be 0 + uint256 protocolFee = rootPricer.getFeeForPrice(0x0, args.price + args.stakeFee); + + if (args.isStakePayment) { // for all root domains or subdomains with stake payment + treasury.stakeForDomain( + args.parentHash, + args.domainHash, + args.registrant, + args.price, + args.stakeFee, + protocolFee + ); + } else { // direct payment for subdomains + treasury.processDirectPayment( + args.parentHash, + args.domainHash, + args.registrant, + args.price, + protocolFee + ); + } + } + + /** + * @notice This function is the main entry point for the Revoke flow. + * Revokes a domain such as `0://wilder`. + * Gets `tokenId` from casted domain hash to uint256, calls `ZNSDomainToken` to burn the token, + * deletes the domain data from the `ZNSRegistry` and calls `ZNSTreasury` to unstake and withdraw funds + * user staked for the domain. Emits a `DomainRevoked` event. + * @dev > Note that we are not clearing the data in `ZNSAddressResolver` as it is considered not necessary + * since none other contracts will have the domain data on them. + * If we are not clearing `ZNSAddressResolver` state slots, we are making the next Register transaction + * for the same name cheaper, since SSTORE on a non-zero slot costs 5k gas, + * while SSTORE on a zero slot costs 20k gas. + * If a user wants to clear his data from `ZNSAddressResolver`, he can call `ZNSAddressResolver` directly himself + * BEFORE he calls to revoke, otherwise, `ZNSRegistry` owner check will fail, since the owner there + * will be 0x0 address. + * Also note that in order to Revoke, a caller has to be the owner of both: + * Name (in `ZNSRegistry`) and Token (in `ZNSDomainToken`). + * @param domainHash Hash of the domain to revoke + */ + function revokeDomain(bytes32 domainHash) + external + override + { + if (!isOwnerOf(domainHash, msg.sender, OwnerOf.BOTH)) + revert NotTheOwnerOf(OwnerOf.BOTH, msg.sender, domainHash); + + subRegistrar.clearMintlistAndLock(domainHash); + _coreRevoke(domainHash, msg.sender); + } + + /** + * @dev Internal part of the `revokeDomain()`. Called by this contract to finalize the Revoke flow of all domains. + * It calls `ZNSDomainToken` to burn the token, deletes the domain data from the `ZNSRegistry` and + * calls `ZNSTreasury` to unstake and withdraw funds user staked for the domain. Also emits + * a `DomainRevoked` event. + */ + function _coreRevoke(bytes32 domainHash, address owner) internal { + uint256 tokenId = uint256(domainHash); + domainToken.revoke(tokenId); + registry.deleteRecord(domainHash); + + // check if user registered a domain with the stake + (, uint256 stakedAmount) = treasury.stakedForDomain(domainHash); + bool stakeRefunded = false; + // send the stake back if it exists + if (stakedAmount > 0) { + uint256 protocolFee = rootPricer.getFeeForPrice(0x0, stakedAmount); + + treasury.unstakeForDomain(domainHash, owner, protocolFee); + stakeRefunded = true; + } + + emit DomainRevoked(domainHash, owner, stakeRefunded); + } + + /** + * @notice This function is the main entry point for the Reclaim flow. This flow is used to + * reclaim full ownership of a domain (through becoming the owner of the Name) from the ownership of the Token. + * This is used for different types of ownership transfers, such as: + * - domain sale - a user will sell the Token, then the new owner has to call this function to reclaim the Name + * - domain transfer - a user will transfer the Token, then the new owner + * has to call this function to reclaim the Name + * + * A user needs to only be the owner of the Token to be able to Reclaim. + * Updates the domain owner in the `ZNSRegistry` to the owner of the token and emits a `DomainReclaimed` event. + */ + function reclaimDomain(bytes32 domainHash) + external + override + { + if (!isOwnerOf(domainHash, msg.sender, OwnerOf.TOKEN)) + revert NotTheOwnerOf(OwnerOf.TOKEN, msg.sender, domainHash); + + registry.updateDomainOwner(domainHash, msg.sender); + + emit DomainReclaimed(domainHash, msg.sender); + } + + /** + * @notice Function to validate that a given candidate is the owner of his Name, Token or both. + * @param domainHash Hash of the domain to check + * @param candidate Address of the candidate to check for ownership of the above domain's properties + * @param ownerOf Enum value to determine which ownership to check for: NAME, TOKEN, BOTH + */ + function isOwnerOf(bytes32 domainHash, address candidate, OwnerOf ownerOf) public view override returns (bool) { + if (ownerOf == OwnerOf.NAME) { + return candidate == registry.getDomainOwner(domainHash); + } else if (ownerOf == OwnerOf.TOKEN) { + return candidate == domainToken.ownerOf(uint256(domainHash)); + } else if (ownerOf == OwnerOf.BOTH) { + return candidate == registry.getDomainOwner(domainHash) + && candidate == domainToken.ownerOf(uint256(domainHash)); + } + + revert InvalidOwnerOfEnumValue(ownerOf); + } + + /** + * @notice Setter function for the `ZNSRegistry` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param registry_ Address of the `ZNSRegistry` contract + */ + function setRegistry(address registry_) public override(ARegistryWired, IZNSRootRegistrar) onlyAdmin { + _setRegistry(registry_); + } + + /** + * @notice Setter for the IZNSPricer type contract that Zero chooses to handle Root Domains. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param rootPricer_ Address of the IZNSPricer type contract to set as pricer of Root Domains + */ + function setRootPricer(address rootPricer_) public override onlyAdmin { + if (rootPricer_ == address(0)) + revert ZeroAddressPassed(); + + rootPricer = IZNSPricer(rootPricer_); + + emit RootPricerSet(rootPricer_); + } + + /** + * @notice Setter function for the `ZNSTreasury` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param treasury_ Address of the `ZNSTreasury` contract + */ + function setTreasury(address treasury_) public override onlyAdmin { + if (treasury_ == address(0)) + revert ZeroAddressPassed(); + + treasury = IZNSTreasury(treasury_); + + emit TreasurySet(treasury_); + } + + /** + * @notice Setter function for the `ZNSDomainToken` address in state. + * Only ADMIN in `ZNSAccessController` can call this function. + * @param domainToken_ Address of the `ZNSDomainToken` contract + */ + function setDomainToken(address domainToken_) public override onlyAdmin { + if (domainToken_ == address(0)) + revert ZeroAddressPassed(); + + domainToken = IZNSDomainToken(domainToken_); + + emit DomainTokenSet(domainToken_); + } + + /** + * @notice Setter for `ZNSSubRegistrar` contract. Only ADMIN in `ZNSAccessController` can call this function. + * @param subRegistrar_ Address of the `ZNSSubRegistrar` contract + */ + function setSubRegistrar(address subRegistrar_) external override onlyAdmin { + if (subRegistrar_ == address(0)) + revert ZeroAddressPassed(); + + subRegistrar = IZNSSubRegistrar(subRegistrar_); + emit SubRegistrarSet(subRegistrar_); + } + + /** + * @notice To use UUPS proxy we override this function and revert if `msg.sender` isn't authorized + * @param newImplementation The implementation contract to upgrade to + */ + // solhint-disable-next-line + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/test/DomainMigration.test.ts b/test/DomainMigration.test.ts new file mode 100644 index 000000000..44d292344 --- /dev/null +++ b/test/DomainMigration.test.ts @@ -0,0 +1,335 @@ +import * as hre from "hardhat"; +import { getConfig } from "../src/deploy/campaign/environments"; +import { runZnsCampaign } from "../src/deploy/zns-campaign"; +import * as ethers from "ethers"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { IZNSContracts } from "../src/deploy/campaign/types"; +import { MongoDBAdapter } from "@zero-tech/zdc"; +import { + AC_UNAUTHORIZED_ERR, + AccessType, + ADMIN_ROLE, PARENT_LOCKED_NOT_EXIST_ERR, + hashDomainLabel, + normalizeName, + PaymentType, +} from "./helpers"; +import { IDistributionConfig } from "./helpers/types"; +import { expect } from "chai"; +import { ZeroAddress } from "ethers"; +import { getDomainHashFromEvent } from "./helpers/events"; + + +describe("Domain Migration Flow Test", () => { + let deployer : SignerWithAddress; + let user : SignerWithAddress; + let governor : SignerWithAddress; + let admin : SignerWithAddress; + let randomUser : SignerWithAddress; + + let zns : IZNSContracts; + let zeroVault : SignerWithAddress; + let userBalanceInitial : bigint; + + let mongoAdapter : MongoDBAdapter; + + const tokenURI = "https://example.com/817c64af"; + let rootDistrConfig : IDistributionConfig; + + const defaultDomain = normalizeName("wilder"); + let rootDomainHash : string; + + before(async () => { + // zeroVault address is used to hold the fee charged to the user when registering + [deployer, zeroVault, user, governor, admin, randomUser] = await hre.ethers.getSigners(); + + const config = await getConfig({ + deployer, + zeroVaultAddress: zeroVault.address, + governors: [deployer.address, governor.address], + admins: [deployer.address, admin.address], + }); + + const campaign = await runZnsCampaign({ + config, + }); + + zns = campaign.state.contracts; + + mongoAdapter = campaign.dbAdapter; + + await zns.meowToken.connect(deployer).approve( + await zns.treasury.getAddress(), + ethers.MaxUint256 + ); + + userBalanceInitial = ethers.parseEther("1000000000000000000"); + // Give funds to user + await zns.meowToken.connect(user).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + await zns.meowToken.mint(user.address, userBalanceInitial); + // Give funds to admin + await zns.meowToken.connect(admin).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + await zns.meowToken.mint(admin.address, userBalanceInitial); + + rootDistrConfig = { + pricerContract: await zns.curvePricer.getAddress(), + paymentType: PaymentType.STAKE, + accessType: AccessType.LOCKED, + }; + }); + + after(async () => { + await mongoAdapter.dropDB(); + }); + + it("Should NOT let non Zero Admin account to register ROOT domains", async () => { + // try registering from user address + await expect( + zns.rootRegistrar.connect(user).registerRootDomain( + defaultDomain, + ZeroAddress, + tokenURI, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ) + ).to.be.revertedWithCustomError(zns.accessController, AC_UNAUTHORIZED_ERR) + .withArgs(user.address, ADMIN_ROLE); + }); + + it("Should let Zero Admin to register root domain", async () => { + rootDomainHash = hashDomainLabel(defaultDomain); + + // try registering from zero admin address + await zns.rootRegistrar.connect(admin).registerRootDomain( + defaultDomain, + ZeroAddress, + tokenURI, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ); + + // check domain existence + const owner = await zns.registry.getDomainOwner(rootDomainHash); + expect(owner).to.be.equal(admin.address); + }); + + it("Should NOT let non Zero Admin account to register SUB domains", async () => { + const subdomainLabel = normalizeName("sub"); + + // try registering from user address + await expect( + zns.subRegistrar.connect(user).registerSubdomain( + rootDomainHash, + subdomainLabel, + ZeroAddress, + `${tokenURI}/sub`, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ) + ).to.be.revertedWithCustomError(zns.subRegistrar, PARENT_LOCKED_NOT_EXIST_ERR) + .withArgs(rootDomainHash); + }); + + it("Should let register SUBdomain and subdomain of subdomain from Zero Admin as a root domain owner", async () => { + const subdomainLabel = normalizeName("sub"); + + const adminBalanceBefore = await zns.meowToken.balanceOf(admin.address); + + // try registering from zero admin address + await zns.subRegistrar.connect(admin).registerSubdomain( + rootDomainHash, + subdomainLabel, + ZeroAddress, + `${tokenURI}/sub`, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ); + + // check domain existence + const subDomainHash = await getDomainHashFromEvent({ zns, user: admin }); + const owner = await zns.registry.getDomainOwner(subDomainHash); + expect(owner).to.be.equal(admin.address); + + // check that no tokens were spent + const adminBalanceAfter = await zns.meowToken.balanceOf(admin.address); + expect(adminBalanceAfter - adminBalanceBefore).to.be.equal(0n); + + // register subdomain of subdomain + await zns.subRegistrar.connect(admin).registerSubdomain( + subDomainHash, + `${subdomainLabel}-subb`, + ZeroAddress, + `${tokenURI}/sub2`, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ); + + const subSubDomainHash = await getDomainHashFromEvent({ zns, user: admin }); + const subSubOwner = await zns.registry.getDomainOwner(subSubDomainHash); + expect(subSubOwner).to.be.equal(admin.address); + }); + + // eslint-disable-next-line max-len + it("Should upgrade RootRegistrar to the previous version with unlocked access to root domain registration", async () => { + // get some OG state values + const stateReads = [ + zns.rootRegistrar.registry(), + zns.rootRegistrar.getAccessController(), + zns.rootRegistrar.rootPricer(), + zns.rootRegistrar.treasury(), + zns.rootRegistrar.domainToken(), + zns.rootRegistrar.subRegistrar(), + ]; + + const statePreUpgrade = await Promise.all(stateReads); + + // Confirm deployer has the correct role first + expect(await zns.accessController.isGovernor(deployer.address)).to.equal(true); + + // upgrade to the contract that simalates the previous version of the RootRegistrar + // (the one that we will change to when ready) + + // deploy impl first + const factory = await hre.ethers.getContractFactory("ZNSRootRegistrarPostMigrationMock"); + const newRegistrar = await factory.deploy(); + await newRegistrar.waitForDeployment(); + + // upgrade to the new impl + await zns.rootRegistrar.connect(deployer).upgradeToAndCall(await newRegistrar.getAddress(), "0x"); + + // validate the upgrade + const statePostUpgrade = await Promise.all(stateReads); + + statePostUpgrade.forEach( + (value, index) => { + expect(value).to.be.equal(statePreUpgrade[index]); + } + ); + }); + + it("Should let anyone register ROOT domains after the upgrade", async () => { + const newDomain = normalizeName("newdomain"); + + await zns.rootRegistrar.connect(user).registerRootDomain( + newDomain, + ZeroAddress, + tokenURI, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ); + + const newDomainHash = hashDomainLabel(newDomain); + const owner = await zns.registry.getDomainOwner(newDomainHash); + expect(owner).to.be.equal(user.address); + + // try from another user + + // Give funds to user + await zns.meowToken.connect(randomUser).approve(await zns.treasury.getAddress(), ethers.MaxUint256); + await zns.meowToken.mint(randomUser.address, userBalanceInitial); + + const newNewDomain = normalizeName("newnewdomain"); + await zns.rootRegistrar.connect(randomUser).registerRootDomain( + newNewDomain, + ZeroAddress, + tokenURI, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: randomUser.address, + } + ); + + const newNewDomainHash = hashDomainLabel(newNewDomain); + const owner2 = await zns.registry.getDomainOwner(newNewDomainHash); + expect(owner2).to.be.equal(randomUser.address); + }); + + it("Should send user the token and let user reclaim", async () => { + // send user the domain token + await zns.domainToken.connect(admin).transferFrom(admin.address, user.address, rootDomainHash); + + // check that user has the domain token, but Name is still owned by admin + const tokenOwner = await zns.domainToken.ownerOf(rootDomainHash); + const nameOwner = await zns.registry.getDomainOwner(rootDomainHash); + expect(tokenOwner).to.be.equal(user.address); + expect(nameOwner).to.be.equal(admin.address); + + // try to reclaim the domain + await zns.rootRegistrar.connect(user).reclaimDomain(rootDomainHash); + const nameOwnerAfterReclaim = await zns.registry.getDomainOwner(rootDomainHash); + expect(nameOwnerAfterReclaim).to.be.equal(user.address); + }); + + // eslint-disable-next-line max-len + it("Should give the user full domain access when they reclaim the domain upon receiving it from Zero Admin", async () => { + // user can change distribution config for the domain + const newDistrConfig = { + pricerContract: await zns.fixedPricer.getAddress(), + paymentType: PaymentType.DIRECT, + accessType: AccessType.OPEN, + }; + + await zns.subRegistrar.connect(user).setDistributionConfigForDomain(rootDomainHash, newDistrConfig); + const configFromContract = await zns.subRegistrar.distrConfigs(rootDomainHash); + expect(configFromContract.pricerContract).to.be.equal(newDistrConfig.pricerContract); + expect(configFromContract.paymentType).to.be.equal(newDistrConfig.paymentType); + expect(configFromContract.accessType).to.be.equal(newDistrConfig.accessType); + + // user can change pricing for subdomains + const priceConfig = { + price: ethers.parseEther("7985"), + feePercentage: 13, + isSet: true, + }; + await zns.fixedPricer.connect(user).setPriceConfig(rootDomainHash, priceConfig); + + const priceConfigFromContract = await zns.fixedPricer.priceConfigs(rootDomainHash); + expect(priceConfigFromContract.price).to.be.equal(priceConfig.price); + expect(priceConfigFromContract.feePercentage).to.be.equal(priceConfig.feePercentage); + expect(priceConfigFromContract.isSet).to.be.equal(priceConfig.isSet); + }); + + it("Should let users register SUBdomains under the user reclaimed domain sent by Zero Admin", async () => { + const subdomainLabel = normalizeName("subby"); + + // try registering from user address + await zns.subRegistrar.connect(randomUser).registerSubdomain( + rootDomainHash, + subdomainLabel, + ZeroAddress, + `${tokenURI}/sub`, + rootDistrConfig, + { + token: await zns.meowToken.getAddress(), + beneficiary: user.address, + } + ); + + // check domain existence + const subDomainHash = await getDomainHashFromEvent({ zns, user: randomUser }); + const owner = await zns.registry.getDomainOwner(subDomainHash); + expect(owner).to.be.equal(randomUser.address); + }); + + // TODO mig: test cases: + // 2. Send domain token to user + // 4. User reclaims the full domain and can manage distribution and sell subs +}); diff --git a/test/ZNSSubRegistrar.test.ts b/test/ZNSSubRegistrar.test.ts index ac20e9803..d4d04f390 100644 --- a/test/ZNSSubRegistrar.test.ts +++ b/test/ZNSSubRegistrar.test.ts @@ -6,7 +6,7 @@ import { DEFAULT_TOKEN_URI, deployZNS, distrConfigEmpty, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR, + PARENT_LOCKED_NOT_EXIST_ERR, fullDistrConfigEmpty, getPriceObject, getStakingOrProtocolFee, @@ -366,7 +366,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); // check that a random non-existent hash can NOT be passed as parentHash @@ -382,7 +382,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); }); @@ -956,7 +956,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); const dataFromReg = await zns.registry.getDomainRecord(domainHash); @@ -1031,7 +1031,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); const dataFromReg = await zns.registry.getDomainRecord(domainHash); @@ -1278,7 +1278,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); // register root back for other tests @@ -1313,7 +1313,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); }); @@ -2624,7 +2624,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); }); @@ -2848,7 +2848,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); // switch to mintlist @@ -2931,7 +2931,7 @@ describe("ZNSSubRegistrar", () => { ) ).to.be.revertedWithCustomError( zns.subRegistrar, - DISTRIBUTION_LOCKED_NOT_EXIST_ERR + PARENT_LOCKED_NOT_EXIST_ERR ); }); }); diff --git a/test/helpers/errors.ts b/test/helpers/errors.ts index cbb42de14..3cf213bca 100644 --- a/test/helpers/errors.ts +++ b/test/helpers/errors.ts @@ -30,7 +30,7 @@ export const NOT_OWNER_OF_ERR = "NotTheOwnerOf"; // Subdomain Registrar // eslint-disable-next-line max-len -export const DISTRIBUTION_LOCKED_NOT_EXIST_ERR = "ParentLockedOrDoesntExist"; +export const PARENT_LOCKED_NOT_EXIST_ERR = "ParentLockedOrDoesntExist"; export const SENDER_NOT_APPROVED_ERR = "SenderNotApprovedForPurchase"; // StringUtils diff --git a/test/helpers/types.ts b/test/helpers/types.ts index 49b03016a..085f6f4cc 100644 --- a/test/helpers/types.ts +++ b/test/helpers/types.ts @@ -16,7 +16,7 @@ import { ZNSRegistry, ZNSRegistryUpgradeMock, ZNSRegistryUpgradeMock__factory, - ZNSRootRegistrar, + ZNSRootRegistrar, ZNSRootRegistrarPostMigrationMock, ZNSRootRegistrarPostMigrationMock__factory, ZNSRootRegistrarUpgradeMock, ZNSRootRegistrarUpgradeMock__factory, ZNSSubRegistrar, @@ -53,7 +53,8 @@ export type ZNSContractMockFactory = ZNSTreasuryUpgradeMock__factory | ZNSRegistryUpgradeMock__factory | ZNSAddressResolverUpgradeMock__factory | - ZNSDomainTokenUpgradeMock__factory; + ZNSDomainTokenUpgradeMock__factory | + ZNSRootRegistrarPostMigrationMock__factory; export type ZNSContractMock = ZNSRootRegistrarUpgradeMock | @@ -63,7 +64,8 @@ export type ZNSContractMock = ZNSTreasuryUpgradeMock | ZNSRegistryUpgradeMock | ZNSAddressResolverUpgradeMock | - ZNSDomainTokenUpgradeMock; + ZNSDomainTokenUpgradeMock | + ZNSRootRegistrarPostMigrationMock; export interface IFixedPriceConfig { price : bigint; diff --git a/test/helpers/validate-upgrade.ts b/test/helpers/validate-upgrade.ts index 2cd9cd7c0..784f294eb 100644 --- a/test/helpers/validate-upgrade.ts +++ b/test/helpers/validate-upgrade.ts @@ -2,7 +2,8 @@ import { expect } from "chai"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { ZNSContractMock, ZNSContractMockFactory, GeneralContractGetter } from "./types"; import { ZNSContract } from "../../src/deploy/campaign/types"; -import { MeowToken, ZNSAccessController } from "../../typechain"; +import { ZNSAccessController } from "../../typechain"; +import { MeowToken } from "@zero-tech/ztoken/typechain-js"; export const validateUpgrade = async (