Skip to content

Commit

Permalink
47 - prevent empty hat levels
Browse files Browse the repository at this point in the history
  • Loading branch information
spengrah committed Mar 15, 2023
1 parent fafcfdf commit 191cde5
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 5 deletions.
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ src = 'src'
out = 'out'
libs = ['lib']
optimizer_runs = 10_000
gas_reports = ["Hats"]
gas_reports = ["Hats", "HatsIdUtilities"]
auto_detect_solc = false
solc = "0.8.17"
remappings = [
Expand Down
7 changes: 3 additions & 4 deletions src/Hats.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,14 @@ contract Hats is IHats, ERC1155, HatsIdUtilities {

if (_eligibility == address(0)) revert ZeroAddress();
if (_toggle == address(0)) revert ZeroAddress();

// check that the admin id is valid, ie does not contain empty levels between filled levels
if (!isValidHatId(_admin)) revert InvalidHatId();
// construct the next hat id
newHatId = getNextId(_admin);

// to create a hat, you must be wearing one of its admin hats
_checkAdmin(newHatId);

// create the new hat
_createHat(newHatId, _details, _maxSupply, _eligibility, _toggle, _mutable, _imageURI);

// increment _admin.lastHatId
// use the overflow check to constrain to correct number of hats per level
++_hats[_admin].lastHatId;
Expand Down
23 changes: 23 additions & 0 deletions src/HatsIdUtilities.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
pragma solidity >=0.8.13;

import "./Interfaces/IHatsIdUtilities.sol";
// import { console2 } from "forge-std/Test.sol"; //remove after testing

/// @title Hats Id Utilities
/// @dev Functions for working with Hat Ids from Hats Protocol. Factored out of Hats.sol
Expand Down Expand Up @@ -142,6 +143,28 @@ contract HatsIdUtilities is IHatsIdUtilities {
_isLocalTopHat = _hatId > 0 && uint224(_hatId) == 0;
}

function isValidHatId(uint256 _hatId) public pure returns (bool validHatId) {
// valid top hats are valid hats
if (isLocalTopHat(_hatId)) return true;

uint32 level = getLocalHatLevel(_hatId);
uint256 admin;
// for each subsequent level up the tree, check if the level is 0 and return false if so
for (uint256 i = level - 1; i > 0;) {
// truncate to find the (truncated) admin at this level
// we don't need to check _hatId's own level since getLocalHatLevel already ensures that its non-empty
admin = _hatId >> (LOWER_LEVEL_ADDRESS_SPACE * (MAX_LEVELS - i));
// if the lowest level of the truncated admin is empty, the hat id is invalid
if (uint16(admin) == 0) return false;

unchecked {
--i;
}
}
// if there are no empty levels, return true
return true;
}

/// @notice Gets the hat id of the admin at a given level of a given hat
/// @dev This function traverses trees by following the linkedTreeAdmin
/// pointer to a hat located in a different tree
Expand Down
3 changes: 3 additions & 0 deletions src/Interfaces/HatsErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ interface HatsErrors {
/// @notice Emitted when attempting to create a hat with a level 14 hat as its admin
error MaxLevelsReached();

/// @notice Emitted when an attempted hat id has empty intermediate level(s)
error InvalidHatId();

/// @notice Emitted when attempting to mint `hatId` to a `wearer` who is already wearing the hat
error AlreadyWearingHat(address wearer, uint256 hatId);

Expand Down
2 changes: 2 additions & 0 deletions src/Interfaces/IHatsIdUtilities.sol
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ interface IHatsIdUtilities {

function isLocalTopHat(uint256 _hatId) external pure returns (bool _localTopHat);

function isValidHatId(uint256 _hatId) external view returns (bool validHatId);

function getAdminAtLevel(uint256 _hatId, uint32 _level) external view returns (uint256 admin);

function getAdminAtLocalLevel(uint256 _hatId, uint32 _level) external pure returns (uint256 admin);
Expand Down
7 changes: 7 additions & 0 deletions test/Hats.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ contract CreateHatsTest is TestSetup {
vm.prank(topHatWearer);
thirdHatId = hats.createHat(topHatId, _details, _maxSupply, _eligibility, address(0), true, thirdHatImageURI);
}

function testCannotCreateHatWithInvalidAdmin() public {
uint256 invalidAdmin = 0x0000000100000001000000000000000000000000000000000000000000000000;
vm.prank(topHatWearer);
vm.expectRevert(HatsErrors.InvalidHatId.selector);
hats.createHat(invalidAdmin, _details, _maxSupply, _eligibility, _toggle, true, "invalid admin id");
}
}

contract BatchCreateHats is TestSetupBatch {
Expand Down
53 changes: 53 additions & 0 deletions test/HatsIdUtils.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,57 @@ contract HatIdUtilTests is Test {
assertEq(utils.getTopHatDomain(1), 0);
assertEq(utils.getTopHatDomain(admin - 1), 0);
}

function testGetAdminAtLocalHatLevel() public {
uint256 hat = 0x000000FF000100020003000400050006000700080009000a000b000c000d000e;
assertEq(utils.getLocalHatLevel(hat), 14);
assertEq(
utils.getAdminAtLocalLevel(hat, 13), 0x000000FF000100020003000400050006000700080009000a000b000c000d0000
);
assertEq(
utils.getAdminAtLocalLevel(hat, 12), 0x000000FF000100020003000400050006000700080009000a000b000c00000000
);
}

function testIsValidHatId_Valid() public {
uint256 good = 0x000000FF000100020003000400050006000700080009000a000b000c000d000e;
assertTrue(utils.isValidHatId(good));
}

function testIsValidHatId_Invalid1() public {
uint256 empty1 = 0x000000FF000000020003000400050006000700080009000a000b000c000d000e;
uint256 empty2 = 0x000000FF000100000003000400050006000700080009000a000b000c000d000e;
uint256 empty3 = 0x000000FF000100020000000400050006000700080009000a000b000c000d000e;
uint256 empty4 = 0x000000FF000100020003000000050006000700080009000a000b000c000d000e;
uint256 empty5 = 0x000000FF000100020003000400000006000700080009000a000b000c000d000e;
uint256 empty6 = 0x000000FF000100020003000400050000000700080009000a000b000c000d000e;
uint256 empty7 = 0x000000FF000100020003000400050006000000080009000a000b000c000d000e;

assertFalse(utils.isValidHatId(empty1));
assertFalse(utils.isValidHatId(empty2));
assertFalse(utils.isValidHatId(empty3));
assertFalse(utils.isValidHatId(empty4));
assertFalse(utils.isValidHatId(empty5));
assertFalse(utils.isValidHatId(empty6));
assertFalse(utils.isValidHatId(empty7));
}

function testIsValidHatId_Invalid2() public {
uint256 empty8 = 0x000000FF000100020003000400050006000700000009000a000b000c000d000e;
uint256 empty9 = 0x000000FF000100020003000400050006000700080000000a000b000c000d000e;
uint256 emptya = 0x000000FF0001000200030004000500060007000800090000000b000c000d000e;
uint256 emptyb = 0x000000FF000100020003000400050006000700080009000a0000000c000d000e;
uint256 emptyc = 0x000000FF000100020003000400050006000700080009000a000b0000000d000e;
uint256 emptyd = 0x000000FF000100020003000400050006000700080009000a000b000c0000000e;
uint256 emptye = 0x000000FF000100020003000400050006000700080009000a000b000c000d0000;

assertFalse(utils.isValidHatId(empty8));
assertFalse(utils.isValidHatId(empty9));
assertFalse(utils.isValidHatId(emptya));
assertFalse(utils.isValidHatId(emptyb));
assertFalse(utils.isValidHatId(emptyc));
assertFalse(utils.isValidHatId(emptyd));
// this is the same as a valid level 13 hat
assertTrue(utils.isValidHatId(emptye));
}
}

0 comments on commit 191cde5

Please sign in to comment.