Skip to content

Commit

Permalink
Merge pull request #3 from Hats-Protocol/chaining-compatibility
Browse files Browse the repository at this point in the history
HatControlledModule
  • Loading branch information
spengrah authored Sep 13, 2024
2 parents c04ff86 + bcd9ce7 commit c83e1e6
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 25 deletions.
27 changes: 22 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
# 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

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

To use Passthrough Module as the eligibility module for a target hat, set Passthrough Module's address as the target hat's eligibility address.
Expand All @@ -22,6 +23,22 @@ To use Passthrough Module as the toggle module for a target hat, set Passthrough

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

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:
Expand Down
67 changes: 67 additions & 0 deletions script/DeployHatControlledModule.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import { Script, console2 } from "forge-std/Script.sol";
import { HatControlledModule } from "../src/HatControlledModule.sol";

contract Deploy is Script {
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.1.0"; // initial version for HatControlledModule

/// @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());

implementation = new HatControlledModule{ salt: SALT }(_version);

vm.stopBroadcast();

_log("");
}
}

/* FORGE CLI COMMANDS
## A. Simulate the deployment locally
forge script script/DeployHatControlledModule.s.sol -f mainnet
## B. Deploy to real network and verify on etherscan
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(string)" "_version") \
--compiler-version v0.8.19 {deploymentAddress} \
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`
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
*/
4 changes: 2 additions & 2 deletions script/Deploy.s.sol → script/DeployPassthroughModule.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ contract Deploy is Script {

// default values
bool internal _verbose = true;
string internal _version = "0.0.1"; // increment this with each new deployment
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 {
Expand Down Expand Up @@ -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();

Expand Down
169 changes: 169 additions & 0 deletions src/HatControlledModule.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// 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 "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
* another hat.
*/
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);

/*//////////////////////////////////////////////////////////////
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 | 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);
}

/*//////////////////////////////////////////////////////////////
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);
emit WearerStatusSet(_wearer, _hatId, _eligible, _standing);
}

/// @inheritdoc HatsEligibilityModule
function getWearerStatus(address _wearer, uint256 _hatId) public view override returns (bool eligible, bool standing) {
IneligibilityData memory data = wearerIneligibility[_hatId][_wearer];
// 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);
}

/*//////////////////////////////////////////////////////////////
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;
emit HatStatusSet(_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, CONTROLLER_HAT())) revert NotAuthorized();
_;
}
}
1 change: 0 additions & 1 deletion src/PassthroughModule.sol
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ contract PassthroughModule is HatsModule {
* 72 | CRITERION_HAT | uint256 | 32 | PassthroughModule |
* ----------------------------------------------------------------------+
*/

function CRITERION_HAT() public pure returns (uint256) {
return _getArgUint256(72);
}
Expand Down
Loading

0 comments on commit c83e1e6

Please sign in to comment.