diff --git a/foundry.toml b/foundry.toml index 34233a3..6ee8c9a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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 = [ diff --git a/src/Hats.sol b/src/Hats.sol index ae41a54..f7d3223 100644 --- a/src/Hats.sol +++ b/src/Hats.sol @@ -104,9 +104,11 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { /// @notice Creates and mints a Hat that is its own admin, i.e. a "topHat" /// @dev A topHat has no eligibility and no toggle /// @param _target The address to which the newly created topHat is minted - /// @param _details A description of the Hat [optional] + /// @param _details A description of the Hat [optional]. Should not be larger than 7000 bytes + /// (enforced in changeHatDetails) /// @param _imageURI The image uri for this top hat and the fallback for its - /// downstream hats [optional] + /// downstream hats [optional]. Should not be large than 7000 bytes + /// (enforced in changeHatImageURI) /// @return topHatId The id of the newly created topHat function mintTopHat(address _target, string calldata _details, string calldata _imageURI) public @@ -131,14 +133,14 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { /// @notice Creates a new hat. The msg.sender must wear the `_admin` hat. /// @dev Initializes a new Hat struct, but does not mint any tokens. - /// @param _details A description of the Hat + /// @param _details A description of the Hat. Should not be larger than 7000 bytes (enforced in changeHatDetails) /// @param _maxSupply The total instances of the Hat that can be worn at once /// @param _admin The id of the Hat that will control who wears the newly created hat /// @param _eligibility The address that can report on the Hat wearer's status /// @param _toggle The address that can deactivate the Hat /// @param _mutable Whether the hat's properties are changeable after creation /// @param _imageURI The image uri for this hat and the fallback for its - /// downstream hats [optional] + /// downstream hats [optional]. Should not be larger than 7000 bytes (enforced in changeHatImageURI) /// @return newHatId The id of the newly created Hat function createHat( uint256 _admin, @@ -155,15 +157,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; @@ -241,20 +242,17 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { function mintHat(uint256 _hatId, address _wearer) public returns (bool success) { Hat storage hat = _hats[_hatId]; if (hat.maxSupply == 0) revert HatDoesNotExist(_hatId); - + // only eligible wearers can receive minted hats if (!isEligible(_wearer, _hatId)) revert NotEligible(); - - // only the wearer of a hat's admin Hat can mint it + // only active hats can be minted + if (!_isActive(hat, _hatId)) revert HatNotActive(); + // only the wearer of one of a hat's admins can mint it _checkAdmin(_hatId); - - if (hat.supply >= hat.maxSupply) { - revert AllHatsWorn(_hatId); - } - - if (_staticBalanceOf(_wearer, _hatId) > 0) { - revert AlreadyWearingHat(_wearer, _hatId); - } - + // hat supply cannot exceed maxSupply + if (hat.supply >= hat.maxSupply) revert AllHatsWorn(_hatId); + // wearers cannot wear the same hat more than once + if (_staticBalanceOf(_wearer, _hatId) > 0) revert AlreadyWearingHat(_wearer, _hatId); + // if we've made it through all the checks, mint the hat _mintHat(_wearer, _hatId); success = true; @@ -298,12 +296,23 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { /// @dev May change the hat's status in storage /// @param _hatId The id of the Hat whose toggle we are checking /// @return toggled Whether there was a new status - function checkHatStatus(uint256 _hatId) external returns (bool toggled) { + function checkHatStatus(uint256 _hatId) public returns (bool toggled) { Hat storage hat = _hats[_hatId]; - bool newStatus; + // attempt to retrieve the hat's status from the toggle module + (bool success, bool newStatus) = _pullHatStatus(hat, _hatId); + + // if unsuccessful (ie toggle was humanistic), process the new status + if (!success) revert NotHatsToggle(); + + // if successful (ie toggle was mechanistic), process the new status + toggled = _processHatStatus(_hatId, newStatus); + } + + function _pullHatStatus(Hat storage _hat, uint256 _hatId) internal view returns (bool success, bool newStatus) { bytes memory data = abi.encodeWithSignature("getHatStatus(uint256)", _hatId); - (bool success, bytes memory returndata) = hat.toggle.staticcall(data); + bytes memory returndata; + (success, returndata) = _hat.toggle.staticcall(data); /* * if function call succeeds with data of length == 32, then we know the contract exists @@ -323,13 +332,11 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { } // invalid condition else { - revert NotHatsToggle(); + success = false; } } else { - revert NotHatsToggle(); + success = false; } - - toggled = _processHatStatus(_hatId, newStatus); } /// @notice Report from a hat's eligibility on the status of one of its wearers and, if `false`, revoke their hat @@ -408,7 +415,7 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { //////////////////////////////////////////////////////////////*/ /// @notice Internal call for creating a new hat - /// @dev Initializes a new Hat struct, but does not mint any tokens + /// @dev Initializes a new Hat in storage, but does not mint any tokens /// @param _id ID of the hat to be stored /// @param _details A description of the hat /// @param _maxSupply The total instances of the Hat that can be worn at once @@ -417,7 +424,6 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { /// @param _mutable Whether the hat's properties are changeable after creation /// @param _imageURI The image uri for this top hat and the fallback for its /// downstream hats [optional] - /// @return hat The contents of the newly created hat function _createHat( uint256 _id, string calldata _details, @@ -426,14 +432,20 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { address _toggle, bool _mutable, string calldata _imageURI - ) internal returns (Hat memory hat) { + ) internal { + /* + We write directly to storage instead of first building the Hat struct in memory. + This allows us to cheaply use the existing lastHatId value in case it was incremented by creating a hat while skipping admin levels. + (Resetting it to 0 would be bad since this hat's child hat(s) would overwrite the previously created hat(s) at that level.) + */ + Hat storage hat = _hats[_id]; hat.details = _details; hat.maxSupply = _maxSupply; hat.eligibility = _eligibility; hat.toggle = _toggle; hat.imageURI = _imageURI; + // config is a concatenation of the status and mutability properties hat.config = _mutable ? uint96(3 << 94) : uint96(1 << 95); - _hats[_id] = hat; emit HatCreated(_id, _details, _maxSupply, _eligibility, _toggle, _mutable, _imageURI); } @@ -537,28 +549,22 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { /// @param _to The new wearer function transferHat(uint256 _hatId, address _from, address _to) public { _checkAdmin(_hatId); - // cannot transfer immutable hats, except for tophats, which can always transfer themselves if (!isTopHat(_hatId)) { if (!_isMutable(_hats[_hatId])) revert Immutable(); } - // Checks storage instead of `isWearerOfHat` since admins may want to transfer revoked Hats to new wearers - if (_staticBalanceOf(_from, _hatId) < 1) { - revert NotHatWearer(); - } - + if (_staticBalanceOf(_from, _hatId) < 1) revert NotHatWearer(); // Check if recipient is already wearing hat; also checks storage to maintain balance == 1 invariant - if (_staticBalanceOf(_to, _hatId) > 0) { - revert AlreadyWearingHat(_to, _hatId); - } - + if (_staticBalanceOf(_to, _hatId) > 0) revert AlreadyWearingHat(_to, _hatId); + // only eligible wearers can receive transferred hats if (!isEligible(_to, _hatId)) revert NotEligible(); - - //Adjust balances + // only active hats can be transferred + if (!_isActive(_hats[_hatId], _hatId)) revert HatNotActive(); + // we've made it passed all the checks, so adjust balances to execute the transfer _balanceOf[_from][_hatId] = 0; _balanceOf[_to][_hatId] = 1; - + // emit the ERC1155 standard transfer event emit TransferSingle(msg.sender, _from, _to, _hatId, 1); } @@ -580,11 +586,14 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { } /// @notice Change a hat's details - /// @dev Hat must be mutable, except for tophats + /// @dev Hat must be mutable, except for tophats. /// @param _hatId The id of the Hat to change - /// @param _newDetails The new details + /// @param _newDetails The new details. Must not be larger than 7000 bytes. function changeHatDetails(uint256 _hatId, string calldata _newDetails) external { + if (bytes(_newDetails).length > 7000) revert StringTooLong(); + _checkAdmin(_hatId); + Hat storage hat = _hats[_hatId]; // a tophat can change its own details, but otherwise only mutable hat details can be changed @@ -630,6 +639,14 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { revert Immutable(); } + // record hat status from old toggle before changing; ensures smooth transition to new toggle, + // especially in case of switching from mechanistic to humanistic toggle + // a) attempt to retrieve hat status from old toggle + (bool success, bool newStatus) = _pullHatStatus(hat, _hatId); + // b) if succeeded, (ie if old toggle was mechanistic), store the retrieved status + if (success) _processHatStatus(_hatId, newStatus); + + // set the new toggle hat.toggle = _newToggle; emit HatToggleChanged(_hatId, _newToggle); @@ -638,8 +655,10 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { /// @notice Change a hat's details /// @dev Hat must be mutable, except for tophats /// @param _hatId The id of the Hat to change - /// @param _newImageURI The new imageURI + /// @param _newImageURI The new imageURI. Must not be larger than 7000 bytes. function changeHatImageURI(uint256 _hatId, string calldata _newImageURI) external { + if (bytes(_newImageURI).length > 7000) revert StringTooLong(); + _checkAdmin(_hatId); Hat storage hat = _hats[_hatId]; @@ -654,7 +673,7 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { } /// @notice Change a hat's details - /// @dev Hat must be mutable; new max supply cannot be greater than current supply + /// @dev Hat must be mutable; new max supply cannot be less than current supply /// @param _hatId The id of the Hat to change /// @param _newMaxSupply The new max supply function changeHatMaxSupply(uint256 _hatId, uint32 _newMaxSupply) external { @@ -677,13 +696,13 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { /// @notice Submits a request to link a Hat Tree under a parent tree. Requests can be /// submitted by either... - /// a) the wearer of a tophat, previous to any linkage, or - /// b) the admin(s) of an already-linked tophat (aka tree root), where such a + /// a) the wearer of a topHat, previous to any linkage, or + /// b) the admin(s) of an already-linked topHat (aka tree root), where such a /// request is to move the tree root to another admin within the same parent /// tree - /// @dev A tophat can have at most 1 request at a time. Submitting a new request will + /// @dev A topHat can have at most 1 request at a time. Submitting a new request will /// replace the existing request. - /// @param _topHatDomain The domain of the tophat to link + /// @param _topHatDomain The domain of the topHat to link /// @param _requestedAdminHat The hat that will administer the linked tree function requestLinkTopHatToTree(uint32 _topHatDomain, uint256 _requestedAdminHat) external { uint256 fullTopHatId = uint256(_topHatDomain) << 224; // (256 - TOPHAT_ADDRESS_SPACE); @@ -695,12 +714,23 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { emit TopHatLinkRequested(_topHatDomain, _requestedAdminHat); } - /// @notice Approve a request to link a Tree under a parent tree + /// @notice Approve a request to link a Tree under a parent tree, with options to add eligibility or toggle modules and change its metadata /// @dev Requests can only be approved by wearer or an admin of the `_newAdminHat`, and there /// can only be one link per tree root at a given time. - /// @param _topHatDomain The 32 bit domain of the tophat to link + /// @param _topHatDomain The 32 bit domain of the topHat to link /// @param _newAdminHat The hat that will administer the linked tree - function approveLinkTopHatToTree(uint32 _topHatDomain, uint256 _newAdminHat) external { + /// @param _eligibility Optional new eligibility module for the linked topHat + /// @param _toggle Optional new toggle module for the linked topHat + /// @param _details Optional new details for the linked topHat + /// @param _imageURI Optional new imageURI for the linked topHat + function approveLinkTopHatToTree( + uint32 _topHatDomain, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external { // for everything but the last hat level, check the admin of `_newAdminHat`'s theoretical child hat, since either wearer or admin of `_newAdminHat` can approve if (getHatLevel(_newAdminHat) < MAX_LEVELS) { _checkAdmin(buildHatId(_newAdminHat, 1)); @@ -717,26 +747,46 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { delete linkedTreeRequests[_topHatDomain]; // execute the link. Replaces existing link, if any. - _linkTopHatToTree(_topHatDomain, _newAdminHat); + _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 + /// @param _topHatDomain The 32 bit domain of the topHat to unlink function unlinkTopHatFromTree(uint32 _topHatDomain) external { uint256 fullTopHatId = uint256(_topHatDomain) << 224; // (256 - TOPHAT_ADDRESS_SPACE); _checkAdmin(fullTopHatId); delete linkedTreeAdmins[_topHatDomain]; + delete linkedTreeRequests[_topHatDomain]; + + // reset eligibility and storage to defaults for unlinked top hats + Hat storage hat = _hats[fullTopHatId]; + delete hat.eligibility; + delete hat.toggle; + emit TopHatLinked(_topHatDomain, 0); } /// @notice Move a tree root to a different position within the same parent tree, - /// without a request - /// @dev Caller must be both an admin tree root and admin or wearer of `_newAdminHat` - /// @param _topHatDomain The 32 bit domain of the tophat to relink + /// without a request. Valid destinations include within the same local tree as the origin, + /// or to the local tree of the tippyTopHat. TippyTopHat wearers can bypass this restriction + /// to relink to anywhere in its full tree. + /// @dev Caller must be both an admin tree root and admin or wearer of `_newAdminHat`. + /// @param _topHatDomain The 32 bit domain of the topHat to relink /// @param _newAdminHat The new admin for the linked tree - function relinkTopHatWithinTree(uint32 _topHatDomain, uint256 _newAdminHat) external { + /// @param _eligibility Optional new eligibility module for the linked topHat + /// @param _toggle Optional new toggle module for the linked topHat + /// @param _details Optional new details for the linked topHat + /// @param _imageURI Optional new imageURI for the linked topHat + function relinkTopHatWithinTree( + uint32 _topHatDomain, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external { uint256 fullTopHatId = uint256(_topHatDomain) << 224; // (256 - TOPHAT_ADDRESS_SPACE); // msg.sender being capable of both requesting and approving allows us to skip the request step @@ -751,23 +801,77 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { } // execute the new link, replacing the old link - _linkTopHatToTree(_topHatDomain, _newAdminHat); + _linkTopHatToTree(_topHatDomain, _newAdminHat, _eligibility, _toggle, _details, _imageURI); } - /// @notice Internal function to link a Tree under a parent Tree, with protection against circular linkages and relinking to a separate Tree + /// @notice Internal function to link a Tree under a parent Tree, with protection against circular linkages and relinking to a separate Tree, + /// with options to add eligibility or toggle modules and change its metadata /// @dev Linking `_topHatDomain` replaces any existing links - /// @param _topHatDomain The 32 bit domain of the tophat to link + /// @param _topHatDomain The 32 bit domain of the topHat to link /// @param _newAdminHat The new admin for the linked tree - function _linkTopHatToTree(uint32 _topHatDomain, uint256 _newAdminHat) internal { + /// @param _eligibility Optional new eligibility module for the linked topHat + /// @param _toggle Optional new toggle module for the linked topHat + /// @param _details Optional new details for the linked topHat + /// @param _imageURI Optional new imageURI for the linked topHat + function _linkTopHatToTree( + uint32 _topHatDomain, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) internal { if (!noCircularLinkage(_topHatDomain, _newAdminHat)) revert CircularLinkage(); - - // disallow relinking to separate tree - if (linkedTreeAdmins[_topHatDomain] > 0) { - if (!sameTippyTopHatDomain(_topHatDomain, _newAdminHat)) { - revert CrossTreeLinkage(); + { + uint256 linkedAdmin = linkedTreeAdmins[_topHatDomain]; + + // disallow relinking to separate tree + if (linkedAdmin > 0) { + uint256 tippyTopHat = uint256(getTippyTopHatDomain(_topHatDomain)) << 224; + if (!isWearerOfHat(msg.sender, tippyTopHat)) { + uint256 destLocalTopHat = uint256(_newAdminHat >> 224 << 224); // (256 - TOPHAT_ADDRESS_SPACE); + // for non-tippyTopHat wearers: destination local tophat must be either... + // a) the same as origin local tophat, or + // b) within the tippy top hat's local tree + uint256 originLocalTopHat = linkedAdmin >> 224 << 224; // (256 - TOPHAT_ADDRESS_SPACE); + if (destLocalTopHat != originLocalTopHat && destLocalTopHat != tippyTopHat) { + revert CrossTreeLinkage(); + } + // for tippyTopHat weerers: destination must be within the same super tree + } else if (!sameTippyTopHatDomain(_topHatDomain, _newAdminHat)) { + revert CrossTreeLinkage(); + } } } + // update and log the linked topHat's modules and metadata, if any changes + uint256 topHatId = uint256(_topHatDomain) << 224; + Hat storage hat = _hats[topHatId]; + + if (_eligibility != address(0)) { + hat.eligibility = _eligibility; + emit HatEligibilityChanged(topHatId, _eligibility); + } + if (_toggle != address(0)) { + hat.toggle = _toggle; + emit HatToggleChanged(topHatId, _toggle); + } + + uint256 length = bytes(_details).length; + if (length > 0) { + if (length > 7000) revert StringTooLong(); + hat.details = _details; + emit HatDetailsChanged(topHatId, _details); + } + + length = bytes(_imageURI).length; + if (length > 0) { + if (length > 7000) revert StringTooLong(); + hat.imageURI = _imageURI; + emit HatImageURIChanged(topHatId, _imageURI); + } + + // store the new linked admin linkedTreeAdmins[_topHatDomain] = _newAdminHat; emit TopHatLinked(_topHatDomain, _newAdminHat); } @@ -891,10 +995,10 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { (bool success, bytes memory returndata) = _hat.toggle.staticcall(abi.encodeWithSignature("getHatStatus(uint256)", _hatId)); - /* - * if function call succeeds with data of length == 32, then we know the contract exists + /* + * if function call succeeds with data of length == 32, then we know the contract exists * and has the getHatStatus function. - * But — since function selectors don't include return types — we still can't assume that the return data is a boolean, + * But — since function selectors don't include return types — we still can't assume that the return data is a boolean, * so we treat it as a uint so it will always safely decode without throwing. */ if (success && returndata.length == 32) { @@ -1215,6 +1319,43 @@ contract Hats is IHats, ERC1155, HatsIdUtilities { revert(); } + /** + * @notice ERC165 interface detection + * @dev While Hats Protocol conforms to the ERC1155 *interface*, it does not fully conform to the ERC1155 *specification* + * since it does not implement the ERC1155Receiver functionality. + * For this reason, this function overrides the ERC1155 implementation to return false for ERC1155. + * @param interfaceId The interface identifier, as specified in ERC-165 + * @return bool True if the contract implements `interfaceId` and false otherwise + */ + function supportsInterface(bytes4 interfaceId) public pure override returns (bool) { + return interfaceId == 0x01ffc9a7 // ERC165 Interface ID for ERC165 + // interfaceId == 0xd9b67a26 || // ERC165 Interface ID for ERC1155 + || interfaceId == 0x0e89341c; // ERC165 Interface ID for ERC1155MetadataURI + } + + /// @notice Batch retrieval for wearer balances + /// @dev Given the higher gas overhead of Hats balanceOf checks, large batches may be high cost or run into gas limits + /// @param _wearers Array of addresses to check balances for + /// @param _hatIds Array of Hat ids to check, using the same index as _wearers + function balanceOfBatch(address[] calldata _wearers, uint256[] calldata _hatIds) + public + view + override(ERC1155, IHats) + returns (uint256[] memory balances) + { + if (_wearers.length != _hatIds.length) revert BatchArrayLengthMismatch(); + + balances = new uint256[](_wearers.length); + + // Unchecked because the only math done is incrementing + // the array index counter which cannot possibly overflow. + unchecked { + for (uint256 i; i < _wearers.length; ++i) { + balances[i] = balanceOf(_wearers[i], _hatIds[i]); + } + } + } + /// @notice View the uri for a Hat /// @param id The id of the Hat /// @return _uri An 1155-compatible JSON object diff --git a/src/HatsIdUtilities.sol b/src/HatsIdUtilities.sol index 10a787a..8475c42 100644 --- a/src/HatsIdUtilities.sol +++ b/src/HatsIdUtilities.sol @@ -17,6 +17,10 @@ pragma solidity >=0.8.13; import "./Interfaces/IHatsIdUtilities.sol"; +// import { console2 } from "forge-std/Test.sol"; //remove after testing + +/// @notice see HatsErrors.sol for description +error MaxLevelsReached(); /// @title Hats Id Utilities /// @dev Functions for working with Hat Ids from Hats Protocol. Factored out of Hats.sol @@ -58,6 +62,7 @@ contract HatsIdUtilities is IHatsIdUtilities { uint256 internal constant MAX_LEVELS = 14; /// @notice Constructs a valid hat id for a new hat underneath a given admin + /// @dev Reverts if the admin has already reached `MAX_LEVELS` /// @param _admin the id of the admin for the new hat /// @param _newHat the uint16 id of the new hat /// @return id The constructed hat id @@ -88,6 +93,9 @@ contract HatsIdUtilities is IHatsIdUtilities { ++i; } } + + // if _admin is already at MAX_LEVELS, child hats are not possible, so we revert + revert MaxLevelsReached(); } /// @notice Identifies the level a given hat in its hat tree @@ -142,6 +150,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 diff --git a/src/Interfaces/HatsErrors.sol b/src/Interfaces/HatsErrors.sol index 6e67525..46db4be 100644 --- a/src/Interfaces/HatsErrors.sol +++ b/src/Interfaces/HatsErrors.sol @@ -33,12 +33,18 @@ 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); /// @notice Emitted when attempting to mint a non-existant hat error HatDoesNotExist(uint256 hatId); + /// @notice Emmitted when attempting to mint or transfer a hat that is not active + error HatNotActive(); + /// @notice Emitted when attempting to mint or transfer a hat to an ineligible wearer error NotEligible(); @@ -66,6 +72,11 @@ interface HatsErrors { /// @notice Emitted when attempting to link a tophat without a request error LinkageNotRequested(); - /// @notice Emmited when attempted to change a hat's eligibility or toggle module to the zero address + /// @notice Emmited when attempting to change a hat's eligibility or toggle module to the zero address error ZeroAddress(); + + /// @notice Emmitted when attempting to change a hat's details or imageURI to a string with over 7000 bytes (~characters) + /// @dev This protects against a DOS attack where an admin iteratively extend's a hat's details or imageURI + /// to be so long that reading it exceeds the block gas limit, breaking `uri()` and `viewHat()` + error StringTooLong(); } diff --git a/src/Interfaces/IHats.sol b/src/Interfaces/IHats.sol index e47f339..1d341a7 100644 --- a/src/Interfaces/IHats.sol +++ b/src/Interfaces/IHats.sol @@ -83,11 +83,25 @@ interface IHats is IHatsIdUtilities, HatsErrors, HatsEvents { function requestLinkTopHatToTree(uint32 _topHatId, uint256 _newAdminHat) external; - function approveLinkTopHatToTree(uint32 _topHatId, uint256 _newAdminHat) external; + function approveLinkTopHatToTree( + uint32 _topHatId, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external; function unlinkTopHatFromTree(uint32 _topHatId) external; - function relinkTopHatWithinTree(uint32 _topHatDomain, uint256 _newAdminHat) external; + function relinkTopHatWithinTree( + uint32 _topHatDomain, + uint256 _newAdminHat, + address _eligibility, + address _toggle, + string calldata _details, + string calldata _imageURI + ) external; /*////////////////////////////////////////////////////////////// VIEW FUNCTIONS @@ -122,5 +136,10 @@ interface IHats is IHatsIdUtilities, HatsErrors, HatsEvents { function balanceOf(address wearer, uint256 hatId) external view returns (uint256 balance); + function balanceOfBatch(address[] calldata _wearers, uint256[] calldata _hatIds) + external + view + returns (uint256[] memory); + function uri(uint256 id) external view returns (string memory _uri); } diff --git a/src/Interfaces/IHatsIdUtilities.sol b/src/Interfaces/IHatsIdUtilities.sol index b6e9901..e4376ce 100644 --- a/src/Interfaces/IHatsIdUtilities.sol +++ b/src/Interfaces/IHatsIdUtilities.sol @@ -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); diff --git a/test/Hats.t.sol b/test/Hats.t.sol index 3e85d6a..4716450 100644 --- a/test/Hats.t.sol +++ b/test/Hats.t.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; import "../src/Hats.sol"; import "./HatsTestSetup.t.sol"; +import { LongStrings } from "./LongStrings.sol"; contract DeployTest is TestSetup { function testDeployWithParams() public { @@ -107,6 +108,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 { @@ -314,6 +322,28 @@ contract BatchCreateHats is TestSetupBatch { imageURIsBatch ); } + + function testCreatingSkippedHatDoesNotOverwriteChildHat() public { + uint32 testMax = 99; + // first, skip a level to create a hat + uint256 level1Hat = 0x000000010001 << (224 - 16); + vm.startPrank(topHatWearer); + uint256 level2HatA = hats.createHat( + level1Hat, "should not be overwritten", testMax, _eligibility, _toggle, false, "amistillhere.com" + ); + + // then, create the hat at the skipped level + uint256 skippedHat = + hats.createHat(topHatId, "At first I was skipped, now I'm here", 1, _eligibility, _toggle, false, "gm"); + assertEq(skippedHat, level1Hat); + + // finally, attempt to create a new child of skippedHat + uint256 level2HatB = hats.createHat(skippedHat, "i should be hat 2", 1, _eligibility, _toggle, false, ""); + assertEq(level2HatB, 0x0000000100010002 << (224 - 32)); + assertFalse(level2HatB == level2HatA); + (, uint32 max,,,,,,,) = hats.viewHat(level2HatA); + assertEq(max, testMax); + } } contract ImageURITest is TestSetup2 { @@ -530,25 +560,6 @@ contract MintHatsTest is TestSetup { assertEq(hats.hatSupply(secondHatId), supply_pre + 2); } - function testMintInactiveHat() public { - // capture pre-values - uint256 hatSupply_pre = hats.hatSupply(secondHatId); - - // deactivate the hat - vm.prank(_toggle); - hats.setHatStatus(secondHatId, false); - - // mint the hat to wearer - vm.prank(topHatWearer); - hats.mintHat(secondHatId, secondWearer); - - // assert that the wearer does not have the hat - assertFalse(hats.isWearerOfHat(secondWearer, secondHatId)); - - // assert that the hat supply increased - assertEq(++hatSupply_pre, hats.hatSupply(secondHatId)); - } - function testCannotMintNonExistentHat() public { vm.prank(topHatWearer); @@ -571,6 +582,16 @@ contract MintHatsTest is TestSetup { hats.mintHat(secondHatId, secondWearer); } + function testCannotMintInactiveHat() public { + // mock a toggle call to return inactive + vm.mockCall(address(_toggle), abi.encodeWithSignature("getHatStatus(uint256)", secondHatId), abi.encode(false)); + + vm.prank(topHatWearer); + // expect hat not active error + vm.expectRevert(HatsErrors.HatNotActive.selector); + hats.mintHat(secondHatId, secondWearer); + } + function testBatchMintHats(uint256 count) public { vm.assume(count <= 255); @@ -758,6 +779,14 @@ contract TransferHatTests is TestSetupMutable { vm.prank(topHatWearer); hats.transferHat(secondHatId, secondWearer, thirdWearer); } + + function testCannotTransferInactiveHat() public { + vm.mockCall(_toggle, abi.encodeWithSignature("getHatStatus(uint256)", secondHatId), abi.encode(false)); + + vm.expectRevert(HatsErrors.HatNotActive.selector); + vm.prank(topHatWearer); + hats.transferHat(secondHatId, secondWearer, thirdWearer); + } } contract EligibilitySetHatsTests is TestSetup2 { @@ -1013,13 +1042,17 @@ contract RenounceHatsTest is TestSetup2 { } function testCanRenounceHatAsNonWearerWithStaticBalance() public { - // hat gets toggled off - // encode mock for function inside toggle contract to return false - vm.mockCall(address(_toggle), abi.encodeWithSignature("getHatStatus(uint256)", secondHatId), abi.encode(false)); + // wearer becomes ineligible + // encode mock for function inside eligibility contract to return false (inelible), true (good standing) + vm.mockCall( + _eligibility, + abi.encodeWithSignature("getWearerStatus(address,uint256)", secondWearer, secondHatId), + abi.encode(false, true) + ); // show that admin can't mint again to secondWearer, ie because they have a static balance vm.prank(topHatWearer); - vm.expectRevert(abi.encodeWithSelector(HatsErrors.AlreadyWearingHat.selector, secondWearer, secondHatId)); + vm.expectRevert(abi.encodeWithSelector(HatsErrors.NotEligible.selector)); hats.mintHat(secondHatId, secondWearer); assertFalse(hats.isWearerOfHat(secondWearer, secondHatId)); @@ -1027,7 +1060,8 @@ contract RenounceHatsTest is TestSetup2 { vm.prank(address(secondWearer)); hats.renounceHat(secondHatId); - // now, admin should be able to mint again + // now, admin should be able to mint again if eligibility no longer returns false + vm.clearMockedCalls(); vm.prank(topHatWearer); hats.mintHat(secondHatId, secondWearer); } @@ -1099,7 +1133,7 @@ contract ToggleSetHatsTest is TestSetup2 { // artificially mint again to secondWearer vm.prank(topHatWearer); - vm.expectRevert(abi.encodeWithSelector(HatsErrors.AlreadyWearingHat.selector, secondWearer, secondHatId)); + vm.expectRevert(abi.encodeWithSelector(HatsErrors.HatNotActive.selector)); hats.mintHat(secondHatId, secondWearer); @@ -1164,7 +1198,7 @@ contract ToggleCheckHatsTest is TestSetup2 { // artificially mint again to secondWearer vm.prank(topHatWearer); - vm.expectRevert(abi.encodeWithSelector(HatsErrors.AlreadyWearingHat.selector, secondWearer, secondHatId)); + vm.expectRevert(abi.encodeWithSelector(HatsErrors.HatNotActive.selector)); hats.mintHat(secondHatId, secondWearer); @@ -1178,7 +1212,7 @@ contract ToggleCheckHatsTest is TestSetup2 { } } -contract MutabilityTests is TestSetupMutable { +contract MutabilityTests is TestSetupMutable, LongStrings { function testAdminCanMakeMutableHatImmutable() public { (,,,,,,, mutable_,) = hats.viewHat(secondHatId); assertTrue(mutable_); @@ -1453,6 +1487,35 @@ contract MutabilityTests is TestSetupMutable { vm.prank(topHatWearer); hats.changeHatToggle(secondHatId, address(0)); } + + function testMechanisticToggleOutputSavedWhenChangingToHumanisticToggle() public { + // mock a getHatStatus call to return false (inactive) for secondHatId + vm.mockCall( + address(_toggle), abi.encodeWithSelector(IHatsToggle.getHatStatus.selector, secondHatId), abi.encode(false) + ); + (,,,,,,,, bool status) = hats.viewHat(secondHatId); + assertFalse(status); + // change the toggle to a humanistic module + vm.prank(topHatWearer); + hats.changeHatToggle(secondHatId, address(5)); + // ensure that hat status is still active + (,,,,,,,, status) = hats.viewHat(secondHatId); + assertFalse(status); + } + + function testAdminCannotChangeDetailsToTooLongString() public { + vm.prank(topHatWearer); + // console2.log("string length", bytes(long7050).length); + vm.expectRevert(HatsErrors.StringTooLong.selector); + hats.changeHatDetails(secondHatId, long7050); + } + + function testAdminCannotChangeImageURIToTooLongString() public { + vm.prank(topHatWearer); + // console2.log("string length", bytes(long7050).length); + vm.expectRevert(HatsErrors.StringTooLong.selector); + hats.changeHatImageURI(secondHatId, long7050); + } } contract OverridesHatTests is TestSetup2 { @@ -1474,6 +1537,50 @@ contract OverridesHatTests is TestSetup2 { string memory jsonUri = hats.uri(topHatId); console2.log("encoded URI", jsonUri); } + + function testBalanceOfBatch() public { + // create and mint two separate hats to two separate wearers + + // build this test result array + // secondWearer wears secondHatId, ie 1 + // thirdWearer wearss thidrHatId, 1 + // nonWearer doesn't wear secondHatId, 0 + uint256[] memory test = new uint256[](3); + test[0] = 1; + test[1] = 1; + test[2] = 0; + + address[] memory wearers = new address[](3); + wearers[0] = secondWearer; + wearers[1] = thirdWearer; + wearers[2] = nonWearer; + + // create and mint thirdHatId to thirdWearer + vm.prank(topHatWearer); + thirdHatId = hats.createHat( + topHatId, + "third hat", + 3, // maxSupply + _eligibility, + _toggle, + true, + "" + ); + + uint256[] memory ids = new uint256[](3); + ids[0] = secondHatId; + ids[1] = thirdHatId; + ids[2] = secondHatId; + + vm.prank(topHatWearer); + hats.mintHat(thirdHatId, thirdWearer); + + uint256[] memory balances = hats.balanceOfBatch(wearers, ids); + + assertEq(balances, test); + + // try balance of batch with three hats and three wearers + } } contract LinkHatsTests is TestSetup2 { @@ -1493,7 +1600,7 @@ contract LinkHatsTests is TestSetup2 { level13HatId = 0x0000000100050001000100010001000100010001000100010001000100010000; vm.prank(topHatWearer); - console2.log("creating level 14 hat"); + level14HatId = hats.createHat(level13HatId, "level 14 hat", _maxSupply, _eligibility, _toggle, false, ""); } @@ -1519,7 +1626,7 @@ contract LinkHatsTests is TestSetup2 { vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, secondHatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 2); @@ -1538,7 +1645,7 @@ contract LinkHatsTests is TestSetup2 { vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, secondHatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 2); @@ -1558,7 +1665,7 @@ contract LinkHatsTests is TestSetup2 { vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, level14HatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, level14HatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, level14HatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 15); @@ -1581,7 +1688,7 @@ contract LinkHatsTests is TestSetup2 { vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, level14HatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, level14HatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, level14HatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 15); @@ -1605,13 +1712,13 @@ contract LinkHatsTests is TestSetup2 { vm.prank(secondWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.NotAdminOrWearer.selector)); - hats.approveLinkTopHatToTree(secondTopHatDomain, level14HatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, level14HatId, address(0), address(0), "", ""); } function testCannotApproveUnrequestedLink() public { vm.prank(topHatWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.LinkageNotRequested.selector)); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); } function testAdminCanRelinkTopHatWithinTree() public { @@ -1621,7 +1728,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(topHatWearer); vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, secondHatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 2); @@ -1629,7 +1736,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(topHatWearer); vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, topHatId); - hats.relinkTopHatWithinTree(secondTopHatDomain, topHatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, topHatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 1); } @@ -1643,14 +1750,14 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, level2HatId); vm.prank(fourthWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, level2HatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, level2HatId, address(0), address(0), "", ""); assertEq(hats.getHatLevel(secondTopHatId), 3); // relink to secondHatId vm.prank(secondWearer); vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, secondHatId); - hats.relinkTopHatWithinTree(secondTopHatDomain, secondHatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 2); } @@ -1660,13 +1767,13 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); // relink vm.prank(topHatWearer); vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, level14HatId); - hats.relinkTopHatWithinTree(secondTopHatDomain, level14HatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, level14HatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 15); assertTrue(hats.isAdminOfHat(topHatWearer, secondTopHatId)); @@ -1678,7 +1785,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertTrue(hats.isAdminOfHat(secondWearer, secondTopHatId)); // attempt relink to tophatId from secondWearer, who is an admin of secondTopHatId but not an admin or wearer of tophatId @@ -1686,7 +1793,7 @@ contract LinkHatsTests is TestSetup2 { vm.expectRevert( abi.encodeWithSelector(HatsErrors.NotAdmin.selector, secondWearer, hats.buildHatId(topHatId, 1)) ); - hats.relinkTopHatWithinTree(secondTopHatDomain, topHatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, topHatId, address(0), address(0), "", ""); } function testNewAdminNonAdminCannotRelinkToLastLevelWithinTree() public { @@ -1694,13 +1801,13 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertTrue(hats.isAdminOfHat(secondWearer, secondTopHatId)); // attempt relink to 14th level from secondhatwearer, who is an admin of secondTopHatId but not an admin or wearer of tophatId vm.startPrank(secondWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.NotAdminOrWearer.selector)); - hats.relinkTopHatWithinTree(secondTopHatDomain, level14HatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, level14HatId, address(0), address(0), "", ""); } function testTreeRootNonAdminCannotRelink() public { @@ -1712,14 +1819,14 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); // attempt relink to new hat from secondWearer vm.startPrank(secondWearer); vm.expectRevert( abi.encodeWithSelector(HatsErrors.NotAdmin.selector, secondWearer, hats.buildHatId(newHatId, 1)) ); - hats.relinkTopHatWithinTree(secondTopHatDomain, newHatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, newHatId, address(0), address(0), "", ""); } function testAdminCanRequestNewLink() public { @@ -1727,7 +1834,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertEq(hats.linkedTreeRequests(secondTopHatDomain), 0); // request new link from secondWearer @@ -1743,7 +1850,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); // request new link from secondWearer vm.prank(secondWearer); @@ -1753,7 +1860,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(topHatWearer); vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, topHatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, topHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, topHatId, address(0), address(0), "", ""); assertEq(hats.linkedTreeRequests(secondTopHatDomain), 0); assertEq(hats.getHatLevel(secondTopHatId), 1); } @@ -1765,7 +1872,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(topHatWearer); vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, secondHatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 2); @@ -1784,19 +1891,19 @@ contract LinkHatsTests is TestSetup2 { // try approving vm.prank(topHatWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.CircularLinkage.selector)); - hats.approveLinkTopHatToTree(topHatDomain, secondHatId); + hats.approveLinkTopHatToTree(topHatDomain, secondHatId, address(0), address(0), "", ""); // test a recursive call vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); vm.prank(topHatWearer); hats.requestLinkTopHatToTree(topHatDomain, secondTopHatId); vm.prank(topHatWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.CircularLinkage.selector)); - hats.approveLinkTopHatToTree(topHatDomain, secondTopHatId); + hats.approveLinkTopHatToTree(topHatDomain, secondTopHatId, address(0), address(0), "", ""); } function testRelinkingCannotCreateCircularLink() public { @@ -1804,7 +1911,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); // second link, under first link uint256 thirdTopHatId = hats.mintTopHat(fourthWearer, "for linking", "http://www.tophat.com/"); @@ -1813,12 +1920,12 @@ contract LinkHatsTests is TestSetup2 { vm.prank(fourthWearer); hats.requestLinkTopHatToTree(thirdTopHatDomain, secondTopHatId); vm.prank(topHatWearer); - hats.approveLinkTopHatToTree(thirdTopHatDomain, secondTopHatId); + hats.approveLinkTopHatToTree(thirdTopHatDomain, secondTopHatId, address(0), address(0), "", ""); // try relink second tophat under third tophat vm.prank(topHatWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.CircularLinkage.selector)); - hats.relinkTopHatWithinTree(secondTopHatDomain, thirdTopHatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, thirdTopHatId, address(0), address(0), "", ""); } function testCannotCrossTreeRelink() public { @@ -1829,14 +1936,14 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(secondWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); // attempt link secondTopHat to third tophat (worn by fourthWearer) vm.prank(secondWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, thirdTopHatId); vm.prank(fourthWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.CrossTreeLinkage.selector)); - hats.approveLinkTopHatToTree(secondTopHatDomain, thirdTopHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, thirdTopHatId, address(0), address(0), "", ""); } function testCannotApproveCrossTreeLink() public { @@ -1847,12 +1954,12 @@ contract LinkHatsTests is TestSetup2 { vm.prank(thirdWearer); hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); vm.prank(secondWearer); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); // attempt relink secondTopHat to third tophat (worn by topHatWearer) vm.prank(topHatWearer); vm.expectRevert(abi.encodeWithSelector(HatsErrors.CrossTreeLinkage.selector)); - hats.relinkTopHatWithinTree(secondTopHatDomain, thirdTopHatId); + hats.relinkTopHatWithinTree(secondTopHatDomain, thirdTopHatId, address(0), address(0), "", ""); } function testTreeLinkingAndUnlinking() public { @@ -1864,7 +1971,7 @@ contract LinkHatsTests is TestSetup2 { vm.prank(topHatWearer); vm.expectEmit(true, true, true, true); emit TopHatLinked(secondTopHatDomain, secondHatId); - hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); assertFalse(hats.isTopHat(secondTopHatId)); assertEq(hats.getHatLevel(secondTopHatId), 2); assertEq(hats.linkedTreeRequests(secondTopHatDomain), 0); @@ -1878,6 +1985,68 @@ contract LinkHatsTests is TestSetup2 { hats.unlinkTopHatFromTree(secondTopHatDomain); assertEq(hats.isTopHat(secondTopHatId), true); } + + function testUnlinkedHatCannotBeLinkedAgainWithoutPermission() public { + // first link of tophat to tree A + vm.prank(thirdWearer); + hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); + vm.prank(topHatWearer); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, address(0), address(0), "", ""); + + // tophat wearer creates a new tree B + vm.startPrank(topHatWearer); + uint256 treeB = hats.mintTopHat(topHatWearer, "for rugging", "http://www.tophat.com/"); + + // tree A requests new link to a different tree B + // this is possible because tree A is an admin for the tophat + hats.requestLinkTopHatToTree(secondTopHatDomain, treeB); + + // tree A unlinks the tophat + hats.unlinkTopHatFromTree(secondTopHatDomain); + + // admin B should not be able to rug the tree by approving the link without the tree's permission + vm.expectRevert(HatsErrors.LinkageNotRequested.selector); + hats.approveLinkTopHatToTree(secondTopHatDomain, treeB, address(0), address(0), "", ""); + + assertTrue(hats.isAdminOfHat(thirdWearer, secondTopHatId)); + } + + function testCanChangeModulesAndMetadataWhenApprovingOrRelinking() public { + // request + vm.prank(thirdWearer); + hats.requestLinkTopHatToTree(secondTopHatDomain, secondHatId); + + // approve with updated modules and metadata + vm.prank(topHatWearer); + hats.approveLinkTopHatToTree(secondTopHatDomain, secondHatId, _eligibility, _toggle, "details", "image"); + (string memory details,,, address eligibility, address toggle, string memory image,,,) = + hats.viewHat(secondTopHatId); + assertEq(details, "details"); + assertEq(image, "image"); + assertEq(eligibility, _eligibility); + assertEq(toggle, _toggle); + + // relink with updated modules and metadata + vm.prank(topHatWearer); + hats.relinkTopHatWithinTree(secondTopHatDomain, secondHatId, address(100), address(101), "details2", "image2"); + (details,,, eligibility, toggle, image,,,) = hats.viewHat(secondTopHatId); + assertEq(details, "details2"); + assertEq(image, "image2"); + assertEq(eligibility, address(100)); + assertEq(toggle, address(101)); + + // check that linked top hat can be toggled off + vm.mockCall(address(101), abi.encodeWithSignature("getHatStatus(uint256)", secondTopHatId), abi.encode(false)); + (,,,,,,,, bool status) = hats.viewHat(secondTopHatId); + assertFalse(status); + + // modules values reset on unlink + vm.prank(topHatWearer); + hats.unlinkTopHatFromTree(secondTopHatDomain); + (,,, eligibility, toggle,,,,) = hats.viewHat(secondTopHatId); + assertEq(eligibility, address(0)); + assertEq(toggle, address(0)); + } } contract MalformedInputsTests is TestSetup2 { diff --git a/test/HatsIdUtils.t.sol b/test/HatsIdUtils.t.sol index 584a275..fa13f46 100644 --- a/test/HatsIdUtils.t.sol +++ b/test/HatsIdUtils.t.sol @@ -71,6 +71,13 @@ contract HatIdUtilTests is Test { } } + function testBuildHatIdRevertsAfterMaxLevel() public { + uint256 admin = 0x0000000100010001000100010001000100010001000100010001000100010001; + vm.expectRevert(MaxLevelsReached.selector); + uint256 invalidChild = utils.buildHatId(admin, 1); + console2.log(invalidChild); + } + function testTopHatDomain() public { uint256 admin = 1 << 224; assertEq(utils.isTopHat(admin), true); @@ -82,4 +89,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)); + } } diff --git a/test/LongStrings.sol b/test/LongStrings.sol new file mode 100644 index 0000000..f9dbb6e --- /dev/null +++ b/test/LongStrings.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract LongStrings { + string public constant long7000 = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut maximus risus. Cras euismod vestibulum aliquet. Proin sit amet arcu scelerisque sem iaculis venenatis et sed magna. Curabitur lobortis venenatis sapien, sed bibendum purus. Etiam ullamcorper ultrices tortor, et convallis lectus viverra non. Vivamus ac nisi sit amet risus consectetur hendrerit. Suspendisse auctor turpis vitae efficitur cursus. Suspendisse potenti. Nulla dapibus placerat mauris sit amet scelerisque. Curabitur nec dui in quam molestie tempor. In vitae porta dolor. In laoreet egestas ante, ut aliquet nulla efficitur sit amet. Nulla blandit odio nec tellus tempor posuere. Vestibulum tellus ante, ultrices quis auctor et, pharetra eu nulla. Duis efficitur tortor mi, vitae posuere orci dapibus vitae. Ut a mi vulputate, rhoncus nisl vel, sagittis metus. Nullam eu vulputate augue. Vestibulum rhoncus mi quis consectetur hendrerit. Etiam cursus diam id consequat pharetra. Mauris tristique ipsum id porta posuere. In pharetra enim eget urna fermentum, et efficitur tortor eleifend. Suspendisse luctus sed sapien vitae placerat. Pellentesque tincidunt maximus ex, vel consequat neque pellentesque at. Etiam accumsan, mi vel sodales dictum, massa lorem tristique justo, non pretium magna lacus pellentesque dolor. Morbi condimentum ullamcorper neque, ut molestie ex laoreet id. Sed imperdiet quam elit, vel semper turpis tristique quis. Duis vehicula ex in arcu vehicula, nec vulputate neque bibendum. Praesent sit amet eros ut odio iaculis pretium eget et lectus. Nullam a viverra erat, id laoreet dolor. Quisque pellentesque eros vel nibh varius ornare. Cras consectetur massa dolor, non lacinia nunc tempus nec. Suspendisse potenti. Curabitur lobortis molestie scelerisque. Aenean posuere mattis bibendum. Sed vel lorem urna. Ut consequat enim ut pretium congue. Fusce ac eros non nisl dictum rutrum ac at tortor. Aenean vulputate fringilla turpis, non imperdiet felis efficitur in. Praesent felis metus, ultrices ut lorem nec, sagittis molestie neque. Nulla ac lectus sed dui aliquet lacinia. Suspendisse id turpis quis nisi cursus dapibus. Curabitur vel lacus convallis purus sollicitudin maximus. Duis imperdiet vehicula erat, eu egestas lectus volutpat id. Sed eget lacus at elit rhoncus pretium. Maecenas facilisis ex diam, non iaculis augue congue non. Aliquam felis metus, semper a eleifend pretium, hendrerit sit amet neque. Curabitur iaculis aliquam lacus, non aliquam nunc lobortis sed. Vivamus placerat ornare risus at porttitor. Quisque eget orci tempus, sodales ex quis, elementum lorem. Sed urna velit, cursus nec turpis eu, bibendum dignissim dui. Nunc eu turpis malesuada, convallis lectus id, egestas est. Donec eleifend dapibus ipsum at vulputate. Nulla ornare magna non faucibus tempus. Nulla at ante vel erat cursus consequat. Suspendisse dapibus in dui vestibulum lobortis. Integer erat augue, suscipit id enim cursus, elementum cursus sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi posuere ultricies magna, eu ultricies sem cursus elementum. Donec commodo orci quis convallis sagittis. In vel urna lacus. Morbi dapibus tellus elit, ac porttitor diam viverra ut. Sed at quam nulla. Integer tincidunt nibh vitae blandit varius. Mauris leo ante, mattis a odio a, rutrum euismod turpis. Maecenas eu magna molestie, tincidunt purus nec, semper magna. Integer malesuada ante tempus risus suscipit, aliquam gravida sapien molestie. Nunc sollicitudin lectus mattis magna pulvinar, in cursus lorem consequat. Proin efficitur lorem velit, ut feugiat quam euismod in. Integer nibh massa, feugiat ut nunc non, ullamcorper euismod nulla. Aliquam nisl ligula, congue commodo ultricies ut, luctus vitae quam. Fusce et dui vel eros dapibus laoreet. Aenean ultricies, nisl a ultrices viverra, lorem lorem sagittis lorem, vel faucibus mi est vitae ante. Cras semper arcu est, eget tincidunt libero congue a. Nunc porttitor, augue et lobortis volutpat, libero massa porttitor augue, id convallis nisi nibh at magna. Quisque vulputate dictum consectetur. Nulla a pharetra sapien. Suspendisse et lorem sit amet lacus consequat congue at id velit. Etiam a laoreet neque. Morbi molestie, ante a ornare rutrum, tortor augue rutrum nulla, a ultrices leo nunc quis leo. Ut rutrum, felis tincidunt lacinia lobortis, tellus arcu sollicitudin tellus, ut bibendum urna odio ac felis. In dignissim, nulla id imperdiet dapibus, purus ipsum commodo felis, sed mattis tortor nibh a nisl. Aliquam accumsan gravida volutpat. Duis facilisis aliquet augue, at tempor odio posuere et. Phasellus justo arcu, hendrerit et semper sit amet, ornare id sem. Fusce sollicitudin tellus a efficitur congue. Sed vulputate lacus id massa porttitor, in finibus justo luctus. Donec et nulla urna. Sed eget augue ut quam viverra rhoncus vel in nisl. Nunc eu enim velit. Suspendisse vitae ullamcorper neque, quis finibus felis. Sed est quam, euismod sed lectus a, tristique aliquet ipsum. Vestibulum purus massa, lacinia eget convallis at, scelerisque sit amet tortor. In hac habitasse platea dictumst. Suspendisse urna lorem, pulvinar et placerat vel, vulputate ac sapien. Fusce sit amet enim vitae risus malesuada luctus. Ut egestas porttitor ipsum eu tincidunt. Cras eget neque quis ligula suscipit volutpat. Sed imperdiet porta est, quis pharetra sapien viverra et. Vestibulum eu fermentum lorem. In odio odio, rutrum a ante in, vehicula hendrerit metus. Sed ut accumsan felis. Fusce sollicitudin ligula arcu, vitae finibus quam consectetur ac. Maecenas id libero quis erat maximus porta. Pellentesque turpis diam, vulputate eu elementum vitae, volutpat vitae urna. Fusce tincidunt condimentum magna vel ullamcorper. Pellentesque iaculis tortor elit, sed finibus leo feugiat ut. Mauris vitae volutpat mi. Nunc mollis tincidunt eros et efficitur. Nunc metus arcu, bibendum ac bibendum quis, varius in ligula. Etiam imperdiet, dui sit amet semper rutrum, massa metus venenatis risus, eget laoreet risus quam at augue. Integer lacinia metus in urna fringilla ullamcorper. Duis vehicula vehicula leo quis blandit. Cras ligula velit, eleifend non mauris at, porta blandit quam. Praesent ut elit ac ligula efficitur condimentum sit amet eu magna. Morbi nisl felis, hendrerit ac velit eget, scelerisque tincidunt ante. Ut in erat tempor neque tincidunt iaculis. Integer venenatis tempor accumsan. Mauris scelerisque eros sem, ut hendrerit tortor placerat eu. Curabitur sodales tortor id erat convallis hendrerit. Nullam arcu ex, porta nec lacinia quis, dignissim eget quam. Vivamus varius nisi eu augue scelerisque, ut pellentesque libero posuere. Morbi pharetra a massa ac eleifend. Donec et nisl nisl. Cras eget ligula et elit pellentesque commodo. Nam in lacus justo. Vivamus in tortor mollis, pellentesque odio id, vehicula diam. Maecenas congue et mauris in rutrum. Nunc justo sem, dignissim et blandit vel, aliquet vitae justo. Praesent diam sem, tempus et purus at, viverra fringilla nunc."; + + string public constant long7050 = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur cursus elit tortor, vulputate dignissim tellus volutpat et. Quisque vitae odio dapibus, ornare tellus in, rhoncus magna. Pellentesque et nisi nibh. Morbi ornare lacinia dictum. Vivamus et gravida eros, nec scelerisque eros. Sed eget massa consequat, sollicitudin enim sed, auctor metus. Mauris condimentum tortor sed mi vulputate vehicula. Nulla massa nulla, vulputate ac convallis id, sodales in diam. Morbi ut rhoncus elit. Sed fringilla aliquet ipsum a interdum. Nullam eleifend arcu diam, quis ornare tortor fermentum et. Pellentesque posuere nisl tempus odio dapibus pellentesque. In laoreet odio dui. Nullam eu purus quam. Nulla metus ipsum, egestas sit amet neque efficitur, tristique porta arcu. Curabitur sodales, nunc quis semper pretium, mi est dictum purus, eget hendrerit tortor sem eget turpis. Mauris tempor et leo id auctor. In faucibus scelerisque nunc, quis cursus ante eleifend eu. Maecenas porttitor est vitae dolor laoreet efficitur in quis diam. In volutpat commodo ornare. Donec arcu risus, maximus vel vestibulum in, mattis ut urna. Integer neque lacus, tincidunt nec nisi at, iaculis efficitur lorem. Aliquam tristique mi mi, vitae suscipit lectus tincidunt eu. Etiam in mauris vel ex congue pretium. Aenean et mollis ex, a dignissim lacus. Donec sit amet diam nec erat pellentesque viverra ac et orci. Duis tellus purus, tempus faucibus fringilla vitae, auctor hendrerit lacus. Nunc vitae justo ac lorem cursus pretium et nec purus. Praesent hendrerit mollis purus, at dictum sapien placerat varius. Praesent elementum vehicula sapien, id egestas tortor gravida dignissim. Proin non ante eget purus pulvinar ornare eget sed odio. Nulla interdum dui eu nulla porta, ac malesuada neque pellentesque. Duis placerat ullamcorper turpis id tempus. Sed magna eros, vulputate sed nunc nec, convallis hendrerit erat. Vivamus blandit eros leo, blandit venenatis lacus vehicula sollicitudin. Ut fermentum leo id diam bibendum cursus. Integer non tempor libero, rutrum gravida tortor. Phasellus quis convallis ante, id accumsan orci. Phasellus volutpat porttitor mauris eu lacinia. Morbi et lorem porta ligula malesuada pretium. Sed vulputate volutpat enim in facilisis. Sed ac tristique magna, vel cursus lectus. Maecenas sit amet mi egestas, dignissim est at, lobortis lorem. Phasellus lobortis erat et congue mollis. Pellentesque lacinia augue quis erat pharetra, vitae egestas arcu tempor. Curabitur mollis ac dui ultricies porta. Donec ultrices rutrum quam, vel finibus nisi tempus in. Vestibulum congue facilisis turpis viverra laoreet. Donec ac nibh iaculis, mollis risus a, tristique arcu. Fusce gravida ac tellus in ullamcorper. Morbi vitae tincidunt sapien. Vestibulum vehicula eu risus et cursus. Aenean at sollicitudin quam. In hac habitasse platea dictumst. Donec at ullamcorper augue. Etiam placerat malesuada tortor, sed sodales nisl. Integer cursus et erat lobortis pellentesque. Phasellus leo dolor, pharetra sed libero eget, sodales mollis enim. Praesent condimentum felis at efficitur maximus. Donec ornare mi facilisis turpis venenatis, sit amet pharetra neque euismod. Nam id nisi purus. Donec porta facilisis augue sit amet facilisis. Donec diam justo, ultrices eget hendrerit et, sollicitudin vitae libero. Pellentesque faucibus tellus faucibus dui accumsan eleifend. Aenean fermentum purus dui, sed congue sapien facilisis et. Aenean ornare tellus felis, eu aliquam purus consectetur quis. Nullam mattis est non libero convallis, et pellentesque sapien laoreet. Morbi vitae urna non turpis sagittis dictum rhoncus ut mauris. Morbi cursus imperdiet erat, non consectetur purus dignissim quis. Morbi diam nibh, egestas sit amet tellus ac, porta euismod leo. Vivamus tempus vel metus in pretium. Maecenas ac blandit tellus. Duis suscipit nec nisl vel accumsan. Vivamus suscipit a eros in finibus. Pellentesque ut convallis lorem. Pellentesque est urna, convallis eu varius non, efficitur a justo. Vivamus vel eleifend erat, sed facilisis libero. Proin gravida orci ac libero sollicitudin, finibus elementum lacus tempus. Proin pulvinar metus vel neque elementum, nec ultricies tortor tincidunt. Nam tincidunt enim consequat nulla volutpat malesuada. Vestibulum eget augue euismod, ultrices ex sit amet, pellentesque sapien. Cras ac hendrerit erat, eu fermentum ex. Praesent consectetur fermentum eros, eget lacinia dui scelerisque nec. In non gravida felis, tempor fermentum dolor. Aenean cursus nunc et dictum venenatis. Ut efficitur dui eget libero tempus cursus. Ut suscipit mattis iaculis. Phasellus tincidunt urna enim, in laoreet est auctor a. Vivamus sollicitudin, velit et tempus lobortis, nunc nisl euismod lectus, a molestie turpis ex quis magna. Sed id est augue. Mauris ac mi nec eros viverra lobortis ac a magna. Nunc ex neque, suscipit sit amet congue nec, placerat fringilla velit. Phasellus in arcu feugiat, consequat lacus vel, pharetra nulla. Sed fringilla sapien orci, a consectetur velit vulputate vitae. Cras eget ornare ex. Curabitur euismod dictum neque nec commodo. Quisque cursus ante blandit eros facilisis, nec maximus odio tristique. Curabitur ultrices vestibulum hendrerit. Praesent sed libero tincidunt, consequat urna non, semper lorem. Ut pellentesque placerat pulvinar. Integer sed sapien felis. Donec in volutpat eros, eget rhoncus risus. Curabitur libero felis, pellentesque id nisl ut, aliquet luctus turpis. Sed ac facilisis arcu, eget pulvinar sem. Phasellus feugiat lacus mattis quam laoreet, quis vulputate lorem tincidunt. Proin mollis ut erat ut mattis. Vivamus tempor quam sit amet justo accumsan ullamcorper. Donec ut mi sodales, scelerisque dui sed, dictum mauris. Ut sollicitudin neque augue, et commodo nunc cursus pretium. Morbi hendrerit turpis neque, et consectetur est congue et. Pellentesque nunc mauris, eleifend id condimentum nec, feugiat et lectus. Pellentesque sed libero nunc. Aliquam luctus erat eros, eu efficitur neque dictum eget. Fusce non aliquam nulla, at ultricies ex. Nulla blandit sed metus eget lacinia. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Morbi auctor eu augue nec viverra. Sed vitae ante eu nulla porttitor semper. Phasellus congue justo erat, sit amet viverra neque mattis eu. Sed sit amet massa vel lectus tempor viverra. Praesent nec sagittis libero. Curabitur eget porttitor lacus. Etiam non ultrices odio, sed tincidunt mi. Pellentesque dictum magna vel ex placerat ullamcorper. Mauris ullamcorper neque mollis lacinia laoreet. Morbi scelerisque libero vel elit cursus, ac vehicula dui pellentesque. Ut pellentesque tellus sit amet accumsan vestibulum. Fusce quis diam at metus tempus ullamcorper. Sed placerat sit amet ante sit amet condimentum. Aliquam eros leo, feugiat sed massa feugiat, tristique vestibulum libero. Nulla sit amet metus et eros fermentum dapibus vel at diam. Sed aliquet magna non tellus molestie, at auctor augue fermentum. Morbi risus erat, facilisis ac venenatis a, venenatis non risus sodales sed."; +}