From be63f58076e3dcc8dde56db596a82827ac415b94 Mon Sep 17 00:00:00 2001 From: spengrah Date: Fri, 6 Sep 2024 16:06:33 -0500 Subject: [PATCH 1/8] use HatsModuleFactory v0.7.0 and some cleanup --- lib/forge-std | 2 +- lib/hats-module | 2 +- script/DeployPassthroughModule.s.sol | 93 ++++++++++++++++++++++++++++ test/PassthroughModule.t.sol | 27 ++++---- 4 files changed, 107 insertions(+), 17 deletions(-) create mode 100644 script/DeployPassthroughModule.s.sol diff --git a/lib/forge-std b/lib/forge-std index 74cfb77..1714bee 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 74cfb77e308dd188d2f58864aaf44963ae6b88b1 +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/lib/hats-module b/lib/hats-module index 5a69da3..e83bd72 160000 --- a/lib/hats-module +++ b/lib/hats-module @@ -1 +1 @@ -Subproject commit 5a69da357e70cc2e3727d6ec02097711439ec32b +Subproject commit e83bd72cb3eebdbeadabcb63e3c6f69ab61a5562 diff --git a/script/DeployPassthroughModule.s.sol b/script/DeployPassthroughModule.s.sol new file mode 100644 index 0000000..d6a9f06 --- /dev/null +++ b/script/DeployPassthroughModule.s.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import { Script, console2 } from "forge-std/Script.sol"; +import { PassthroughModule } from "../src/PassthroughModule.sol"; + +contract Deploy is Script { + PassthroughModule public implementation; + bytes32 public constant SALT = bytes32(abi.encode(0x4a75)); // ~ H(4) A(a) T(7) S(5) + + // default values + bool internal _verbose = true; + string internal _version = "0.0.2"; // increment this with each new deployment + + /// @dev Override default values, if desired + function prepare(bool verbose, string memory version) public { + _verbose = verbose; + _version = version; + } + + /// @dev Set up the deployer via their private key from the environment + function deployer() public returns (address) { + uint256 privKey = vm.envUint("PRIVATE_KEY"); + return vm.rememberKey(privKey); + } + + function _log(string memory prefix) internal view { + if (_verbose) { + console2.log(string.concat(prefix, "Module:"), address(implementation)); + } + } + + /// @dev Deploy the contract to a deterministic address via forge's create2 deployer factory. + function run() public virtual { + vm.startBroadcast(deployer()); + + /** + * @dev Deploy the contract to a deterministic address via forge's create2 deployer factory, which is at this + * address on all chains: `0x4e59b44847b379578588920cA78FbF26c0B4956C`. + * The resulting deployment address is determined by only two factors: + * 1. The bytecode hash of the contract to deploy. Setting `bytecode_hash` to "none" in foundry.toml ensures that + * never differs regardless of where its being compiled + * 2. The provided salt, `SALT` + */ + implementation = new PassthroughModule{ salt: SALT}(_version /* insert constructor args here */); + + vm.stopBroadcast(); + + _log(""); + } +} + +/// @dev Deploy pre-compiled ir-optimized bytecode to a non-deterministic address +contract DeployPrecompiled is Deploy { + /// @dev Update SALT and default values in Deploy contract + + function run() public override { + vm.startBroadcast(deployer()); + + bytes memory args = abi.encode( /* insert constructor args here */ ); + + /// @dev Load and deploy pre-compiled ir-optimized bytecode. + implementation = PassthroughModule(deployCode("optimized-out/Module.sol/Module.json", args)); + + vm.stopBroadcast(); + + _log("Precompiled "); + } +} + +/* FORGE CLI COMMANDS + +## A. Simulate the deployment locally +forge script script/Deploy.s.sol -f mainnet + +## B. Deploy to real network and verify on etherscan +forge script script/Deploy.s.sol -f mainnet --broadcast --verify + +## C. Fix verification issues (replace values in curly braces with the actual values) +forge verify-contract --chain-id 1 --num-of-optimizations 1000000 --watch --constructor-args $(cast abi-encode \ + "constructor({args})" "{arg1}" "{arg2}" "{argN}" ) \ + --compiler-version v0.8.19 {deploymentAddress} \ + src/{Counter}.sol:{Counter} --etherscan-api-key $ETHERSCAN_KEY + +## D. To verify ir-optimized contracts on etherscan... + 1. Run (C) with the following additional flag: `--show-standard-json-input > etherscan.json` + 2. Patch `etherscan.json`: `"optimizer":{"enabled":true,"runs":100}` => +`"optimizer":{"enabled":true,"runs":100},"viaIR":true` + 3. Upload the patched `etherscan.json` to etherscan manually + + See this github issue for more: https://github.com/foundry-rs/foundry/issues/3507#issuecomment-1465382107 + +*/ diff --git a/test/PassthroughModule.t.sol b/test/PassthroughModule.t.sol index b3793ab..411bb06 100644 --- a/test/PassthroughModule.t.sol +++ b/test/PassthroughModule.t.sol @@ -3,23 +3,22 @@ pragma solidity ^0.8.19; import { Test, console2 } from "forge-std/Test.sol"; import { PassthroughModule, NotAuthorized } from "../src/PassthroughModule.sol"; -import { Deploy, DeployPrecompiled } from "../script/Deploy.s.sol"; +import { Deploy } from "../script/DeployPassthroughModule.s.sol"; import { HatsModuleFactory, IHats, deployModuleInstance, deployModuleFactory } from "hats-module/utils/DeployFunctions.sol"; import { IHats } from "hats-protocol/Interfaces/IHats.sol"; contract PassthroughModuleTest is Deploy, Test { - /// @dev Inherit from DeployPrecompiled instead of Deploy if working with pre-compiled contracts - /// @dev variables inhereted from Deploy script // PassthroughModule public implementation; // bytes32 public SALT; uint256 public fork; - uint256 public BLOCK_NUMBER = 17_671_864; // deployment block for Hats.sol + uint256 public BLOCK_NUMBER = 19_467_227; // deployment block HatsModuleFactory v0.7.0 IHats public HATS = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); // v1.hatsprotocol.eth - HatsModuleFactory public factory; + HatsModuleFactory public factory = HatsModuleFactory(0x0a3f85fa597B6a967271286aA0724811acDF5CD9); + uint256 public SALT_NONCE = 1; PassthroughModule public instance; bytes public otherImmutableArgs; bytes public initArgs; @@ -48,9 +47,6 @@ contract PassthroughModuleTest is Deploy, Test { // deploy implementation via the script prepare(false, MODULE_VERSION); run(); - - // deploy the hats module factory - factory = deployModuleFactory(HATS, SALT, "test factory"); } } @@ -73,8 +69,9 @@ contract WithInstanceTest is PassthroughModuleTest { initArgs; // deploy an instance of the module - instance = - PassthroughModule(deployModuleInstance(factory, address(implementation), 0, otherImmutableArgs, initArgs)); + instance = PassthroughModule( + deployModuleInstance(factory, address(implementation), 0, otherImmutableArgs, initArgs, SALT_NONCE) + ); } modifier caller_(address _caller) { @@ -94,23 +91,23 @@ contract Deployment is WithInstanceTest { instance.setUp("setUp attempt"); } - function test_version() public { + function test_version() public view { assertEq(instance.version(), MODULE_VERSION); } - function test_implementation() public { + function test_implementation() public view { assertEq(address(instance.IMPLEMENTATION()), address(implementation)); } - function test_hats() public { + function test_hats() public view { assertEq(address(instance.HATS()), address(HATS)); } - function test_hatId() public { + function test_hatId() public view { assertEq(instance.hatId(), 0); } - function test_criterionHat() public { + function test_criterionHat() public view { assertEq(instance.CRITERION_HAT(), moduleHat); } } From b0ad84d5db32571c1be71a50424daf4e952968b8 Mon Sep 17 00:00:00 2001 From: spengrah Date: Fri, 6 Sep 2024 16:07:00 -0500 Subject: [PATCH 2/8] add HatControlledModule --- ....s.sol => DeployHatControlledModule.s.sol} | 42 +---- src/HatControlledModule.sol | 150 ++++++++++++++++ test/HatControlledModule.t.sol | 170 ++++++++++++++++++ 3 files changed, 328 insertions(+), 34 deletions(-) rename script/{Deploy.s.sol => DeployHatControlledModule.s.sol} (54%) create mode 100644 src/HatControlledModule.sol create mode 100644 test/HatControlledModule.t.sol diff --git a/script/Deploy.s.sol b/script/DeployHatControlledModule.s.sol similarity index 54% rename from script/Deploy.s.sol rename to script/DeployHatControlledModule.s.sol index f05c0ac..e32acc6 100644 --- a/script/Deploy.s.sol +++ b/script/DeployHatControlledModule.s.sol @@ -2,15 +2,15 @@ pragma solidity ^0.8.19; import { Script, console2 } from "forge-std/Script.sol"; -import { PassthroughModule } from "../src/PassthroughModule.sol"; +import { HatControlledModule } from "../src/HatControlledModule.sol"; contract Deploy is Script { - PassthroughModule public implementation; + HatControlledModule public implementation; bytes32 public constant SALT = bytes32(abi.encode(0x4a75)); // ~ H(4) A(a) T(7) S(5) // default values bool internal _verbose = true; - string internal _version = "0.0.1"; // increment this with each new deployment + string internal _version = "0.1.0"; // initial version for HatControlledModule /// @dev Override default values, if desired function prepare(bool verbose, string memory version) public { @@ -34,15 +34,7 @@ contract Deploy is Script { function run() public virtual { vm.startBroadcast(deployer()); - /** - * @dev Deploy the contract to a deterministic address via forge's create2 deployer factory, which is at this - * address on all chains: `0x4e59b44847b379578588920cA78FbF26c0B4956C`. - * The resulting deployment address is determined by only two factors: - * 1. The bytecode hash of the contract to deploy. Setting `bytecode_hash` to "none" in foundry.toml ensures that - * never differs regardless of where its being compiled - * 2. The provided salt, `SALT` - */ - implementation = new PassthroughModule{ salt: SALT}(_version /* insert constructor args here */); + implementation = new HatControlledModule{ salt: SALT }(_version); vm.stopBroadcast(); @@ -50,37 +42,19 @@ contract Deploy is Script { } } -/// @dev Deploy pre-compiled ir-optimized bytecode to a non-deterministic address -contract DeployPrecompiled is Deploy { - /// @dev Update SALT and default values in Deploy contract - - function run() public override { - vm.startBroadcast(deployer()); - - bytes memory args = abi.encode( /* insert constructor args here */ ); - - /// @dev Load and deploy pre-compiled ir-optimized bytecode. - implementation = PassthroughModule(deployCode("optimized-out/Module.sol/Module.json", args)); - - vm.stopBroadcast(); - - _log("Precompiled "); - } -} - /* FORGE CLI COMMANDS ## A. Simulate the deployment locally -forge script script/Deploy.s.sol -f mainnet +forge script script/DeployHatControlledModule.s.sol -f mainnet ## B. Deploy to real network and verify on etherscan -forge script script/Deploy.s.sol -f mainnet --broadcast --verify +forge script script/DeployHatControlledModule.s.sol -f mainnet --broadcast --verify ## C. Fix verification issues (replace values in curly braces with the actual values) forge verify-contract --chain-id 1 --num-of-optimizations 1000000 --watch --constructor-args $(cast abi-encode \ - "constructor({args})" "{arg1}" "{arg2}" "{argN}" ) \ + "constructor(string)" "_version") \ --compiler-version v0.8.19 {deploymentAddress} \ - src/{Counter}.sol:{Counter} --etherscan-api-key $ETHERSCAN_KEY + src/HatControlledModule.sol:HatControlledModule --etherscan-api-key $ETHERSCAN_KEY ## D. To verify ir-optimized contracts on etherscan... 1. Run (C) with the following additional flag: `--show-standard-json-input > etherscan.json` diff --git a/src/HatControlledModule.sol b/src/HatControlledModule.sol new file mode 100644 index 0000000..ed3961d --- /dev/null +++ b/src/HatControlledModule.sol @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +// import { console2 } from "forge-std/Test.sol"; // remove before deploy +import { HatsEligibilityModule, HatsModule } from "hats-module/HatsEligibilityModule.sol"; +import { HatsToggleModule } from "hats-module/HatsToggleModule.sol"; + +/*////////////////////////////////////////////////////////////// + CUSTOM ERRORS +//////////////////////////////////////////////////////////////*/ + +/// @notice Thrown when the caller is not wearing the {hatId} hat +error NotAuthorized(); + +/** + * @title HatControlledModule + * @author spengrah + * @author Haberdasher Labs + * @notice This module allows the wearer(s) of a given "criterion" hat to serve as the eligibilty and/or toggle module + * for a different hat. It is compatible with module chaining. + * @dev This contract inherits from HatsModule, and is intended to be deployed as minimal proxy clone(s) via + * HatsModuleFactory. For this contract to be used, it must be set as either the eligibility or toggle module for + * another hat. + */ +contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { + /*////////////////////////////////////////////////////////////// + DATA MODELS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Ineligibility and standing data for an account, defaulting to positives. + * @param ineligible Whether the account is ineligible to wear the hat. Defaults to eligible. + * @param badStanding Whether the account is in bad standing for the hat. Defaults to good standing. + */ + struct IneligibilityData { + bool ineligible; + 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 | CRITERION_HAT | uint256 | 32 | PassthroughModule | + * ----------------------------------------------------------------------+ + */ + function CRITERION_HAT() public pure returns (uint256) { + return _getArgUint256(72); + } + + /*////////////////////////////////////////////////////////////// + MUTABLE STORAGE + //////////////////////////////////////////////////////////////*/ + + /// @notice Ineligibility and standing data for a given hat and wearer, defaulting to eligible and good standing + mapping(uint256 hatId => mapping(address wearer => IneligibilityData ineligibility)) internal wearerIneligibility; + + /// @notice Status of a given hat + mapping(uint256 hatId => bool inactive) internal hatInactivity; + + /*////////////////////////////////////////////////////////////// + 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) internal override { + // no initial values to set + } + + /*////////////////////////////////////////////////////////////// + ELIGIBILITY FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Set the eligibility status of a `_hatId` for a `_wearer`, in this contract. When this contract is set as + * the eligibility module for that `hatId`, including as part of a module chain, Hats Protocol will pull this data + * when checking the wearer's eligibility. + * @dev Only callable by the wearer(s) of the {hatId} hat. + * @param _wearer The address to set the eligibility status for + * @param _hatId The hat to set the eligibility status for + * @param _eligible The new _wearer's eligibility, where TRUE = eligible + * @param _standing The new _wearer's standing, where TRUE = in good standing + */ + function setWearerStatus(address _wearer, uint256 _hatId, bool _eligible, bool _standing) public onlyController { + wearerIneligibility[_hatId][_wearer] = IneligibilityData(!_eligible, !_standing); + } + + /// @inheritdoc HatsEligibilityModule + function getWearerStatus(address _wearer, uint256 _hatId) public view override returns (bool eligible, bool standing) { + IneligibilityData memory data = wearerIneligibility[_hatId][_wearer]; + return (!data.ineligible, !data.badStanding); + } + + /*////////////////////////////////////////////////////////////// + TOGGLE FUNCTIONS + //////////////////////////////////////////////////////////////*/ + + /** + * @notice Toggle the status of `_hatId` in this contract. When this contract is set as the toggle module for that + * `hatId`, including as part of a module chain, Hats Protocol will pull this data when checking the status of the + * hat. + * @dev Only callable by the wearer(s) of the {hatId} hat. + * @param _hatId The hat to set the status for + * @param _newStatus The new status, where TRUE = active + */ + function setHatStatus(uint256 _hatId, bool _newStatus) public onlyController { + hatInactivity[_hatId] = !_newStatus; + } + + /// @inheritdoc HatsToggleModule + function getHatStatus(uint256 _hatId) public view override returns (bool active) { + return !hatInactivity[_hatId]; + } + + /*////////////////////////////////////////////////////////////// + MODIFIERS + //////////////////////////////////////////////////////////////*/ + + /// @notice Reverts if the caller is not wearing the {hatId} hat + modifier onlyController() { + if (!HATS().isWearerOfHat(msg.sender, CRITERION_HAT())) revert NotAuthorized(); + _; + } +} diff --git a/test/HatControlledModule.t.sol b/test/HatControlledModule.t.sol new file mode 100644 index 0000000..6c37648 --- /dev/null +++ b/test/HatControlledModule.t.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.19; + +import { Test, console2 } from "forge-std/Test.sol"; +import { HatControlledModule, NotAuthorized } from "../src/HatControlledModule.sol"; +import { Deploy } from "../script/DeployHatControlledModule.s.sol"; +import { + HatsModuleFactory, IHats, deployModuleInstance, deployModuleFactory +} from "hats-module/utils/DeployFunctions.sol"; +import { IHats } from "hats-protocol/Interfaces/IHats.sol"; + +contract HatControlledModuleTest is Deploy, Test { + /// @dev variables inhereted from Deploy script + // HatControlledModule public implementation; + // bytes32 public SALT; + + uint256 public fork; + uint256 public BLOCK_NUMBER = 19_467_227; // deployment block HatsModuleFactory v0.7.0 + IHats public HATS = IHats(0x3bc1A0Ad72417f2d411118085256fC53CBdDd137); // v1.hatsprotocol.eth + HatsModuleFactory public factory = HatsModuleFactory(0x0a3f85fa597B6a967271286aA0724811acDF5CD9); + uint256 public SALT_NONCE = 1; + HatControlledModule public instance; + bytes public otherImmutableArgs; + bytes public initArgs; + + uint256 public tophat; + uint256 public targetHat; + uint256 public criterionHat; + + address public caller; + address public controller = makeAddr("controller"); + address public org = makeAddr("org"); + address public wearer = makeAddr("wearer"); + address public nonWearer = makeAddr("nonWearer"); + + string public MODULE_VERSION; + + function setUp() public virtual { + fork = vm.createSelectFork(vm.rpcUrl("mainnet"), BLOCK_NUMBER); + + prepare(false, MODULE_VERSION); + run(); + } +} + +contract WithInstanceTest is HatControlledModuleTest { + function setUp() public virtual override { + super.setUp(); + + tophat = HATS.mintTopHat(org, "org's tophat", ""); + vm.startPrank(org); + targetHat = HATS.createHat(tophat, "target hat", 2, address(999), address(999), true, ""); + criterionHat = HATS.createHat(tophat, "criterion hat", 2, address(999), address(999), true, ""); + HATS.mintHat(criterionHat, controller); + vm.stopPrank(); + + otherImmutableArgs = abi.encodePacked(criterionHat); + initArgs; + + instance = HatControlledModule( + deployModuleInstance(factory, address(implementation), 0, otherImmutableArgs, initArgs, SALT_NONCE) + ); + } + + modifier caller_(address _caller) { + caller = _caller; + vm.prank(caller); + _; + } +} + +contract Deployment is WithInstanceTest { + function test_initialization() public { + vm.expectRevert("Initializable: contract is already initialized"); + implementation.setUp("setUp attempt"); + vm.expectRevert("Initializable: contract is already initialized"); + instance.setUp("setUp attempt"); + } + + function test_version() public view { + assertEq(instance.version(), MODULE_VERSION); + } + + function test_implementation() public view { + assertEq(address(instance.IMPLEMENTATION()), address(implementation)); + } + + function test_hats() public view { + assertEq(address(instance.HATS()), address(HATS)); + } + + function test_hatId() public view { + assertEq(instance.hatId(), 0); + } + + function test_criterionHat() public view { + assertEq(instance.CRITERION_HAT(), criterionHat); + } +} + +contract Eligibility is WithInstanceTest { + function test_controller(address _wearer, uint256 _hatId, bool _eligible, bool _standing) public { + // set a wearer's status + vm.prank(controller); + instance.setWearerStatus(_wearer, _hatId, _eligible, _standing); + + (bool eligible, bool standing) = instance.getWearerStatus(_wearer, _hatId); + assertEq(eligible, _eligible); + assertEq(standing, _standing); + + // set the same wearer's status for a different hat + uint256 differentHat = uint256(keccak256(abi.encodePacked(_hatId))); + + vm.prank(controller); + instance.setWearerStatus(_wearer, differentHat, _eligible, _standing); + + (eligible, standing) = instance.getWearerStatus(_wearer, differentHat); + assertEq(eligible, _eligible); + assertEq(standing, _standing); + + // set a different wearer's status for the first hat + address otherWearer = address(bytes20(keccak256(abi.encodePacked(_wearer)))); + + vm.prank(controller); + instance.setWearerStatus(otherWearer, _hatId, _eligible, _standing); + + (eligible, standing) = instance.getWearerStatus(otherWearer, _hatId); + assertEq(eligible, _eligible); + assertEq(standing, _standing); + } + + function test_default() public view { + (bool eligible, bool standing) = instance.getWearerStatus(wearer, targetHat); + assertTrue(eligible); + assertTrue(standing); + } + + function test_revert_nonController_cannotSetWearerStatus() public { + vm.expectRevert(NotAuthorized.selector); + vm.prank(nonWearer); + instance.setWearerStatus(wearer, targetHat, false, true); + } +} + +contract Toggle is WithInstanceTest { + function test_controller(uint256 _hatId, bool _status) public { + vm.prank(controller); + instance.setHatStatus(_hatId, _status); + + assertEq(instance.getHatStatus(_hatId), _status); + + // set a different hat's status + uint256 differentHat = uint256(keccak256(abi.encodePacked(_hatId))); + + vm.prank(controller); + instance.setHatStatus(differentHat, _status); + + assertEq(instance.getHatStatus(differentHat), _status); + } + + function test_default() public view { + assertTrue(instance.getHatStatus(targetHat)); + } + + function test_revert_nonController_cannotSetHatStatus() public { + vm.expectRevert(NotAuthorized.selector); + vm.prank(nonWearer); + instance.setHatStatus(targetHat, false); + } +} From 91d8ca57354c5d2ec65600eef6367b307b031444 Mon Sep 17 00:00:00 2001 From: spengrah Date: Fri, 6 Sep 2024 16:18:49 -0500 Subject: [PATCH 3/8] fmt --- script/DeployPassthroughModule.s.sol | 2 +- src/PassthroughModule.sol | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/script/DeployPassthroughModule.s.sol b/script/DeployPassthroughModule.s.sol index d6a9f06..bd3d0b4 100644 --- a/script/DeployPassthroughModule.s.sol +++ b/script/DeployPassthroughModule.s.sol @@ -42,7 +42,7 @@ contract Deploy is Script { * never differs regardless of where its being compiled * 2. The provided salt, `SALT` */ - implementation = new PassthroughModule{ salt: SALT}(_version /* insert constructor args here */); + implementation = new PassthroughModule{ salt: SALT }(_version /* insert constructor args here */ ); vm.stopBroadcast(); diff --git a/src/PassthroughModule.sol b/src/PassthroughModule.sol index 4cd73f3..03e03a9 100644 --- a/src/PassthroughModule.sol +++ b/src/PassthroughModule.sol @@ -48,7 +48,6 @@ contract PassthroughModule is HatsModule { * 72 | CRITERION_HAT | uint256 | 32 | PassthroughModule | * ----------------------------------------------------------------------+ */ - function CRITERION_HAT() public pure returns (uint256) { return _getArgUint256(72); } From af2113f952f55b3e07d1c2c4e3191f548aa224ad Mon Sep 17 00:00:00 2001 From: spengrah Date: Tue, 10 Sep 2024 12:45:01 -0500 Subject: [PATCH 4/8] deem ineligible if in bad standing --- src/HatControlledModule.sol | 7 ++++++- test/HatControlledModule.t.sol | 23 +++++++++++------------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/HatControlledModule.sol b/src/HatControlledModule.sol index ed3961d..b6c07d0 100644 --- a/src/HatControlledModule.sol +++ b/src/HatControlledModule.sol @@ -114,7 +114,12 @@ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { /// @inheritdoc HatsEligibilityModule function getWearerStatus(address _wearer, uint256 _hatId) public view override returns (bool eligible, bool standing) { IneligibilityData memory data = wearerIneligibility[_hatId][_wearer]; - return (!data.ineligible, !data.badStanding); + // bad standing means not eligible, as well + if (data.badStanding) return (false, false); + // good standing but ineligible + if (data.ineligible) return (false, true); + // eligible and in good standing + return (true, true); } /*////////////////////////////////////////////////////////////// diff --git a/test/HatControlledModule.t.sol b/test/HatControlledModule.t.sol index 6c37648..c0ce434 100644 --- a/test/HatControlledModule.t.sol +++ b/test/HatControlledModule.t.sol @@ -62,6 +62,13 @@ contract WithInstanceTest is HatControlledModuleTest { ); } + function assertWearerStatus(address _wearer, uint256 _hatId, bool _eligible, bool _standing) public view { + (bool eligible, bool standing) = instance.getWearerStatus(_wearer, _hatId); + assertEq(standing, _standing); + if (_standing) assertEq(eligible, _eligible); + else assertFalse(eligible); + } + modifier caller_(address _caller) { caller = _caller; vm.prank(caller); @@ -104,9 +111,7 @@ contract Eligibility is WithInstanceTest { vm.prank(controller); instance.setWearerStatus(_wearer, _hatId, _eligible, _standing); - (bool eligible, bool standing) = instance.getWearerStatus(_wearer, _hatId); - assertEq(eligible, _eligible); - assertEq(standing, _standing); + assertWearerStatus(_wearer, _hatId, _eligible, _standing); // set the same wearer's status for a different hat uint256 differentHat = uint256(keccak256(abi.encodePacked(_hatId))); @@ -114,9 +119,7 @@ contract Eligibility is WithInstanceTest { vm.prank(controller); instance.setWearerStatus(_wearer, differentHat, _eligible, _standing); - (eligible, standing) = instance.getWearerStatus(_wearer, differentHat); - assertEq(eligible, _eligible); - assertEq(standing, _standing); + assertWearerStatus(_wearer, differentHat, _eligible, _standing); // set a different wearer's status for the first hat address otherWearer = address(bytes20(keccak256(abi.encodePacked(_wearer)))); @@ -124,15 +127,11 @@ contract Eligibility is WithInstanceTest { vm.prank(controller); instance.setWearerStatus(otherWearer, _hatId, _eligible, _standing); - (eligible, standing) = instance.getWearerStatus(otherWearer, _hatId); - assertEq(eligible, _eligible); - assertEq(standing, _standing); + assertWearerStatus(otherWearer, _hatId, _eligible, _standing); } function test_default() public view { - (bool eligible, bool standing) = instance.getWearerStatus(wearer, targetHat); - assertTrue(eligible); - assertTrue(standing); + assertWearerStatus(wearer, targetHat, true, true); } function test_revert_nonController_cannotSetWearerStatus() public { From 2e5e867b7409d721d8995d5c2c6e08983f009c8f Mon Sep 17 00:00:00 2001 From: spengrah Date: Tue, 10 Sep 2024 12:50:56 -0500 Subject: [PATCH 5/8] add *StatusSet events --- src/HatControlledModule.sol | 9 +++++++++ test/HatControlledModule.t.sol | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/HatControlledModule.sol b/src/HatControlledModule.sol index b6c07d0..a5bbf3e 100644 --- a/src/HatControlledModule.sol +++ b/src/HatControlledModule.sol @@ -23,6 +23,13 @@ error NotAuthorized(); * another hat. */ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { + /*////////////////////////////////////////////////////////////// + EVENTS + //////////////////////////////////////////////////////////////*/ + + event WearerStatusSet(address wearer, uint256 hatId, bool eligible, bool standing); + event HatStatusSet(uint256 hatId, bool active); + /*////////////////////////////////////////////////////////////// DATA MODELS //////////////////////////////////////////////////////////////*/ @@ -109,6 +116,7 @@ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { */ function setWearerStatus(address _wearer, uint256 _hatId, bool _eligible, bool _standing) public onlyController { wearerIneligibility[_hatId][_wearer] = IneligibilityData(!_eligible, !_standing); + emit WearerStatusSet(_wearer, _hatId, _eligible, _standing); } /// @inheritdoc HatsEligibilityModule @@ -136,6 +144,7 @@ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { */ function setHatStatus(uint256 _hatId, bool _newStatus) public onlyController { hatInactivity[_hatId] = !_newStatus; + emit HatStatusSet(_hatId, _newStatus); } /// @inheritdoc HatsToggleModule diff --git a/test/HatControlledModule.t.sol b/test/HatControlledModule.t.sol index c0ce434..37bd16d 100644 --- a/test/HatControlledModule.t.sol +++ b/test/HatControlledModule.t.sol @@ -35,6 +35,9 @@ contract HatControlledModuleTest is Deploy, Test { string public MODULE_VERSION; + event WearerStatusSet(address wearer, uint256 hatId, bool eligible, bool standing); + event HatStatusSet(uint256 hatId, bool active); + function setUp() public virtual { fork = vm.createSelectFork(vm.rpcUrl("mainnet"), BLOCK_NUMBER); @@ -130,6 +133,13 @@ contract Eligibility is WithInstanceTest { assertWearerStatus(otherWearer, _hatId, _eligible, _standing); } + function test_emit_WearerStatusSet(address _wearer, uint256 _hatId, bool _eligible, bool _standing) public { + vm.expectEmit(true, true, true, true); + emit WearerStatusSet(_wearer, _hatId, _eligible, _standing); + vm.prank(controller); + instance.setWearerStatus(_wearer, _hatId, _eligible, _standing); + } + function test_default() public view { assertWearerStatus(wearer, targetHat, true, true); } @@ -157,6 +167,13 @@ contract Toggle is WithInstanceTest { assertEq(instance.getHatStatus(differentHat), _status); } + function test_emit_HatStatusSet(uint256 _hatId, bool _status) public { + vm.expectEmit(true, true, true, true); + emit HatStatusSet(_hatId, _status); + vm.prank(controller); + instance.setHatStatus(_hatId, _status); + } + function test_default() public view { assertTrue(instance.getHatStatus(targetHat)); } From 167bfd715163ed06b5106be23987eba1cf38f294 Mon Sep 17 00:00:00 2001 From: spengrah Date: Wed, 11 Sep 2024 10:39:10 -0500 Subject: [PATCH 6/8] re-label "criterion" as "controller" --- src/HatControlledModule.sol | 6 +++--- test/HatControlledModule.t.sol | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/HatControlledModule.sol b/src/HatControlledModule.sol index a5bbf3e..c2822af 100644 --- a/src/HatControlledModule.sol +++ b/src/HatControlledModule.sol @@ -66,10 +66,10 @@ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { * 0 | IMPLEMENTATION | address | 20 | HatsModule | * 20 | HATS | address | 20 | HatsModule | * 40 | hatId | uint256 | 32 | HatsModule | - * 72 | CRITERION_HAT | uint256 | 32 | PassthroughModule | + * 72 | CONTROLLER_HAT | uint256 | 32 | HatControlledModule | * ----------------------------------------------------------------------+ */ - function CRITERION_HAT() public pure returns (uint256) { + function CONTROLLER_HAT() public pure returns (uint256) { return _getArgUint256(72); } @@ -158,7 +158,7 @@ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { /// @notice Reverts if the caller is not wearing the {hatId} hat modifier onlyController() { - if (!HATS().isWearerOfHat(msg.sender, CRITERION_HAT())) revert NotAuthorized(); + if (!HATS().isWearerOfHat(msg.sender, CONTROLLER_HAT())) revert NotAuthorized(); _; } } diff --git a/test/HatControlledModule.t.sol b/test/HatControlledModule.t.sol index 37bd16d..b0f5eed 100644 --- a/test/HatControlledModule.t.sol +++ b/test/HatControlledModule.t.sol @@ -25,7 +25,7 @@ contract HatControlledModuleTest is Deploy, Test { uint256 public tophat; uint256 public targetHat; - uint256 public criterionHat; + uint256 public controllerHat; address public caller; address public controller = makeAddr("controller"); @@ -53,11 +53,11 @@ contract WithInstanceTest is HatControlledModuleTest { tophat = HATS.mintTopHat(org, "org's tophat", ""); vm.startPrank(org); targetHat = HATS.createHat(tophat, "target hat", 2, address(999), address(999), true, ""); - criterionHat = HATS.createHat(tophat, "criterion hat", 2, address(999), address(999), true, ""); - HATS.mintHat(criterionHat, controller); + controllerHat = HATS.createHat(tophat, "controller hat", 2, address(999), address(999), true, ""); + HATS.mintHat(controllerHat, controller); vm.stopPrank(); - otherImmutableArgs = abi.encodePacked(criterionHat); + otherImmutableArgs = abi.encodePacked(controllerHat); initArgs; instance = HatControlledModule( @@ -103,8 +103,8 @@ contract Deployment is WithInstanceTest { assertEq(instance.hatId(), 0); } - function test_criterionHat() public view { - assertEq(instance.CRITERION_HAT(), criterionHat); + function test_controllerHat() public view { + assertEq(instance.CONTROLLER_HAT(), controllerHat); } } From 9e02dae25a7b1ee1e8fcf8748930e4099ee14b2e Mon Sep 17 00:00:00 2001 From: spengrah Date: Fri, 13 Sep 2024 15:47:59 -0500 Subject: [PATCH 7/8] update readme and natspec --- README.md | 36 +++++++++++++++++++++++++++++------- src/HatControlledModule.sol | 7 ++++++- 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4c9798b..3a2e760 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,49 @@ -# Passthrough Module +# Passthrough Modules -A [Hats Protocol](https://github.com/hats-protocol/hats-protocol) module that enables an authorized "criterion" hat to serve as the eligibility and/or toggle module for other hat(s). +This repo contains two passthroughmodules for Hats Protocol: -## Overview and Usage +- [PassthroughModule](./src/PassthroughModule.sol): enables an authorized "criterion" hat to serve as the eligibility and/or toggle module for other hat(s), not compatible with module chaining. +- [HatControlledModule](./src/HatControlledModule.sol): enables an authorized "controller" hat to serve as the eligibility and/or toggle module for other hat(s), compatible with module chaining. + + +## 1. Passthrough Module + +### Overview and Usage In Hats Protocol v1, eligibility and toggle modules are set as addresses. This creates a lot of flexibility, since addresses can be EOAs, multisigs, DAOs, or even other smart contracts. But hats themselves cannot be set explicitly as eligibility or toggle modules because hats are identified by a uint256 hatId, not an address. Passthrough Module is a contract that can be set as the eligibility and/or toggle module for a target hat, and allows the wearer(s) of another hat to call the eligibility and/or toggle functions of the target hat. This allows hats themselves to be used as eligibility and toggle modules. -This contract is a "humanistic" module, not a "mechanistic" module. It does not inherit from `IHatsEligibility.sol` or `IHatsToggle.sol`, so Hats Protocol cannot pull any data from it. It serves only as a passthrough, enabling the wearer(s) of the authorized hat to push eligibility and toggle data about the target hat to Hats Protocol. - -### Passthrough Eligibility +#### Passthrough Eligibility To use Passthrough Module as the eligibility module for a target hat, set Passthrough Module's address as the target hat's eligibility address. Then, the wearer(s) of Passthrough Module's authorized `CRITERION_HAT` can call the `PassthroughEligibility.setHatWearerStatus()` function — which is a thin wrapper around `Hats.setHatWearerStatus()` — to push eligibility data to Hats Protocol. -### Passthrough Toggle +#### Passthrough Toggle To use Passthrough Module as the toggle module for a target hat, set Passthrough Module's address as the target hat's toggle address. Then, the wearer(s) of Passthrough Module's authorized `CRITERION_HAT` can call the `PassthroughToggle.setHatWearerStatus()` function — which is a thin wrapper around `Hats.setHatWearerStatus()` — to push toggle data to Hats Protocol. +## 2. Hat Controlled Module + +### Overview and Usage + +Unlike Passthrough Module, Hat Controlled Module is compatible with module chaining. It achieves this by enabling a "controller" hat to set wearer status and hat status for a given target hat in the Hat Controlled Module contract, which Hats Protocol then pulls in when checking for wearers or status of the target hat. + +#### Hat Controlled Eligibility + +To use Hat Controlled Module as the eligibility module for a target hat, set Hat Controlled Module's address as the target hat's eligibility address. + +Then, the wearer(s) of the "controller" hat can call the `HatControlledModule.setWearerStatus()` function to set eligibility data for the target hat for Hats Protocol to pull. + +#### Hat Controlled Toggle + +To use Hat Controlled Module as the toggle module for a target hat, set Hat Controlled Module's address as the target hat's toggle address. + +Then, the wearer(s) of the "controller" hat can call the `HatControlledModule.setHatStatus()` function to set the toggle data for the target hat for Hats Protocol to pull. + ## Development This repo uses Foundry for development and testing. To get started: diff --git a/src/HatControlledModule.sol b/src/HatControlledModule.sol index c2822af..7f6eede 100644 --- a/src/HatControlledModule.sol +++ b/src/HatControlledModule.sol @@ -16,7 +16,7 @@ error NotAuthorized(); * @title HatControlledModule * @author spengrah * @author Haberdasher Labs - * @notice This module allows the wearer(s) of a given "criterion" hat to serve as the eligibilty and/or toggle module + * @notice This module allows the wearer(s) of a given "controller" hat to serve as the eligibilty and/or toggle module * for a different hat. It is compatible with module chaining. * @dev This contract inherits from HatsModule, and is intended to be deployed as minimal proxy clone(s) via * HatsModuleFactory. For this contract to be used, it must be set as either the eligibility or toggle module for @@ -27,7 +27,10 @@ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { EVENTS //////////////////////////////////////////////////////////////*/ + /// @notice Emitted when the wearer status is set event WearerStatusSet(address wearer, uint256 hatId, bool eligible, bool standing); + + /// @notice Emitted when the hat status is set event HatStatusSet(uint256 hatId, bool active); /*////////////////////////////////////////////////////////////// @@ -69,6 +72,8 @@ contract HatControlledModule is HatsEligibilityModule, HatsToggleModule { * 72 | CONTROLLER_HAT | uint256 | 32 | HatControlledModule | * ----------------------------------------------------------------------+ */ + + /// @notice The hat that controls this module instance and can set wearer and hat statuses function CONTROLLER_HAT() public pure returns (uint256) { return _getArgUint256(72); } From bcd9ce75fc563f84e7bce9bb70be462d83a45858 Mon Sep 17 00:00:00 2001 From: spengrah Date: Fri, 13 Sep 2024 15:52:15 -0500 Subject: [PATCH 8/8] fmt --- README.md | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3a2e760..63f88d4 100644 --- a/README.md +++ b/README.md @@ -5,22 +5,19 @@ This repo contains two passthroughmodules for Hats Protocol: - [PassthroughModule](./src/PassthroughModule.sol): enables an authorized "criterion" hat to serve as the eligibility and/or toggle module for other hat(s), not compatible with module chaining. - [HatControlledModule](./src/HatControlledModule.sol): enables an authorized "controller" hat to serve as the eligibility and/or toggle module for other hat(s), compatible with module chaining. - ## 1. Passthrough Module -### Overview and Usage - In Hats Protocol v1, eligibility and toggle modules are set as addresses. This creates a lot of flexibility, since addresses can be EOAs, multisigs, DAOs, or even other smart contracts. But hats themselves cannot be set explicitly as eligibility or toggle modules because hats are identified by a uint256 hatId, not an address. Passthrough Module is a contract that can be set as the eligibility and/or toggle module for a target hat, and allows the wearer(s) of another hat to call the eligibility and/or toggle functions of the target hat. This allows hats themselves to be used as eligibility and toggle modules. -#### Passthrough Eligibility +### Passthrough Eligibility To use Passthrough Module as the eligibility module for a target hat, set Passthrough Module's address as the target hat's eligibility address. Then, the wearer(s) of Passthrough Module's authorized `CRITERION_HAT` can call the `PassthroughEligibility.setHatWearerStatus()` function — which is a thin wrapper around `Hats.setHatWearerStatus()` — to push eligibility data to Hats Protocol. -#### Passthrough Toggle +### Passthrough Toggle To use Passthrough Module as the toggle module for a target hat, set Passthrough Module's address as the target hat's toggle address. @@ -28,17 +25,15 @@ Then, the wearer(s) of Passthrough Module's authorized `CRITERION_HAT` can call ## 2. Hat Controlled Module -### Overview and Usage - Unlike Passthrough Module, Hat Controlled Module is compatible with module chaining. It achieves this by enabling a "controller" hat to set wearer status and hat status for a given target hat in the Hat Controlled Module contract, which Hats Protocol then pulls in when checking for wearers or status of the target hat. -#### Hat Controlled Eligibility +### Hat Controlled Eligibility To use Hat Controlled Module as the eligibility module for a target hat, set Hat Controlled Module's address as the target hat's eligibility address. Then, the wearer(s) of the "controller" hat can call the `HatControlledModule.setWearerStatus()` function to set eligibility data for the target hat for Hats Protocol to pull. -#### Hat Controlled Toggle +### Hat Controlled Toggle To use Hat Controlled Module as the toggle module for a target hat, set Hat Controlled Module's address as the target hat's toggle address.