// SPDX-License-Identifier: MIT pragma solidity ^0.8.13; import "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; import "@openzeppelin/contracts/proxy/utils/Initializable.sol"; import "@openzeppelin/contracts/utils/Multicall.sol"; import {IListingTermsRegistry as LTR} from "../../../listing-terms-registry/IListingTermsRegistry.sol"; import "../mechanics/listing/ICanListAssets.sol"; import "../../../../acl/delegated/DelegatedAccessControlled.sol"; import "../../AbstractListingConfigurator.sol"; contract GeneralGuildPreset is ICanListAssets, Initializable, Multicall, DelegatedAccessControlled, AbstractListingConfigurator { using EnumerableSet for EnumerableSet.AddressSet; using EnumerableSet for EnumerableSet.Bytes32Set; using Assets for Assets.AssetId[]; using Assets for Assets.Asset[]; error ForbiddenGroupName(string name); error CannotRemoveGroupWithMembers(string name); error InvalidZeroAddress(); error EmptyGroupName(); event GroupRemoved(string indexed groupName); event MembersAdded(string indexed groupName, address[] members); event MembersRemoved(string indexed groupName, address[] members); event ListingTermsUpdated( uint256 indexed universeId, string indexed groupName, Assets.AssetId[] assetIds, LTR.ListingTerms config ); /// @dev Group fallback name for Guild members string private constant _GUILD_MEMBER = "__GUILD_MEMBER"; /// @dev Group fallback name for non-guild members string private constant _NON_GUILD_MEMBER = "__NON_GUILD_MEMBER"; string private constant _AUTHORIZED_LC_PRESET_MANAGER = "AUTHORIZED_LC_PRESET_MANAGER"; bytes32 private constant _GUILD_MEMBER_HASH = keccak256(abi.encodePacked(_GUILD_MEMBER)); bytes32 private constant _NON_GUILD_MEMBER_HASH = keccak256(abi.encodePacked(_NON_GUILD_MEMBER)); struct ListingTermsStore { bool exists; LTR.ListingTerms config; } /// @dev Group to members mapping mapping(string => EnumerableSet.AddressSet) internal _members; /// @dev Member => Hashed Group Set mapping(address => EnumerableSet.Bytes32Set) internal _memberOf; /// @dev Group Hash to Group Name mapping mapping(bytes32 => string) internal _groupNames; /// @dev Registered group set EnumerableSet.Bytes32Set internal _groups; /// @dev Hashed Group Name => Universe ID => Asset IDs hash => Listing Terms mapping(bytes32 => mapping(uint256 => mapping(bytes32 => ListingTermsStore))) internal _configs; IMetahub internal _metahubContract; /// @dev Contract which holds access control (e.g. ListingConfiguratorRegistry) IDelegatedAccessControl internal _dacContract; modifier whenValidName(string memory name) { if (bytes(name).length == 0) revert EmptyGroupName(); bytes32 hashed = _hash(name); if (hashed == _GUILD_MEMBER_HASH || hashed == _NON_GUILD_MEMBER_HASH) revert ForbiddenGroupName(name); _; } modifier onlyAuthorized() { if ( _hasRole(_AUTHORIZED_LC_PRESET_MANAGER, _msgSender()) || _hasRole(Roles.DELEGATED_MANAGER, _msgSender()) || _hasRole(Roles.DELEGATED_ADMIN, _msgSender()) ) { _; } else { revert Forbidden(); } } /** * @dev Constructor that gets called for the implementation contract. * @custom:oz-upgrades-unsafe-allow constructor */ constructor() { _disableInitializers(); } function initialize(address dac, address metahub) external initializer { if (dac == address(0)) revert InvalidZeroAddress(); if (metahub == address(0)) revert InvalidZeroAddress(); _dacContract = IDelegatedAccessControl(dac); _metahubContract = IMetahub(metahub); } /** * @dev Creates a group (if needed) and adds members to given group * @param group Group name * @param members Group member addresses */ function addMembers(string calldata group, address[] calldata members) external onlyAuthorized whenValidName(group) { bytes32 hashed = _hash(group); EnumerableSet.AddressSet storage memberSet = _members[group]; _groupNames[hashed] = group; _groups.add(hashed); for (uint256 i = 0; i < members.length; i++) { memberSet.add(members[i]); _memberOf[members[i]].add(hashed); } emit MembersAdded(group, members); } /** * @dev Removes members from given group * @param group Group name * @param members Members to remove */ function removeMembers(string calldata group, address[] calldata members) external onlyAuthorized whenValidName(group) { bytes32 hashed = _hash(group); EnumerableSet.AddressSet storage memberSet = _members[group]; for (uint256 i = 0; i < members.length; i++) { memberSet.remove(members[i]); _memberOf[members[i]].remove(hashed); } emit MembersRemoved(group, members); } /** * @dev Removes group completly. Note: all members must be removed * from group using {removeMembers} prior calling this function * @param group Group name */ function removeGroup(string calldata group) external onlyAuthorized whenValidName(group) { if (_members[group].length() > 0) revert CannotRemoveGroupWithMembers(group); bytes32 hashed = _hash(group); _groups.remove(hashed); delete _groupNames[hashed]; emit GroupRemoved(group); } /** * @dev Sets listing terms for given group and universe * @param universeId - Universe ID * @param group - Group name * @param config - Listing terms */ function setListingTerms( uint256 universeId, string calldata group, Assets.AssetId[] calldata assetIds, LTR.ListingTerms calldata config ) external onlyAuthorized whenValidName(group) whenSorted(assetIds) { _configs[_hash(group)][universeId][assetIds.hash()] = ListingTermsStore(true, config); emit ListingTermsUpdated(universeId, group, assetIds, config); } /** * @dev Sets listing terms for guild member when listing terms are not specified * for groups in which member is participated */ function setGuildMemberListingTerms( uint256 universeId, Assets.AssetId[] calldata assetIds, LTR.ListingTerms calldata config ) external onlyAuthorized whenSorted(assetIds) { _configs[_GUILD_MEMBER_HASH][universeId][assetIds.hash()] = ListingTermsStore(true, config); emit ListingTermsUpdated(universeId, _GUILD_MEMBER, assetIds, config); } /** * @dev Sets listing terms for non-guild members */ function setNonGuildMemberListingTerms( uint256 universeId, Assets.AssetId[] calldata assetIds, LTR.ListingTerms calldata config ) external onlyAuthorized whenSorted(assetIds) { _configs[_NON_GUILD_MEMBER_HASH][universeId][assetIds.hash()] = ListingTermsStore(true, config); emit ListingTermsUpdated(universeId, _NON_GUILD_MEMBER, assetIds, config); } /** * @dev Gets listing terms for universe and group * @param universeId Universe ID * @param assetIds Asset IDs * @param group Group Name */ function getListingTerms( uint256 universeId, string calldata group, Assets.AssetId[] calldata assetIds ) external view whenValidName(group) whenSorted(assetIds) returns (LTR.ListingTerms memory config) { return _configs[_hash(group)][universeId][assetIds.hash()].config; } /** * @dev Gets listing terms for guild members * @param universeId Universe ID */ function getGuildMemberListingTerms(uint256 universeId, Assets.AssetId[] calldata assetIds) external view whenSorted(assetIds) returns (LTR.ListingTerms memory config) { return _configs[_GUILD_MEMBER_HASH][universeId][assetIds.hash()].config; } /** * @dev Gets listing terms for non-guild members * @param universeId Universe ID */ function getNonGuildMemberListingTerms(uint256 universeId, Assets.AssetId[] calldata assetIds) external view whenSorted(assetIds) returns (LTR.ListingTerms memory config) { return _configs[_NON_GUILD_MEMBER_HASH][universeId][assetIds.hash()].config; } /** * @dev Gets pagable list of registered groups * @param offset List offset * @param limit List limit */ function getGroups(uint256 offset, uint256 limit) external view returns (string[] memory groups, uint256 total) { return _getPagedGroups(_groups, offset, limit); } /** * @dev Gets pagable list of group members * @param offset List offset * @param limit List limit */ function getGroupMembers( string calldata name, uint256 offset, uint256 limit ) external view returns (address[] memory members, uint256 total) { EnumerableSet.AddressSet storage memberSet = _members[name]; total = memberSet.length(); if (offset >= total) return (new address[](0), total); if (limit > total - offset) { limit = total - offset; } members = new address[](limit); for (uint256 i = 0; i < limit; i++) { members[i] = memberSet.at(offset + i); } } /** * @dev Gets pagable list of groups for given account * @param account Member account * @param offset List offset * @param limit List limit */ function getMemberOf( address account, uint256 offset, uint256 limit ) external view returns (string[] memory groups, uint256 total) { return _getPagedGroups(_memberOf[account], offset, limit); } /// @inheritdoc IListingTermsAware function __getListingTerms( // solhint-disable-previous-line private-vars-leading-underscore Rentings.Params calldata params, Listings.Listing calldata listing, uint256 universeId ) external view override returns (LTR.ListingTerms[] memory listingTerms) { EnumerableSet.Bytes32Set storage groups = _memberOf[params.renter]; uint256 groupCount = groups.length(); if (groupCount == 0) { return _getSingleListingTerms(_configs[_NON_GUILD_MEMBER_HASH][universeId][listing.assets.hashIds()]); } return _getListingTermsForGroups(groups, universeId, listing.assets.hashIds(), groupCount); } /// @inheritdoc ICanListAssets function __canListAssets( // solhint-disable-previous-line private-vars-leading-underscore Assets.Asset[] calldata, Listings.Params calldata params, uint32, bool ) external view override returns (bool canList, string memory errorMessage) { if (_hasRole(Roles.DELEGATED_MANAGER, params.lister) || _hasRole(Roles.DELEGATED_ADMIN, params.lister)) { return (true, ""); } return (false, "Lister is not admin or manager"); } function getDAC() external view returns (address) { return address(_dacContract); } function supportsInterface(bytes4 interfaceId) public view virtual override(AbstractListingConfigurator) returns (bool) { return interfaceId == type(ICanListAssets).interfaceId || super.supportsInterface(interfaceId); } function _getListingTermsForGroups( EnumerableSet.Bytes32Set storage groups, uint256 universeId, bytes32 assetsHash, uint256 groupCount ) internal view returns (LTR.ListingTerms[] memory) { uint256 termsFound; LTR.ListingTerms[] memory listingTerms = new LTR.ListingTerms[](groupCount); for (uint256 i = 0; i < groupCount; i++) { bytes32 group = groups.at(i); ListingTermsStore storage store = _configs[group][universeId][assetsHash]; if (store.exists) { listingTerms[termsFound] = store.config; termsFound++; } } if (termsFound == 0) return _getSingleListingTerms(_configs[_GUILD_MEMBER_HASH][universeId][assetsHash]); if (termsFound == groupCount) return listingTerms; // reduce listingTerms array size assembly { mstore(listingTerms, termsFound) } return listingTerms; } function _getPagedGroups( EnumerableSet.Bytes32Set storage groupSet, uint256 offset, uint256 limit ) internal view returns (string[] memory result, uint256 total) { total = groupSet.length(); if (offset >= total) return (new string[](0), total); if (limit > total - offset) { limit = total - offset; } result = new string[](limit); for (uint256 i = 0; i < limit; i++) { result[i] = _groupNames[groupSet.at(offset + i)]; } } function _getSingleListingTerms(ListingTermsStore storage store) internal view returns (LTR.ListingTerms[] memory result) { if (!store.exists) return result; result = new LTR.ListingTerms[](1); result[0] = store.config; } function _dac() internal view override returns (IDelegatedAccessControl) { return _dacContract; } function _metahub() internal view override returns (IMetahub) { return _metahubContract; } /** * @dev returns hash of given string */ function _hash(string memory data) internal pure returns (bytes32) { return keccak256(abi.encodePacked(data)); } }