diff --git a/contracts/DecentAutonomousAdmin.sol b/contracts/DecentAutonomousAdmin.sol new file mode 100644 index 00000000..feb879b9 --- /dev/null +++ b/contracts/DecentAutonomousAdmin.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {IHats} from "./interfaces/hats/full/IHats.sol"; +import {IHatsElectionsEligibility} from "./interfaces/hats/full/modules/IHatsElectionsEligibility.sol"; +import {FactoryFriendly} from "@gnosis.pm/zodiac/contracts/factory/FactoryFriendly.sol"; +import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol"; +import {IDecentAutonomousAdmin} from "./interfaces/IDecentAutonomousAdmin.sol"; + +contract DecentAutonomousAdmin is + IDecentAutonomousAdmin, + ERC165, + FactoryFriendly +{ + // ////////////////////////////////////////////////////////////// + // initializer + // ////////////////////////////////////////////////////////////// + function setUp(bytes memory initializeParams) public override initializer {} + + // ////////////////////////////////////////////////////////////// + // Public Functions + // ////////////////////////////////////////////////////////////// + function triggerStartNextTerm(TriggerStartArgs calldata args) public { + if ( + args.hatsProtocol.isWearerOfHat(args.currentWearer, args.hatId) == + false + ) revert NotCurrentWearer(); + + IHatsElectionsEligibility hatsElectionModule = IHatsElectionsEligibility( + args.hatsProtocol.getHatEligibilityModule(args.hatId) + ); + + hatsElectionModule.startNextTerm(); + + // This will burn the hat since wearer is no longer eligible + args.hatsProtocol.checkHatWearerStatus(args.hatId, args.currentWearer); + // This will mint the hat to the nominated wearer + args.hatsProtocol.mintHat(args.hatId, args.nominatedWearer); + } + + function supportsInterface( + bytes4 interfaceId + ) public view override returns (bool) { + return + interfaceId == type(IDecentAutonomousAdmin).interfaceId || + super.supportsInterface(interfaceId); + } +} diff --git a/contracts/DecentHatsCreationModule.sol b/contracts/DecentHatsCreationModule.sol new file mode 100644 index 00000000..34bf0600 --- /dev/null +++ b/contracts/DecentHatsCreationModule.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {Enum} from "@gnosis.pm/safe-contracts/contracts/common/Enum.sol"; +import {IAvatar} from "@gnosis.pm/zodiac/contracts/interfaces/IAvatar.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {IERC6551Registry} from "./interfaces/IERC6551Registry.sol"; +import {IHats} from "./interfaces/hats/full/IHats.sol"; +import {LockupLinear, Broker} from "./interfaces/sablier/full/types/DataTypes.sol"; +import {IHatsModuleFactory} from "./interfaces/hats/full/IHatsModuleFactory.sol"; +import {IHatsElectionsEligibility} from "./interfaces/hats/full/modules/IHatsElectionsEligibility.sol"; +import {ModuleProxyFactory} from "@gnosis.pm/zodiac/contracts/factory/ModuleProxyFactory.sol"; +import {ISablierV2LockupLinear} from "./interfaces/sablier/ISablierV2LockupLinear.sol"; + +contract DecentHatsCreationModule { + bytes32 public constant SALT = + 0x5d0e6ce4fd951366cc55da93f6e79d8b81483109d79676a04bcc2bed6a4b5072; + + struct TopHatParams { + string details; + string imageURI; + } + + struct AdminHatParams { + string details; + string imageURI; + bool isMutable; + } + + struct SablierStreamParams { + ISablierV2LockupLinear sablier; + address sender; + address asset; + LockupLinear.Timestamps timestamps; + Broker broker; + uint128 totalAmount; + bool cancelable; + bool transferable; + } + + struct HatParams { + address wearer; + string details; + string imageURI; + uint32 maxSupply; + bool isMutable; + uint128 termEndDateTs; + SablierStreamParams[] sablierStreamsParams; + } + + struct CreateTreeParams { + IHats hatsProtocol; + IERC6551Registry erc6551Registry; + IHatsModuleFactory hatsModuleFactory; + ModuleProxyFactory moduleProxyFactory; + address keyValuePairs; + address decentAutonomousAdminMasterCopy; + address hatsAccountImplementation; + address hatsElectionsEligibilityImplementation; + TopHatParams topHat; + AdminHatParams adminHat; + HatParams[] hats; + } + + /* ///////////////////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + ///////////////////////////////////////////////////////////////////////////// */ + /** + * @notice For a safe without any roles previously created on it, this function should be called. It sets up the + * top hat and admin hat, as well as any other hats and their streams that are provided, then transfers the top hat + * to the calling safe. + * + * @notice This contract should be enabled a module on the Safe for which the role(s) are to be created, and disabled after. + * + * @dev For each hat that is included, if the hat is: + * - termed, its stream funds on are targeted directly at the nominated wearer. The wearer should directly call `withdraw-` + * on the Sablier contract. + * - untermed, its stream funds are targeted at the hat's smart account. In order to withdraw funds from the stream, the + * hat's smart account must be the one call to `withdraw-` on the Sablier contract, setting the recipient arg to its wearer. + * + * @dev In order for a Safe to seamlessly create roles even if it has never previously created a role and thus has + * no hat tree, we defer the creation of the hat tree and its setup to this contract. This way, in a single tx block, + * the resulting topHatId of the newly created hat can be used to create an admin hat and any other hats needed. + * We also make use of `KeyValuePairs` to associate the topHatId with the Safe. + */ + function createAndDeclareTree(CreateTreeParams calldata params) external { + IHats hatsProtocol = params.hatsProtocol; + address hatsAccountImplementation = params.hatsAccountImplementation; + IERC6551Registry registry = params.erc6551Registry; + + // Create Top Hat + (uint256 topHatId, address topHatAccount) = processTopHat( + hatsProtocol, + registry, + hatsAccountImplementation, + params.keyValuePairs, + params.topHat + ); + + // Create Admin Hat + uint256 adminHatId = processAdminHat( + hatsProtocol, + registry, + hatsAccountImplementation, + topHatId, + topHatAccount, + params.moduleProxyFactory, + params.decentAutonomousAdminMasterCopy, + params.adminHat + ); + + for (uint256 i = 0; i < params.hats.length; ) { + HatParams memory hat = params.hats[i]; + processHat( + hatsProtocol, + registry, + hatsAccountImplementation, + topHatId, + topHatAccount, + params.hatsModuleFactory, + params.hatsElectionsEligibilityImplementation, + adminHatId, + hat + ); + + unchecked { + ++i; + } + } + + hatsProtocol.transferHat(topHatId, address(this), msg.sender); + } + + /* ///////////////////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + ///////////////////////////////////////////////////////////////////////////// */ + + function processTopHat( + IHats hatsProtocol, + IERC6551Registry registry, + address hatsAccountImplementation, + address keyValuePairs, + TopHatParams memory topHat + ) internal returns (uint256 topHatId, address topHatAccount) { + // Mint top hat + topHatId = hatsProtocol.mintTopHat( + address(this), + topHat.details, + topHat.imageURI + ); + + // Create top hat account + topHatAccount = registry.createAccount( + hatsAccountImplementation, + SALT, + block.chainid, + address(hatsProtocol), + topHatId + ); + + // Declare Top Hat ID to Safe via KeyValuePairs + string[] memory keys = new string[](1); + string[] memory values = new string[](1); + keys[0] = "topHatId"; + values[0] = Strings.toString(topHatId); + IAvatar(msg.sender).execTransactionFromModule( + keyValuePairs, + 0, + abi.encodeWithSignature( + "updateValues(string[],string[])", + keys, + values + ), + Enum.Operation.Call + ); + } + + function processAdminHat( + IHats hatsProtocol, + IERC6551Registry registry, + address hatsAccountImplementation, + uint256 topHatId, + address topHatAccount, + ModuleProxyFactory moduleProxyFactory, + address decentAutonomousAdminMasterCopy, + AdminHatParams memory adminHat + ) internal returns (uint256 adminHatId) { + // Create Admin Hat + adminHatId = hatsProtocol.createHat( + topHatId, + adminHat.details, + 1, // only one Admin Hat + topHatAccount, + topHatAccount, + adminHat.isMutable, + adminHat.imageURI + ); + + // Create Admin Hat's ERC6551 Account + registry.createAccount( + hatsAccountImplementation, + SALT, + block.chainid, + address(hatsProtocol), + adminHatId + ); + + // Deploy Decent Autonomous Admin Module, which will wear the Admin Hat + address autonomousAdminModule = moduleProxyFactory.deployModule( + decentAutonomousAdminMasterCopy, + abi.encodeWithSignature("setUp(bytes)", bytes("")), + uint256( + keccak256( + abi.encodePacked( + // for the salt, we'll concatenate our static salt with the id of the Admin Hat + SALT, + adminHatId + ) + ) + ) + ); + + // Mint Hat to the Decent Autonomous Admin Module + hatsProtocol.mintHat(adminHatId, autonomousAdminModule); + } + + function processHat( + IHats hatsProtocol, + IERC6551Registry registry, + address hatsAccountImplementation, + uint256 topHatId, + address topHatAccount, + IHatsModuleFactory hatsModuleFactory, + address hatsElectionsEligibilityImplementation, + uint256 adminHatId, + HatParams memory hat + ) internal { + // Create eligibility module if needed + address eligibilityAddress = createEligibilityModule( + hatsProtocol, + hatsModuleFactory, + hatsElectionsEligibilityImplementation, + topHatId, + topHatAccount, + adminHatId, + hat.termEndDateTs + ); + + // Create and Mint the Role Hat + uint256 hatId = createAndMintHat( + hatsProtocol, + adminHatId, + hat, + eligibilityAddress, + topHatAccount + ); + + // Get the stream recipient (based on termed or not) + address streamRecipient = setupStreamRecipient( + registry, + hatsAccountImplementation, + address(hatsProtocol), + hat.termEndDateTs, + hat.wearer, + hatId + ); + + // Create streams + createSablierStreams(hat.sablierStreamsParams, streamRecipient); + } + + // Exists to avoid stack too deep errors + function createEligibilityModule( + IHats hatsProtocol, + IHatsModuleFactory hatsModuleFactory, + address hatsElectionsEligibilityImplementation, + uint256 topHatId, + address topHatAccount, + uint256 adminHatId, + uint128 termEndDateTs + ) internal returns (address) { + if (termEndDateTs != 0) { + return + hatsModuleFactory.createHatsModule( + hatsElectionsEligibilityImplementation, + hatsProtocol.getNextId(adminHatId), + abi.encode(topHatId, uint256(0)), // [BALLOT_BOX_ID, ADMIN_HAT_ID] + abi.encode(termEndDateTs), + uint256(SALT) + ); + } + return topHatAccount; + } + + // Exists to avoid stack too deep errors + function createAndMintHat( + IHats hatsProtocol, + uint256 adminHatId, + HatParams memory hat, + address eligibilityAddress, + address topHatAccount + ) internal returns (uint256) { + uint256 hatId = hatsProtocol.createHat( + adminHatId, + hat.details, + hat.maxSupply, + eligibilityAddress, + topHatAccount, + hat.isMutable, + hat.imageURI + ); + + // If the hat is termed, nominate the wearer as the eligible member + if (hat.termEndDateTs != 0) { + address[] memory nominatedWearers = new address[](1); + nominatedWearers[0] = hat.wearer; + IHatsElectionsEligibility(eligibilityAddress).elect( + hat.termEndDateTs, + nominatedWearers + ); + } + + hatsProtocol.mintHat(hatId, hat.wearer); + return hatId; + } + + // Exists to avoid stack too deep errors + function setupStreamRecipient( + IERC6551Registry registry, + address hatsAccountImplementation, + address hatsProtocol, + uint128 termEndDateTs, + address wearer, + uint256 hatId + ) internal returns (address) { + // If the hat is termed, the wearer is the stream recipient + if (termEndDateTs != 0) { + return wearer; + } + + // Otherwise, the Hat's smart account is the stream recipient + return + registry.createAccount( + hatsAccountImplementation, + SALT, + block.chainid, + hatsProtocol, + hatId + ); + } + + // Exists to avoid stack too deep errors + function processSablierStream( + SablierStreamParams memory streamParams, + address streamRecipient + ) internal { + // Approve tokens for Sablier via a proxy call through the Safe + IAvatar(msg.sender).execTransactionFromModule( + streamParams.asset, + 0, + abi.encodeWithSignature( + "approve(address,uint256)", + streamParams.sablier, + streamParams.totalAmount + ), + Enum.Operation.Call + ); + + // Proxy the Sablier call through the Safe + IAvatar(msg.sender).execTransactionFromModule( + address(streamParams.sablier), + 0, + abi.encodeWithSignature( + "createWithTimestamps((address,address,uint128,address,bool,bool,(uint40,uint40,uint40),(address,uint256)))", + LockupLinear.CreateWithTimestamps({ + sender: streamParams.sender, + recipient: streamRecipient, + totalAmount: streamParams.totalAmount, + asset: IERC20(streamParams.asset), + cancelable: streamParams.cancelable, + transferable: streamParams.transferable, + timestamps: streamParams.timestamps, + broker: streamParams.broker + }) + ), + Enum.Operation.Call + ); + } + + // Exists to avoid stack too deep errors + function createSablierStreams( + SablierStreamParams[] memory streamParams, + address streamRecipient + ) internal { + for (uint256 i = 0; i < streamParams.length; ) { + processSablierStream(streamParams[i], streamRecipient); + + unchecked { + ++i; + } + } + } +} diff --git a/contracts/interfaces/IDecentAutonomousAdmin.sol b/contracts/interfaces/IDecentAutonomousAdmin.sol new file mode 100644 index 00000000..2591b02e --- /dev/null +++ b/contracts/interfaces/IDecentAutonomousAdmin.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.28; + +import {IHats} from "./hats/full/IHats.sol"; + +interface IDecentAutonomousAdmin { + error NotCurrentWearer(); + struct TriggerStartArgs { + address currentWearer; + IHats hatsProtocol; + uint256 hatId; + address nominatedWearer; + } + + function triggerStartNextTerm(TriggerStartArgs calldata args) external; +} diff --git a/contracts/interfaces/hats/full/HatsErrors.sol b/contracts/interfaces/hats/full/HatsErrors.sol new file mode 100644 index 00000000..b592529c --- /dev/null +++ b/contracts/interfaces/hats/full/HatsErrors.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: AGPL-3.0 +// Copyright (C) 2023 Haberdasher Labs +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.13; + +interface HatsErrors { + /// @notice Emitted when `user` is attempting to perform an action on `hatId` but is not wearing one of `hatId`'s admin hats + /// @dev Can be equivalent to `NotHatWearer(buildHatId(hatId))`, such as when emitted by `approveLinkTopHatToTree` or `relinkTopHatToTree` + error NotAdmin(address user, uint256 hatId); + + /// @notice Emitted when attempting to perform an action as or for an account that is not a wearer of a given hat + error NotHatWearer(); + + /// @notice Emitted when attempting to perform an action that requires being either an admin or wearer of a given hat + error NotAdminOrWearer(); + + /// @notice Emitted when attempting to mint `hatId` but `hatId`'s maxSupply has been reached + error AllHatsWorn(uint256 hatId); + + /// @notice Emitted when attempting to create a hat with a level 14 hat as its admin + error MaxLevelsReached(); + + /// @notice Emitted when an attempted hat id has empty intermediate level(s) + error InvalidHatId(); + + /// @notice Emitted when attempting to mint `hatId` to a `wearer` who is already wearing the hat + error AlreadyWearingHat(address wearer, uint256 hatId); + + /// @notice Emitted when attempting to mint a non-existant hat + error HatDoesNotExist(uint256 hatId); + + /// @notice Emmitted when attempting to mint or transfer a hat that is not active + error HatNotActive(); + + /// @notice Emitted when attempting to mint or transfer a hat to an ineligible wearer + error NotEligible(); + + /// @notice Emitted when attempting to check or set a hat's status from an account that is not that hat's toggle module + error NotHatsToggle(); + + /// @notice Emitted when attempting to check or set a hat wearer's status from an account that is not that hat's eligibility module + error NotHatsEligibility(); + + /// @notice Emitted when array arguments to a batch function have mismatching lengths + error BatchArrayLengthMismatch(); + + /// @notice Emitted when attempting to mutate or transfer an immutable hat + error Immutable(); + + /// @notice Emitted when attempting to change a hat's maxSupply to a value lower than its current supply + error NewMaxSupplyTooLow(); + + /// @notice Emitted when attempting to link a tophat to a new admin for which the tophat serves as an admin + error CircularLinkage(); + + /// @notice Emitted when attempting to link or relink a tophat to a separate tree + error CrossTreeLinkage(); + + /// @notice Emitted when attempting to link a tophat without a request + error LinkageNotRequested(); + + /// @notice Emitted when attempting to unlink a tophat that does not have a wearer + /// @dev This ensures that unlinking never results in a bricked tophat + error InvalidUnlink(); + + /// @notice Emmited when attempting to change a hat's eligibility or toggle module to the zero address + error ZeroAddress(); + + /// @notice Emmitted when attempting to change a hat's details or imageURI to a string with over 7000 bytes (~characters) + /// @dev This protects against a DOS attack where an admin iteratively extend's a hat's details or imageURI + /// to be so long that reading it exceeds the block gas limit, breaking `uri()` and `viewHat()` + error StringTooLong(); +} diff --git a/contracts/interfaces/hats/full/HatsEvents.sol b/contracts/interfaces/hats/full/HatsEvents.sol new file mode 100644 index 00000000..817e4ec1 --- /dev/null +++ b/contracts/interfaces/hats/full/HatsEvents.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: AGPL-3.0 +// Copyright (C) 2023 Haberdasher Labs +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.13; + +interface HatsEvents { + /// @notice Emitted when a new hat is created + /// @param id The id for the new hat + /// @param details A description of the Hat + /// @param maxSupply The total instances of the Hat that can be worn at once + /// @param eligibility The address that can report on the Hat wearer's status + /// @param toggle The address that can deactivate the Hat + /// @param mutable_ Whether the hat's properties are changeable after creation + /// @param imageURI The image uri for this hat and the fallback for its + event HatCreated( + uint256 id, + string details, + uint32 maxSupply, + address eligibility, + address toggle, + bool mutable_, + string imageURI + ); + + /// @notice Emitted when a hat wearer's standing is updated + /// @dev Eligibility is excluded since the source of truth for eligibility is the eligibility module and may change without a transaction + /// @param hatId The id of the wearer's hat + /// @param wearer The wearer's address + /// @param wearerStanding Whether the wearer is in good standing for the hat + event WearerStandingChanged( + uint256 hatId, + address wearer, + bool wearerStanding + ); + + /// @notice Emitted when a hat's status is updated + /// @param hatId The id of the hat + /// @param newStatus Whether the hat is active + event HatStatusChanged(uint256 hatId, bool newStatus); + + /// @notice Emitted when a hat's details are updated + /// @param hatId The id of the hat + /// @param newDetails The updated details + event HatDetailsChanged(uint256 hatId, string newDetails); + + /// @notice Emitted when a hat's eligibility module is updated + /// @param hatId The id of the hat + /// @param newEligibility The updated eligibiliy module + event HatEligibilityChanged(uint256 hatId, address newEligibility); + + /// @notice Emitted when a hat's toggle module is updated + /// @param hatId The id of the hat + /// @param newToggle The updated toggle module + event HatToggleChanged(uint256 hatId, address newToggle); + + /// @notice Emitted when a hat's mutability is updated + /// @param hatId The id of the hat + event HatMutabilityChanged(uint256 hatId); + + /// @notice Emitted when a hat's maximum supply is updated + /// @param hatId The id of the hat + /// @param newMaxSupply The updated max supply + event HatMaxSupplyChanged(uint256 hatId, uint32 newMaxSupply); + + /// @notice Emitted when a hat's image URI is updated + /// @param hatId The id of the hat + /// @param newImageURI The updated image URI + event HatImageURIChanged(uint256 hatId, string newImageURI); + + /// @notice Emitted when a tophat linkage is requested by its admin + /// @param domain The domain of the tree tophat to link + /// @param newAdmin The tophat's would-be admin in the parent tree + event TopHatLinkRequested(uint32 domain, uint256 newAdmin); + + /// @notice Emitted when a tophat is linked to a another tree + /// @param domain The domain of the newly-linked tophat + /// @param newAdmin The tophat's new admin in the parent tree + event TopHatLinked(uint32 domain, uint256 newAdmin); +} diff --git a/contracts/interfaces/hats/full/IHats.sol b/contracts/interfaces/hats/full/IHats.sol new file mode 100644 index 00000000..5d0cc188 --- /dev/null +++ b/contracts/interfaces/hats/full/IHats.sol @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: AGPL-3.0 +// Copyright (C) 2023 Haberdasher Labs +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.13; + +import "./IHatsIdUtilities.sol"; +import "./HatsErrors.sol"; +import "./HatsEvents.sol"; + +interface IHats is IHatsIdUtilities, HatsErrors, HatsEvents { + function mintTopHat( + address _target, + string memory _details, + string memory _imageURI + ) external returns (uint256 topHatId); + + function createHat( + uint256 _admin, + string calldata _details, + uint32 _maxSupply, + address _eligibility, + address _toggle, + bool _mutable, + string calldata _imageURI + ) external returns (uint256 newHatId); + + function batchCreateHats( + uint256[] calldata _admins, + string[] calldata _details, + uint32[] calldata _maxSupplies, + address[] memory _eligibilityModules, + address[] memory _toggleModules, + bool[] calldata _mutables, + string[] calldata _imageURIs + ) external returns (bool success); + + function getNextId(uint256 _admin) external view returns (uint256 nextId); + + function mintHat( + uint256 _hatId, + address _wearer + ) external returns (bool success); + + function batchMintHats( + uint256[] calldata _hatIds, + address[] calldata _wearers + ) external returns (bool success); + + function setHatStatus( + uint256 _hatId, + bool _newStatus + ) external returns (bool toggled); + + function checkHatStatus(uint256 _hatId) external returns (bool toggled); + + function setHatWearerStatus( + uint256 _hatId, + address _wearer, + bool _eligible, + bool _standing + ) external returns (bool updated); + + function checkHatWearerStatus( + uint256 _hatId, + address _wearer + ) external returns (bool updated); + + function renounceHat(uint256 _hatId) external; + + function transferHat(uint256 _hatId, address _from, address _to) external; + + /*////////////////////////////////////////////////////////////// + HATS ADMIN FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function makeHatImmutable(uint256 _hatId) external; + + function changeHatDetails( + uint256 _hatId, + string memory _newDetails + ) external; + + function changeHatEligibility( + uint256 _hatId, + address _newEligibility + ) external; + + function changeHatToggle(uint256 _hatId, address _newToggle) external; + + function changeHatImageURI( + uint256 _hatId, + string memory _newImageURI + ) external; + + function changeHatMaxSupply(uint256 _hatId, uint32 _newMaxSupply) external; + + function requestLinkTopHatToTree( + uint32 _topHatId, + uint256 _newAdminHat + ) external; + + function approveLinkTopHatToTree( + uint32 _topHatId, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external; + + function unlinkTopHatFromTree(uint32 _topHatId, address _wearer) external; + + function relinkTopHatWithinTree( + uint32 _topHatDomain, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external; + + /*////////////////////////////////////////////////////////////// + VIEW FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + function viewHat( + uint256 _hatId + ) + external + view + returns ( + string memory details, + uint32 maxSupply, + uint32 supply, + address eligibility, + address toggle, + string memory imageURI, + uint16 lastHatId, + bool mutable_, + bool active + ); + + function isWearerOfHat( + address _user, + uint256 _hatId + ) external view returns (bool isWearer); + + function isAdminOfHat( + address _user, + uint256 _hatId + ) external view returns (bool isAdmin); + + function isInGoodStanding( + address _wearer, + uint256 _hatId + ) external view returns (bool standing); + + function isEligible( + address _wearer, + uint256 _hatId + ) external view returns (bool eligible); + + function getHatEligibilityModule( + uint256 _hatId + ) external view returns (address eligibility); + + function getHatToggleModule( + uint256 _hatId + ) external view returns (address toggle); + + function getHatMaxSupply( + uint256 _hatId + ) external view returns (uint32 maxSupply); + + function hatSupply(uint256 _hatId) external view returns (uint32 supply); + + function getImageURIForHat( + uint256 _hatId + ) external view returns (string memory _uri); + + function balanceOf( + address wearer, + uint256 hatId + ) external view returns (uint256 balance); + + function balanceOfBatch( + address[] calldata _wearers, + uint256[] calldata _hatIds + ) external view returns (uint256[] memory); + + function uri(uint256 id) external view returns (string memory _uri); +} diff --git a/contracts/interfaces/hats/full/IHatsIdUtilities.sol b/contracts/interfaces/hats/full/IHatsIdUtilities.sol new file mode 100644 index 00000000..dcf640fd --- /dev/null +++ b/contracts/interfaces/hats/full/IHatsIdUtilities.sol @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: AGPL-3.0 +// Copyright (C) 2023 Haberdasher Labs +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +pragma solidity >=0.8.13; + +interface IHatsIdUtilities { + function buildHatId( + uint256 _admin, + uint16 _newHat + ) external pure returns (uint256 id); + + function getHatLevel(uint256 _hatId) external view returns (uint32 level); + + function getLocalHatLevel( + uint256 _hatId + ) external pure returns (uint32 level); + + function isTopHat(uint256 _hatId) external view returns (bool _topHat); + + function isLocalTopHat( + uint256 _hatId + ) external pure returns (bool _localTopHat); + + function isValidHatId( + uint256 _hatId + ) external view returns (bool validHatId); + + function getAdminAtLevel( + uint256 _hatId, + uint32 _level + ) external view returns (uint256 admin); + + function getAdminAtLocalLevel( + uint256 _hatId, + uint32 _level + ) external pure returns (uint256 admin); + + function getTopHatDomain( + uint256 _hatId + ) external view returns (uint32 domain); + + function getTippyTopHatDomain( + uint32 _topHatDomain + ) external view returns (uint32 domain); + + function noCircularLinkage( + uint32 _topHatDomain, + uint256 _linkedAdmin + ) external view returns (bool notCircular); + + function sameTippyTopHatDomain( + uint32 _topHatDomain, + uint256 _newAdminHat + ) external view returns (bool sameDomain); +} diff --git a/contracts/interfaces/hats/full/IHatsModuleFactory.sol b/contracts/interfaces/hats/full/IHatsModuleFactory.sol new file mode 100644 index 00000000..1aa8804d --- /dev/null +++ b/contracts/interfaces/hats/full/IHatsModuleFactory.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +interface IHatsModuleFactory { + error HatsModuleFactory_ModuleAlreadyDeployed( + address implementation, + uint256 hatId, + bytes otherImmutableArgs, + uint256 saltNonce + ); + + error BatchArrayLengthMismatch(); + + event HatsModuleFactory_ModuleDeployed( + address implementation, + address instance, + uint256 hatId, + bytes otherImmutableArgs, + bytes initData, + uint256 saltNonce + ); + + function HATS() external view returns (address); + + function version() external view returns (string memory); + + function createHatsModule( + address _implementation, + uint256 _hatId, + bytes calldata _otherImmutableArgs, + bytes calldata _initData, + uint256 _saltNonce + ) external returns (address _instance); + + function batchCreateHatsModule( + address[] calldata _implementations, + uint256[] calldata _hatIds, + bytes[] calldata _otherImmutableArgsArray, + bytes[] calldata _initDataArray, + uint256[] calldata _saltNonces + ) external returns (bool success); + + function getHatsModuleAddress( + address _implementation, + uint256 _hatId, + bytes calldata _otherImmutableArgs, + uint256 _saltNonce + ) external view returns (address); + + function deployed( + address _implementation, + uint256 _hatId, + bytes calldata _otherImmutableArgs, + uint256 _saltNonce + ) external view returns (bool); +} diff --git a/contracts/interfaces/hats/full/modules/IHatsElectionsEligibility.sol b/contracts/interfaces/hats/full/modules/IHatsElectionsEligibility.sol new file mode 100644 index 00000000..8d2fb835 --- /dev/null +++ b/contracts/interfaces/hats/full/modules/IHatsElectionsEligibility.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.13; + +interface IHatsElectionsEligibility { + event ElectionOpened(uint128 nextTermEnd); + event ElectionCompleted(uint128 termEnd, address[] winners); + event NewTermStarted(uint128 termEnd); + event Recalled(uint128 termEnd, address[] accounts); + + /// @notice Returns the first second after the current term ends. + /// @dev Also serves as the id for the current term. + function currentTermEnd() external view returns (uint128); + + /// @notice Returns the first second after the next term ends. + /// @dev Also serves as the id for the next term. + function nextTermEnd() external view returns (uint128); + + /// @notice Returns the election status (open or closed) for a given term end. + /// @param termEnd The term end timestamp to query. + function electionStatus( + uint128 termEnd + ) external view returns (bool isElectionOpen); + + /// @notice Returns whether a candidate was elected in a given term. + /// @param termEnd The term end timestamp to query. + /// @param candidate The address of the candidate. + function electionResults( + uint128 termEnd, + address candidate + ) external view returns (bool elected); + + /// @notice Returns the BALLOT_BOX_HAT constant. + function BALLOT_BOX_HAT() external pure returns (uint256); + + /// @notice Returns the ADMIN_HAT constant. + function ADMIN_HAT() external pure returns (uint256); + + /** + * @notice Submit the results of an election for a specified term. + * @dev Only callable by the wearer(s) of the BALLOT_BOX_HAT. + * @param _termEnd The id of the term for which the election results are being submitted. + * @param _winners The addresses of the winners of the election. + */ + function elect(uint128 _termEnd, address[] calldata _winners) external; + + /** + * @notice Submit the results of a recall election for a specified term. + * @dev Only callable by the wearer(s) of the BALLOT_BOX_HAT. + * @param _termEnd The id of the term for which the recall results are being submitted. + * @param _recallees The addresses to be recalled. + */ + function recall(uint128 _termEnd, address[] calldata _recallees) external; + + /** + * @notice Set the next term and open the election for it. + * @dev Only callable by the wearer(s) of the ADMIN_HAT. + * @param _newTermEnd The id of the term that will be opened. + */ + function setNextTerm(uint128 _newTermEnd) external; + + /** + * @notice Start the next term, updating the current term. + * @dev Can be called by anyone, but will revert if conditions are not met. + */ + function startNextTerm() external; + + /** + * @notice Determine the eligibility and standing of a wearer for a hat. + * @param _wearer The address of the hat wearer. + * @param _hatId The ID of the hat. + * @return eligible True if the wearer is eligible for the hat. + * @return standing True if the wearer is in good standing. + */ + function getWearerStatus( + address _wearer, + uint256 _hatId + ) external view returns (bool eligible, bool standing); +} diff --git a/contracts/mocks/MockHats.sol b/contracts/mocks/MockHats.sol index 0eb011a0..2a3b2b4a 100644 --- a/contracts/mocks/MockHats.sol +++ b/contracts/mocks/MockHats.sol @@ -1,55 +1,285 @@ // SPDX-License-Identifier: MIT pragma solidity =0.8.19; -import {IHats} from "../interfaces/hats/IHats.sol"; +import {IHats} from "../interfaces/hats/full/IHats.sol"; contract MockHats is IHats { - uint256 public count = 0; - mapping(uint256 => address) hatWearers; + uint256 public hatId = 0; + mapping(uint256 => address) public wearers; + mapping(uint256 => address) public eligibility; + + event HatCreated(uint256 hatId); function mintTopHat( address _target, string memory, string memory ) external returns (uint256 topHatId) { - topHatId = count; - count++; - hatWearers[topHatId] = _target; + topHatId = hatId; + hatId++; + wearers[topHatId] = _target; } function createHat( uint256, string calldata, uint32, - address, + address _eligibility, address, bool, string calldata ) external returns (uint256 newHatId) { - newHatId = count; - count++; + newHatId = hatId; + hatId++; + eligibility[hatId] = _eligibility; + emit HatCreated(hatId); } function mintHat( - uint256 hatId, - address wearer + uint256 _hatId, + address _wearer ) external returns (bool success) { success = true; - hatWearers[hatId] = wearer; + wearers[_hatId] = _wearer; } function transferHat(uint256 _hatId, address _from, address _to) external { - require( - hatWearers[_hatId] == _from, - "MockHats: Invalid current wearer" - ); - hatWearers[_hatId] = _to; + require(wearers[_hatId] == _from, "MockHats: Invalid current wearer"); + wearers[_hatId] = _to; } function isWearerOfHat( - address _user, + address _wearer, + uint256 _hatId + ) external view override returns (bool) { + return _wearer == wearers[_hatId]; + } + + function getHatEligibilityModule( + uint256 _hatId + ) external view override returns (address) { + return eligibility[_hatId]; + } + + function changeHatEligibility( + uint256 _hatId, + address _newEligibility + ) external override { + eligibility[_hatId] = _newEligibility; + } + + function buildHatId( + uint256 _admin, + uint16 _newHat + ) external pure override returns (uint256 id) {} + + function getHatLevel( + uint256 _hatId + ) external view override returns (uint32 level) {} + + function getLocalHatLevel( + uint256 _hatId + ) external pure override returns (uint32 level) {} + + function isTopHat( + uint256 _hatId + ) external view override returns (bool _topHat) {} + + function isLocalTopHat( + uint256 _hatId + ) external pure override returns (bool _localTopHat) {} + + function isValidHatId( uint256 _hatId - ) external view returns (bool isWearer) { - isWearer = hatWearers[_hatId] == _user; + ) external view override returns (bool validHatId) {} + + function getAdminAtLevel( + uint256 _hatId, + uint32 _level + ) external view override returns (uint256 admin) {} + + function getAdminAtLocalLevel( + uint256 _hatId, + uint32 _level + ) external pure override returns (uint256 admin) {} + + function getTopHatDomain( + uint256 _hatId + ) external view override returns (uint32 domain) {} + + function getTippyTopHatDomain( + uint32 _topHatDomain + ) external view override returns (uint32 domain) {} + + function noCircularLinkage( + uint32 _topHatDomain, + uint256 _linkedAdmin + ) external view override returns (bool notCircular) {} + + function sameTippyTopHatDomain( + uint32 _topHatDomain, + uint256 _newAdminHat + ) external view override returns (bool sameDomain) {} + + function batchCreateHats( + uint256[] calldata _admins, + string[] calldata _details, + uint32[] calldata _maxSupplies, + address[] memory _eligibilityModules, + address[] memory _toggleModules, + bool[] calldata _mutables, + string[] calldata _imageURIs + ) external override returns (bool success) {} + + function getNextId( + uint256 + ) external view override returns (uint256 nextId) { + nextId = hatId; } + + function batchMintHats( + uint256[] calldata _hatIds, + address[] calldata _wearers + ) external override returns (bool success) {} + + function setHatStatus( + uint256 _hatId, + bool _newStatus + ) external override returns (bool toggled) {} + + function checkHatStatus( + uint256 _hatId + ) external override returns (bool toggled) {} + + function setHatWearerStatus( + uint256 _hatId, + address _wearer, + bool _eligible, + bool _standing + ) external override returns (bool updated) {} + + function checkHatWearerStatus( + uint256 _hatId, + address + ) external override returns (bool updated) { + // 'burns' the hat if the wearer is no longer eligible + wearers[_hatId] = address(0); + return true; + } + + function renounceHat(uint256 _hatId) external override {} + + function makeHatImmutable(uint256 _hatId) external override {} + + function changeHatDetails( + uint256 _hatId, + string memory _newDetails + ) external override {} + + function changeHatToggle( + uint256 _hatId, + address _newToggle + ) external override {} + + function changeHatImageURI( + uint256 _hatId, + string memory _newImageURI + ) external override {} + + function changeHatMaxSupply( + uint256 _hatId, + uint32 _newMaxSupply + ) external override {} + + function requestLinkTopHatToTree( + uint32 _topHatId, + uint256 _newAdminHat + ) external override {} + + function approveLinkTopHatToTree( + uint32 _topHatId, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external override {} + + function unlinkTopHatFromTree( + uint32 _topHatId, + address _wearer + ) external override {} + + function relinkTopHatWithinTree( + uint32 _topHatDomain, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external override {} + + function viewHat( + uint256 _hatId + ) + external + view + override + returns ( + string memory _details, + uint32 _maxSupply, + uint32 _supply, + address _eligibility, + address _toggle, + string memory _imageURI, + uint16 _lastHatId, + bool _mutable, + bool _active + ) + {} + + function isAdminOfHat( + address _user, + uint256 _hatId + ) external view override returns (bool isAdmin) {} + + function isInGoodStanding( + address _wearer, + uint256 _hatId + ) external view override returns (bool standing) {} + + function isEligible( + address _wearer, + uint256 _hatId + ) external view override returns (bool eligible) {} + + function getHatToggleModule( + uint256 _hatId + ) external view override returns (address toggle) {} + + function getHatMaxSupply( + uint256 _hatId + ) external view override returns (uint32 maxSupply) {} + + function hatSupply( + uint256 _hatId + ) external view override returns (uint32 supply) {} + + function getImageURIForHat( + uint256 _hatId + ) external view override returns (string memory _uri) {} + + function balanceOf( + address _wearer, + uint256 _hatId + ) external view override returns (uint256 balance) {} + + function balanceOfBatch( + address[] calldata _wearers, + uint256[] calldata _hatIds + ) external view override returns (uint256[] memory) {} + + function uri( + uint256 id + ) external view override returns (string memory _uri) {} } diff --git a/contracts/mocks/MockHatsElectionEligibility.sol b/contracts/mocks/MockHatsElectionEligibility.sol new file mode 100644 index 00000000..6dec84c8 --- /dev/null +++ b/contracts/mocks/MockHatsElectionEligibility.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; +import {IHatsElectionsEligibility} from "../interfaces/hats/full/modules/IHatsElectionsEligibility.sol"; + +contract MockHatsElectionsEligibility is IHatsElectionsEligibility { + function currentTermEnd() external view returns (uint128) {} + + function nextTermEnd() external view returns (uint128) {} + + function electionStatus( + uint128 termEnd + ) external view returns (bool isElectionOpen) {} + + function electionResults( + uint128 termEnd, + address candidate + ) external view returns (bool elected) {} + + function BALLOT_BOX_HAT() external pure returns (uint256) {} + + function ADMIN_HAT() external pure returns (uint256) {} + + function elect(uint128 _termEnd, address[] calldata _winners) external {} + + function recall(uint128 _termEnd, address[] calldata _recallees) external {} + + function setNextTerm(uint128 _newTermEnd) external {} + + function startNextTerm() external {} + + function getWearerStatus( + address, + uint256 + ) external pure returns (bool eligible, bool standing) { + return (true, true); + } +} diff --git a/contracts/mocks/MockHatsModuleFactory.sol b/contracts/mocks/MockHatsModuleFactory.sol new file mode 100644 index 00000000..57ae687b --- /dev/null +++ b/contracts/mocks/MockHatsModuleFactory.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import {IHatsModuleFactory} from "../interfaces/hats/full/IHatsModuleFactory.sol"; +import {MockHatsElectionsEligibility} from "./MockHatsElectionEligibility.sol"; + +contract MockHatsModuleFactory is IHatsModuleFactory { + function createHatsModule( + address, + uint256, + bytes calldata, + bytes calldata, + uint256 + ) external override returns (address _instance) { + // Deploy a new instance of MockHatsElectionEligibility + MockHatsElectionsEligibility newModule = new MockHatsElectionsEligibility(); + _instance = address(newModule); + } + + function getHatsModuleAddress( + address _implementation, + uint256 _hatId, + bytes calldata _otherImmutableArgs, + uint256 _saltNonce + ) external view returns (address) {} + + function HATS() external view override returns (address) {} + + function version() external view override returns (string memory) {} + + function batchCreateHatsModule( + address[] calldata _implementations, + uint256[] calldata _hatIds, + bytes[] calldata _otherImmutableArgsArray, + bytes[] calldata _initDataArray, + uint256[] calldata _saltNonces + ) external override returns (bool success) {} + + function deployed( + address _implementation, + uint256 _hatId, + bytes calldata _otherImmutableArgs, + uint256 _saltNonce + ) external view override returns (bool) {} +} diff --git a/deploy/core/017_deploy_DecentHats_0_1_0.ts b/deploy/core/017_deploy_DecentHats_0_1_0.ts index 89a6ec97..7a2def8c 100644 --- a/deploy/core/017_deploy_DecentHats_0_1_0.ts +++ b/deploy/core/017_deploy_DecentHats_0_1_0.ts @@ -1,9 +1,11 @@ -import { HardhatRuntimeEnvironment } from 'hardhat/types'; +// import { HardhatRuntimeEnvironment } from "hardhat/types" import { DeployFunction } from 'hardhat-deploy/types'; -import { deployNonUpgradeable } from '../helpers/deployNonUpgradeable'; +// import { deployNonUpgradeable } from "../helpers/deployNonUpgradeable"; -const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { - await deployNonUpgradeable(hre, 'DecentHats_0_1_0'); +const func: DeployFunction = async (/* hre: HardhatRuntimeEnvironment */) => { + // No longer deploying DecentHats_0_1_0 to any new networks.. + // This contract has been depreciated. + // await deployNonUpgradeable(hre, "DecentHats_0_1_0"); }; export default func; diff --git a/deploy/core/019_deploy_DecentHats.ts b/deploy/core/019_deploy_DecentHats.ts new file mode 100644 index 00000000..6f48e56d --- /dev/null +++ b/deploy/core/019_deploy_DecentHats.ts @@ -0,0 +1,9 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; +import { deployNonUpgradeable } from '../helpers/deployNonUpgradeable'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + await deployNonUpgradeable(hre, 'DecentHats'); +}; + +export default func; diff --git a/deploy/core/020_deploy_DecentAutonomousAdmin.ts b/deploy/core/020_deploy_DecentAutonomousAdmin.ts new file mode 100644 index 00000000..a04d62d7 --- /dev/null +++ b/deploy/core/020_deploy_DecentAutonomousAdmin.ts @@ -0,0 +1,9 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; +import { deployNonUpgradeable } from '../helpers/deployNonUpgradeable'; + +const func: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + await deployNonUpgradeable(hre, 'DecentAutonomousAdmin'); +}; + +export default func; diff --git a/test/DecentAutonomousAdmin.test.ts b/test/DecentAutonomousAdmin.test.ts new file mode 100644 index 00000000..5f009833 --- /dev/null +++ b/test/DecentAutonomousAdmin.test.ts @@ -0,0 +1,107 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +import hre from 'hardhat'; +import { + DecentAutonomousAdmin, + DecentAutonomousAdmin__factory, + MockHats, + MockHats__factory, + MockHatsElectionsEligibility, + MockHatsElectionsEligibility__factory, +} from '../typechain-types'; + +describe('DecentAutonomousAdminHat', function () { + // Signer accounts + let deployer: SignerWithAddress; + let currentWearer: SignerWithAddress; + let randomUser: SignerWithAddress; + let nominatedWearer: SignerWithAddress; + + // Contract instances + let hatsProtocol: MockHats; + let hatsElectionModule: MockHatsElectionsEligibility; + let decentAutonomousAdminInstance: DecentAutonomousAdmin; + + // Variables + let userHatId: bigint; + + beforeEach(async function () { + // Get signers + [deployer, currentWearer, nominatedWearer, randomUser] = await hre.ethers.getSigners(); + + // Deploy MockHatsAutoAdmin (Mock Hats Protocol) + hatsProtocol = await new MockHats__factory(deployer).deploy(); + + // Deploy MockHatsElectionEligibility (Eligibility Module) + hatsElectionModule = await new MockHatsElectionsEligibility__factory(deployer).deploy(); + + // Create Admin Hat + const createAdminTx = await hatsProtocol.createHat( + hre.ethers.ZeroAddress, // Admin address (self-administered), currently unused + 'Details', // Hat details + 100, // Max supply + hre.ethers.ZeroAddress, // Eligibility module (none) + hre.ethers.ZeroAddress, // Toggle module (none) + true, // Is mutable + 'imageURI', // Image URI + ); + const createAdminTxReceipt = await createAdminTx.wait(); + const adminHatId = createAdminTxReceipt?.toJSON().logs[0].args[0]; + + // Deploy DecentAutonomousAdminHat contract with the admin hat ID + decentAutonomousAdminInstance = await new DecentAutonomousAdmin__factory(deployer).deploy(); + const adminHatAddress = await decentAutonomousAdminInstance.getAddress(); + // Mint the admin hat to adminHatWearer + await hatsProtocol.mintHat(adminHatId, adminHatAddress); + + // Create User Hat under the admin hat + const createUserTx = await hatsProtocol.createHat( + hre.ethers.ZeroAddress, // Admin address (decentAutonomousAdminInstance contract), currently unused + 'Details', // Hat details + 100, // Max supply + await hatsElectionModule.getAddress(), // Eligibility module (election module) + hre.ethers.ZeroAddress, // Toggle module (none) + false, // Is mutable + 'imageURI', // Image URI + ); + + const createUserTxReceipt = await createUserTx.wait(); + userHatId = createUserTxReceipt?.toJSON().logs[0].args[0]; + + // Mint the user hat to currentWearer + await hatsProtocol.mintHat(userHatId, await currentWearer.getAddress()); + }); + + describe('triggerStartNextTerm', function () { + it('should correctly validate current wearer and transfer', async function () { + const args = { + currentWearer: currentWearer.address, + hatsProtocol: await hatsProtocol.getAddress(), + hatId: userHatId, + nominatedWearer: nominatedWearer.address, + }; + + // Call triggerStartNextTerm on the decentAutonomousAdminInstance contract + await decentAutonomousAdminInstance.triggerStartNextTerm(args); + + // Verify the hat is now worn by the nominated wearer + expect((await hatsProtocol.isWearerOfHat(nominatedWearer.address, userHatId)) === true); + + expect((await hatsProtocol.isWearerOfHat(currentWearer.address, userHatId)) === false); + }); + it('should correctly invalidate random address as current wearer', async function () { + const args = { + currentWearer: randomUser.address, + hatsProtocol: await hatsProtocol.getAddress(), + hatId: userHatId, + nominatedWearer: nominatedWearer.address, + sablierStreamInfo: [], // No Sablier stream info for this test + }; + + // revert if not the current wearer + await expect( + decentAutonomousAdminInstance.connect(randomUser).triggerStartNextTerm(args), + ).to.be.revertedWithCustomError(decentAutonomousAdminInstance, 'NotCurrentWearer'); + }); + }); +}); diff --git a/test/DecentHatsCreationModule.test.ts b/test/DecentHatsCreationModule.test.ts new file mode 100644 index 00000000..982ca014 --- /dev/null +++ b/test/DecentHatsCreationModule.test.ts @@ -0,0 +1,628 @@ +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; +import { expect } from 'chai'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import { ethers } from 'ethers'; +import hre from 'hardhat'; + +import { + GnosisSafeL2, + GnosisSafeL2__factory, + DecentHatsCreationModule__factory, + KeyValuePairs, + KeyValuePairs__factory, + ERC6551Registry__factory, + MockHatsAccount__factory, + ERC6551Registry, + DecentHatsCreationModule, + MockHatsAccount, + MockHats, + MockHats__factory, + MockSablierV2LockupLinear__factory, + MockSablierV2LockupLinear, + MockERC20__factory, + MockERC20, + DecentAutonomousAdmin, + DecentAutonomousAdmin__factory, + MockHatsElectionsEligibility__factory, + MockHatsModuleFactory__factory, + ModuleProxyFactory, + ModuleProxyFactory__factory, +} from '../typechain-types'; + +import { getGnosisSafeL2Singleton, getGnosisSafeProxyFactory } from './GlobalSafeDeployments.test'; +import { executeSafeTransaction, getHatAccount, predictGnosisSafeAddress } from './helpers'; + +describe('DecentHatsCreationModule', () => { + let dao: SignerWithAddress; + + let mockHats: MockHats; + let mockHatsAddress: string; + + let keyValuePairs: KeyValuePairs; + let gnosisSafe: GnosisSafeL2; + + let decentHatsCreationModule: DecentHatsCreationModule; + let decentHatsCreationModuleAddress: string; + + let gnosisSafeAddress: string; + let erc6551Registry: ERC6551Registry; + + let mockHatsAccountImplementation: MockHatsAccount; + let mockHatsAccountImplementationAddress: string; + + let mockSablier: MockSablierV2LockupLinear; + let mockSablierAddress: string; + + let mockERC20: MockERC20; + let mockERC20Address: string; + + let mockHatsElectionsEligibilityImplementationAddress: string; + let mockHatsModuleFactoryAddress: string; + + let moduleProxyFactory: ModuleProxyFactory; + let decentAutonomousAdminMasterCopy: DecentAutonomousAdmin; + beforeEach(async () => { + try { + const signers = await hre.ethers.getSigners(); + const [deployer] = signers; + [, dao] = signers; + + mockHats = await new MockHats__factory(deployer).deploy(); + mockHatsAddress = await mockHats.getAddress(); + + const mockHatsElectionsEligibilityImplementation = + await new MockHatsElectionsEligibility__factory(deployer).deploy(); + mockHatsElectionsEligibilityImplementationAddress = + await mockHatsElectionsEligibilityImplementation.getAddress(); + + const mockHatsModuleFactory = await new MockHatsModuleFactory__factory(deployer).deploy(); + mockHatsModuleFactoryAddress = await mockHatsModuleFactory.getAddress(); + + keyValuePairs = await new KeyValuePairs__factory(deployer).deploy(); + erc6551Registry = await new ERC6551Registry__factory(deployer).deploy(); + mockHatsAccountImplementation = await new MockHatsAccount__factory(deployer).deploy(); + mockHatsAccountImplementationAddress = await mockHatsAccountImplementation.getAddress(); + decentHatsCreationModule = await new DecentHatsCreationModule__factory(deployer).deploy(); + decentHatsCreationModuleAddress = await decentHatsCreationModule.getAddress(); + moduleProxyFactory = await new ModuleProxyFactory__factory(deployer).deploy(); + decentAutonomousAdminMasterCopy = await new DecentAutonomousAdmin__factory(deployer).deploy(); + + const gnosisSafeProxyFactory = getGnosisSafeProxyFactory(); + const gnosisSafeL2Singleton = getGnosisSafeL2Singleton(); + const gnosisSafeL2SingletonAddress = await gnosisSafeL2Singleton.getAddress(); + + const createGnosisSetupCalldata = GnosisSafeL2__factory.createInterface().encodeFunctionData( + 'setup', + [ + [dao.address], + 1, + hre.ethers.ZeroAddress, + hre.ethers.ZeroHash, + hre.ethers.ZeroAddress, + hre.ethers.ZeroAddress, + 0, + hre.ethers.ZeroAddress, + ], + ); + const saltNum = BigInt(`0x${Buffer.from(hre.ethers.randomBytes(32)).toString('hex')}`); + + const predictedGnosisSafeAddress = await predictGnosisSafeAddress( + createGnosisSetupCalldata, + saltNum, + gnosisSafeL2SingletonAddress, + gnosisSafeProxyFactory, + ); + gnosisSafeAddress = predictedGnosisSafeAddress; + + await gnosisSafeProxyFactory.createProxyWithNonce( + gnosisSafeL2SingletonAddress, + createGnosisSetupCalldata, + saltNum, + ); + + gnosisSafe = GnosisSafeL2__factory.connect(predictedGnosisSafeAddress, deployer); + + // Deploy MockSablierV2LockupLinear + mockSablier = await new MockSablierV2LockupLinear__factory(deployer).deploy(); + mockSablierAddress = await mockSablier.getAddress(); + + mockERC20 = await new MockERC20__factory(deployer).deploy('MockERC20', 'MCK'); + mockERC20Address = await mockERC20.getAddress(); + + await mockERC20.mint(gnosisSafeAddress, ethers.parseEther('1000000')); + } catch (e) { + console.error('AHHHHHH', e); + } + }); + + describe('DecentHats as a Module', () => { + let enableModuleTx: ethers.ContractTransactionResponse; + + beforeEach(async () => { + enableModuleTx = await executeSafeTransaction({ + safe: gnosisSafe, + to: gnosisSafeAddress, + transactionData: GnosisSafeL2__factory.createInterface().encodeFunctionData( + 'enableModule', + [decentHatsCreationModuleAddress], + ), + signers: [dao], + }); + }); + + it('Emits an ExecutionSuccess event', async () => { + await expect(enableModuleTx).to.emit(gnosisSafe, 'ExecutionSuccess'); + }); + + it('Emits an EnabledModule event', async () => { + await expect(enableModuleTx) + .to.emit(gnosisSafe, 'EnabledModule') + .withArgs(decentHatsCreationModuleAddress); + }); + + describe('Creating a new Top Hat and Tree', () => { + let createAndDeclareTreeTx: ethers.ContractTransactionResponse; + + beforeEach(async () => { + createAndDeclareTreeTx = await executeSafeTransaction({ + safe: gnosisSafe, + to: decentHatsCreationModuleAddress, + transactionData: DecentHatsCreationModule__factory.createInterface().encodeFunctionData( + 'createAndDeclareTree', + [ + { + hatsProtocol: mockHatsAddress, + erc6551Registry: await erc6551Registry.getAddress(), + hatsModuleFactory: mockHatsModuleFactoryAddress, + moduleProxyFactory: await moduleProxyFactory.getAddress(), + decentAutonomousAdminMasterCopy: await decentAutonomousAdminMasterCopy.getAddress(), + hatsAccountImplementation: mockHatsAccountImplementationAddress, + keyValuePairs: await keyValuePairs.getAddress(), + hatsElectionsEligibilityImplementation: + mockHatsElectionsEligibilityImplementationAddress, + topHat: { + details: '', + imageURI: '', + }, + adminHat: { + details: '', + imageURI: '', + isMutable: false, + }, + hats: [ + { + wearer: ethers.ZeroAddress, + details: '', + imageURI: '', + maxSupply: 1, + isMutable: false, + termEndDateTs: 0, + sablierStreamsParams: [], + }, + { + wearer: ethers.ZeroAddress, + details: '', + imageURI: '', + maxSupply: 1, + isMutable: false, + termEndDateTs: 0, + sablierStreamsParams: [], + }, + ], + }, + ], + ), + signers: [dao], + }); + }); + + it('Emits an ExecutionSuccess event', async () => { + await expect(createAndDeclareTreeTx).to.emit(gnosisSafe, 'ExecutionSuccess'); + }); + + it('Emits an ExecutionFromModuleSuccess event', async () => { + await expect(createAndDeclareTreeTx) + .to.emit(gnosisSafe, 'ExecutionFromModuleSuccess') + .withArgs(decentHatsCreationModuleAddress); + }); + + it('Emits some hatsTreeId ValueUpdated events', async () => { + await expect(createAndDeclareTreeTx) + .to.emit(keyValuePairs, 'ValueUpdated') + .withArgs(gnosisSafeAddress, 'topHatId', '0'); + }); + + describe('Multiple calls', () => { + let createAndDeclareTreeTx2: ethers.ContractTransactionResponse; + + beforeEach(async () => { + createAndDeclareTreeTx2 = await executeSafeTransaction({ + safe: gnosisSafe, + to: decentHatsCreationModuleAddress, + transactionData: DecentHatsCreationModule__factory.createInterface().encodeFunctionData( + 'createAndDeclareTree', + [ + { + hatsProtocol: mockHatsAddress, + hatsAccountImplementation: mockHatsAccountImplementationAddress, + erc6551Registry: await erc6551Registry.getAddress(), + keyValuePairs: await keyValuePairs.getAddress(), + topHat: { + details: '', + imageURI: '', + }, + decentAutonomousAdminMasterCopy: + await decentAutonomousAdminMasterCopy.getAddress(), + moduleProxyFactory: await moduleProxyFactory.getAddress(), + adminHat: { + details: '', + imageURI: '', + isMutable: false, + }, + hats: [], + hatsModuleFactory: mockHatsModuleFactoryAddress, + hatsElectionsEligibilityImplementation: + mockHatsElectionsEligibilityImplementationAddress, + }, + ], + ), + signers: [dao], + }); + }); + + it('Emits an ExecutionSuccess event', async () => { + await expect(createAndDeclareTreeTx2).to.emit(gnosisSafe, 'ExecutionSuccess'); + }); + + it('Emits an ExecutionFromModuleSuccess event', async () => { + await expect(createAndDeclareTreeTx2) + .to.emit(gnosisSafe, 'ExecutionFromModuleSuccess') + .withArgs(decentHatsCreationModuleAddress); + }); + + it('Creates Top Hats with sequential IDs', async () => { + await expect(createAndDeclareTreeTx2) + .to.emit(keyValuePairs, 'ValueUpdated') + .withArgs(gnosisSafeAddress, 'topHatId', '4'); + }); + }); + + describe('Creating Hats Accounts', () => { + it('Generates the correct Addresses for the current Hats', async () => { + const currentCount = await mockHats.hatId(); + for (let i = 0n; i < currentCount; i++) { + const hatAccount = await getHatAccount( + i, + erc6551Registry, + mockHatsAccountImplementationAddress, + mockHatsAddress, + ); + expect(await hatAccount.tokenId()).eq(i); + expect(await hatAccount.tokenImplementation()).eq(mockHatsAddress); + } + }); + }); + }); + describe('Creating a new Top Hat and Tree with Termed Roles', () => { + let createAndDeclareTreeTx: ethers.ContractTransactionResponse; + + beforeEach(async () => { + createAndDeclareTreeTx = await executeSafeTransaction({ + safe: gnosisSafe, + to: decentHatsCreationModuleAddress, + transactionData: DecentHatsCreationModule__factory.createInterface().encodeFunctionData( + 'createAndDeclareTree', + [ + { + hatsProtocol: mockHatsAddress, + hatsAccountImplementation: mockHatsAccountImplementationAddress, + erc6551Registry: await erc6551Registry.getAddress(), + keyValuePairs: await keyValuePairs.getAddress(), + topHat: { + details: '', + imageURI: '', + }, + decentAutonomousAdminMasterCopy: await decentAutonomousAdminMasterCopy.getAddress(), + moduleProxyFactory: await moduleProxyFactory.getAddress(), + adminHat: { + details: '', + imageURI: '', + isMutable: true, + }, + hats: [ + { + maxSupply: 1, + details: '', + imageURI: '', + isMutable: false, + wearer: '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955', + sablierStreamsParams: [], + termEndDateTs: BigInt(Date.now() + 100000), + }, + { + maxSupply: 1, + details: '', + imageURI: '', + isMutable: false, + wearer: '0x14dC79964da2C08b23698B3D3cc7Ca32193d9955', + sablierStreamsParams: [], + termEndDateTs: BigInt(Date.now() + 100000), + }, + ], + hatsModuleFactory: mockHatsModuleFactoryAddress, + hatsElectionsEligibilityImplementation: + mockHatsElectionsEligibilityImplementationAddress, + }, + ], + ), + signers: [dao], + }); + }); + + it('Emits an ExecutionSuccess event', async () => { + await expect(createAndDeclareTreeTx).to.emit(gnosisSafe, 'ExecutionSuccess'); + }); + + it('Emits an ExecutionFromModuleSuccess event', async () => { + await expect(createAndDeclareTreeTx) + .to.emit(gnosisSafe, 'ExecutionFromModuleSuccess') + .withArgs(decentHatsCreationModuleAddress); + }); + + it('Emits some hatsTreeId ValueUpdated events', async () => { + await expect(createAndDeclareTreeTx) + .to.emit(keyValuePairs, 'ValueUpdated') + .withArgs(gnosisSafeAddress, 'topHatId', '0'); + }); + }); + + describe('Creating a new Top Hat and Tree with Sablier Streams', () => { + let createAndDeclareTreeTx: ethers.ContractTransactionResponse; + let currentBlockTimestamp: number; + + beforeEach(async () => { + currentBlockTimestamp = (await hre.ethers.provider.getBlock('latest'))!.timestamp; + + createAndDeclareTreeTx = await executeSafeTransaction({ + safe: gnosisSafe, + to: decentHatsCreationModuleAddress, + transactionData: DecentHatsCreationModule__factory.createInterface().encodeFunctionData( + 'createAndDeclareTree', + [ + { + hatsProtocol: mockHatsAddress, + hatsAccountImplementation: mockHatsAccountImplementationAddress, + erc6551Registry: await erc6551Registry.getAddress(), + keyValuePairs: await keyValuePairs.getAddress(), + topHat: { + details: '', + imageURI: '', + }, + decentAutonomousAdminMasterCopy: await decentAutonomousAdminMasterCopy.getAddress(), + moduleProxyFactory: await moduleProxyFactory.getAddress(), + adminHat: { + details: '', + imageURI: '', + isMutable: false, + }, + hats: [ + { + maxSupply: 1, + details: '', + imageURI: '', + isMutable: false, + wearer: ethers.ZeroAddress, + sablierStreamsParams: [ + { + sablier: mockSablierAddress, + sender: gnosisSafeAddress, + totalAmount: ethers.parseEther('100'), + asset: mockERC20Address, + cancelable: true, + transferable: false, + timestamps: { + start: currentBlockTimestamp, + cliff: 0, + end: currentBlockTimestamp + 2592000, // 30 days from now + }, + broker: { account: ethers.ZeroAddress, fee: 0 }, + }, + ], + termEndDateTs: 0, + }, + { + maxSupply: 1, + details: '', + imageURI: '', + isMutable: false, + wearer: ethers.ZeroAddress, + sablierStreamsParams: [], + termEndDateTs: 0, + }, + ], + hatsModuleFactory: mockHatsModuleFactoryAddress, + hatsElectionsEligibilityImplementation: + mockHatsElectionsEligibilityImplementationAddress, + }, + ], + ), + signers: [dao], + }); + }); + + it('Emits an ExecutionSuccess event', async () => { + await expect(createAndDeclareTreeTx).to.emit(gnosisSafe, 'ExecutionSuccess'); + }); + + it('Emits an ExecutionFromModuleSuccess event', async () => { + await expect(createAndDeclareTreeTx) + .to.emit(gnosisSafe, 'ExecutionFromModuleSuccess') + .withArgs(decentHatsCreationModuleAddress); + }); + + it('Emits some hatsTreeId ValueUpdated events', async () => { + await expect(createAndDeclareTreeTx) + .to.emit(keyValuePairs, 'ValueUpdated') + .withArgs(gnosisSafeAddress, 'topHatId', '0'); + }); + + it('Creates a Sablier stream for the hat with stream parameters', async () => { + const streamCreatedEvents = await mockSablier.queryFilter( + mockSablier.filters.StreamCreated(), + ); + expect(streamCreatedEvents.length).to.equal(1); + + const event = streamCreatedEvents[0]; + expect(event.args.sender).to.equal(gnosisSafeAddress); + expect(event.args.recipient).to.not.equal(ethers.ZeroAddress); + expect(event.args.totalAmount).to.equal(ethers.parseEther('100')); + }); + + it('Does not create a Sablier stream for hats without stream parameters', async () => { + const streamCreatedEvents = await mockSablier.queryFilter( + mockSablier.filters.StreamCreated(), + ); + expect(streamCreatedEvents.length).to.equal(1); // Only one stream should be created + }); + + it('Creates a Sablier stream with correct timestamps', async () => { + const streamCreatedEvents = await mockSablier.queryFilter( + mockSablier.filters.StreamCreated(), + ); + expect(streamCreatedEvents.length).to.equal(1); + + const streamId = streamCreatedEvents[0].args.streamId; + const stream = await mockSablier.getStream(streamId); + + expect(stream.startTime).to.equal(currentBlockTimestamp); + expect(stream.endTime).to.equal(currentBlockTimestamp + 2592000); + }); + }); + + describe('Creating a new Top Hat and Tree with Multiple Sablier Streams per Hat', () => { + let currentBlockTimestamp: number; + + beforeEach(async () => { + currentBlockTimestamp = (await hre.ethers.provider.getBlock('latest'))!.timestamp; + + await executeSafeTransaction({ + safe: gnosisSafe, + to: decentHatsCreationModuleAddress, + transactionData: DecentHatsCreationModule__factory.createInterface().encodeFunctionData( + 'createAndDeclareTree', + [ + { + hatsProtocol: mockHatsAddress, + hatsAccountImplementation: mockHatsAccountImplementationAddress, + erc6551Registry: await erc6551Registry.getAddress(), + keyValuePairs: await keyValuePairs.getAddress(), + topHat: { + details: '', + imageURI: '', + }, + decentAutonomousAdminMasterCopy: await decentAutonomousAdminMasterCopy.getAddress(), + moduleProxyFactory: await moduleProxyFactory.getAddress(), + adminHat: { + details: '', + imageURI: '', + isMutable: false, + }, + hats: [ + { + maxSupply: 1, + details: '', + imageURI: '', + isMutable: false, + wearer: ethers.ZeroAddress, + sablierStreamsParams: [ + { + sablier: mockSablierAddress, + sender: gnosisSafeAddress, + totalAmount: ethers.parseEther('100'), + asset: mockERC20Address, + cancelable: true, + transferable: false, + timestamps: { + start: currentBlockTimestamp, + cliff: currentBlockTimestamp + 86400, // 1 day cliff + end: currentBlockTimestamp + 2592000, // 30 days from now + }, + broker: { account: ethers.ZeroAddress, fee: 0 }, + }, + { + sablier: mockSablierAddress, + sender: gnosisSafeAddress, + totalAmount: ethers.parseEther('50'), + asset: mockERC20Address, + cancelable: false, + transferable: true, + timestamps: { + start: currentBlockTimestamp, + cliff: 0, // No cliff + end: currentBlockTimestamp + 1296000, // 15 days from now + }, + broker: { account: ethers.ZeroAddress, fee: 0 }, + }, + ], + termEndDateTs: 0, + }, + ], + hatsModuleFactory: mockHatsModuleFactoryAddress, + hatsElectionsEligibilityImplementation: + mockHatsElectionsEligibilityImplementationAddress, + }, + ], + ), + signers: [dao], + }); + }); + + it('Creates multiple Sablier streams for a single hat', async () => { + const streamCreatedEvents = await mockSablier.queryFilter( + mockSablier.filters.StreamCreated(), + ); + expect(streamCreatedEvents.length).to.equal(2); + + const event1 = streamCreatedEvents[0]; + expect(event1.args.sender).to.equal(gnosisSafeAddress); + expect(event1.args.recipient).to.not.equal(ethers.ZeroAddress); + expect(event1.args.totalAmount).to.equal(ethers.parseEther('100')); + + const event2 = streamCreatedEvents[1]; + expect(event2.args.sender).to.equal(gnosisSafeAddress); + expect(event2.args.recipient).to.equal(event1.args.recipient); + expect(event2.args.totalAmount).to.equal(ethers.parseEther('50')); + }); + + it('Creates streams with correct parameters', async () => { + const streamCreatedEvents = await mockSablier.queryFilter( + mockSablier.filters.StreamCreated(), + ); + + const stream1 = await mockSablier.getStream(streamCreatedEvents[0].args.streamId); + expect(stream1.cancelable === true); + expect(stream1.transferable === false); + expect(stream1.endTime - stream1.startTime).to.equal(2592000); + + const stream2 = await mockSablier.getStream(streamCreatedEvents[1].args.streamId); + expect(stream2.cancelable === false); + expect(stream2.transferable === true); + expect(stream2.endTime - stream2.startTime).to.equal(1296000); + }); + + it('Creates streams with correct timestamps', async () => { + const streamCreatedEvents = await mockSablier.queryFilter( + mockSablier.filters.StreamCreated(), + ); + + const stream1 = await mockSablier.getStream(streamCreatedEvents[0].args.streamId); + expect(stream1.startTime).to.equal(currentBlockTimestamp); + expect(stream1.endTime).to.equal(currentBlockTimestamp + 2592000); + + const stream2 = await mockSablier.getStream(streamCreatedEvents[1].args.streamId); + expect(stream2.startTime).to.equal(currentBlockTimestamp); + expect(stream2.endTime).to.equal(currentBlockTimestamp + 1296000); + }); + }); + }); +}); diff --git a/test/DecentHats_0_1_0.test.ts b/test/DecentHats_0_1_0.test.ts index 7ffb285e..194049ef 100644 --- a/test/DecentHats_0_1_0.test.ts +++ b/test/DecentHats_0_1_0.test.ts @@ -1,6 +1,8 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; -import hre, { ethers } from 'hardhat'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import { ethers } from 'ethers'; +import hre from 'hardhat'; import { GnosisSafeL2, GnosisSafeL2__factory, @@ -252,7 +254,7 @@ describe('DecentHats_0_1_0', () => { describe('Creating Hats Accounts', () => { it('Generates the correct Addresses for the current Hats', async () => { - const currentCount = await mockHats.count(); + const currentCount = await mockHats.hatId(); for (let i = 0n; i < currentCount; i++) { const topHatAccount = await getHatAccount( @@ -619,12 +621,12 @@ describe('DecentHats_0_1_0', () => { // First transfer the top hat to the Safe await mockHats.transferHat(topHatId, gnosisSafeAddress, decentHatsAddress); - const hatsCountBeforeCreate = await mockHats.count(); + const hatsCountBeforeCreate = await mockHats.hatId(); expect(hatsCountBeforeCreate).to.equal(2); // Top hat + admin hat await createRoleHatPromise; - const newHatId = await mockHats.count(); + const newHatId = await mockHats.hatId(); expect(newHatId).to.equal(3); // + newly created hat }); }); diff --git a/test/DecentSablierStreamManagement.test.ts b/test/DecentSablierStreamManagement.test.ts index de11ac61..360eaa4a 100644 --- a/test/DecentSablierStreamManagement.test.ts +++ b/test/DecentSablierStreamManagement.test.ts @@ -1,6 +1,8 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; -import hre, { ethers } from 'hardhat'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import { ethers } from 'ethers'; +import hre from 'hardhat'; import { DecentHats_0_1_0, DecentHats_0_1_0__factory, diff --git a/test/helpers.ts b/test/helpers.ts index 17826296..d8b42c2e 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -1,5 +1,7 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; -import hre, { ethers } from 'hardhat'; +/* eslint-disable-next-line import/no-extraneous-dependencies */ +import { ethers } from 'ethers'; +import hre from 'hardhat'; import { ERC6551Registry, GnosisSafeL2,