Skip to content

Commit

Permalink
Merge pull request #121 from Hats-Protocol/fix/unlinking
Browse files Browse the repository at this point in the history
Prevent unlinking for tophats with no wearers
deploy v1.0
  • Loading branch information
spengrah authored Mar 31, 2023
2 parents 4cf4170 + c899496 commit 9d1275f
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 25 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ In these cases, Hat trees can be grafted onto other trees. This is done via a re
2. The hat to which it is linked becomes its new admin; it is no longer its own admin
3. On linking, the linked topHat can be assigned eligibility and/or toggle modules like any other hat

Linked Hat trees can also be unlinked by the tree root from its linked admin, via `Hats.unlinkTopHatFromTree`. This causes the tree root to regain its status as a top hat and to once again become its own admin. Any eligibility or toggle modules added on linking are cleared.
Linked Hat trees can also be unlinked by the tree root from its linked admin, via `Hats.unlinkTopHatFromTree`. This causes the tree root to regain its status as a top hat and to once again become its own admin. Any eligibility or toggle modules added on linking are cleared. Note that unlinking is only allowed if the tree root is active and has an eligible wearer.

⚠️ **CAUTION**: Be careful when nesting multiple Hat trees. If the nested linkages become too long, the higher level admins may lose control of the lowest level Hats because admin actions at that distance may cost-prohibitive or even exceed the gas limit. Best practice is to not attach external authorities (e.g. via token gating) to Hats in trees that are more than ~10 nested trees deep (varies by network).

Expand Down
23 changes: 8 additions & 15 deletions script/Hats.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,42 +7,35 @@ import { Hats } from "../src/Hats.sol";
contract DeployHats is Script {
string public constant baseImageURI = "ipfs://bafybeigcimbqwfajsnhoq7fqnbdllz7kye7cpdy3adj2sob3wku2llu5bi";

string public constant name = "Hats Protocol v1"; // increment this each deployment
string public constant name = "Hats Protocol v1.0"; // increment this each deployment

bytes32 internal constant SALT = bytes32(abi.encode(0x4a75)); // ~ H(4) A(a) T(7) S(5)

function run() external {
// set up deployer
uint256 privKey = vm.envUint("PRIVATE_KEY");

address deployer = vm.rememberKey(privKey);
// log deployer data
console2.log("Deployer: ", deployer);
console2.log("Deployer Nonce: ", vm.getNonce(deployer));

vm.startBroadcast(deployer);

// deploy Hats
// deploy Hats to a deterministic address via CREATE2
Hats hats = new Hats{ salt: SALT }(name, baseImageURI);

// mint Hats Protocol Governance topHat
// Note: This topHat is not connected to any protocol authorities. The protocol is fully permissionless and not upgradeable.
hats.mintTopHat(
0x2D785497c6C8ce3f4cCff4937D321C37e80705E8, // hatsprotocol.eth
"Hats Protocol Governance",
baseImageURI
);

vm.stopBroadcast();

// log deployment data
console2.log("Salt: ", vm.toString(SALT));
console2.log("Hats contract: ", address(hats));
}

// forge script script/Hats.s.sol:DeployHats -f ethereum
// forge script script/Hats.s.sol:DeployHats -f ethereum --broadcast --verify
// forge script script/Hats.s.sol:DeployHats -f mainnet
// forge script script/Hats.s.sol:DeployHats -f mainnet --broadcast --verify

// forge script script/Hats.s.sol:DeployHats --rpc-url http://localhost:8545 --broadcast

// forge verify-contract --chain-id 1 --num-of-optimizations 10000 --watch --constructor-args $(cast abi-encode "constructor(string,string)" "Hats Protocol v1" "ipfs://bafybeigcimbqwfajsnhoq7fqnbdllz7kye7cpdy3adj2sob3wku2llu5bi") --compiler-version v0.8.17 0x850f3384829D7bab6224D141AFeD9A559d745E3D src/Hats.sol:Hats --etherscan-api-key $ETHERSCAN_KEY
// forge verify-contract --chain-id 1 --num-of-optimizations 10000 --watch --constructor-args $(cast abi-encode "constructor(string,string)" "Hats Protocol v1.0" "ipfs://bafybeigcimbqwfajsnhoq7fqnbdllz7kye7cpdy3adj2sob3wku2llu5bi") --compiler-version v0.8.17 0x9D2dfd6066d5935267291718E8AA16C8Ab729E9d src/Hats.sol:Hats --etherscan-api-key $ETHERSCAN_KEY
}

contract DeployHatsAndMintTopHat is Script {
Expand Down
48 changes: 44 additions & 4 deletions src/Hats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -750,14 +750,26 @@ contract Hats is IHats, ERC1155, HatsIdUtilities {
_linkTopHatToTree(_topHatDomain, _newAdminHat, _eligibility, _toggle, _details, _imageURI);
}

/// @notice Unlink a Tree from the parent tree
/// @dev This can only be called by an admin of the tree root
/// @param _topHatDomain The 32 bit domain of the topHat to unlink
function unlinkTopHatFromTree(uint32 _topHatDomain) external {
/**
* @notice Unlink a Tree from the parent tree
* @dev This can only be called by an admin of the tree root. Fails if the topHat to unlink has no non-zero wearer, which can occur if...
* - It's wearer is in badStanding
* - It has been revoked from its wearer (and possibly burned)˘
* - It is not active (ie toggled off)
* @param _topHatDomain The 32 bit domain of the topHat to unlink
* @param _wearer The current wearer of the topHat to unlink
*/
function unlinkTopHatFromTree(uint32 _topHatDomain, address _wearer) external {
uint256 fullTopHatId = uint256(_topHatDomain) << 224; // (256 - TOPHAT_ADDRESS_SPACE);
_checkAdmin(fullTopHatId);

// prevent unlinking if the topHat has no non-zero wearer
// since we cannot search the entire address space for a wearer, we require the caller to provide the wearer
if (_wearer == address(0) || !isWearerOfHat(_wearer, fullTopHatId)) revert HatsErrors.InvalidUnlink();

// execute the unlink
delete linkedTreeAdmins[_topHatDomain];
// remove the request — ensures all linkages are initialized by unique requests
delete linkedTreeRequests[_topHatDomain];

// reset eligibility and storage to defaults for unlinked top hats
Expand Down Expand Up @@ -1020,6 +1032,13 @@ contract Hats is IHats, ERC1155, HatsIdUtilities {
}
}

/// @notice Checks the active status of a hat
/// @param _hatId The id of the hat
/// @return active Whether the hat is active
function isActive(uint256 _hatId) external view returns (bool active) {
active = _isActive(_hats[_hatId], _hatId);
}

/// @notice Internal function to retrieve a hat's status from storage
/// @dev reads the 0th bit of the hat's config
/// @param _hat The hat object
Expand Down Expand Up @@ -1118,6 +1137,27 @@ contract Hats is IHats, ERC1155, HatsIdUtilities {
supply = _hats[_hatId].supply;
}

/// @notice Gets the eligibility module for a hat
/// @param _hatId The hat whose eligibility module we're looking for
/// @return eligibility The eligibility module for this hat
function getHatEligibilityModule(uint256 _hatId) external view returns (address eligibility) {
eligibility = _hats[_hatId].eligibility;
}

/// @notice Gets the toggle module for a hat
/// @param _hatId The hat whose toggle module we're looking for
/// @return toggle The toggle module for this hat
function getHatToggleModule(uint256 _hatId) external view returns (address toggle) {
toggle = _hats[_hatId].toggle;
}

/// @notice Gets the max supply for a hat
/// @param _hatId The hat whose max supply we're looking for
/// @return maxSupply The maximum possible quantity of this hat that could be minted
function getHatMaxSupply(uint256 _hatId) external view returns (uint32 maxSupply) {
maxSupply = _hats[_hatId].maxSupply;
}

/// @notice Gets the imageURI for a given hat
/// @dev If this hat does not have an imageURI set, recursively get the imageURI from
/// its admin
Expand Down
4 changes: 4 additions & 0 deletions src/Interfaces/HatsErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ interface HatsErrors {
/// @notice Emitted when attempting to link a tophat without a request
error LinkageNotRequested();

/// @notice Emitted when attempting to unlink a tophat that does not have a wearer
/// @dev This ensures that unlinking never results in a bricked tophat
error InvalidUnlink();

/// @notice Emmited when attempting to change a hat's eligibility or toggle module to the zero address
error ZeroAddress();

Expand Down
8 changes: 7 additions & 1 deletion src/Interfaces/IHats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ interface IHats is IHatsIdUtilities, HatsErrors, HatsEvents {
string calldata _imageURI
) external;

function unlinkTopHatFromTree(uint32 _topHatId) external;
function unlinkTopHatFromTree(uint32 _topHatId, address _wearer) external;

function relinkTopHatWithinTree(
uint32 _topHatDomain,
Expand Down Expand Up @@ -130,6 +130,12 @@ interface IHats is IHatsIdUtilities, HatsErrors, HatsEvents {

function isEligible(address _wearer, uint256 _hatId) external view returns (bool eligible);

function getHatEligibilityModule(uint256 _hatId) external view returns (address eligibility);

function getHatToggleModule(uint256 _hatId) external view returns (address toggle);

function getHatMaxSupply(uint256 _hatId) external view returns (uint32 maxSupply);

function hatSupply(uint256 _hatId) external view returns (uint32 supply);

function getImageURIForHat(uint256 _hatId) external view returns (string memory _uri);
Expand Down
179 changes: 175 additions & 4 deletions test/Hats.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -1977,12 +1977,12 @@ contract LinkHatsTests is TestSetup2 {
assertEq(hats.linkedTreeRequests(secondTopHatDomain), 0);

vm.expectRevert(abi.encodeWithSelector(HatsErrors.NotAdmin.selector, address(this), secondTopHatId));
hats.unlinkTopHatFromTree(secondTopHatDomain);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);

vm.prank(secondWearer);
vm.expectEmit(true, true, true, true);
emit TopHatLinked(secondTopHatDomain, 0);
hats.unlinkTopHatFromTree(secondTopHatDomain);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);
assertEq(hats.isTopHat(secondTopHatId), true);
}

Expand All @@ -2002,7 +2002,7 @@ contract LinkHatsTests is TestSetup2 {
hats.requestLinkTopHatToTree(secondTopHatDomain, treeB);

// tree A unlinks the tophat
hats.unlinkTopHatFromTree(secondTopHatDomain);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);

// admin B should not be able to rug the tree by approving the link without the tree's permission
vm.expectRevert(HatsErrors.LinkageNotRequested.selector);
Expand Down Expand Up @@ -2041,12 +2041,183 @@ contract LinkHatsTests is TestSetup2 {
assertFalse(status);

// modules values reset on unlink
// first need to toggle back on
vm.mockCall(address(101), abi.encodeWithSignature("getHatStatus(uint256)", secondTopHatId), abi.encode(true));
(,,,,,,,, status) = hats.viewHat(secondTopHatId);
assertTrue(status);
vm.prank(topHatWearer);
hats.unlinkTopHatFromTree(secondTopHatDomain);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);
(,,, eligibility, toggle,,,,) = hats.viewHat(secondTopHatId);
assertEq(eligibility, address(0));
assertEq(toggle, address(0));
}

function testAdminCanBurnAndRemintLinkedTopHat() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, _eligibility, address(0), "", "");

// mock wearer ineligible
vm.mockCall(
_eligibility,
abi.encodeWithSignature("getWearerStatus(address,uint256)", thirdWearer, secondTopHatId),
abi.encode(false, true)
);
assertFalse(hats.isEligible(thirdWearer, secondTopHatId));
// burn the hat
hats.checkHatWearerStatus(secondTopHatId, thirdWearer);

// remint
vm.expectEmit(true, true, true, true);
emit TransferSingle(topHatWearer, address(0), address(99), secondTopHatId, 1);
vm.prank(topHatWearer);
hats.mintHat(secondTopHatId, address(99));
}

function testAdminCannotTransferLinkedTopHat() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", "");

// attempt transfer
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.Immutable.selector);
hats.transferHat(secondTopHatId, thirdWearer, address(99));
}

function testAdminCannotUnlinkInactivefTopHat() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(101), "", "");

// toggle off linked tophat
vm.mockCall(address(101), abi.encodeWithSignature("getHatStatus(uint256)", secondTopHatId), abi.encode(false));
hats.checkHatStatus(secondTopHatId);
(,,,,,,,, bool status) = hats.viewHat(secondTopHatId);
assertFalse(status);

// attempt unlink
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.InvalidUnlink.selector);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);
}

function testAdminCannotUnlinkBurnedTopHat() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, _eligibility, address(0), "", "");

// mock wearer ineligible
vm.mockCall(
_eligibility,
abi.encodeWithSignature("getWearerStatus(address,uint256)", thirdWearer, secondTopHatId),
abi.encode(false, true)
);
assertFalse(hats.isEligible(thirdWearer, secondTopHatId));
// burn the hat
hats.checkHatWearerStatus(secondTopHatId, thirdWearer);

// attempt unlink
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.InvalidUnlink.selector);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);
}

function testAdminCannotUnlinkRevokedTopHat() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, _eligibility, address(0), "", "");

// mock wearer ineligible
vm.mockCall(
_eligibility,
abi.encodeWithSignature("getWearerStatus(address,uint256)", thirdWearer, secondTopHatId),
abi.encode(false, true)
);
assertFalse(hats.isEligible(thirdWearer, secondTopHatId));

// attempt unlink
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.InvalidUnlink.selector);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);
}

function testAdminCannotUnlinkTopHatWhenWearerIsInBadStanding() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, _eligibility, address(0), "", "");

// mock wearer ineligible
vm.mockCall(
_eligibility,
abi.encodeWithSignature("getWearerStatus(address,uint256)", thirdWearer, secondTopHatId),
abi.encode(true, false)
);
assertFalse(hats.isEligible(thirdWearer, secondTopHatId));

// attempt unlink
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.InvalidUnlink.selector);
hats.unlinkTopHatFromTree(secondTopHatDomain, thirdWearer);
}

function testAdminCannotUnlinkTopHatWornByZeroAddress() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, _eligibility, address(0), "", "");

// revoke top hat
vm.prank(_eligibility);
hats.setHatWearerStatus(secondTopHatId, thirdWearer, false, true);

// remint it to address(0)
vm.prank(topHatWearer);
hats.mintHat(secondTopHatId, address(0));

// attempt unlink
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.InvalidUnlink.selector);
hats.unlinkTopHatFromTree(secondTopHatDomain, address(0));
}

function testAdminCannotUnlinkRenouncedTopHat() public {
// request
vm.prank(thirdWearer);
hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId);
// approve
vm.prank(topHatWearer);
hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, _eligibility, address(0), "", "");

// the tophat is renounced
vm.prank(thirdWearer);
hats.renounceHat(secondTopHatId);

// attempt unlink
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.InvalidUnlink.selector);
hats.unlinkTopHatFromTree(secondTopHatDomain, address(0));
}
}

contract MalformedInputsTests is TestSetup2 {
Expand Down

0 comments on commit 9d1275f

Please sign in to comment.