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,