Skip to content

Commit

Permalink
add HatControlledModule
Browse files Browse the repository at this point in the history
  • Loading branch information
spengrah committed Sep 6, 2024
1 parent be63f58 commit b0ad84d
Show file tree
Hide file tree
Showing 3 changed files with 328 additions and 34 deletions.
42 changes: 8 additions & 34 deletions script/Deploy.s.sol → script/DeployHatControlledModule.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -34,53 +34,27 @@ 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();

_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
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`
Expand Down
150 changes: 150 additions & 0 deletions src/HatControlledModule.sol
Original file line number Diff line number Diff line change
@@ -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();
_;
}
}
170 changes: 170 additions & 0 deletions test/HatControlledModule.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit b0ad84d

Please sign in to comment.