diff --git a/foundry.toml b/foundry.toml index 240798a..a039509 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ libs = ['lib'] optimizer_runs = 1000000 gas_reports = ["*"] auto_detect_solc = false -solc = "0.8.15" +solc = "0.8.16" remappings = [ "ds-test/=lib/forge-std/lib/ds-test/src/", "forge-std/=lib/forge-std/src/", diff --git a/lib/forge-std b/lib/forge-std index 1c418a0..2a2ce36 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit 1c418a04f6f6b002a1631838da5761641391858b +Subproject commit 2a2ce3692b8c1523b29de3ec9d961ee9fbbc43a6 diff --git a/lib/openzeppelin-contracts b/lib/openzeppelin-contracts index 98c3a79..7201e67 160000 --- a/lib/openzeppelin-contracts +++ b/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit 98c3a79b5765d58ef27856b8211c70a4907c63be +Subproject commit 7201e6707f6631d9499a569f492870ebdd4133cf diff --git a/src/Hats.sol b/src/Hats.sol index 36d5f75..93222c0 100644 --- a/src/Hats.sol +++ b/src/Hats.sol @@ -12,7 +12,7 @@ import "@openzeppelin/contracts/utils/Strings.sol"; /// @title Hats Protocol /// @notice Hats are DAO-native revocable roles that are represented as semi-fungable tokens for composability -/// @dev This contract can manage all Hats for a given chain +/// @dev This is a multitenant contract that can manage all Hats for a given chain /// @author Hats Protocol contract Hats is ERC1155, HatsIdUtilities { /*////////////////////////////////////////////////////////////// @@ -36,6 +36,30 @@ contract Hats is ERC1155, HatsIdUtilities { error SafeTransfersNotNecessary(); error MaxLevelsReached(); + /*////////////////////////////////////////////////////////////// + HATS EVENTS + //////////////////////////////////////////////////////////////*/ + + event HatCreated( + uint256 id, + string details, + uint32 maxSupply, + address eligibility, + address toggle, + string imageURI + ); + + event HatRenounced(uint256 hatId, address wearer); + + event WearerStatus( + uint256 hatId, + address wearer, + bool eligible, + bool wearerStanding + ); + + event HatStatusChanged(uint256 hatId, bool newStatus); + /*////////////////////////////////////////////////////////////// HATS DATA MODELS //////////////////////////////////////////////////////////////*/ @@ -73,29 +97,9 @@ contract Hats is ERC1155, HatsIdUtilities { mapping(uint256 => mapping(address => bool)) public badStandings; // key: hatId => value: (key: wearer => value: badStanding?) /*////////////////////////////////////////////////////////////// - HATS EVENTS + CONSTRUCTOR //////////////////////////////////////////////////////////////*/ - event HatCreated( - uint256 id, - string details, - uint32 maxSupply, - address eligibility, - address toggle, - string imageURI - ); - - event HatRenounced(uint256 hatId, address wearer); - - event WearerStatus( - uint256 hatId, - address wearer, - bool eligible, - bool wearerStanding - ); - - event HatStatusChanged(uint256 hatId, bool newStatus); - constructor(string memory _name, string memory _baseImageURI) { name = _name; baseImageURI = _baseImageURI; @@ -180,15 +184,17 @@ contract Hats is ERC1155, HatsIdUtilities { address _toggle, string memory _imageURI ) public returns (uint256 newHatId) { - // to create a hat, you must be wearing the Hat of its admin - if (!isWearerOfHat(msg.sender, _admin)) { - revert NotAdmin(msg.sender, _admin); - } if (uint8(_admin) > 0) { revert MaxLevelsReached(); } newHatId = getNextId(_admin); + + // to create a hat, you must be wearing one of its admin hats + if (!isAdminOfHat(msg.sender, newHatId)) { + revert NotAdmin(msg.sender, newHatId); + } + // create the new hat _createHat( newHatId, @@ -198,10 +204,55 @@ contract Hats is ERC1155, HatsIdUtilities { _toggle, _imageURI ); + // increment _admin.lastHatId ++_hats[_admin].lastHatId; } + /// @notice Creates new hats in batch. The msg.sender must be an admin of each hat. + /// @dev This is a convenience function that loops through the arrays and calls `createHat`. + /// @param _admins Array of ids of admins for each hat to create + /// @param _details Array of details for each hat to create + /// @param _maxSupplies Array of supply caps for each hat to create + /// @param _eligibilityModules Array of eligibility module addresses for each hat to + /// create + /// @param _toggleModules Array of toggle module addresses for each hat to create + /// @param _imageURIs Array of imageURIs for each hat to create + /// @return bool True if all createHat calls succeeded + function batchCreateHats( + uint256[] memory _admins, + string[] memory _details, + uint32[] memory _maxSupplies, + address[] memory _eligibilityModules, + address[] memory _toggleModules, + string[] memory _imageURIs + ) public returns (bool) { + // check if array lengths are the same + uint256 length = _admins.length; // save an MLOAD + + bool sameLengths = (length == _details.length && + length == _maxSupplies.length && + length == _eligibilityModules.length && + length == _toggleModules.length && + length == _imageURIs.length); + + if (!sameLengths) revert BatchArrayLengthMismatch(); + + // loop through and create each hat + for (uint256 i = 0; i < length; ++i) { + createHat( + _admins[i], + _details[i], + _maxSupplies[i], + _eligibilityModules[i], + _toggleModules[i], + _imageURIs[i] + ); + } + + return true; + } + function getNextId(uint256 _admin) public view returns (uint256) { uint8 nextHatId = _hats[_admin].lastHatId + 1; return buildHatId(_admin, nextHatId); diff --git a/src/HatsIdUtilities.sol b/src/HatsIdUtilities.sol index 8c7b7ef..b9e236a 100644 --- a/src/HatsIdUtilities.sol +++ b/src/HatsIdUtilities.sol @@ -30,11 +30,11 @@ contract HatsIdUtilities { /// @dev Check hats[_admin].lastHatId for the previous hat created underneath _admin /// @param _admin the id of the admin for the new hat /// @param _newHat the uint8 id of the new hat - /// @return uint256 the constructed hat id + /// @return id The constructed hat id function buildHatId(uint256 _admin, uint8 _newHat) public pure - returns (uint256) + returns (uint256 id) { uint256 mask; for (uint256 i = 0; i < MAX_LEVELS; ++i) { @@ -43,10 +43,11 @@ contract HatsIdUtilities { (TOPHAT_ADDRESS_SPACE + (LOWER_LEVEL_ADDRESS_SPACE * i)) ); if (_admin & mask == 0) { - return + id = _admin | (uint256(_newHat) << (LOWER_LEVEL_ADDRESS_SPACE * (MAX_LEVELS - 1 - i))); + return id; } } } @@ -54,7 +55,7 @@ contract HatsIdUtilities { /// @notice Identifies the level a given hat in its hat tree /// @param _hatId the id of the hat in question /// @return level (0 to 28) - function getHatLevel(uint256 _hatId) public pure returns (uint8 level) { + function getHatLevel(uint256 _hatId) public pure returns (uint8) { uint256 mask; uint256 i; for (i = 0; i < MAX_LEVELS; ++i) { @@ -65,6 +66,8 @@ contract HatsIdUtilities { if (_hatId & mask == 0) return uint8(i); } + + return uint8(MAX_LEVELS); } /// @notice Checks whether a hat is a topHat diff --git a/test/Hats.t.sol b/test/Hats.t.sol index e38553c..73e01b6 100644 --- a/test/Hats.t.sol +++ b/test/Hats.t.sol @@ -89,6 +89,208 @@ contract CreateHatsTest is TestSetup { } } +contract BatchCreateHats is TestSetupBatch { + function testBatchCreateTwoHats() public { + testBatchCreateHatsSameAdmin(2); + } + + function testBatchCreateOneHat() public { + testBatchCreateHatsSameAdmin(1); + } + + function testBatchCreateHatsSameAdmin(uint256 count) public { + // this is inefficient, but bound() is not working correctly + vm.assume(count >= 1); + vm.assume(count < 256); + + adminsBatch = new uint256[](count); + detailsBatch = new string[](count); + maxSuppliesBatch = new uint32[](count); + eligibilityModulesBatch = new address[](count); + toggleModulesBatch = new address[](count); + imageURIsBatch = new string[](count); + + vm.prank(topHatWearer); + + // populate the creating arrays + for (uint256 i = 0; i < count; ++i) { + adminsBatch[i] = topHatId; + detailsBatch[i] = "deets"; + maxSuppliesBatch[i] = 10; + eligibilityModulesBatch[i] = _eligibility; + toggleModulesBatch[i] = _toggle; + imageURIsBatch[i] = ""; + } + + hats.batchCreateHats( + adminsBatch, + detailsBatch, + maxSuppliesBatch, + eligibilityModulesBatch, + toggleModulesBatch, + imageURIsBatch + ); + + (, , , , , , uint8 lastHatId, ) = hats.viewHat(topHatId); + + assertEq(lastHatId, count); + + (, , , , address t, , , ) = hats.viewHat( + hats.buildHatId(topHatId, uint8(count)) + ); + assertEq(t, _toggle); + } + + function testTemp() public { + hats.getHatLevel( + 27065671948198289362489238675596178244906309694785829628088330289409 + ); + } + + function testBatchCreateHatsSkinnyFullBranch() public { + uint256 count = 28; + + adminsBatch = new uint256[](count); + detailsBatch = new string[](count); + maxSuppliesBatch = new uint32[](count); + eligibilityModulesBatch = new address[](count); + toggleModulesBatch = new address[](count); + imageURIsBatch = new string[](count); + + uint256 adminId = topHatId; + + // populate the creating arrays + for (uint256 i = 0; i < count; ++i) { + uint32 level = uint32(i) + 1; + + adminsBatch[i] = adminId; + detailsBatch[i] = string.concat("level ", vm.toString(level)); + maxSuppliesBatch[i] = level; + eligibilityModulesBatch[i] = _eligibility; + toggleModulesBatch[i] = _toggle; + imageURIsBatch[i] = vm.toString(level); + + adminId = hats.buildHatId(adminId, 1); + } + + vm.prank(topHatWearer); + + hats.batchCreateHats( + adminsBatch, + detailsBatch, + maxSuppliesBatch, + eligibilityModulesBatch, + toggleModulesBatch, + imageURIsBatch + ); + + assertEq( + hats.getHatLevel( // should be adminId + hats.buildHatId( + hats.getAdminAtLevel(adminId, uint8(count - 1)), + 1 + ) + ), + count + ); + } + + function testBatchCreateHatsErrorArrayLength( + uint256 count, + uint256 offset, + uint256 array + ) public { + count = bound(count, 1, 254); + // count = 2; + offset = bound(offset, 1, 255 - count); + // offset = 1; + array = bound(array, 1, 6); + + uint256 extra = count + offset; + // initiate the creation arrays + if (array == 1) { + adminsBatch = new uint256[](extra); + detailsBatch = new string[](count); + maxSuppliesBatch = new uint32[](count); + eligibilityModulesBatch = new address[](count); + toggleModulesBatch = new address[](count); + imageURIsBatch = new string[](count); + } else if (array == 2) { + adminsBatch = new uint256[](count); + detailsBatch = new string[](extra); + maxSuppliesBatch = new uint32[](count); + eligibilityModulesBatch = new address[](count); + toggleModulesBatch = new address[](count); + imageURIsBatch = new string[](count); + } else if (array == 3) { + adminsBatch = new uint256[](count); + detailsBatch = new string[](count); + maxSuppliesBatch = new uint32[](extra); + eligibilityModulesBatch = new address[](count); + toggleModulesBatch = new address[](count); + imageURIsBatch = new string[](count); + } else if (array == 4) { + adminsBatch = new uint256[](count); + detailsBatch = new string[](count); + maxSuppliesBatch = new uint32[](count); + eligibilityModulesBatch = new address[](extra); + toggleModulesBatch = new address[](count); + imageURIsBatch = new string[](count); + } else if (array == 5) { + adminsBatch = new uint256[](count); + detailsBatch = new string[](count); + maxSuppliesBatch = new uint32[](count); + eligibilityModulesBatch = new address[](count); + toggleModulesBatch = new address[](extra); + imageURIsBatch = new string[](count); + } else if (array == 6) { + adminsBatch = new uint256[](count); + detailsBatch = new string[](count); + maxSuppliesBatch = new uint32[](count); + eligibilityModulesBatch = new address[](count); + toggleModulesBatch = new address[](count); + imageURIsBatch = new string[](extra); + } + + vm.prank(topHatWearer); + + // populate the creation arrays + for (uint32 i = 0; i < count; ++i) { + adminsBatch[i] = topHatId; + detailsBatch[i] = vm.toString(i); + maxSuppliesBatch[i] = i; + eligibilityModulesBatch[i] = _eligibility; + toggleModulesBatch[i] = _toggle; + imageURIsBatch[i] = vm.toString(i); + } + + // add `offset` number of hats to the batch, but only with one array filled out + for (uint32 j = 0; j < offset; ++j) { + if (array == 1) adminsBatch[j] = topHatId; + if (array == 2) detailsBatch[j] = vm.toString(j); + if (array == 3) maxSuppliesBatch[j] = j; + if (array == 4) eligibilityModulesBatch[j] = _eligibility; + if (array == 5) toggleModulesBatch[j] = _toggle; + if (array == 6) imageURIsBatch[j] = vm.toString(j); + } + + // adminsBatch[count] = topHatId; + + vm.expectRevert( + abi.encodeWithSelector(Hats.BatchArrayLengthMismatch.selector) + ); + + hats.batchCreateHats( + adminsBatch, + detailsBatch, + maxSuppliesBatch, + eligibilityModulesBatch, + toggleModulesBatch, + imageURIsBatch + ); + } +} + contract ImageURITest is TestSetup2 { function testTopHatImageURI() public { string memory uri = hats.getImageURIForHat(topHatId); diff --git a/test/HatsTestSetup.t.sol b/test/HatsTestSetup.t.sol index 9261002..29a9155 100644 --- a/test/HatsTestSetup.t.sol +++ b/test/HatsTestSetup.t.sol @@ -30,6 +30,13 @@ abstract contract TestVariables { string internal name; + uint256[] adminsBatch; + string[] detailsBatch; + uint32[] maxSuppliesBatch; + address[] eligibilityModulesBatch; + address[] toggleModulesBatch; + string[] imageURIsBatch; + event HatCreated( uint256 id, string details, @@ -158,3 +165,12 @@ abstract contract TestSetup2 is TestSetup { hats.mintHat(secondHatId, secondWearer); } } + +abstract contract TestSetupBatch is TestSetup { + function setUp() public override { + // expand on TestSetup + super.setUp(); + + // create empty batch create arrays + } +}