From 599816857f9979d58bd470ed7a91493d54b3a4a1 Mon Sep 17 00:00:00 2001 From: spengrah Date: Thu, 24 Aug 2023 15:19:08 -0500 Subject: [PATCH] initial logic compiling --- script/Deploy.s.sol | 8 +- src/AllowlistEligibility.sol | 272 ++++++++++++++++++ src/Module.sol | 82 ------ ...odule.t.sol => AllowlistEligibility.t.sol} | 15 +- 4 files changed, 284 insertions(+), 93 deletions(-) create mode 100644 src/AllowlistEligibility.sol delete mode 100644 src/Module.sol rename test/{Module.t.sol => AllowlistEligibility.t.sol} (82%) diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol index 4d29002..8712ac7 100644 --- a/script/Deploy.s.sol +++ b/script/Deploy.s.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.18; import { Script, console2 } from "forge-std/Script.sol"; -import { Module } from "../src/Module.sol"; +import { AllowlistEligibility } from "../src/AllowlistEligibility.sol"; contract Deploy is Script { - Module public implementation; + AllowlistEligibility public implementation; bytes32 public SALT = bytes32(abi.encode("change this to the value of your choice")); // default values @@ -42,7 +42,7 @@ contract Deploy is Script { * never differs regardless of where its being compiled * 2. The provided salt, `SALT` */ - implementation = new Module{ salt: SALT}(_version /* insert constructor args here */); + implementation = new AllowlistEligibility{ salt: SALT}(_version /* insert constructor args here */); vm.stopBroadcast(); @@ -60,7 +60,7 @@ contract DeployPrecompiled is Deploy { bytes memory args = abi.encode( /* insert constructor args here */ ); /// @dev Load and deploy pre-compiled ir-optimized bytecode. - implementation = Module(deployCode("optimized-out/Module.sol/Module.json", args)); + implementation = AllowlistEligibility(deployCode("optimized-out/Module.sol/Module.json", args)); vm.stopBroadcast(); diff --git a/src/AllowlistEligibility.sol b/src/AllowlistEligibility.sol new file mode 100644 index 0000000..d4016c4 --- /dev/null +++ b/src/AllowlistEligibility.sol @@ -0,0 +1,272 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +// import { console2 } from "forge-std/Test.sol"; // remove before deploy +import { HatsEligibilityModule, HatsModule, IHatsEligibility } from "hats-module/HatsEligibilityModule.sol"; + +/*////////////////////////////////////////////////////////////// + CUSTOM ERRORS +//////////////////////////////////////////////////////////////*/ + +/// @dev Thrown when the caller does not wear the `OWNER_HAT` +error AllowlistEligibility_NotOwner(); +/// @dev Thrown when the caller does not wear the `ARBITRATOR_HAT` +error AllowlistEligibility_NotArbitrator(); +/// @dev Thrown when array args are not the same length +error AllowlistEligibility_ArrayLengthMismatch(); + +/// @title AllowlistEligibility +/// @author spengrah +/// @author Haberdasher Labs +/// @notice A Hats Protocol eligibility that allows the owner to add and remove accounts from an allowlist +/// @dev This contract inherits from HatsEligibilityModule and is designed to deployed as a clone via HatsModuleFactory +contract AllowlistEligibility is HatsEligibilityModule { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + /// @notice Emitted when an account is added to the allowlist + event AccountAdded(address account); + /// @notice Emitted when multiple accounts are added to the allowlist + event AccountsAdded(address[] accounts); + /// @notice Emitted when an account is removed from the allowlist + event AccountRemoved(address account); + /// @notice Emitted when multiple accounts are removed from the allowlist + event AccountsRemoved(address[] accounts); + /// @notice Emitted when an account's standing is changed + event AccountStandingChanged(address account, bool standing); + /// @notice Emitted when multiple accounts' standing are changed + event AccountsStandingChanged(address[] accounts, bool[] standing); + + /*////////////////////////////////////////////////////////////// + DATA MODELS + //////////////////////////////////////////////////////////////*/ + + struct EligibilityData { + /// @notice Whether the wearer is eligible for the hat + /// @dev Defaults to false, ie not eligible + bool eligible; + /// @notice Whether the wearer is in bad standing + /// @dev Defaults to false, ie good standing + bool badStanding; + } + + /*////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /** + * This contract is a clone with immutable args, which means that it is deployed with a set of + * immutable storage variables (ie constants). Accessing these constants is cheaper than accessing + * regular storage variables (such as those set on initialization of a typical EIP-1167 clone), + * but requires a slightly different approach since they are read from calldata instead of storage. + * + * Below is a table of constants and their location. + * + * For more, see here: https://github.com/Saw-mon-and-Natalie/clones-with-immutable-args + * + * ----------------------------------------------------------------------+ + * CLONE IMMUTABLE "STORAGE" | + * ----------------------------------------------------------------------| + * Offset | Constant | Type | Length | Source | + * ----------------------------------------------------------------------| + * 0 | IMPLEMENTATION | address | 20 | HatsModule | + * 20 | HATS | address | 20 | HatsModule | + * 40 | hatId | uint256 | 32 | HatsModule | + * 72 | OWNER_HAT | uint256 | 32 | this | + * 104 | ARBITRATOR_HAT | uint256 | 32 | this | + * ----------------------------------------------------------------------+ + */ + + function OWNER_HAT() public pure returns (uint256) { + return _getArgUint256(72); + } + + function ARBITRATOR_HAT() public pure returns (uint256) { + return _getArgUint256(104); + } + + /*////////////////////////////////////////////////////////////// + MUTABLE STATE + //////////////////////////////////////////////////////////////*/ + + /** + * @notice The eligibility data for each account + * @custom:param account The account to get eligibility data for + * @custom:return eligibility The eligibility data for the account + */ + mapping(address account => EligibilityData eligibility) public allowlist; + + /*////////////////////////////////////////////////////////////// + CONSTRUCTOR + //////////////////////////////////////////////////////////////*/ + + /// @notice Deploy the implementation contract and set its version + /// @dev This is only used to deploy the implementation contract, and should not be used to deploy clones + constructor(string memory _version) HatsModule(_version) { } + + /*////////////////////////////////////////////////////////////// + INITIALIZER + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc HatsModule + function setUp(bytes calldata _initData) public override initializer { + // decode init data + address[] memory _accounts = abi.decode(_initData, (address[])); + // add initial accounts to allowlist + _addAccountsMemory(_accounts); + } + + /*////////////////////////////////////////////////////////////// + HATS ELIGIBILITY FUNCTION + //////////////////////////////////////////////////////////////*/ + + /// @inheritdoc IHatsEligibility + function getWearerStatus(address _wearer, uint256 /* _hatId */ ) + public + view + override + returns (bool _eligible, bool _standing) + { + // load a pointer to the eligibility data in storage + EligibilityData storage eligibility = allowlist[_wearer]; + + // wearer is always ineligible if in bad standing + if (eligibility.badStanding) return (false, false); + + _standing = true; + _eligible = eligibility.eligible; + } + + /*////////////////////////////////////////////////////////////// + ADMIN FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Add an account to the allowlist + * @dev Only callable by a wearer of the OWNER_HAT + * Note: overwrites existing eligibility data for the account + * @param _account The account to add + */ + function addAccount(address _account) public onlyOwner { + allowlist[_account].eligible = true; + + emit AccountAdded(_account); + } + + /** + * @notice Add multiple accounts to the allowlist + * @dev Only callable by a wearer of the OWNER_HAT + * Note: overwrites existing eligibility data for the accounts + * @param _accounts The array of accounts to add + */ + function addAccounts(address[] calldata _accounts) public onlyOwner { + for (uint256 i; i < _accounts.length;) { + allowlist[_accounts[i]].eligible = true; + unchecked { + ++i; + } + } + + emit AccountsAdded(_accounts); + } + + /** + * @notice Remove an account from the allowlist + * @dev Only callable by a wearer of the OWNER_HAT + * Note: overwrites existing eligibility data for the account + * @param _account The account to remove + */ + function removeAccount(address _account) public onlyOwner { + allowlist[_account].eligible = false; + + emit AccountRemoved(_account); + } + + /** + * @notice Remove multiple accounts from the allowlist + * @dev Only callable by a wearer of the OWNER_HAT + * Note: overwrites existing eligibility data for the accounts + * @param _accounts The array of accounts to remove + */ + function removeAccounts(address[] calldata _accounts) public onlyOwner { + for (uint256 i; i < _accounts.length;) { + allowlist[_accounts[i]].eligible = false; + unchecked { + ++i; + } + } + + emit AccountsRemoved(_accounts); + } + + /** + * @notice Set the standing for an account + * @dev Only callable by a wearer of the ARBITRATOR_HAT + * Note: overwrites existing standing data for the account + * @param _account The account to set standing for + * @param _standing The standing to set + */ + function setStandingForAccount(address _account, bool _standing) public onlyArbitrator { + allowlist[_account].badStanding = !_standing; + + emit AccountStandingChanged(_account, _standing); + } + + /** + * @notice Set the standing for multiple accounts + * @dev Only callable by a wearer of the ARBITRATOR_HAT + * Note: overwrites existing standing data for the accounts + * @param _accounts The array of accounts to set standing for + * @param _standing The array of standings to set, indexed to the accounts array + */ + function setStandingForAccounts(address[] calldata _accounts, bool[] calldata _standing) public onlyArbitrator { + // arrays must be the same length + if (_accounts.length != _standing.length) revert AllowlistEligibility_ArrayLengthMismatch(); + + for (uint256 i; i < _accounts.length;) { + allowlist[_accounts[i]].badStanding = !_standing[i]; + unchecked { + ++i; + } + } + + emit AccountsStandingChanged(_accounts, _standing); + } + + /*////////////////////////////////////////////////////////////// + INTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @dev Add multiple accounts to the allowlist, using memory instead of calldata for compatibility with {setUp} + * Note: overwrites existing eligibility data for the accounts + * @param _accounts The array of accounts to add + */ + function _addAccountsMemory(address[] memory _accounts) internal { + for (uint256 i; i < _accounts.length;) { + allowlist[_accounts[i]].eligible = true; + unchecked { + ++i; + } + } + + emit AccountsAdded(_accounts); + } + + /*////////////////////////////////////////////////////////////// + MODIFERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Reverts if the caller is not wearing the OWNER_HAT. + modifier onlyOwner() { + if (!HATS().isWearerOfHat(msg.sender, OWNER_HAT())) revert AllowlistEligibility_NotOwner(); + _; + } + + /// @notice Reverts if the caller is not wearing the ARBITRATOR_HAT. + modifier onlyArbitrator() { + if (!HATS().isWearerOfHat(msg.sender, ARBITRATOR_HAT())) revert AllowlistEligibility_NotArbitrator(); + _; + } +} diff --git a/src/Module.sol b/src/Module.sol deleted file mode 100644 index 9319206..0000000 --- a/src/Module.sol +++ /dev/null @@ -1,82 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.18; - -// import { console2 } from "forge-std/Test.sol"; // remove before deploy -import { HatsModule } from "hats-module/HatsModule.sol"; - -contract Module is HatsModule { - /*////////////////////////////////////////////////////////////// - CUSTOM ERRORS - //////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////// - EVENTS - //////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////// - DATA MODELS - //////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////// - CONSTANTS - //////////////////////////////////////////////////////////////*/ - - /** - * This contract is a clone with immutable args, which means that it is deployed with a set of - * immutable storage variables (ie constants). Accessing these constants is cheaper than accessing - * regular storage variables (such as those set on initialization of a typical EIP-1167 clone), - * but requires a slightly different approach since they are read from calldata instead of storage. - * - * Below is a table of constants and their location. - * - * For more, see here: https://github.com/Saw-mon-and-Natalie/clones-with-immutable-args - * - * ----------------------------------------------------------------------+ - * CLONE IMMUTABLE "STORAGE" | - * ----------------------------------------------------------------------| - * Offset | Constant | Type | Length | Source | - * ----------------------------------------------------------------------| - * 0 | IMPLEMENTATION | address | 20 | HatsModule | - * 20 | HATS | address | 20 | HatsModule | - * 40 | hatId | uint256 | 32 | HatsModule | - * 72+ | {other constants} | address | - | {this} | - * ----------------------------------------------------------------------+ - */ - - /*////////////////////////////////////////////////////////////// - MUTABLE STATE - //////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - /// @notice Deploy the implementation contract and set its version - /// @dev This is only used to deploy the implementation contract, and should not be used to deploy clones - constructor(string memory _version) HatsModule(_version) { } - - /*////////////////////////////////////////////////////////////// - CONSTRUCTOR - //////////////////////////////////////////////////////////////*/ - - /// @inheritdoc HatsModule - function setUp(bytes calldata _initData) public override initializer { - // decode init data - } - - /*////////////////////////////////////////////////////////////// - PUBLIC FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////// - VIEW FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////// - INTERNAL FUNCTIONS - //////////////////////////////////////////////////////////////*/ - - /*////////////////////////////////////////////////////////////// - MODIFERS - //////////////////////////////////////////////////////////////*/ -} diff --git a/test/Module.t.sol b/test/AllowlistEligibility.t.sol similarity index 82% rename from test/Module.t.sol rename to test/AllowlistEligibility.t.sol index 049223f..2c59c37 100644 --- a/test/Module.t.sol +++ b/test/AllowlistEligibility.t.sol @@ -1,26 +1,26 @@ // SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.18; +pragma solidity ^0.8.19; import { Test, console2 } from "forge-std/Test.sol"; -import { Module } from "../src/Module.sol"; +import { AllowlistEligibility } from "../src/AllowlistEligibility.sol"; import { Deploy, DeployPrecompiled } from "../script/Deploy.s.sol"; import { HatsModuleFactory, IHats, deployModuleInstance, deployModuleFactory } from "hats-module/utils/DeployFunctions.sol"; import { IHats } from "hats-protocol/Interfaces/IHats.sol"; -contract ModuleTest is Deploy, Test { +contract AllowlistEligibilityTest is Deploy, Test { /// @dev Inherit from DeployPrecompiled instead of Deploy if working with pre-compiled contracts /// @dev variables inhereted from Deploy script - // Module public implementation; + // AllowlistEligibility public implementation; // bytes32 public SALT; uint256 public fork; uint256 public BLOCK_NUMBER = 17_671_864; // deployment block for Hats.sol IHats public HATS = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); // v1.hatsprotocol.eth HatsModuleFactory public factory; - Module public instance; + AllowlistEligibility public instance; bytes public otherImmutableArgs; bytes public initArgs; uint256 public hatId; @@ -40,7 +40,7 @@ contract ModuleTest is Deploy, Test { } } -contract WithInstanceTest is ModuleTest { +contract WithInstanceTest is AllowlistEligibilityTest { function setUp() public virtual override { super.setUp(); @@ -53,7 +53,8 @@ contract WithInstanceTest is ModuleTest { initArgs = abi.encode(); // deploy an instance of the module - instance = Module(deployModuleInstance(factory, address(implementation), hatId, otherImmutableArgs, initArgs)); + instance = + AllowlistEligibility(deployModuleInstance(factory, address(implementation), hatId, otherImmutableArgs, initArgs)); } }