diff --git a/contracts/registrar/IZNSSubRegistrar.sol b/contracts/registrar/IZNSSubRegistrar.sol index a8feb7b55..bf7fbe142 100644 --- a/contracts/registrar/IZNSSubRegistrar.sol +++ b/contracts/registrar/IZNSSubRegistrar.sol @@ -1,140 +1,140 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -import { IDistributionConfig } from "../types/IDistributionConfig.sol"; -import { PaymentConfig } from "../treasury/IZNSTreasury.sol"; -import { IZNSPricer } from "../types/IZNSPricer.sol"; - -/** - * @title IZNSSubRegistrar.sol - Interface for the ZNSSubRegistrar contract responsible for registering subdomains. - */ -interface IZNSSubRegistrar is IDistributionConfig { - /** - * @notice Reverted when someone other than parent owner is trying to buy - a subdomain under the parent that is locked\ - * or when the parent provided does not exist. - */ - error ParentLockedOrDoesntExist(bytes32 parentHash); - - /** - * @notice Reverted when the buyer of subdomain is not approved by the parent in it's mintlist. - */ - error SenderNotApprovedForPurchase(bytes32 parentHash, address sender); - - /** - * @notice Emitted when a new `DistributionConfig.pricerContract` is set for a domain. - */ - event PricerContractSet( - bytes32 indexed domainHash, - address indexed pricerContract - ); - - /** - * @notice Emitted when a new `DistributionConfig.paymentType` is set for a domain. - */ - event PaymentTypeSet(bytes32 indexed domainHash, PaymentType paymentType); - - /** - * @notice Emitted when a new `DistributionConfig.accessType` is set for a domain. - */ - event AccessTypeSet(bytes32 indexed domainHash, AccessType accessType); - - /** - * @notice Emitted when a new full `DistributionConfig` is set for a domain at once. - */ - event DistributionConfigSet( - bytes32 indexed domainHash, - IZNSPricer pricerContract, - PaymentType paymentType, - AccessType accessType - ); - - /** - * @notice Emitted when a `mintlist` is updated for a domain. - */ - event MintlistUpdated( - bytes32 indexed domainHash, - uint256 indexed ownerIndex, - address[] candidates, - bool[] allowed - ); - - /* - * @notice Emitted when a `mintlist` is removed for a domain by the owner or through - * `ZNSRootRegistrar.revokeDomain()`. - */ - event MintlistCleared(bytes32 indexed domainHash); - - /** - * @notice Emitted when the ZNSRootRegistrar address is set in state. - */ - event RootRegistrarSet(address registrar); - - function distrConfigs( - bytes32 domainHash - ) - external view returns ( - IZNSPricer pricerContract, - PaymentType paymentType, - AccessType accessType - ); - - function isMintlistedForDomain( - bytes32 domainHash, - address candidate - ) external view returns (bool); - - function initialize( - address _accessController, - address _registry, - address _rootRegistrar - ) external; - - function registerSubdomain( - bytes32 parentHash, - string calldata label, - address domainAddress, - string calldata tokenURI, - DistributionConfig calldata configForSubdomains, - PaymentConfig calldata paymentConfig - ) external returns (bytes32); - - function hashWithParent( - bytes32 parentHash, - string calldata label - ) external pure returns (bytes32); - - function setDistributionConfigForDomain( - bytes32 parentHash, - DistributionConfig calldata config - ) external; - - function setPricerContractForDomain( - bytes32 domainHash, - IZNSPricer pricerContract - ) external; - - function setPaymentTypeForDomain( - bytes32 domainHash, - PaymentType paymentType - ) external; - - function setAccessTypeForDomain( - bytes32 domainHash, - 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 setRootRegistrar(address registrar_) external; -} +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { IDistributionConfig } from "../types/IDistributionConfig.sol"; +import { PaymentConfig } from "../treasury/IZNSTreasury.sol"; +import { IZNSPricer } from "../types/IZNSPricer.sol"; + + +/** + * @title IZNSSubRegistrar.sol - Interface for the ZNSSubRegistrar contract responsible for registering subdomains. + */ +interface IZNSSubRegistrar is IDistributionConfig { + /** + * @notice Reverted when someone other than parent owner is trying to buy + * a subdomain under the parent that is locked + * or when the parent provided does not exist. + */ + error ParentLockedOrDoesntExist(bytes32 parentHash); + + /** + * @notice Reverted when the buyer of subdomain is not approved by the parent in it's mintlist. + */ + error SenderNotApprovedForPurchase(bytes32 parentHash, address sender); + + /** + * @notice Emitted when a new `DistributionConfig.pricerContract` is set for a domain. + */ + event PricerContractSet( + bytes32 indexed domainHash, + address indexed pricerContract + ); + + /** + * @notice Emitted when a new `DistributionConfig.paymentType` is set for a domain. + */ + event PaymentTypeSet(bytes32 indexed domainHash, PaymentType paymentType); + + /** + * @notice Emitted when a new `DistributionConfig.accessType` is set for a domain. + */ + event AccessTypeSet(bytes32 indexed domainHash, AccessType accessType); + + /** + * @notice Emitted when a new full `DistributionConfig` is set for a domain at once. + */ + event DistributionConfigSet( + bytes32 indexed domainHash, + IZNSPricer pricerContract, + PaymentType paymentType, + AccessType accessType + ); + + /** + * @notice Emitted when a `mintlist` is updated for a domain. + */ + event MintlistUpdated( + bytes32 indexed domainHash, + uint256 indexed ownerIndex, + address[] candidates, + bool[] allowed + ); + + /* + * @notice Emitted when a `mintlist` is removed for a domain by the owner or through + * `ZNSRootRegistrar.revokeDomain()`. + */ + event MintlistCleared(bytes32 indexed domainHash); + + /** + * @notice Emitted when the ZNSRootRegistrar address is set in state. + */ + event RootRegistrarSet(address registrar); + + function distrConfigs( + bytes32 domainHash + ) external view returns ( + IZNSPricer pricerContract, + PaymentType paymentType, + AccessType accessType + ); + + function isMintlistedForDomain( + bytes32 domainHash, + address candidate + ) external view returns (bool); + + function initialize( + address _accessController, + address _registry, + address _rootRegistrar + ) external; + + function registerSubdomain( + bytes32 parentHash, + string calldata label, + address domainAddress, + string calldata tokenURI, + DistributionConfig calldata configForSubdomains, + PaymentConfig calldata paymentConfig + ) external returns (bytes32); + + function hashWithParent( + bytes32 parentHash, + string calldata label + ) external pure returns (bytes32); + + function setDistributionConfigForDomain( + bytes32 parentHash, + DistributionConfig calldata config + ) external; + + function setPricerContractForDomain( + bytes32 domainHash, + IZNSPricer pricerContract + ) external; + + function setPaymentTypeForDomain( + bytes32 domainHash, + PaymentType paymentType + ) external; + + function setAccessTypeForDomain( + bytes32 domainHash, + 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 setRootRegistrar(address registrar_) external; +} diff --git a/contracts/resolver/IZNSStringResolver.sol b/contracts/resolver/IZNSStringResolver.sol new file mode 100644 index 000000000..81ac436fd --- /dev/null +++ b/contracts/resolver/IZNSStringResolver.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + + +interface IZNSStringResolver { + /** + * @param domainHash The identifying hash of a domain's name + * @param newString - content of string type set by the owner/operator to which a domain will resolve to + */ + event StringSet(bytes32 indexed domainHash, string indexed newString); + + function supportsInterface(bytes4 interfaceId) external view returns (bool); + + function resolveDomainString(bytes32 domainHash) external view returns (string memory); + + function setString( + bytes32 domainHash, + string calldata newString + ) external; + + function getInterfaceId() external pure returns (bytes4); + + function setRegistry(address _registry) external; + + function initialize(address _accessController, address _registry) external; +} diff --git a/contracts/resolver/ZNSStringResolver.sol b/contracts/resolver/ZNSStringResolver.sol new file mode 100644 index 000000000..36d932569 --- /dev/null +++ b/contracts/resolver/ZNSStringResolver.sol @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ERC165 } from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import { UUPSUpgradeable } from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { IZNSStringResolver } from "./IZNSStringResolver.sol"; +import { AAccessControlled } from "../access/AAccessControlled.sol"; +import { ARegistryWired } from "../registry/ARegistryWired.sol"; +import { NotAuthorizedForDomain } from "../utils/CommonErrors.sol"; + + +/** + * @title The specific Resolver for ZNS that maps domain hashes to strings. + */ +contract ZNSStringResolver is + UUPSUpgradeable, + AAccessControlled, + ARegistryWired, + ERC165, + IZNSStringResolver { + + mapping(bytes32 domainHash => string resolvedString) internal resolvedStrings; + + /// @custom:oz-upgrades-unsafe-allow constructor + constructor() { + _disableInitializers(); + } + + /** + * @notice Initializer for the `ZNSStringResolver` proxy. + * Note that setter functions are used instead of direct state variable assignments + * to use access control at deploy time. Only ADMIN can call this function. + * @param accessController_ The address of the `ZNSAccessController` contract + * @param registry_ The address of the `ZNSRegistry` contract + */ + function initialize(address accessController_, address registry_) external override initializer { + _setAccessController(accessController_); + setRegistry(registry_); + } + + /** + * @dev Returns string associated with a given domain name hash. + * @param domainHash The identifying hash of a domain's name + */ + function resolveDomainString( + bytes32 domainHash + ) external view override returns (string memory) { + return resolvedStrings[domainHash]; + } + + /** + * @dev Sets the string for a domain name hash. + * @param domainHash The identifying hash of a domain's name + * @param newString The new string to map the domain to + */ + function setString( + bytes32 domainHash, + string calldata newString + ) external override { + // only owner or operator of the current domain can set the string + + if (!registry.isOwnerOrOperator(domainHash, msg.sender)) { + revert NotAuthorizedForDomain(msg.sender, domainHash); + } + + resolvedStrings[domainHash] = newString; + + emit StringSet(domainHash, newString); + } + + /** + * @dev ERC-165 check for implementation identifier + * @dev Supports interfaces IZNSStringResolver and IERC165 + * @param interfaceId ID to check, XOR of the first 4 bytes of each function signature + */ + function supportsInterface( + bytes4 interfaceId + ) public view virtual override(ERC165, IZNSStringResolver) returns (bool) { + return + interfaceId == getInterfaceId() || + super.supportsInterface(interfaceId); + } + + /** + * @dev Exposes IZNSStringResolver interfaceId + */ + function getInterfaceId() public pure override returns (bytes4) { + return type(IZNSStringResolver).interfaceId; + } + + /** + * @dev Sets the address of the `ZNSRegistry` contract that holds all crucial data + * for every domain in the system. This function can only be called by the ADMIN. + * @param _registry The address of the `ZNSRegistry` contract + */ + function setRegistry(address _registry) public override(ARegistryWired, IZNSStringResolver) onlyAdmin { + _setRegistry(_registry); + } + + /** + * @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 no-unused-vars + function _authorizeUpgrade(address newImplementation) internal view override { + accessController.checkGovernor(msg.sender); + } +} diff --git a/contracts/types/IZNSPricer.sol b/contracts/types/IZNSPricer.sol index 9f59dbe9e..7ed3f3330 100644 --- a/contracts/types/IZNSPricer.sol +++ b/contracts/types/IZNSPricer.sol @@ -1,57 +1,58 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.26; - -/** - * @title IZNSPricer.sol - * @notice Base interface required to be inherited by all Pricing contracts to work with zNS - */ -interface IZNSPricer { - /** - * @notice Reverted when someone is trying to buy a subdomain under a parent that is not set up for distribution. - * Specifically it's prices for subdomains. - */ - error ParentPriceConfigNotSet(bytes32 parentHash); - - /** - * @notice Reverted when domain owner is trying to set it's stake fee percentage higher than 100% - (uint256 "10,000"). - */ - error FeePercentageValueTooLarge(uint256 feePercentage, uint256 maximum); - - /** - * @dev `parentHash` param is here to allow pricer contracts - * to have different price configs for different subdomains - * `skipValidityCheck` param is added to provide proper revert when the user is - * calling this to find out the price of a domain that is not valid. But in Registrar contracts - * we want to do this explicitly and before we get the price to have lower tx cost for reverted tx. - * So Registrars will pass this bool as "true" to not repeat the validity check. - * Note that if calling this function directly to find out the price, a user should always pass "false" - * as `skipValidityCheck` param, otherwise, the price will be returned for an invalid label that is not - * possible to register. - */ - function getPrice( - bytes32 parentHash, - string calldata label, - bool skipValidityCheck - ) external view returns (uint256); - - /** - * @dev Fees are only supported for PaymentType.STAKE ! - * This function will NOT be called if PaymentType != PaymentType.STAKE - * Instead `getPrice()` will be called. - */ - function getPriceAndFee( - bytes32 parentHash, - string calldata label, - bool skipValidityCheck - ) external view returns (uint256 price, uint256 fee); - - /** - * @notice Returns the fee for a given price. - * @dev Fees are only supported for PaymentType.STAKE ! - */ - function getFeeForPrice( - bytes32 parentHash, - uint256 price - ) external view returns (uint256); -} +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + + +/** + * @title IZNSPricer.sol + * @notice Base interface required to be inherited by all Pricing contracts to work with zNS + */ +interface IZNSPricer { + /** + * @notice Reverted when someone is trying to buy a subdomain under a parent that is not set up for distribution. + * Specifically it's prices for subdomains. + */ + error ParentPriceConfigNotSet(bytes32 parentHash); + + /** + * @notice Reverted when domain owner is trying to set it's stake fee percentage + * higher than 100% (uint256 "10,000"). + */ + error FeePercentageValueTooLarge(uint256 feePercentage, uint256 maximum); + + /** + * @dev `parentHash` param is here to allow pricer contracts + * to have different price configs for different subdomains + * `skipValidityCheck` param is added to provide proper revert when the user is + * calling this to find out the price of a domain that is not valid. But in Registrar contracts + * we want to do this explicitly and before we get the price to have lower tx cost for reverted tx. + * So Registrars will pass this bool as "true" to not repeat the validity check. + * Note that if calling this function directly to find out the price, a user should always pass "false" + * as `skipValidityCheck` param, otherwise, the price will be returned for an invalid label that is not + * possible to register. + */ + function getPrice( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view returns (uint256); + + /** + * @dev Fees are only supported for PaymentType.STAKE ! + * This function will NOT be called if PaymentType != PaymentType.STAKE + * Instead `getPrice()` will be called. + */ + function getPriceAndFee( + bytes32 parentHash, + string calldata label, + bool skipValidityCheck + ) external view returns (uint256 price, uint256 fee); + + /** + * @notice Returns the fee for a given price. + * @dev Fees are only supported for PaymentType.STAKE ! + */ + function getFeeForPrice( + bytes32 parentHash, + uint256 price + ) external view returns (uint256); +} diff --git a/contracts/upgrade-test-mocks/resolver/ZNSStringResolverMock.sol b/contracts/upgrade-test-mocks/resolver/ZNSStringResolverMock.sol new file mode 100644 index 000000000..e971a5374 --- /dev/null +++ b/contracts/upgrade-test-mocks/resolver/ZNSStringResolverMock.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.26; + +import { ZNSStringResolver } from "../../resolver/ZNSStringResolver.sol"; +import { UpgradeMock } from "../UpgradeMock.sol"; + +/* solhint-disable-next-line */ +contract ZNSStringResolverUpgradeMock is ZNSStringResolver, UpgradeMock {} \ No newline at end of file diff --git a/src/deploy/campaign/environments.ts b/src/deploy/campaign/environments.ts index 1a585b6e8..eba845c9a 100644 --- a/src/deploy/campaign/environments.ts +++ b/src/deploy/campaign/environments.ts @@ -1,4 +1,5 @@ import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; + import { IZNSCampaignConfig } from "./types"; import { DEFAULT_PROTOCOL_FEE_PERCENT, @@ -6,7 +7,7 @@ import { ZNS_DOMAIN_TOKEN_NAME, ZNS_DOMAIN_TOKEN_SYMBOL, DEFAULT_DECIMALS, - DECAULT_PRECISION, + DEFAULT_PRECISION, DEFAULT_PRICE_CONFIG, getCurvePrice, NO_MOCK_PROD_ERR, @@ -227,7 +228,7 @@ const getValidateRootPriceConfig = () => { : DEFAULT_PRICE_CONFIG.baseLength; const decimals = process.env.DECIMALS ? BigInt(process.env.DECIMALS) : DEFAULT_DECIMALS; - const precision = process.env.PRECISION ? BigInt(process.env.PRECISION) : DECAULT_PRECISION; + const precision = process.env.PRECISION ? BigInt(process.env.PRECISION) : DEFAULT_PRECISION; const precisionMultiplier = BigInt(10) ** (decimals - precision); const feePercentage = diff --git a/src/deploy/campaign/types.ts b/src/deploy/campaign/types.ts index f32096341..4c9cce795 100644 --- a/src/deploy/campaign/types.ts +++ b/src/deploy/campaign/types.ts @@ -14,6 +14,7 @@ import { ZNSSubRegistrar, ZNSTreasury, MeowToken, + ZNSStringResolver, } from "../../../typechain"; export type IZNSSigner = HardhatEthersSigner | DefenderRelaySigner | SignerWithAddress; @@ -47,6 +48,7 @@ export type ZNSContract = MeowTokenMock | MeowToken | ZNSAddressResolver | + ZNSStringResolver | ZNSCurvePricer | ZNSTreasury | ZNSRootRegistrar | @@ -59,6 +61,7 @@ export interface IZNSContracts extends IContractState { domainToken : ZNSDomainToken; meowToken : MeowTokenMock; addressResolver : ZNSAddressResolver; + stringResolver : ZNSStringResolver; curvePricer : ZNSCurvePricer; treasury : ZNSTreasury; rootRegistrar : ZNSRootRegistrar; diff --git a/src/deploy/constants.ts b/src/deploy/constants.ts index 3b0bdad4d..50972e8d0 100644 --- a/src/deploy/constants.ts +++ b/src/deploy/constants.ts @@ -28,4 +28,7 @@ export const EXECUTOR_ROLE = ethers.solidityPackedKeccak256( export const ResolverTypes = { address: "address", + // TODO: Which word to use for a string type of resolver?? + // eslint-disable-next-line id-blacklist + string: "string", }; diff --git a/src/deploy/missions/contracts/fixed-pricer.ts b/src/deploy/missions/contracts/fixed-pricer.ts index f8857e66e..3f6e5e263 100644 --- a/src/deploy/missions/contracts/fixed-pricer.ts +++ b/src/deploy/missions/contracts/fixed-pricer.ts @@ -8,7 +8,6 @@ import { HardhatRuntimeEnvironment } from "hardhat/types"; import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; import { IZNSCampaignConfig, IZNSContracts } from "../../campaign/types"; - export class ZNSFixedPricerDM extends BaseDeployMission< HardhatRuntimeEnvironment, SignerWithAddress, diff --git a/src/deploy/missions/contracts/index.ts b/src/deploy/missions/contracts/index.ts index 9d86beae1..2a684a19d 100644 --- a/src/deploy/missions/contracts/index.ts +++ b/src/deploy/missions/contracts/index.ts @@ -1,4 +1,5 @@ export * from "./address-resolver"; +export * from "./string-resolver"; export * from "./registry"; export * from "./root-registrar"; export * from "./domain-token"; diff --git a/src/deploy/missions/contracts/names.ts b/src/deploy/missions/contracts/names.ts index d92727652..c9f7c3dac 100644 --- a/src/deploy/missions/contracts/names.ts +++ b/src/deploy/missions/contracts/names.ts @@ -23,6 +23,10 @@ export const znsNames = { contract: "ZNSAddressResolver", instance: "addressResolver", }, + stringResolver: { + contract: "ZNSStringResolver", + instance: "stringResolver", + }, curvePricer: { contract: "ZNSCurvePricer", instance: "curvePricer", diff --git a/src/deploy/missions/contracts/string-resolver.ts b/src/deploy/missions/contracts/string-resolver.ts new file mode 100644 index 000000000..305aaf5f4 --- /dev/null +++ b/src/deploy/missions/contracts/string-resolver.ts @@ -0,0 +1,66 @@ +import { BaseDeployMission, TDeployArgs } from "@zero-tech/zdc"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { IZNSCampaignConfig, IZNSContracts } from "../../campaign/types"; +import { ProxyKinds, ResolverTypes } from "../../constants"; +import { znsNames } from "./names"; + + +export class ZNSStringResolverDM extends BaseDeployMission< +HardhatRuntimeEnvironment, +SignerWithAddress, +IZNSCampaignConfig, +IZNSContracts +> { + proxyData = { + isProxy: true, + kind: ProxyKinds.uups, + }; + + contractName = znsNames.stringResolver.contract; + instanceName = znsNames.stringResolver.instance; + + async deployArgs () : Promise { + const { accessController, registry } = this.campaign; + + return [ + await accessController.getAddress(), + await registry.getAddress(), + ]; + } + + async needsPostDeploy () { + const { + registry, + stringResolver, + } = this.campaign; + + const resolverInReg = await registry.getResolverType( + ResolverTypes.string, + ); + + const needs = resolverInReg !== await stringResolver.getAddress(); + const msg = needs ? "needs" : "doesn't need"; + + this.logger.debug(`${this.contractName} ${msg} post deploy sequence`); + + return needs; + } + + async postDeploy () { + const { + registry, + stringResolver, + config: { + deployAdmin, + }, + } = this.campaign; + + await registry.connect(deployAdmin).addResolverType( + ResolverTypes.string, + await stringResolver.getAddress(), + ); + + this.logger.debug(`${this.contractName} post deploy sequence completed`); + } +} diff --git a/src/deploy/zns-campaign.ts b/src/deploy/zns-campaign.ts index 0d3a1f5c3..a93dd339c 100644 --- a/src/deploy/zns-campaign.ts +++ b/src/deploy/zns-campaign.ts @@ -9,6 +9,7 @@ import { MeowTokenDM, ZNSAccessControllerDM, ZNSAddressResolverDM, + ZNSStringResolverDM, ZNSDomainTokenDM, ZNSCurvePricerDM, ZNSRootRegistrarDM, ZNSRegistryDM, ZNSTreasuryDM, ZNSFixedPricerDM, ZNSSubRegistrarDM, } from "./missions/contracts"; @@ -52,6 +53,7 @@ export const runZnsCampaign = async ({ ZNSDomainTokenDM, MeowTokenDM, ZNSAddressResolverDM, + ZNSStringResolverDM, ZNSCurvePricerDM, ZNSTreasuryDM, ZNSRootRegistrarDM, diff --git a/test/DeployCampaignInt.test.ts b/test/DeployCampaignInt.test.ts index 98bf69e07..32f86fcb6 100644 --- a/test/DeployCampaignInt.test.ts +++ b/test/DeployCampaignInt.test.ts @@ -1,1179 +1,1181 @@ -/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-ts-comment, max-classes-per-file */ -import * as hre from "hardhat"; -import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; -import { expect } from "chai"; -import { - TLogger, - HardhatDeployer, - DeployCampaign, - resetMongoAdapter, - TDeployMissionCtor, - MongoDBAdapter, - ITenderlyContractData, - TDeployArgs, - VERSION_TYPES, -} from "@zero-tech/zdc"; -import { - DEFAULT_ROYALTY_FRACTION, - DEFAULT_PRICE_CONFIG, - ZNS_DOMAIN_TOKEN_NAME, - ZNS_DOMAIN_TOKEN_SYMBOL, - INVALID_ENV_ERR, - NO_MOCK_PROD_ERR, - STAKING_TOKEN_ERR, - INVALID_CURVE_ERR, - MONGO_URI_ERR, -} from "./helpers"; -import { - MeowTokenDM, - meowTokenName, - meowTokenSymbol, - ZNSAccessControllerDM, - ZNSAddressResolverDM, - ZNSCurvePricerDM, - ZNSDomainTokenDM, ZNSFixedPricerDM, - ZNSRegistryDM, ZNSRootRegistrarDM, ZNSSubRegistrarDM, ZNSTreasuryDM, -} from "../src/deploy/missions/contracts"; -import { znsNames } from "../src/deploy/missions/contracts/names"; -import { runZnsCampaign } from "../src/deploy/zns-campaign"; -import { MeowMainnet } from "../src/deploy/missions/contracts/meow-token/mainnet-data"; -import { ResolverTypes } from "../src/deploy/constants"; -import { getConfig } from "../src/deploy/campaign/environments"; -import { ethers } from "ethers"; -import { promisify } from "util"; -import { exec } from "child_process"; -import { saveTag } from "../src/utils/git-tag/save-tag"; -import { IZNSCampaignConfig, IZNSContracts } from "../src/deploy/campaign/types"; -import { HardhatRuntimeEnvironment } from "hardhat/types"; -import { getZnsMongoAdapter } from "../src/deploy/mongo"; - - -const execAsync = promisify(exec); - -describe("Deploy Campaign Test", () => { - let deployAdmin : SignerWithAddress; - let admin : SignerWithAddress; - let governor : SignerWithAddress; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - let userA : SignerWithAddress; - let userB : SignerWithAddress; - let zeroVault : SignerWithAddress; - let campaignConfig : IZNSCampaignConfig; - - let mongoAdapter : MongoDBAdapter; - - const env = "dev"; - - before(async () => { - [deployAdmin, admin, governor, zeroVault, userA, userB] = await hre.ethers.getSigners(); - }); - - describe("MEOW Token Ops", () => { - before(async () => { - campaignConfig = { - env, - deployAdmin, - governorAddresses: [deployAdmin.address], - adminAddresses: [deployAdmin.address, admin.address], - domainToken: { - name: ZNS_DOMAIN_TOKEN_NAME, - symbol: ZNS_DOMAIN_TOKEN_SYMBOL, - defaultRoyaltyReceiver: deployAdmin.address, - defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, - }, - rootPriceConfig: DEFAULT_PRICE_CONFIG, - zeroVaultAddress: zeroVault.address, - stakingTokenAddress: MeowMainnet.address, - mockMeowToken: true, - postDeploy: { - tenderlyProjectSlug: "", - monitorContracts: false, - verifyContracts: false, - }, - }; - }); - - it("should deploy new MeowTokenMock when `mockMeowToken` is true", async () => { - const campaign = await runZnsCampaign({ - config: campaignConfig, - }); - - const { meowToken, dbAdapter } = campaign; - - const toMint = hre.ethers.parseEther("972315"); - - const balanceBefore = await meowToken.balanceOf(userA.address); - // `mint()` only exists on the Mocked contract - await meowToken.connect(deployAdmin).mint( - userA.address, - toMint - ); - - const balanceAfter = await meowToken.balanceOf(userA.address); - expect(balanceAfter - balanceBefore).to.equal(toMint); - - await dbAdapter.dropDB(); - }); - - it("should use existing deployed non-mocked MeowToken contract when `mockMeowToken` is false", async () => { - campaignConfig.mockMeowToken = false; - - // deploy MeowToken contract - const factory = await hre.ethers.getContractFactory("MeowTokenMock"); - const meow = await hre.upgrades.deployProxy( - factory, - [meowTokenName, meowTokenSymbol], - { - kind: "transparent", - }); - - await meow.waitForDeployment(); - - campaignConfig.stakingTokenAddress = await meow.getAddress(); - - const campaign = await runZnsCampaign({ - config: campaignConfig, - }); - - const { - meowToken, - dbAdapter, - state: { - instances: { - meowToken: meowDMInstance, - }, - }, - } = campaign; - - expect(meowToken.address).to.equal(meow.address); - expect(meowDMInstance.contractName).to.equal(znsNames.meowToken.contract); - - const toMint = hre.ethers.parseEther("972315"); - // `mint()` only exists on the Mocked contract - try { - await meowToken.connect(deployAdmin).mint( - userA.address, - toMint - ); - } catch (e) { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(e.message).to.include( - ".mint is not a function" - ); - } - - // Cannot call to real db to - await dbAdapter.dropDB(); - }); - }); - - describe("Failure Recovery", () => { - const errorMsgDeploy = "FailMissionDeploy"; - const errorMsgPostDeploy = "FailMissionPostDeploy"; - - const loggerMock = { - info: () => { - }, - debug: () => { - }, - error: () => { - }, - }; - - interface IDeployedData { - contract : string; - instance : string; - address ?: string; - } - - const runTest = async ({ - missionList, - placeOfFailure, - deployedNames, - undeployedNames, - failingInstanceName, - callback, - } : { - missionList : Array, - IZNSContracts - >>; - placeOfFailure : string; - deployedNames : Array<{ contract : string; instance : string; }>; - undeployedNames : Array<{ contract : string; instance : string; }>; - failingInstanceName : string; - // eslint-disable-next-line no-shadow - callback ?: (failingCampaign : DeployCampaign< - HardhatRuntimeEnvironment, - SignerWithAddress, - IZNSCampaignConfig, - IZNSContracts - >) => Promise; - }) => { - const deployer = new HardhatDeployer< - HardhatRuntimeEnvironment, - SignerWithAddress - >({ - hre, - signer: deployAdmin, - env, - }); - let dbAdapter = await getZnsMongoAdapter(); - - let toMatchErr = errorMsgDeploy; - if (placeOfFailure === "postDeploy") { - toMatchErr = errorMsgPostDeploy; - } - - const failingCampaign = new DeployCampaign< - HardhatRuntimeEnvironment, - SignerWithAddress, - IZNSCampaignConfig, - IZNSContracts - >({ - missions: missionList, - deployer, - dbAdapter, - // @ts-ignore - logger: loggerMock, - config: campaignConfig, - }); - - try { - await failingCampaign.execute(); - } catch (e) { - // @ts-ignore - expect(e.message).to.include(toMatchErr); - } - - // check the correct amount of contracts in state - const { contracts } = failingCampaign.state; - expect(Object.keys(contracts).length).to.equal(deployedNames.length); - - if (placeOfFailure === "deploy") { - // it should not deploy AddressResolver - expect(contracts[failingInstanceName]).to.be.undefined; - } else { - // it should deploy AddressResolver - expect(await contracts[failingInstanceName].getAddress()).to.be.properAddress; - } - - // check DB to verify we only deployed half - const firstRunDeployed = await deployedNames.reduce( - async ( - acc : Promise>, - { contract, instance } : { contract : string; instance : string; } - ) : Promise> => { - const akk = await acc; - const fromDB = await dbAdapter.getContract(contract); - expect(fromDB?.address).to.be.properAddress; - - return [...akk, { contract, instance, address: fromDB?.address }]; - }, - Promise.resolve([]) - ); - - await undeployedNames.reduce( - async ( - acc : Promise, - { contract, instance } : { contract : string; instance : string; } - ) : Promise => { - await acc; - const fromDB = await dbAdapter.getContract(contract); - const fromState = failingCampaign[instance]; - - expect(fromDB).to.be.null; - expect(fromState).to.be.undefined; - }, - Promise.resolve() - ); - - // call whatever callback we passed before the next campaign run - await callback?.(failingCampaign); - - const { curVersion: initialDbVersion } = dbAdapter; - - // reset mongoAdapter instance to make sure we pick up the correct DB version - resetMongoAdapter(); - - // run Campaign again, but normally - const nextCampaign = await runZnsCampaign({ - config: campaignConfig, - }); - - ({ dbAdapter } = nextCampaign); - - // make sure MongoAdapter is using the correct TEMP version - const { curVersion: nextDbVersion } = dbAdapter; - expect(nextDbVersion).to.equal(initialDbVersion); - - // 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); - // it should deploy AddressResolver - expect(await state.contracts.addressResolver.getAddress()).to.be.properAddress; - - // check DB to verify we deployed everything - const allNames = deployedNames.concat(undeployedNames); - - await allNames.reduce( - async ( - acc : Promise, - { contract } : { contract : string; } - ) : Promise => { - await acc; - const fromDB = await dbAdapter.getContract(contract); - expect(fromDB?.address).to.be.properAddress; - }, - Promise.resolve() - ); - - // check that previously deployed contracts were NOT redeployed - await firstRunDeployed.reduce( - async (acc : Promise, { contract, instance, address } : IDeployedData) : Promise => { - await acc; - const fromDB = await nextCampaign.dbAdapter.getContract(contract); - const fromState = nextCampaign[instance]; - - expect(fromDB?.address).to.equal(address); - expect(await fromState.getAddress()).to.equal(address); - }, - Promise.resolve() - ); - - return { - failingCampaign, - nextCampaign, - firstRunDeployed, - }; - }; - - beforeEach(async () => { - [deployAdmin, admin, zeroVault] = await hre.ethers.getSigners(); - - campaignConfig = { - env, - deployAdmin, - governorAddresses: [deployAdmin.address], - adminAddresses: [deployAdmin.address, admin.address], - domainToken: { - name: ZNS_DOMAIN_TOKEN_NAME, - symbol: ZNS_DOMAIN_TOKEN_SYMBOL, - defaultRoyaltyReceiver: deployAdmin.address, - defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, - }, - rootPriceConfig: DEFAULT_PRICE_CONFIG, - zeroVaultAddress: zeroVault.address, - // TODO dep: what do we pass here for test flow? we don't have a deployed MeowToken contract - stakingTokenAddress: "", - mockMeowToken: true, // 1700083028872 - postDeploy: { - tenderlyProjectSlug: "", - monitorContracts: false, - verifyContracts: false, - }, - }; - - mongoAdapter = await getZnsMongoAdapter({ - logger: loggerMock as TLogger, - }); - }); - - afterEach(async () => { - await mongoAdapter.dropDB(); - }); - - // eslint-disable-next-line max-len - it("[in AddressResolver.deploy() hook] should ONLY deploy undeployed contracts in the run following a failed run", async () => { - // ZNSAddressResolverDM sits in the middle of the Campaign deploy list - // we override this class to add a failure to the deploy() method - class FailingZNSAddressResolverDM extends ZNSAddressResolverDM { - async deploy () { - throw new Error(errorMsgDeploy); - } - } - - const deployedNames = [ - znsNames.accessController, - znsNames.registry, - znsNames.domainToken, - { - contract: znsNames.meowToken.contractMock, - instance: znsNames.meowToken.instance, - }, - ]; - - const undeployedNames = [ - znsNames.addressResolver, - znsNames.curvePricer, - znsNames.treasury, - znsNames.rootRegistrar, - znsNames.fixedPricer, - znsNames.subRegistrar, - ]; - - // call test flow runner - await runTest({ - missionList: [ - ZNSAccessControllerDM, - ZNSRegistryDM, - ZNSDomainTokenDM, - MeowTokenDM, - FailingZNSAddressResolverDM, // failing DM - ZNSCurvePricerDM, - ZNSTreasuryDM, - ZNSRootRegistrarDM, - ZNSFixedPricerDM, - ZNSSubRegistrarDM, - ], - placeOfFailure: "deploy", - deployedNames, - undeployedNames, - failingInstanceName: "addressResolver", - }); - }); - - // eslint-disable-next-line max-len - it("[in AddressResolver.postDeploy() hook] should start from post deploy sequence that failed on the previous run", async () => { - class FailingZNSAddressResolverDM extends ZNSAddressResolverDM { - async postDeploy () { - throw new Error(errorMsgPostDeploy); - } - } - - const deployedNames = [ - znsNames.accessController, - znsNames.registry, - znsNames.domainToken, - { - contract: znsNames.meowToken.contractMock, - instance: znsNames.meowToken.instance, - }, - znsNames.addressResolver, - ]; - - const undeployedNames = [ - znsNames.curvePricer, - znsNames.treasury, - znsNames.rootRegistrar, - znsNames.fixedPricer, - znsNames.subRegistrar, - ]; - - const checkPostDeploy = async (failingCampaign : DeployCampaign< - HardhatRuntimeEnvironment, - SignerWithAddress, - IZNSCampaignConfig, - IZNSContracts - >) => { - const { - // eslint-disable-next-line no-shadow - registry, - } = failingCampaign; - - // we are checking that postDeploy did not add resolverType to Registry - expect(await registry.getResolverType(ResolverTypes.address)).to.be.equal(ethers.ZeroAddress); - }; - - // check contracts are deployed correctly - const { - nextCampaign, - } = await runTest({ - missionList: [ - ZNSAccessControllerDM, - ZNSRegistryDM, - ZNSDomainTokenDM, - MeowTokenDM, - FailingZNSAddressResolverDM, // failing DM - ZNSCurvePricerDM, - ZNSTreasuryDM, - ZNSRootRegistrarDM, - ZNSFixedPricerDM, - ZNSSubRegistrarDM, - ], - placeOfFailure: "postDeploy", - deployedNames, - undeployedNames, - failingInstanceName: "addressResolver", - callback: checkPostDeploy, - }); - - // make sure postDeploy() ran properly on the next run - const { - registry, - addressResolver, - } = nextCampaign; - expect(await registry.getResolverType(ResolverTypes.address)).to.be.equal(await addressResolver.getAddress()); - }); - - // eslint-disable-next-line max-len - it("[in RootRegistrar.deploy() hook] should ONLY deploy undeployed contracts in the run following a failed run", async () => { - class FailingZNSRootRegistrarDM extends ZNSRootRegistrarDM { - async deploy () { - throw new Error(errorMsgDeploy); - } - } - - const deployedNames = [ - znsNames.accessController, - znsNames.registry, - znsNames.domainToken, - { - contract: znsNames.meowToken.contractMock, - instance: znsNames.meowToken.instance, - }, - znsNames.addressResolver, - znsNames.curvePricer, - znsNames.treasury, - ]; - - const undeployedNames = [ - znsNames.rootRegistrar, - znsNames.fixedPricer, - znsNames.subRegistrar, - ]; - - // call test flow runner - await runTest({ - missionList: [ - ZNSAccessControllerDM, - ZNSRegistryDM, - ZNSDomainTokenDM, - MeowTokenDM, - ZNSAddressResolverDM, - ZNSCurvePricerDM, - ZNSTreasuryDM, - FailingZNSRootRegistrarDM, // failing DM - ZNSFixedPricerDM, - ZNSSubRegistrarDM, - ], - placeOfFailure: "deploy", - deployedNames, - undeployedNames, - failingInstanceName: "rootRegistrar", - }); - }); - - // eslint-disable-next-line max-len - it("[in RootRegistrar.postDeploy() hook] should start from post deploy sequence that failed on the previous run", async () => { - class FailingZNSRootRegistrarDM extends ZNSRootRegistrarDM { - async postDeploy () { - throw new Error(errorMsgPostDeploy); - } - } - - const deployedNames = [ - znsNames.accessController, - znsNames.registry, - znsNames.domainToken, - { - contract: znsNames.meowToken.contractMock, - instance: znsNames.meowToken.instance, - }, - znsNames.addressResolver, - znsNames.curvePricer, - znsNames.treasury, - znsNames.rootRegistrar, - ]; - - const undeployedNames = [ - znsNames.fixedPricer, - znsNames.subRegistrar, - ]; - - const checkPostDeploy = async (failingCampaign : DeployCampaign< - HardhatRuntimeEnvironment, - SignerWithAddress, - IZNSCampaignConfig, - IZNSContracts - >) => { - const { - // eslint-disable-next-line no-shadow - accessController, - // eslint-disable-next-line no-shadow - rootRegistrar, - } = failingCampaign; - - // we are checking that postDeploy did not grant REGISTRAR_ROLE to RootRegistrar - expect(await accessController.isRegistrar(await rootRegistrar.getAddress())).to.be.false; - }; - - // check contracts are deployed correctly - const { - nextCampaign, - } = await runTest({ - missionList: [ - ZNSAccessControllerDM, - ZNSRegistryDM, - ZNSDomainTokenDM, - MeowTokenDM, - ZNSAddressResolverDM, - ZNSCurvePricerDM, - ZNSTreasuryDM, - FailingZNSRootRegistrarDM, // failing DM - ZNSFixedPricerDM, - ZNSSubRegistrarDM, - ], - placeOfFailure: "postDeploy", - deployedNames, - undeployedNames, - failingInstanceName: "rootRegistrar", - callback: checkPostDeploy, - }); - - // make sure postDeploy() ran properly on the next run - const { - accessController, - rootRegistrar, - } = nextCampaign; - expect(await accessController.isRegistrar(await rootRegistrar.getAddress())).to.be.true; - }); - }); - - describe("Configurable Environment & Validation", () => { - let envInitial : string; - - beforeEach(async () => { - envInitial = JSON.stringify(process.env); - }); - - afterEach(async () => { - process.env = JSON.parse(envInitial); - }); - - // The `validate` function accepts the environment parameter only for the - // purpose of testing here as manipulating actual environment variables - // like `process.env. = "value"` is not possible in a test environment - // because the Hardhat process for running these tests will not respect these - // changes. `getConfig` calls to `validate` on its own, but never passes a value - // for the environment specifically, that is ever only inferred from the `process.env.ENV_LEVEL` - it("Gets the default configuration correctly", async () => { - // set the environment to get the appropriate variables - const localConfig : IZNSCampaignConfig = await getConfig({ - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [governor.address], - admins: [admin.address], - }); - - expect(await localConfig.deployAdmin.getAddress()).to.eq(deployAdmin.address); - expect(localConfig.governorAddresses[0]).to.eq(governor.address); - expect(localConfig.governorAddresses[1]).to.eq(deployAdmin.address); - expect(localConfig.adminAddresses[0]).to.eq(admin.address); - expect(localConfig.adminAddresses[1]).to.eq(deployAdmin.address); - expect(localConfig.domainToken.name).to.eq(ZNS_DOMAIN_TOKEN_NAME); - expect(localConfig.domainToken.symbol).to.eq(ZNS_DOMAIN_TOKEN_SYMBOL); - expect(localConfig.domainToken.defaultRoyaltyReceiver).to.eq(zeroVault.address); - expect(localConfig.domainToken.defaultRoyaltyFraction).to.eq(DEFAULT_ROYALTY_FRACTION); - expect(localConfig.rootPriceConfig).to.deep.eq(DEFAULT_PRICE_CONFIG); - }); - - it("Confirms encoding functionality works for env variables", async () => { - const sample = "0x123,0x456,0x789"; - const sampleFormatted = ["0x123", "0x456", "0x789"]; - const encoded = btoa(sample); - const decoded = atob(encoded).split(","); - expect(decoded).to.deep.eq(sampleFormatted); - }); - - it("Modifies config to use a random account as the deployer", async () => { - // Run the deployment a second time, clear the DB so everything is deployed - - let zns : IZNSContracts; - - const config : IZNSCampaignConfig = await getConfig({ - deployer: userB, - zeroVaultAddress: userA.address, - governors: [userB.address, admin.address], // governors - admins: [userB.address, governor.address], // admins - }); - - const campaign = await runZnsCampaign({ - config, - }); - - const { dbAdapter } = campaign; - - /* eslint-disable-next-line prefer-const */ - zns = campaign.state.contracts; - - const rootPaymentConfig = await zns.treasury.paymentConfigs(ethers.ZeroHash); - - expect(await zns.accessController.isAdmin(userB.address)).to.be.true; - expect(await zns.accessController.isAdmin(governor.address)).to.be.true; - expect(await zns.accessController.isGovernor(admin.address)).to.be.true; - expect(rootPaymentConfig.token).to.eq(await zns.meowToken.getAddress()); - expect(rootPaymentConfig.beneficiary).to.eq(userA.address); - - await dbAdapter.dropDB(); - }); - - it("Fails when governor or admin addresses are given wrong", async () => { - // Custom addresses must given as the base64 encoded string of comma separated addresses - // e.g. btoa("0x123,0x456,0x789") = 'MHgxMjMsMHg0NTYsMHg3ODk=', which is what should be provided - // We could manipulate envariables through `process.env.` for this test and call `getConfig()` - // but the async nature of HH mocha tests causes this to mess up other tests - // Instead we use the same encoding functions used in `getConfig()` to test the functionality - - /* eslint-disable @typescript-eslint/no-explicit-any */ - try { - atob("[0x123,0x456]"); - } catch (e : any) { - expect(e.message).includes("Invalid character"); - } - - try { - atob("0x123, 0x456"); - } catch (e : any) { - expect(e.message).includes("Invalid character"); - } - - try { - atob("0x123 0x456"); - } catch (e : any) { - expect(e.message).includes("Invalid character"); - } - - try { - atob("'MHgxM jMsMHg0 NTYs MHg3ODk='"); - } catch (e : any) { - expect(e.message).includes("Invalid character"); - } - }); - - it("Throws if env variable is invalid", async () => { - try { - await getConfig({ - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [deployAdmin.address, governor.address], - admins: [deployAdmin.address, admin.address], - }); - - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (e : any) { - expect(e.message).includes(INVALID_ENV_ERR); - } - }); - - it("Fails to validate when mocking MEOW on prod", async () => { - process.env.MOCK_MEOW_TOKEN = "true"; - - try { - await getConfig({ - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [deployAdmin.address, governor.address], - admins: [deployAdmin.address, admin.address], - }); - - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (e : any) { - expect(e.message).includes(NO_MOCK_PROD_ERR); - } - }); - - it("Fails to validate if not using the MEOW token on prod", async () => { - process.env.MOCK_MEOW_TOKEN = "false"; - process.env.STAKING_TOKEN_ADDRESS = "0x123"; - - try { - await getConfig({ - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [deployAdmin.address, governor.address], - admins: [deployAdmin.address, admin.address], - }); - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (e : any) { - expect(e.message).includes(STAKING_TOKEN_ERR); - } - }); - - it("Fails to validate if invalid curve for pricing", async () => { - process.env.MOCK_MEOW_TOKEN = "false"; - process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; - process.env.BASE_LENGTH = "3"; - process.env.MAX_LENGTH = "5"; - process.env.MAX_PRICE = "0"; - process.env.MIN_PRICE = ethers.parseEther("3").toString(); - - try { - await getConfig({ - env: "prod", - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [deployAdmin.address, governor.address], - admins: [deployAdmin.address, admin.address], - }); - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (e : any) { - expect(e.message).includes(INVALID_CURVE_ERR); - } - }); - - it("Fails to validate if no mongo uri or local URI in prod", async () => { - process.env.MOCK_MEOW_TOKEN = "false"; - process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; - // Falls back onto the default URI which is for localhost and fails in prod - process.env.MONGO_DB_URI = ""; - process.env.ROYALTY_RECEIVER = "0x123"; - process.env.ROYALTY_FRACTION = "100"; - - try { - await getConfig({ - env: "prod", - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [deployAdmin.address, governor.address], - admins: [deployAdmin.address, admin.address], - }); - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (e : any) { - expect(e.message).includes("Must provide a Mongo URI used for prod environment!"); - } - - process.env.MOCK_MEOW_TOKEN = "false"; - process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; - process.env.MONGO_DB_URI = "mongodb://localhost:27018"; - process.env.ZERO_VAULT_ADDRESS = "0x123"; - - try { - await getConfig({ - env: "prod", - deployer: deployAdmin, - zeroVaultAddress: zeroVault.address, - governors: [deployAdmin.address, governor.address], - admins: [deployAdmin.address, admin.address], - }); - /* eslint-disable @typescript-eslint/no-explicit-any */ - } catch (e : any) { - expect(e.message).includes(MONGO_URI_ERR); - } - }); - }); - - describe("Versioning", () => { - let campaign : DeployCampaign< - HardhatRuntimeEnvironment, - SignerWithAddress, - IZNSCampaignConfig, - IZNSContracts - >; - - before(async () => { - await saveTag(); - - campaignConfig = { - env, - deployAdmin, - governorAddresses: [deployAdmin.address, governor.address], - adminAddresses: [deployAdmin.address, admin.address], - domainToken: { - name: ZNS_DOMAIN_TOKEN_NAME, - symbol: ZNS_DOMAIN_TOKEN_SYMBOL, - defaultRoyaltyReceiver: deployAdmin.address, - defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, - }, - rootPriceConfig: DEFAULT_PRICE_CONFIG, - zeroVaultAddress: zeroVault.address, - stakingTokenAddress: MeowMainnet.address, - mockMeowToken: true, - postDeploy: { - tenderlyProjectSlug: "", - monitorContracts: false, - verifyContracts: false, - }, - }; - - campaign = await runZnsCampaign({ - config: campaignConfig, - }); - }); - - it("should get the correct git tag + commit hash and write to DB", async () => { - const latestGitTag = (await execAsync("git describe --tags --abbrev=0")).stdout.trim(); - const latestCommit = (await execAsync(`git rev-list -n 1 ${latestGitTag}`)).stdout.trim(); - - const fullGitTag = `${latestGitTag}:${latestCommit}`; - - const { dbAdapter } = campaign; - - const versionDoc = await dbAdapter.versioner.getLatestVersion(); - expect(versionDoc?.contractsVersion).to.equal(fullGitTag); - - const deployedVersion = await dbAdapter.versioner.getDeployedVersion(); - expect(deployedVersion?.contractsVersion).to.equal(fullGitTag); - }); - - // eslint-disable-next-line max-len - it("should create new DB version and KEEP old data if ARCHIVE is true and no TEMP versions currently exist", async () => { - const { dbAdapter } = campaign; - - const versionDocInitial = await dbAdapter.versioner.getLatestVersion(); - const initialDBVersion = versionDocInitial?.dbVersion; - const registryDocInitial = await dbAdapter.getContract(znsNames.registry.contract); - - expect( - process.env.MONGO_DB_VERSION === undefined - || process.env.MONGO_DB_VERSION === "" - ).to.be.true; - - // set archiving for the new mongo adapter - const initialArchiveVal = process.env.ARCHIVE_PREVIOUS_DB_VERSION; - process.env.ARCHIVE_PREVIOUS_DB_VERSION = "true"; - - // run a new campaign - const { dbAdapter: newDbAdapter } = await runZnsCampaign({ - config: campaignConfig, - }); - - expect(newDbAdapter.curVersion).to.not.equal(initialDBVersion); - - // get some data from new DB version - const registryDocNew = await newDbAdapter.getContract(znsNames.registry.contract); - expect(registryDocNew?.version).to.not.equal(registryDocInitial?.version); - - const versionDocNew = await newDbAdapter.versioner.getLatestVersion(); - expect(versionDocNew?.dbVersion).to.not.equal(initialDBVersion); - expect(versionDocNew?.type).to.equal(VERSION_TYPES.deployed); - - // make sure old contracts from previous DB version are still there - const oldRegistryDocFromNewDB = await newDbAdapter.getContract( - znsNames.registry.contract, - initialDBVersion - ); - - expect(oldRegistryDocFromNewDB?.version).to.equal(registryDocInitial?.version); - expect(oldRegistryDocFromNewDB?.address).to.equal(registryDocInitial?.address); - expect(oldRegistryDocFromNewDB?.name).to.equal(registryDocInitial?.name); - expect(oldRegistryDocFromNewDB?.abi).to.equal(registryDocInitial?.abi); - expect(oldRegistryDocFromNewDB?.bytecode).to.equal(registryDocInitial?.bytecode); - - // reset back to default - process.env.ARCHIVE_PREVIOUS_DB_VERSION = initialArchiveVal; - }); - - // eslint-disable-next-line max-len - it("should create new DB version and WIPE all existing data if ARCHIVE is false and no TEMP versions currently exist", async () => { - const { dbAdapter } = campaign; - - const versionDocInitial = await dbAdapter.versioner.getLatestVersion(); - const initialDBVersion = versionDocInitial?.dbVersion; - const registryDocInitial = await dbAdapter.getContract(znsNames.registry.contract); - - expect( - process.env.MONGO_DB_VERSION === undefined - || process.env.MONGO_DB_VERSION === "" - ).to.be.true; - - // set archiving for the new mongo adapter - const initialArchiveVal = process.env.ARCHIVE_PREVIOUS_DB_VERSION; - process.env.ARCHIVE_PREVIOUS_DB_VERSION = "false"; - - // run a new campaign - const { dbAdapter: newDbAdapter } = await runZnsCampaign({ - config: campaignConfig, - }); - - expect(newDbAdapter.curVersion).to.not.equal(initialDBVersion); - - // get some data from new DB version - const registryDocNew = await newDbAdapter.getContract(znsNames.registry.contract); - expect(registryDocNew?.version).to.not.equal(registryDocInitial?.version); - - const versionDocNew = await newDbAdapter.versioner.getLatestVersion(); - expect(versionDocNew?.dbVersion).to.not.equal(initialDBVersion); - expect(versionDocNew?.type).to.equal(VERSION_TYPES.deployed); - - // make sure old contracts from previous DB version are NOT there - const oldRegistryDocFromNewDB = await newDbAdapter.getContract( - znsNames.registry.contract, - initialDBVersion - ); - - expect(oldRegistryDocFromNewDB).to.be.null; - - // reset back to default - process.env.ARCHIVE_PREVIOUS_DB_VERSION = initialArchiveVal; - }); - - // eslint-disable-next-line max-len - it("should pick up existing contracts and NOT deploy new ones into state if MONGO_DB_VERSION is specified", async () => { - const { dbAdapter } = campaign; - - const versionDocInitial = await dbAdapter.versioner.getLatestVersion(); - const initialDBVersion = versionDocInitial?.dbVersion; - const registryDocInitial = await dbAdapter.getContract(znsNames.registry.contract); - - // set DB version for the new mongo adapter - const initialDBVersionVal = process.env.MONGO_DB_VERSION; - process.env.MONGO_DB_VERSION = initialDBVersion; - - // run a new campaign - const { state: { contracts: newContracts } } = await runZnsCampaign({ - config: campaignConfig, - }); - - // make sure we picked up the correct DB version - const versionDocNew = await dbAdapter.versioner.getLatestVersion(); - expect(versionDocNew?.dbVersion).to.equal(initialDBVersion); - - // make sure old contracts from previous DB version are still there - const oldRegistryDocFromNewDB = await dbAdapter.getContract( - znsNames.registry.contract, - initialDBVersion - ); - - expect(oldRegistryDocFromNewDB?.version).to.equal(registryDocInitial?.version); - expect(oldRegistryDocFromNewDB?.address).to.equal(registryDocInitial?.address); - expect(oldRegistryDocFromNewDB?.name).to.equal(registryDocInitial?.name); - expect(oldRegistryDocFromNewDB?.abi).to.equal(registryDocInitial?.abi); - expect(oldRegistryDocFromNewDB?.bytecode).to.equal(registryDocInitial?.bytecode); - - // make sure contracts in state have been picked up correctly from DB - expect(await newContracts.registry.getAddress()).to.equal(registryDocInitial?.address); - - // reset back to default - process.env.MONGO_DB_VERSION = initialDBVersionVal; - }); - }); - - describe("Verify - Monitor", () => { - let config : IZNSCampaignConfig; - - before (async () => { - [deployAdmin, admin, governor, zeroVault] = await hre.ethers.getSigners(); - - config = { - env: "dev", - deployAdmin, - governorAddresses: [deployAdmin.address, governor.address], - adminAddresses: [deployAdmin.address, admin.address], - domainToken: { - name: ZNS_DOMAIN_TOKEN_NAME, - symbol: ZNS_DOMAIN_TOKEN_SYMBOL, - defaultRoyaltyReceiver: deployAdmin.address, - defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, - }, - rootPriceConfig: DEFAULT_PRICE_CONFIG, - zeroVaultAddress: zeroVault.address, - stakingTokenAddress: MeowMainnet.address, - mockMeowToken: true, - postDeploy: { - tenderlyProjectSlug: "", - monitorContracts: false, - verifyContracts: true, - }, - }; - }); - - afterEach(async () => { - await mongoAdapter.dropDB(); - }); - - it("should prepare the correct data for each contract when verifying on Etherscan", async () => { - const verifyData : Array<{ address : string; ctorArgs ?: TDeployArgs; }> = []; - class HardhatDeployerMock extends HardhatDeployer< - HardhatRuntimeEnvironment, - SignerWithAddress - > { - async etherscanVerify (args : { - address : string; - ctorArgs ?: TDeployArgs; - }) { - verifyData.push(args); - } - } - - const deployer = new HardhatDeployerMock({ - hre, - signer: deployAdmin, - env, - }); - - const campaign = await runZnsCampaign({ - config, - deployer, - }); - - const { state: { contracts } } = campaign; - ({ dbAdapter: mongoAdapter } = campaign); - - await Object.values(contracts).reduce( - async (acc, contract, idx) => { - await acc; - - if (idx === 0) { - expect(verifyData[idx].ctorArgs).to.be.deep.eq([config.governorAddresses, config.adminAddresses]); - } - - expect(verifyData[idx].address).to.equal(await contract.getAddress()); - }, - Promise.resolve() - ); - }); - - it("should prepare the correct contract data when pushing to Tenderly Project", async () => { - let tenderlyData : Array = []; - class HardhatDeployerMock extends HardhatDeployer< - HardhatRuntimeEnvironment, - SignerWithAddress - > { - async tenderlyPush (contracts : Array) { - tenderlyData = contracts; - } - } - - const deployer = new HardhatDeployerMock({ - hre, - signer: deployAdmin, - env, - }); - - config.postDeploy.monitorContracts = true; - config.postDeploy.verifyContracts = false; - - const campaign = await runZnsCampaign({ - config, - deployer, - }); - - const { state: { instances } } = campaign; - ({ dbAdapter: mongoAdapter } = campaign); - - let idx = 0; - await Object.values(instances).reduce( - async (acc, instance) => { - await acc; - - const dbData = await instance.getFromDB(); - - if (instance.proxyData.isProxy) { - // check proxy - expect(tenderlyData[idx].address).to.be.eq(dbData?.address); - expect(tenderlyData[idx].display_name).to.be.eq(`${instance.contractName}Proxy`); - - // check impl - expect(tenderlyData[idx + 1].address).to.be.eq(dbData?.implementation); - expect(tenderlyData[idx + 1].display_name).to.be.eq(`${dbData?.name}Impl`); - expect(tenderlyData[idx + 1].display_name).to.be.eq(`${instance.contractName}Impl`); - idx += 2; - } else { - expect(tenderlyData[idx].address).to.equal(dbData?.address); - expect(tenderlyData[idx].display_name).to.equal(dbData?.name); - expect(tenderlyData[idx].display_name).to.equal(instance.contractName); - idx++; - } - }, - Promise.resolve() - ); - }); - }); -}); +/* eslint-disable @typescript-eslint/no-empty-function, @typescript-eslint/ban-ts-comment, max-classes-per-file */ +import * as hre from "hardhat"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { + TLogger, + HardhatDeployer, + DeployCampaign, + resetMongoAdapter, + TDeployMissionCtor, + MongoDBAdapter, + ITenderlyContractData, + TDeployArgs, + VERSION_TYPES, +} from "@zero-tech/zdc"; +import { + DEFAULT_ROYALTY_FRACTION, + DEFAULT_PRICE_CONFIG, + ZNS_DOMAIN_TOKEN_NAME, + ZNS_DOMAIN_TOKEN_SYMBOL, + INVALID_ENV_ERR, + NO_MOCK_PROD_ERR, + STAKING_TOKEN_ERR, + INVALID_CURVE_ERR, + MONGO_URI_ERR, +} from "./helpers"; +import { + MeowTokenDM, + meowTokenName, + meowTokenSymbol, + ZNSAccessControllerDM, + ZNSAddressResolverDM, + ZNSCurvePricerDM, + ZNSDomainTokenDM, ZNSFixedPricerDM, + ZNSRegistryDM, ZNSRootRegistrarDM, ZNSSubRegistrarDM, ZNSTreasuryDM, +} from "../src/deploy/missions/contracts"; +import { ZNSStringResolverDM } from "../src/deploy/missions/contracts/string-resolver"; +import { znsNames } from "../src/deploy/missions/contracts/names"; +import { runZnsCampaign } from "../src/deploy/zns-campaign"; +import { MeowMainnet } from "../src/deploy/missions/contracts/meow-token/mainnet-data"; +import { ResolverTypes } from "../src/deploy/constants"; +import { getConfig } from "../src/deploy/campaign/environments"; +import { ethers } from "ethers"; +import { promisify } from "util"; +import { exec } from "child_process"; +import { saveTag } from "../src/utils/git-tag/save-tag"; +import { IZNSCampaignConfig, IZNSContracts } from "../src/deploy/campaign/types"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { getZnsMongoAdapter } from "../src/deploy/mongo"; + + +const execAsync = promisify(exec); + +describe("Deploy Campaign Test", () => { + let deployAdmin : SignerWithAddress; + let admin : SignerWithAddress; + let governor : SignerWithAddress; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let userA : SignerWithAddress; + let userB : SignerWithAddress; + let zeroVault : SignerWithAddress; + let campaignConfig : IZNSCampaignConfig; + + let mongoAdapter : MongoDBAdapter; + + const env = "dev"; + + before(async () => { + [deployAdmin, admin, governor, zeroVault, userA, userB] = await hre.ethers.getSigners(); + }); + + describe("MEOW Token Ops", () => { + before(async () => { + campaignConfig = { + env, + deployAdmin, + governorAddresses: [deployAdmin.address], + adminAddresses: [deployAdmin.address, admin.address], + domainToken: { + name: ZNS_DOMAIN_TOKEN_NAME, + symbol: ZNS_DOMAIN_TOKEN_SYMBOL, + defaultRoyaltyReceiver: deployAdmin.address, + defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, + }, + rootPriceConfig: DEFAULT_PRICE_CONFIG, + zeroVaultAddress: zeroVault.address, + stakingTokenAddress: MeowMainnet.address, + mockMeowToken: true, + postDeploy: { + tenderlyProjectSlug: "", + monitorContracts: false, + verifyContracts: false, + }, + }; + }); + + it("should deploy new MeowTokenMock when `mockMeowToken` is true", async () => { + const campaign = await runZnsCampaign({ + config: campaignConfig, + }); + + const { meowToken, dbAdapter } = campaign; + + const toMint = hre.ethers.parseEther("972315"); + + const balanceBefore = await meowToken.balanceOf(userA.address); + // `mint()` only exists on the Mocked contract + await meowToken.connect(deployAdmin).mint( + userA.address, + toMint + ); + + const balanceAfter = await meowToken.balanceOf(userA.address); + expect(balanceAfter - balanceBefore).to.equal(toMint); + + await dbAdapter.dropDB(); + }); + + it("should use existing deployed non-mocked MeowToken contract when `mockMeowToken` is false", async () => { + campaignConfig.mockMeowToken = false; + + // deploy MeowToken contract + const factory = await hre.ethers.getContractFactory("MeowTokenMock"); + const meow = await hre.upgrades.deployProxy( + factory, + [meowTokenName, meowTokenSymbol], + { + kind: "transparent", + }); + + await meow.waitForDeployment(); + + campaignConfig.stakingTokenAddress = await meow.getAddress(); + + const campaign = await runZnsCampaign({ + config: campaignConfig, + }); + + const { + meowToken, + dbAdapter, + state: { + instances: { + meowToken: meowDMInstance, + }, + }, + } = campaign; + + expect(meowToken.address).to.equal(meow.address); + expect(meowDMInstance.contractName).to.equal(znsNames.meowToken.contract); + + const toMint = hre.ethers.parseEther("972315"); + // `mint()` only exists on the Mocked contract + try { + await meowToken.connect(deployAdmin).mint( + userA.address, + toMint + ); + } catch (e) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(e.message).to.include( + ".mint is not a function" + ); + } + + // Cannot call to real db to + await dbAdapter.dropDB(); + }); + }); + + describe("Failure Recovery", () => { + const errorMsgDeploy = "FailMissionDeploy"; + const errorMsgPostDeploy = "FailMissionPostDeploy"; + + const loggerMock = { + info: () => { + }, + debug: () => { + }, + error: () => { + }, + }; + + interface IDeployedData { + contract : string; + instance : string; + address ?: string; + } + + const runTest = async ({ + missionList, + placeOfFailure, + deployedNames, + undeployedNames, + failingInstanceName, + callback, + } : { + missionList : Array, + IZNSContracts + >>; + placeOfFailure : string; + deployedNames : Array<{ contract : string; instance : string; }>; + undeployedNames : Array<{ contract : string; instance : string; }>; + failingInstanceName : string; + // eslint-disable-next-line no-shadow + callback ?: (failingCampaign : DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + IZNSCampaignConfig, + IZNSContracts + >) => Promise; + }) => { + const deployer = new HardhatDeployer< + HardhatRuntimeEnvironment, + SignerWithAddress + >({ + hre, + signer: deployAdmin, + env, + }); + let dbAdapter = await getZnsMongoAdapter(); + + let toMatchErr = errorMsgDeploy; + if (placeOfFailure === "postDeploy") { + toMatchErr = errorMsgPostDeploy; + } + + const failingCampaign = new DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + IZNSCampaignConfig, + IZNSContracts + >({ + missions: missionList, + deployer, + dbAdapter, + // @ts-ignore + logger: loggerMock, + config: campaignConfig, + }); + + try { + await failingCampaign.execute(); + } catch (e) { + // @ts-ignore + expect(e.message).to.include(toMatchErr); + } + + // check the correct amount of contracts in state + const { contracts } = failingCampaign.state; + expect(Object.keys(contracts).length).to.equal(deployedNames.length); + + if (placeOfFailure === "deploy") { + // it should not deploy AddressResolver + expect(contracts[failingInstanceName]).to.be.undefined; + } else { + // it should deploy AddressResolver + expect(await contracts[failingInstanceName].getAddress()).to.be.properAddress; + } + + // check DB to verify we only deployed half + const firstRunDeployed = await deployedNames.reduce( + async ( + acc : Promise>, + { contract, instance } : { contract : string; instance : string; } + ) : Promise> => { + const akk = await acc; + const fromDB = await dbAdapter.getContract(contract); + expect(fromDB?.address).to.be.properAddress; + + return [...akk, { contract, instance, address: fromDB?.address }]; + }, + Promise.resolve([]) + ); + + await undeployedNames.reduce( + async ( + acc : Promise, + { contract, instance } : { contract : string; instance : string; } + ) : Promise => { + await acc; + const fromDB = await dbAdapter.getContract(contract); + const fromState = failingCampaign[instance]; + + expect(fromDB).to.be.null; + expect(fromState).to.be.undefined; + }, + Promise.resolve() + ); + + // call whatever callback we passed before the next campaign run + await callback?.(failingCampaign); + + const { curVersion: initialDbVersion } = dbAdapter; + + // reset mongoAdapter instance to make sure we pick up the correct DB version + resetMongoAdapter(); + + // run Campaign again, but normally + const nextCampaign = await runZnsCampaign({ + config: campaignConfig, + }); + + ({ dbAdapter } = nextCampaign); + + // make sure MongoAdapter is using the correct TEMP version + const { curVersion: nextDbVersion } = dbAdapter; + expect(nextDbVersion).to.equal(initialDbVersion); + + // state should have 11 contracts in it + const { state } = nextCampaign; + 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; + + // check DB to verify we deployed everything + const allNames = deployedNames.concat(undeployedNames); + + await allNames.reduce( + async ( + acc : Promise, + { contract } : { contract : string; } + ) : Promise => { + await acc; + const fromDB = await dbAdapter.getContract(contract); + expect(fromDB?.address).to.be.properAddress; + }, + Promise.resolve() + ); + + // check that previously deployed contracts were NOT redeployed + await firstRunDeployed.reduce( + async (acc : Promise, { contract, instance, address } : IDeployedData) : Promise => { + await acc; + const fromDB = await nextCampaign.dbAdapter.getContract(contract); + const fromState = nextCampaign[instance]; + + expect(fromDB?.address).to.equal(address); + expect(await fromState.getAddress()).to.equal(address); + }, + Promise.resolve() + ); + + return { + failingCampaign, + nextCampaign, + firstRunDeployed, + }; + }; + + beforeEach(async () => { + [deployAdmin, admin, zeroVault] = await hre.ethers.getSigners(); + + campaignConfig = { + env, + deployAdmin, + governorAddresses: [deployAdmin.address], + adminAddresses: [deployAdmin.address, admin.address], + domainToken: { + name: ZNS_DOMAIN_TOKEN_NAME, + symbol: ZNS_DOMAIN_TOKEN_SYMBOL, + defaultRoyaltyReceiver: deployAdmin.address, + defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, + }, + rootPriceConfig: DEFAULT_PRICE_CONFIG, + zeroVaultAddress: zeroVault.address, + // TODO dep: what do we pass here for test flow? we don't have a deployed MeowToken contract + stakingTokenAddress: "", + mockMeowToken: true, // 1700083028872 + postDeploy: { + tenderlyProjectSlug: "", + monitorContracts: false, + verifyContracts: false, + }, + }; + + mongoAdapter = await getZnsMongoAdapter({ + logger: loggerMock as TLogger, + }); + }); + + afterEach(async () => { + await mongoAdapter.dropDB(); + }); + + // eslint-disable-next-line max-len + it("[in AddressResolver.deploy() hook] should ONLY deploy undeployed contracts in the run following a failed run", async () => { + // ZNSAddressResolverDM sits in the middle of the Campaign deploy list + // we override this class to add a failure to the deploy() method + class FailingZNSAddressResolverDM extends ZNSAddressResolverDM { + async deploy () { + throw new Error(errorMsgDeploy); + } + } + + const deployedNames = [ + znsNames.accessController, + znsNames.registry, + znsNames.domainToken, + { + contract: znsNames.meowToken.contractMock, + instance: znsNames.meowToken.instance, + }, + ]; + + const undeployedNames = [ + znsNames.addressResolver, + znsNames.curvePricer, + znsNames.treasury, + znsNames.rootRegistrar, + znsNames.fixedPricer, + znsNames.subRegistrar, + ]; + + // call test flow runner + await runTest({ + missionList: [ + ZNSAccessControllerDM, + ZNSRegistryDM, + ZNSDomainTokenDM, + MeowTokenDM, + FailingZNSAddressResolverDM, // failing DM + ZNSStringResolverDM, + ZNSCurvePricerDM, + ZNSTreasuryDM, + ZNSRootRegistrarDM, + ZNSFixedPricerDM, + ZNSSubRegistrarDM, + ], + placeOfFailure: "deploy", + deployedNames, + undeployedNames, + failingInstanceName: "addressResolver", + }); + }); + + // eslint-disable-next-line max-len + it("[in AddressResolver.postDeploy() hook] should start from post deploy sequence that failed on the previous run", async () => { + class FailingZNSAddressResolverDM extends ZNSAddressResolverDM { + async postDeploy () { + throw new Error(errorMsgPostDeploy); + } + } + + const deployedNames = [ + znsNames.accessController, + znsNames.registry, + znsNames.domainToken, + { + contract: znsNames.meowToken.contractMock, + instance: znsNames.meowToken.instance, + }, + znsNames.addressResolver, + ]; + + const undeployedNames = [ + znsNames.curvePricer, + znsNames.treasury, + znsNames.rootRegistrar, + znsNames.fixedPricer, + znsNames.subRegistrar, + ]; + + const checkPostDeploy = async (failingCampaign : DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + IZNSCampaignConfig, + IZNSContracts + >) => { + const { + // eslint-disable-next-line no-shadow + registry, + } = failingCampaign; + + // we are checking that postDeploy did not add resolverType to Registry + expect(await registry.getResolverType(ResolverTypes.address)).to.be.equal(ethers.ZeroAddress); + }; + + // check contracts are deployed correctly + const { + nextCampaign, + } = await runTest({ + missionList: [ + ZNSAccessControllerDM, + ZNSRegistryDM, + ZNSDomainTokenDM, + MeowTokenDM, + FailingZNSAddressResolverDM, // failing DM + ZNSCurvePricerDM, + ZNSTreasuryDM, + ZNSRootRegistrarDM, + ZNSFixedPricerDM, + ZNSSubRegistrarDM, + ], + placeOfFailure: "postDeploy", + deployedNames, + undeployedNames, + failingInstanceName: "addressResolver", + callback: checkPostDeploy, + }); + + // make sure postDeploy() ran properly on the next run + const { + registry, + addressResolver, + } = nextCampaign; + expect(await registry.getResolverType(ResolverTypes.address)).to.be.equal(await addressResolver.getAddress()); + }); + + // eslint-disable-next-line max-len + it("[in RootRegistrar.deploy() hook] should ONLY deploy undeployed contracts in the run following a failed run", async () => { + class FailingZNSRootRegistrarDM extends ZNSRootRegistrarDM { + async deploy () { + throw new Error(errorMsgDeploy); + } + } + + const deployedNames = [ + znsNames.accessController, + znsNames.registry, + znsNames.domainToken, + { + contract: znsNames.meowToken.contractMock, + instance: znsNames.meowToken.instance, + }, + znsNames.addressResolver, + znsNames.curvePricer, + znsNames.treasury, + ]; + + const undeployedNames = [ + znsNames.rootRegistrar, + znsNames.fixedPricer, + znsNames.subRegistrar, + ]; + + // call test flow runner + await runTest({ + missionList: [ + ZNSAccessControllerDM, + ZNSRegistryDM, + ZNSDomainTokenDM, + MeowTokenDM, + ZNSAddressResolverDM, + ZNSCurvePricerDM, + ZNSTreasuryDM, + FailingZNSRootRegistrarDM, // failing DM + ZNSFixedPricerDM, + ZNSSubRegistrarDM, + ], + placeOfFailure: "deploy", + deployedNames, + undeployedNames, + failingInstanceName: "rootRegistrar", + }); + }); + + // eslint-disable-next-line max-len + it("[in RootRegistrar.postDeploy() hook] should start from post deploy sequence that failed on the previous run", async () => { + class FailingZNSRootRegistrarDM extends ZNSRootRegistrarDM { + async postDeploy () { + throw new Error(errorMsgPostDeploy); + } + } + + const deployedNames = [ + znsNames.accessController, + znsNames.registry, + znsNames.domainToken, + { + contract: znsNames.meowToken.contractMock, + instance: znsNames.meowToken.instance, + }, + znsNames.addressResolver, + znsNames.curvePricer, + znsNames.treasury, + znsNames.rootRegistrar, + ]; + + const undeployedNames = [ + znsNames.fixedPricer, + znsNames.subRegistrar, + ]; + + const checkPostDeploy = async (failingCampaign : DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + IZNSCampaignConfig, + IZNSContracts + >) => { + const { + // eslint-disable-next-line no-shadow + accessController, + // eslint-disable-next-line no-shadow + rootRegistrar, + } = failingCampaign; + + // we are checking that postDeploy did not grant REGISTRAR_ROLE to RootRegistrar + expect(await accessController.isRegistrar(await rootRegistrar.getAddress())).to.be.false; + }; + + // check contracts are deployed correctly + const { + nextCampaign, + } = await runTest({ + missionList: [ + ZNSAccessControllerDM, + ZNSRegistryDM, + ZNSDomainTokenDM, + MeowTokenDM, + ZNSAddressResolverDM, + ZNSCurvePricerDM, + ZNSTreasuryDM, + FailingZNSRootRegistrarDM, // failing DM + ZNSFixedPricerDM, + ZNSSubRegistrarDM, + ], + placeOfFailure: "postDeploy", + deployedNames, + undeployedNames, + failingInstanceName: "rootRegistrar", + callback: checkPostDeploy, + }); + + // make sure postDeploy() ran properly on the next run + const { + accessController, + rootRegistrar, + } = nextCampaign; + expect(await accessController.isRegistrar(await rootRegistrar.getAddress())).to.be.true; + }); + }); + + describe("Configurable Environment & Validation", () => { + let envInitial : string; + + beforeEach(async () => { + envInitial = JSON.stringify(process.env); + }); + + afterEach(async () => { + process.env = JSON.parse(envInitial); + }); + + // The `validate` function accepts the environment parameter only for the + // purpose of testing here as manipulating actual environment variables + // like `process.env. = "value"` is not possible in a test environment + // because the Hardhat process for running these tests will not respect these + // changes. `getConfig` calls to `validate` on its own, but never passes a value + // for the environment specifically, that is ever only inferred from the `process.env.ENV_LEVEL` + it("Gets the default configuration correctly", async () => { + // set the environment to get the appropriate variables + const localConfig : IZNSCampaignConfig = await getConfig({ + deployer: deployAdmin, + zeroVaultAddress: zeroVault.address, + governors: [governor.address], + admins: [admin.address], + }); + + expect(await localConfig.deployAdmin.getAddress()).to.eq(deployAdmin.address); + expect(localConfig.governorAddresses[0]).to.eq(governor.address); + expect(localConfig.governorAddresses[1]).to.eq(deployAdmin.address); + expect(localConfig.adminAddresses[0]).to.eq(admin.address); + expect(localConfig.adminAddresses[1]).to.eq(deployAdmin.address); + expect(localConfig.domainToken.name).to.eq(ZNS_DOMAIN_TOKEN_NAME); + expect(localConfig.domainToken.symbol).to.eq(ZNS_DOMAIN_TOKEN_SYMBOL); + expect(localConfig.domainToken.defaultRoyaltyReceiver).to.eq(zeroVault.address); + expect(localConfig.domainToken.defaultRoyaltyFraction).to.eq(DEFAULT_ROYALTY_FRACTION); + expect(localConfig.rootPriceConfig).to.deep.eq(DEFAULT_PRICE_CONFIG); + }); + + it("Confirms encoding functionality works for env variables", async () => { + const sample = "0x123,0x456,0x789"; + const sampleFormatted = ["0x123", "0x456", "0x789"]; + const encoded = btoa(sample); + const decoded = atob(encoded).split(","); + expect(decoded).to.deep.eq(sampleFormatted); + }); + + it("Modifies config to use a random account as the deployer", async () => { + // Run the deployment a second time, clear the DB so everything is deployed + + let zns : IZNSContracts; + + const config : IZNSCampaignConfig = await getConfig({ + deployer: userB, + zeroVaultAddress: userA.address, + governors: [userB.address, admin.address], // governors + admins: [userB.address, governor.address], // admins + }); + + const campaign = await runZnsCampaign({ + config, + }); + + const { dbAdapter } = campaign; + + /* eslint-disable-next-line prefer-const */ + zns = campaign.state.contracts; + + const rootPaymentConfig = await zns.treasury.paymentConfigs(ethers.ZeroHash); + + expect(await zns.accessController.isAdmin(userB.address)).to.be.true; + expect(await zns.accessController.isAdmin(governor.address)).to.be.true; + expect(await zns.accessController.isGovernor(admin.address)).to.be.true; + expect(rootPaymentConfig.token).to.eq(await zns.meowToken.getAddress()); + expect(rootPaymentConfig.beneficiary).to.eq(userA.address); + + await dbAdapter.dropDB(); + }); + + it("Fails when governor or admin addresses are given wrong", async () => { + // Custom addresses must given as the base64 encoded string of comma separated addresses + // e.g. btoa("0x123,0x456,0x789") = 'MHgxMjMsMHg0NTYsMHg3ODk=', which is what should be provided + // We could manipulate envariables through `process.env.` for this test and call `getConfig()` + // but the async nature of HH mocha tests causes this to mess up other tests + // Instead we use the same encoding functions used in `getConfig()` to test the functionality + + /* eslint-disable @typescript-eslint/no-explicit-any */ + try { + atob("[0x123,0x456]"); + } catch (e : any) { + expect(e.message).includes("Invalid character"); + } + + try { + atob("0x123, 0x456"); + } catch (e : any) { + expect(e.message).includes("Invalid character"); + } + + try { + atob("0x123 0x456"); + } catch (e : any) { + expect(e.message).includes("Invalid character"); + } + + try { + atob("'MHgxM jMsMHg0 NTYs MHg3ODk='"); + } catch (e : any) { + expect(e.message).includes("Invalid character"); + } + }); + + it("Throws if env variable is invalid", async () => { + try { + await getConfig({ + deployer: deployAdmin, + zeroVaultAddress: zeroVault.address, + governors: [deployAdmin.address, governor.address], + admins: [deployAdmin.address, admin.address], + }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (e : any) { + expect(e.message).includes(INVALID_ENV_ERR); + } + }); + + it("Fails to validate when mocking MEOW on prod", async () => { + process.env.MOCK_MEOW_TOKEN = "true"; + + try { + await getConfig({ + deployer: deployAdmin, + zeroVaultAddress: zeroVault.address, + governors: [deployAdmin.address, governor.address], + admins: [deployAdmin.address, admin.address], + }); + + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (e : any) { + expect(e.message).includes(NO_MOCK_PROD_ERR); + } + }); + + it("Fails to validate if not using the MEOW token on prod", async () => { + process.env.MOCK_MEOW_TOKEN = "false"; + process.env.STAKING_TOKEN_ADDRESS = "0x123"; + + try { + await getConfig({ + deployer: deployAdmin, + zeroVaultAddress: zeroVault.address, + governors: [deployAdmin.address, governor.address], + admins: [deployAdmin.address, admin.address], + }); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (e : any) { + expect(e.message).includes(STAKING_TOKEN_ERR); + } + }); + + it("Fails to validate if invalid curve for pricing", async () => { + process.env.MOCK_MEOW_TOKEN = "false"; + process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; + process.env.BASE_LENGTH = "3"; + process.env.MAX_LENGTH = "5"; + process.env.MAX_PRICE = "0"; + process.env.MIN_PRICE = ethers.parseEther("3").toString(); + + try { + await getConfig({ + env: "prod", + deployer: deployAdmin, + zeroVaultAddress: zeroVault.address, + governors: [deployAdmin.address, governor.address], + admins: [deployAdmin.address, admin.address], + }); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (e : any) { + expect(e.message).includes(INVALID_CURVE_ERR); + } + }); + + it("Fails to validate if no mongo uri or local URI in prod", async () => { + process.env.MOCK_MEOW_TOKEN = "false"; + process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; + // Falls back onto the default URI which is for localhost and fails in prod + process.env.MONGO_DB_URI = ""; + process.env.ROYALTY_RECEIVER = "0x123"; + process.env.ROYALTY_FRACTION = "100"; + + try { + await getConfig({ + env: "prod", + deployer: deployAdmin, + zeroVaultAddress: zeroVault.address, + governors: [deployAdmin.address, governor.address], + admins: [deployAdmin.address, admin.address], + }); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (e : any) { + expect(e.message).includes("Must provide a Mongo URI used for prod environment!"); + } + + process.env.MOCK_MEOW_TOKEN = "false"; + process.env.STAKING_TOKEN_ADDRESS = MeowMainnet.address; + process.env.MONGO_DB_URI = "mongodb://localhost:27018"; + process.env.ZERO_VAULT_ADDRESS = "0x123"; + + try { + await getConfig({ + env: "prod", + deployer: deployAdmin, + zeroVaultAddress: zeroVault.address, + governors: [deployAdmin.address, governor.address], + admins: [deployAdmin.address, admin.address], + }); + /* eslint-disable @typescript-eslint/no-explicit-any */ + } catch (e : any) { + expect(e.message).includes(MONGO_URI_ERR); + } + }); + }); + + describe("Versioning", () => { + let campaign : DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + IZNSCampaignConfig, + IZNSContracts + >; + + before(async () => { + await saveTag(); + + campaignConfig = { + env, + deployAdmin, + governorAddresses: [deployAdmin.address, governor.address], + adminAddresses: [deployAdmin.address, admin.address], + domainToken: { + name: ZNS_DOMAIN_TOKEN_NAME, + symbol: ZNS_DOMAIN_TOKEN_SYMBOL, + defaultRoyaltyReceiver: deployAdmin.address, + defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, + }, + rootPriceConfig: DEFAULT_PRICE_CONFIG, + zeroVaultAddress: zeroVault.address, + stakingTokenAddress: MeowMainnet.address, + mockMeowToken: true, + postDeploy: { + tenderlyProjectSlug: "", + monitorContracts: false, + verifyContracts: false, + }, + }; + + campaign = await runZnsCampaign({ + config: campaignConfig, + }); + }); + + it("should get the correct git tag + commit hash and write to DB", async () => { + const latestGitTag = (await execAsync("git describe --tags --abbrev=0")).stdout.trim(); + const latestCommit = (await execAsync(`git rev-list -n 1 ${latestGitTag}`)).stdout.trim(); + + const fullGitTag = `${latestGitTag}:${latestCommit}`; + + const { dbAdapter } = campaign; + + const versionDoc = await dbAdapter.versioner.getLatestVersion(); + expect(versionDoc?.contractsVersion).to.equal(fullGitTag); + + const deployedVersion = await dbAdapter.versioner.getDeployedVersion(); + expect(deployedVersion?.contractsVersion).to.equal(fullGitTag); + }); + + // eslint-disable-next-line max-len + it("should create new DB version and KEEP old data if ARCHIVE is true and no TEMP versions currently exist", async () => { + const { dbAdapter } = campaign; + + const versionDocInitial = await dbAdapter.versioner.getLatestVersion(); + const initialDBVersion = versionDocInitial?.dbVersion; + const registryDocInitial = await dbAdapter.getContract(znsNames.registry.contract); + + expect( + process.env.MONGO_DB_VERSION === undefined + || process.env.MONGO_DB_VERSION === "" + ).to.be.true; + + // set archiving for the new mongo adapter + const initialArchiveVal = process.env.ARCHIVE_PREVIOUS_DB_VERSION; + process.env.ARCHIVE_PREVIOUS_DB_VERSION = "true"; + + // run a new campaign + const { dbAdapter: newDbAdapter } = await runZnsCampaign({ + config: campaignConfig, + }); + + expect(newDbAdapter.curVersion).to.not.equal(initialDBVersion); + + // get some data from new DB version + const registryDocNew = await newDbAdapter.getContract(znsNames.registry.contract); + expect(registryDocNew?.version).to.not.equal(registryDocInitial?.version); + + const versionDocNew = await newDbAdapter.versioner.getLatestVersion(); + expect(versionDocNew?.dbVersion).to.not.equal(initialDBVersion); + expect(versionDocNew?.type).to.equal(VERSION_TYPES.deployed); + + // make sure old contracts from previous DB version are still there + const oldRegistryDocFromNewDB = await newDbAdapter.getContract( + znsNames.registry.contract, + initialDBVersion + ); + + expect(oldRegistryDocFromNewDB?.version).to.equal(registryDocInitial?.version); + expect(oldRegistryDocFromNewDB?.address).to.equal(registryDocInitial?.address); + expect(oldRegistryDocFromNewDB?.name).to.equal(registryDocInitial?.name); + expect(oldRegistryDocFromNewDB?.abi).to.equal(registryDocInitial?.abi); + expect(oldRegistryDocFromNewDB?.bytecode).to.equal(registryDocInitial?.bytecode); + + // reset back to default + process.env.ARCHIVE_PREVIOUS_DB_VERSION = initialArchiveVal; + }); + + // eslint-disable-next-line max-len + it("should create new DB version and WIPE all existing data if ARCHIVE is false and no TEMP versions currently exist", async () => { + const { dbAdapter } = campaign; + + const versionDocInitial = await dbAdapter.versioner.getLatestVersion(); + const initialDBVersion = versionDocInitial?.dbVersion; + const registryDocInitial = await dbAdapter.getContract(znsNames.registry.contract); + + expect( + process.env.MONGO_DB_VERSION === undefined + || process.env.MONGO_DB_VERSION === "" + ).to.be.true; + + // set archiving for the new mongo adapter + const initialArchiveVal = process.env.ARCHIVE_PREVIOUS_DB_VERSION; + process.env.ARCHIVE_PREVIOUS_DB_VERSION = "false"; + + // run a new campaign + const { dbAdapter: newDbAdapter } = await runZnsCampaign({ + config: campaignConfig, + }); + + expect(newDbAdapter.curVersion).to.not.equal(initialDBVersion); + + // get some data from new DB version + const registryDocNew = await newDbAdapter.getContract(znsNames.registry.contract); + expect(registryDocNew?.version).to.not.equal(registryDocInitial?.version); + + const versionDocNew = await newDbAdapter.versioner.getLatestVersion(); + expect(versionDocNew?.dbVersion).to.not.equal(initialDBVersion); + expect(versionDocNew?.type).to.equal(VERSION_TYPES.deployed); + + // make sure old contracts from previous DB version are NOT there + const oldRegistryDocFromNewDB = await newDbAdapter.getContract( + znsNames.registry.contract, + initialDBVersion + ); + + expect(oldRegistryDocFromNewDB).to.be.null; + + // reset back to default + process.env.ARCHIVE_PREVIOUS_DB_VERSION = initialArchiveVal; + }); + + // eslint-disable-next-line max-len + it("should pick up existing contracts and NOT deploy new ones into state if MONGO_DB_VERSION is specified", async () => { + const { dbAdapter } = campaign; + + const versionDocInitial = await dbAdapter.versioner.getLatestVersion(); + const initialDBVersion = versionDocInitial?.dbVersion; + const registryDocInitial = await dbAdapter.getContract(znsNames.registry.contract); + + // set DB version for the new mongo adapter + const initialDBVersionVal = process.env.MONGO_DB_VERSION; + process.env.MONGO_DB_VERSION = initialDBVersion; + + // run a new campaign + const { state: { contracts: newContracts } } = await runZnsCampaign({ + config: campaignConfig, + }); + + // make sure we picked up the correct DB version + const versionDocNew = await dbAdapter.versioner.getLatestVersion(); + expect(versionDocNew?.dbVersion).to.equal(initialDBVersion); + + // make sure old contracts from previous DB version are still there + const oldRegistryDocFromNewDB = await dbAdapter.getContract( + znsNames.registry.contract, + initialDBVersion + ); + + expect(oldRegistryDocFromNewDB?.version).to.equal(registryDocInitial?.version); + expect(oldRegistryDocFromNewDB?.address).to.equal(registryDocInitial?.address); + expect(oldRegistryDocFromNewDB?.name).to.equal(registryDocInitial?.name); + expect(oldRegistryDocFromNewDB?.abi).to.equal(registryDocInitial?.abi); + expect(oldRegistryDocFromNewDB?.bytecode).to.equal(registryDocInitial?.bytecode); + + // make sure contracts in state have been picked up correctly from DB + expect(await newContracts.registry.getAddress()).to.equal(registryDocInitial?.address); + + // reset back to default + process.env.MONGO_DB_VERSION = initialDBVersionVal; + }); + }); + + describe("Verify - Monitor", () => { + let config : IZNSCampaignConfig; + + before (async () => { + [deployAdmin, admin, governor, zeroVault] = await hre.ethers.getSigners(); + + config = { + env: "dev", + deployAdmin, + governorAddresses: [deployAdmin.address, governor.address], + adminAddresses: [deployAdmin.address, admin.address], + domainToken: { + name: ZNS_DOMAIN_TOKEN_NAME, + symbol: ZNS_DOMAIN_TOKEN_SYMBOL, + defaultRoyaltyReceiver: deployAdmin.address, + defaultRoyaltyFraction: DEFAULT_ROYALTY_FRACTION, + }, + rootPriceConfig: DEFAULT_PRICE_CONFIG, + zeroVaultAddress: zeroVault.address, + stakingTokenAddress: MeowMainnet.address, + mockMeowToken: true, + postDeploy: { + tenderlyProjectSlug: "", + monitorContracts: false, + verifyContracts: true, + }, + }; + }); + + afterEach(async () => { + await mongoAdapter.dropDB(); + }); + + it("should prepare the correct data for each contract when verifying on Etherscan", async () => { + const verifyData : Array<{ address : string; ctorArgs ?: TDeployArgs; }> = []; + class HardhatDeployerMock extends HardhatDeployer< + HardhatRuntimeEnvironment, + SignerWithAddress + > { + async etherscanVerify (args : { + address : string; + ctorArgs ?: TDeployArgs; + }) { + verifyData.push(args); + } + } + + const deployer = new HardhatDeployerMock({ + hre, + signer: deployAdmin, + env, + }); + + const campaign = await runZnsCampaign({ + config, + deployer, + }); + + const { state: { contracts } } = campaign; + ({ dbAdapter: mongoAdapter } = campaign); + + await Object.values(contracts).reduce( + async (acc, contract, idx) => { + await acc; + + if (idx === 0) { + expect(verifyData[idx].ctorArgs).to.be.deep.eq([config.governorAddresses, config.adminAddresses]); + } + + expect(verifyData[idx].address).to.equal(await contract.getAddress()); + }, + Promise.resolve() + ); + }); + + it("should prepare the correct contract data when pushing to Tenderly Project", async () => { + let tenderlyData : Array = []; + class HardhatDeployerMock extends HardhatDeployer< + HardhatRuntimeEnvironment, + SignerWithAddress + > { + async tenderlyPush (contracts : Array) { + tenderlyData = contracts; + } + } + + const deployer = new HardhatDeployerMock({ + hre, + signer: deployAdmin, + env, + }); + + config.postDeploy.monitorContracts = true; + config.postDeploy.verifyContracts = false; + + const campaign = await runZnsCampaign({ + config, + deployer, + }); + + const { state: { instances } } = campaign; + ({ dbAdapter: mongoAdapter } = campaign); + + let idx = 0; + await Object.values(instances).reduce( + async (acc, instance) => { + await acc; + + const dbData = await instance.getFromDB(); + + if (instance.proxyData.isProxy) { + // check proxy + expect(tenderlyData[idx].address).to.be.eq(dbData?.address); + expect(tenderlyData[idx].display_name).to.be.eq(`${instance.contractName}Proxy`); + + // check impl + expect(tenderlyData[idx + 1].address).to.be.eq(dbData?.implementation); + expect(tenderlyData[idx + 1].display_name).to.be.eq(`${dbData?.name}Impl`); + expect(tenderlyData[idx + 1].display_name).to.be.eq(`${instance.contractName}Impl`); + idx += 2; + } else { + expect(tenderlyData[idx].address).to.equal(dbData?.address); + expect(tenderlyData[idx].display_name).to.equal(dbData?.name); + expect(tenderlyData[idx].display_name).to.equal(instance.contractName); + idx++; + } + }, + Promise.resolve() + ); + }); + }); +}); diff --git a/test/ZNSStringResolver.test.ts b/test/ZNSStringResolver.test.ts new file mode 100644 index 000000000..13d6f19d8 --- /dev/null +++ b/test/ZNSStringResolver.test.ts @@ -0,0 +1,445 @@ +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import * as hre from "hardhat"; +import { + AC_UNAUTHORIZED_ERR, + distrConfigEmpty, + GOVERNOR_ROLE, + hashDomainLabel, + INITIALIZED_ERR, NOT_AUTHORIZED_ERR, + paymentConfigEmpty, + validateUpgrade, +} from "./helpers"; +import { IZNSCampaignConfig, IZNSContracts } from "../src/deploy/campaign/types"; +import { runZnsCampaign } from "../src/deploy/zns-campaign"; +import { expect } from "chai"; +import * as ethers from "ethers"; +import { registrationWithSetup } from "./helpers/register-setup"; +import { + ERC165__factory, + MeowTokenMock, ZNSAccessController, ZNSDomainToken, ZNSRegistry, ZNSRootRegistrar, + ZNSStringResolver, + ZNSStringResolverUpgradeMock__factory, + ZNSTreasury, +} from "../typechain"; +import { DeployCampaign, MongoDBAdapter } from "@zero-tech/zdc"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DefenderRelayProvider } from "@openzeppelin/defender-sdk-relay-signer-client/lib/ethers"; +import { getConfig } from "../src/deploy/campaign/environments"; +import { IZNSContractsLocal } from "./helpers/types"; + + +describe("ZNSStringResolver", () => { + describe("Single state tests", () => { + let zeroVault : SignerWithAddress; + let user : SignerWithAddress; + let deployAdmin : SignerWithAddress; + let deployer : SignerWithAddress; + let admin : SignerWithAddress; + + const env = "dev"; + + let stringResolver : ZNSStringResolver; + let registry : ZNSRegistry; + let campaign : DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + IZNSCampaignConfig, + IZNSContracts + >; + let rootRegistrar : ZNSRootRegistrar; + let accessController : ZNSAccessController; + + let userBalance : bigint; + + const uri = "https://example.com/817c64af"; + const domainName = "domain"; + const domainNameHash = hashDomainLabel(domainName); + + let mongoAdapter : MongoDBAdapter; + + before(async () => { + [ + deployer, + zeroVault, + user, + deployAdmin, + admin, + ] = await hre.ethers.getSigners(); + + const campaignConfig = await getConfig({ + deployer, + governors: [deployAdmin.address], + admins: [admin.address], + zeroVaultAddress: zeroVault.address, + env, + }); + + campaign = await runZnsCampaign({ + config: campaignConfig, + }); + + let meowToken : MeowTokenMock; + let treasury : ZNSTreasury; + + ({ + accessController, + stringResolver, + registry, + meowToken, + treasury, + rootRegistrar, + dbAdapter: mongoAdapter, + } = campaign); + + userBalance = ethers.parseEther("1000000000000000000"); + await meowToken.mint(user.address, userBalance); + await meowToken.connect(user).approve(await treasury.getAddress(), ethers.MaxUint256); + }); + + after(async () => { + await mongoAdapter.dropDB(); + }); + + it("Should not let initialize the contract twice", async () => { + await expect( + stringResolver.initialize( + await campaign.state.contracts.accessController.getAddress(), + await registry.getAddress(), + ) + ).to.be.revertedWithCustomError( + stringResolver, + INITIALIZED_ERR + ); + }); + + it("Should correctly attach the string to the domain", async () => { + + const newString = "hippopotamus"; + + await rootRegistrar.connect(user).registerRootDomain( + domainName, + ethers.ZeroAddress, + uri, + distrConfigEmpty, + paymentConfigEmpty, + ); + await stringResolver.connect(user).setString(domainNameHash, newString); + + expect( + await stringResolver.resolveDomainString(domainNameHash) + ).to.eq( + newString + ); + }); + + it("Should setRegistry() using ADMIN_ROLE and emit an event", async () => { + await expect( + stringResolver.connect(admin).setRegistry(admin.address) + ) + .to.emit(stringResolver, "RegistrySet") + .withArgs(admin.address); + + expect(await stringResolver.registry()).to.equal(admin.address); + + // reset regestry address on stringResolver + await stringResolver.connect(admin).setRegistry(registry.target); + }); + + it("Should revert when setRegistry() without ADMIN_ROLE", async () => { + await expect( + stringResolver.connect(user).setRegistry(user.address) + ).to.be.revertedWithCustomError( + accessController, + AC_UNAUTHORIZED_ERR + ); + + // reset regestry address on stringResolver + await stringResolver.connect(admin).setRegistry(registry.target); + }); + + it("Should revert when setAccessController() without ADMIN_ROLE " + + "(It cannot rewrite AC address after an incorrect address has been submitted to it)", async () => { + await expect( + stringResolver.connect(user).setAccessController(user.address) + ).to.be.revertedWithCustomError( + accessController, + AC_UNAUTHORIZED_ERR + ); + }); + + it("Should setAccessController() correctly with ADMIN_ROLE " + + "(It cannot rewrite AC address after an incorrect address has been submitted to it)", async () => { + + await expect( + stringResolver.connect(admin).setAccessController(admin.address) + ).to.emit( + stringResolver, "AccessControllerSet" + ).withArgs(admin.address); + + expect( + await stringResolver.getAccessController() + ).to.equal(admin.address); + }); + }); + + describe("New campaign for each test", () => { + + let deployer : SignerWithAddress; + let zeroVault : SignerWithAddress; + let operator : SignerWithAddress; + let user : SignerWithAddress; + let deployAdmin : SignerWithAddress; + let admin : SignerWithAddress; + + const env = "dev"; + + let stringResolver : ZNSStringResolver; + let registry : ZNSRegistry; + let campaign : DeployCampaign< + HardhatRuntimeEnvironment, + SignerWithAddress, + IZNSCampaignConfig, + IZNSContracts + >; + let accessController : ZNSAccessController; + let domainToken : ZNSDomainToken; + let operatorBalance : bigint; + let userBalance : bigint; + let deployerBalance : bigint; + + let zns : IZNSContractsLocal; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let mongoAdapter : MongoDBAdapter; + + beforeEach(async () => { + + [ + deployer, + zeroVault, + user, + deployAdmin, + admin, + operator, + ] = await hre.ethers.getSigners(); + + const campaignConfig = await getConfig({ + deployer: deployer as unknown as SignerWithAddress, + governors: [deployAdmin.address], + admins: [admin.address], + zeroVaultAddress: zeroVault.address, + env, + }); + + campaign = await runZnsCampaign({ + config: campaignConfig, + }); + + let meowToken : MeowTokenMock; + let treasury : ZNSTreasury; + + zns = campaign.state.contracts as unknown as IZNSContractsLocal; + + // eslint-disable-next-line max-len + ({ stringResolver, registry, meowToken, treasury, accessController, domainToken, dbAdapter: mongoAdapter } = campaign); + + operatorBalance = ethers.parseEther("1000000000000000000"); + await meowToken.mint(operator.address, operatorBalance); + await meowToken.connect(operator).approve(await treasury.getAddress(), ethers.MaxUint256); + + userBalance = ethers.parseEther("1000000000000000000"); + await meowToken.mint(user.address, userBalance); + await meowToken.connect(user).approve(await treasury.getAddress(), ethers.MaxUint256); + + deployerBalance = ethers.parseEther("1000000000000000000"); + await meowToken.mint(deployer.address, deployerBalance); + await meowToken.connect(deployer).approve(await treasury.getAddress(), ethers.MaxUint256); + }); + + it("Should not allow non-owner address to setString (similar domain and string)", async () => { + + const curStringDomain = "shouldbrake"; + + await registrationWithSetup({ + zns, + user: operator, + domainLabel: curStringDomain, + domainContent: ethers.ZeroAddress, + }); + + await expect( + stringResolver.connect(user).setString(hashDomainLabel(curStringDomain), curStringDomain) + ).to.be.revertedWithCustomError( + stringResolver, + NOT_AUTHORIZED_ERR + ); + }); + + it("Should allow OWNER to setString and emit event (similar domain and string)", async () => { + + const curString = "wolf"; + const hash = hashDomainLabel(curString); + + await registrationWithSetup({ + zns, + user, + domainLabel: curString, + domainContent: ethers.ZeroAddress, + }); + + await expect( + stringResolver.connect(user).setString(hash, curString) + ).to.emit( + stringResolver, + "StringSet" + ).withArgs( + hash, + curString + ); + + expect( + await stringResolver.resolveDomainString(hash) + ).to.equal(curString); + }); + + it("Should allow OPERATOR to setString and emit event (different domain and string)", async () => { + + const curDomain = "wild"; + const curString = "wildlife"; + const hash = hashDomainLabel(curDomain); + + await registrationWithSetup({ + zns, + user: deployer, + domainLabel: curDomain, + domainContent: ethers.ZeroAddress, + }); + + await registry.connect(deployer).setOwnersOperator(operator, true); + + await expect( + stringResolver.connect(operator).setString(hash, curString) + ).to.emit( + stringResolver, + "StringSet" + ).withArgs( + hash, + curString + ); + + expect( + await stringResolver.resolveDomainString(hash) + ).to.equal(curString); + }); + + it("Should setAccessController() correctly with ADMIN_ROLE", async () => { + await expect( + stringResolver.connect(admin).setAccessController(admin.address) + ).to.emit( + stringResolver, "AccessControllerSet" + ).withArgs(admin.address); + + expect( + await stringResolver.getAccessController() + ).to.equal(admin.address); + }); + + it("Should revert when setAccessController() without ADMIN_ROLE", async () => { + await expect( + stringResolver.connect(user).setAccessController(user.address) + ).to.be.revertedWithCustomError( + accessController, + AC_UNAUTHORIZED_ERR + ); + }); + + it("Should support the IZNSAddressResolver interface ID", async () => { + const interfaceId = await stringResolver.getInterfaceId(); + const supported = await stringResolver.supportsInterface(interfaceId); + expect(supported).to.be.true; + }); + + it("Should support the ERC-165 interface ID", async () => { + expect( + await stringResolver.supportsInterface( + ERC165__factory.createInterface() + .getFunction("supportsInterface").selector + ) + ).to.be.true; + }); + + it("Should not support other interface IDs", async () => { + expect( + await stringResolver.supportsInterface("0xffffffff") + ).to.be.false; + }); + + + describe("UUPS", () => { + + it("Allows an authorized user to upgrade the StringResolver", async () => { + // deployer deployed, deployAdmin wanna edit + const factory = new ZNSStringResolverUpgradeMock__factory(deployer); + const newStringResolver = await factory.deploy(); + await newStringResolver.waitForDeployment(); + + // Confirm the deployer is a governor + expect( + await accessController.hasRole(GOVERNOR_ROLE, deployAdmin.address) + ).to.be.true; + + const upgradeTx = domainToken.connect(deployAdmin).upgradeToAndCall(await newStringResolver.getAddress(), "0x"); + + await expect(upgradeTx).to.not.be.reverted; + }); + + it("Fails to upgrade if the caller is not authorized", async () => { + const factory = new ZNSStringResolverUpgradeMock__factory(deployer); + + // DomainToken to upgrade to + const newStringResolver = await factory.deploy(); + await newStringResolver.waitForDeployment(); + + // Confirm the operator is not a governor + await expect( + accessController.checkGovernor(operator.address) + ).to.be.revertedWithCustomError( + accessController, + AC_UNAUTHORIZED_ERR + ); + + const upgradeTx = domainToken.connect(operator).upgradeToAndCall(await newStringResolver.getAddress(), "0x"); + + await expect(upgradeTx).to.be.revertedWithCustomError( + accessController, + AC_UNAUTHORIZED_ERR + ); + }); + + // TODO: Falls on the role. I think, cannot give a REGISTRAR_ROLE to mock "deployAdmin". + it("Verifies that variable values are not changed in the upgrade process", async () => { + const curString = "variableschange"; + + await registrationWithSetup({ + zns, + user: deployer, + domainLabel: curString, + domainContent: ethers.ZeroAddress, + }); + + const factory = new ZNSStringResolverUpgradeMock__factory(deployer); + const newStringResolver = await factory.deploy(); + await newStringResolver.waitForDeployment(); + + await stringResolver.connect(deployer).setString(hashDomainLabel(curString), curString); + + const contractCalls = [ + stringResolver.registry(), + stringResolver.resolveDomainString(hashDomainLabel(curString)), + ]; + + await validateUpgrade(deployAdmin, stringResolver, newStringResolver, factory, contractCalls); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/ZNSSubRegistrar.test.ts b/test/ZNSSubRegistrar.test.ts index ac20e9803..8f7b71baa 100644 --- a/test/ZNSSubRegistrar.test.ts +++ b/test/ZNSSubRegistrar.test.ts @@ -18,7 +18,7 @@ import { NOT_AUTHORIZED_ERR, paymentConfigEmpty, PaymentType, - DECAULT_PRECISION, + DEFAULT_PRECISION, DEFAULT_PRICE_CONFIG, validateUpgrade, AC_UNAUTHORIZED_ERR, @@ -1753,7 +1753,7 @@ describe("ZNSSubRegistrar", () => { maxLength: BigInt(50), baseLength: BigInt(4), precisionMultiplier: BigInt(10) ** ( - decimalValues.thirteen - DECAULT_PRECISION + decimalValues.thirteen - DEFAULT_PRECISION ), feePercentage: BigInt(185), isSet: true, diff --git a/test/helpers/constants.ts b/test/helpers/constants.ts index a3adc1bc1..588ede8c1 100644 --- a/test/helpers/constants.ts +++ b/test/helpers/constants.ts @@ -11,8 +11,8 @@ export const DEFAULT_PROTOCOL_FEE_PERCENT = BigInt("222"); export const DEFAULT_PERCENTAGE_BASIS = BigInt("10000"); export const DEFAULT_DECIMALS = BigInt(18); -export const DECAULT_PRECISION = BigInt(2); -export const DEFAULT_PRECISION_MULTIPLIER = BigInt(10) ** (DEFAULT_DECIMALS - DECAULT_PRECISION); +export const DEFAULT_PRECISION = BigInt(2); +export const DEFAULT_PRECISION_MULTIPLIER = BigInt(10) ** (DEFAULT_DECIMALS - DEFAULT_PRECISION); // eslint-disable-next-line no-shadow export const AccessType = {