generated from Hats-Protocol/hats-module-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
3 changed files
with
328 additions
and
34 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
_; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |