diff --git a/.gitmodules b/.gitmodules index 2935700..6d0ab03 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "lib/micro-onchain-metadata-utils"] path = lib/micro-onchain-metadata-utils url = https://github.com/iainnash/micro-onchain-metadata-utils +[submodule "lib/onchain"] + path = lib/onchain + url = https://github.com/public-assembly/onchain diff --git a/lib/onchain b/lib/onchain new file mode 160000 index 0000000..252d72a --- /dev/null +++ b/lib/onchain @@ -0,0 +1 @@ +Subproject commit 252d72a4c804b1dcb5d0eb33867bd1704d22a8a7 diff --git a/remappings.txt b/remappings.txt index 6df5311..9ecd82c 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,4 +1,5 @@ forge-std/=lib/forge-std/src/ micro-onchain-metadata-utils/=lib/micro-onchain-metadata-utils/src/ @openzeppelin/contracts-upgradeable/=node_modules/@openzeppelin/contracts-upgradeable/ -@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ \ No newline at end of file +@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/ +onchain/=lib/onchain/tokenized-access-control/src \ No newline at end of file diff --git a/src/Curator.sol b/src/Curator.sol index 6fccca7..59b0448 100644 --- a/src/Curator.sol +++ b/src/Curator.sol @@ -1,7 +1,6 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import { UUPS } from "./lib/proxy/UUPS.sol"; import { ICurator } from "./interfaces/ICurator.sol"; import { Ownable } from "./lib/utils/Ownable.sol"; @@ -9,6 +8,7 @@ import { ICuratorFactory } from "./interfaces/ICuratorFactory.sol"; import { CuratorSkeletonNFT } from "./CuratorSkeletonNFT.sol"; import { IMetadataRenderer } from "./interfaces/IMetadataRenderer.sol"; import { CuratorStorageV1 } from "./CuratorStorageV1.sol"; +import { IAccessControlRegistry } from "onchain/interfaces/IAccessControlRegistry.sol"; /** * @notice Base contract for curation functioanlity. Inherits ERC721 standard from CuratorSkeletonNFT.sol @@ -35,14 +35,27 @@ contract Curator is uint16 public constant CURATION_TYPE_WALLET = 5; uint16 public constant CURATION_TYPE_ZORA_ERC721 = 6; + enum AccessRoles { + noAccess + curator, + manager, + admin + } + + AccessRoles public accessRoles; + /// @notice Reference to factory contract ICuratorFactory private immutable curatorFactory; /// @notice Modifier that ensures curation functionality is active and not frozen modifier onlyActive() { - if (isPaused && msg.sender != owner()) { + if (isPaused + && (IAccessControlRegistry(accessControl).getAccessLevel(address(this), msg.sender) < accessRoles.manager + && msg.sender != owner()) + ) { revert CURATION_PAUSED(); - } + } + if (frozenAt != 0 && frozenAt < block.timestamp) { revert CURATION_FROZEN(); @@ -51,10 +64,24 @@ contract Curator is _; } - /// @notice Modifier that restricts entry access to an admin or curator + /// @notice Modifier that restricts entry access to an owner or admin + modifier onlyOwnerOrAdminAccess() { + if (IAccessControlRegistry(accessControl).getAccessLevel(address(this), msg.sender) < accessRoles.admin + && owner() != msg.sender + ) { + revert ACCESS_NOT_ALLOWED(); + } + + _; + } + + /// @notice Modifier that restricts entry access to an manager/admin or curator /// @param listingId to check access for - modifier onlyCuratorOrAdmin(uint256 listingId) { - if (owner() != msg.sender && idToListing[listingId].curator != msg.sender) { + modifier onlyCuratorOrManagerAccess(uint256 listingId) { + if ( + IAccessControlRegistry(accessControl).getAccessLevel(address(this), msg.sender) < accessRoles.manager + && idToListing[listingId].curator != msg.sender + ) { revert ACCESS_NOT_ALLOWED(); } @@ -67,37 +94,38 @@ contract Curator is curatorFactory = ICuratorFactory(_curatorFactory); } - /// @dev Create a new curation contract /// @param _owner User that owns and can accesss contract admin functionality /// @param _name Contract name /// @param _symbol Contract symbol - /// @param _curationPass ERC721 contract whose ownership gates access to curation functionality /// @param _pause Sets curation active state upon initialization /// @param _curationLimit Sets cap for number of listings that can be curated at any time. Doubles as MaxSupply check. 0 = uncapped /// @param _renderer Renderer contract to use /// @param _rendererInitializer Bytes encoded string to pass into renderer. Leave blank if using SVGMetadataRenderer + /// @param _accessControl access control contract to use + /// @param _accessControlInitializer Bytes encoded data to pass into accessControl. Leave blank if ??? /// @param _initialListings Array of Listing structs to curate (aka mint) upon initialization function initialize( address _owner, string memory _name, string memory _symbol, - address _curationPass, bool _pause, uint256 _curationLimit, address _renderer, bytes memory _rendererInitializer, - Listing[] memory _initialListings + address _accessControl, + bytes memory _accessControlInitializer, + Listing[] memory _initialListings ) external initializer { // Setup owner role __Ownable_init(_owner); // Setup contract name + symbol contractName = _name; contractSymbol = _symbol; - // Setup curation pass. MUST be set to a valid ERC721 address - curationPass = IERC721Upgradeable(_curationPass); // Setup metadata renderer _updateRenderer(IMetadataRenderer(_renderer), _rendererInitializer); + // Setup accessControl + _updateAccessControl(IAccessControlRegistry(_accessControl), _accessControlInitializer); // Setup initial curation active state if (_pause) { _setCurationPaused(_pause); @@ -147,7 +175,7 @@ contract Curator is /// @dev Allows contract owner to update curation limit /// @param newLimit new curationLimit to assign - function updateCurationLimit(uint256 newLimit) external onlyOwner { + function updateCurationLimit(uint256 newLimit) external onlyOwnerOrAdminAccess { _updateCurationLimit(newLimit); } @@ -163,7 +191,7 @@ contract Curator is /// @dev Allows contract owner to freeze all contract functionality starting from a given Unix timestamp /// @param timestamp unix timestamp in seconds - function freezeAt(uint256 timestamp) external onlyOwner { + function freezeAt(uint256 timestamp) external onlyOwnerOrAdminAccess { // Prevents owner from adjusting freezeAt time if contract alrady frozen if (frozenAt != 0 && frozenAt < block.timestamp) { @@ -176,7 +204,7 @@ contract Curator is /// @dev Allows contract owner to update renderer address and pass in an optional initializer for the new renderer /// @param _newRenderer address of new renderer /// @param _rendererInitializer bytes encoded string value passed into new renderer - function updateRenderer(address _newRenderer, bytes memory _rendererInitializer) external onlyOwner { + function updateRenderer(address _newRenderer, bytes memory _rendererInitializer) external onlyOwnerOrAdminAccess { _updateRenderer(IMetadataRenderer(_newRenderer), _rendererInitializer); } @@ -190,17 +218,26 @@ contract Curator is emit SetRenderer(address(renderer)); } - /// @dev Allows contract owner to update the ERC721 Curation Pass being used to restrict access to curation functionality - /// @param _curationPass address of new ERC721 Curation Pass - function updateCurationPass(IERC721Upgradeable _curationPass) public onlyOwner { - curationPass = _curationPass; + /// @dev Allows contract owner to update accessControl address and pass in an optional initializer for the new accessControl + /// @param _newAccessControl address of new accessControl + /// @param _accessControlInitializer bytes encoded data passed into new accessControl + function updateAccessControl(address _newAccessControl, bytes memory _accessControlInitializer) external onlyOwnerOrAdminAccess { + _updateAccessControl(IAccessControlRegistry(_newAccessControl), _accessControlInitializer); + } + + function _updateAccessControl(IAccessControlRegistry _newAccessControl, bytes memory _accessControlInitializer) internal { + accessControl = _newAccessControl; - emit CurationPassUpdated(msg.sender, address(_curationPass)); + // If data provided, call initalize to new renderer replacement. + if (_accessControlInitializer.length > 0) { + accessControl.initializeWithData(_accessControlInitializer); + } + emit SetAccessControl(address(accessControl)); } /// @dev Allows contract owner to update the ERC721 Curation Pass being used to restrict access to curation functionality /// @param _setPaused boolean of new curation active state - function setCurationPaused(bool _setPaused) public onlyOwner { + function setCurationPaused(bool _setPaused) public onlyOwnerOrAdminAccess { // Prevents owner from updating the curation active state to the current active state if (isPaused == _setPaused) { @@ -226,25 +263,12 @@ contract Curator is /// @dev Allows owner or curator to curate Listings --> which mints a listingRecord token to the msg.sender /// @param listings array of Listing structs - function addListings(Listing[] memory listings) external onlyActive { - - // Access control for non owners to acess addListings functionality - if (msg.sender != owner()) { + function addListings(Listing[] memory listings) external onlyActive { - // ensures that curationPass is a valid ERC721 address - if (address(curationPass).code.length == 0) { - revert PASS_REQUIRED(); - } - - // checks if non-owner msg.sender owns the Curation Pass - try curationPass.balanceOf(msg.sender) returns (uint256 count) { - if (count == 0) { - revert PASS_REQUIRED(); - } - } catch { - revert PASS_REQUIRED(); - } - } + // Access control to prevent non curators/manager/admins from accessing + if (IAccessControlRegistry(accessControl).getAccessLevel(address(this), msg.sender) < accessRoles.curator ) { + revert ACCESS_NOT_ALLOWED(); + } _addListings(listings, msg.sender); } @@ -283,7 +307,7 @@ contract Curator is } // prevents non-owners from updating the SortOrder on a listingRecord they did not curate themselves - function _setSortOrder(uint256 listingId, int32 sortOrder) internal onlyCuratorOrAdmin(listingId) { + function _setSortOrder(uint256 listingId, int32 sortOrder) internal onlyCuratorOrManagerAccess(listingId) { idToListing[listingId].sortOrder = sortOrder; } @@ -303,7 +327,6 @@ contract Curator is _burnTokenWithChecks(listingId); } - /// @dev allows owner or curators to burn specfic listingRecord NFTs which also removes them from the listings mapping /// @param listingIds array of listingIds to burn function removeListings(uint256[] calldata listingIds) external onlyActive { @@ -357,7 +380,7 @@ contract Curator is return renderer.contractURI(); } - function _burnTokenWithChecks(uint256 listingId) internal onlyActive onlyCuratorOrAdmin(listingId) { + function _burnTokenWithChecks(uint256 listingId) internal onlyActive onlyCuratorOrManagerAccess(listingId) { Listing memory _listing = idToListing[listingId]; // Process NFT Burn _burn(listingId); diff --git a/src/CuratorFactory.sol b/src/CuratorFactory.sol index cadc307..1f039bf 100644 --- a/src/CuratorFactory.sol +++ b/src/CuratorFactory.sol @@ -14,6 +14,8 @@ import { Curator } from "./Curator.sol"; abstract contract CuratorFactoryStorageV1 { address public defaultMetadataRenderer; + address public defaultAccessControl; + mapping(address => mapping(address => bool)) internal isUpgrade; uint256[50] __gap; @@ -41,25 +43,36 @@ contract CuratorFactory is ICuratorFactory, UUPS, Ownable, CuratorFactoryStorage emit HasNewMetadataRenderer(_renderer); } - function initialize(address _owner, address _defaultMetadataRenderer) external initializer { + function setDefaultAccessControl(address _accessControl) external { + defaultAccessControl = _accessControl; + + emit HasNewAccessControl(_accessControl); + } + + function initialize(address _owner, address _defaultMetadataRenderer, address _defaultAccessControl) external initializer { __Ownable_init(_owner); defaultMetadataRenderer = _defaultMetadataRenderer; + defaultAccessControl = _defaultAccessControl; } function deploy( address curationManager, string memory name, string memory symbol, - address curationPass, bool initialPause, uint256 curationLimit, address renderer, bytes memory rendererInitializer, + address accessControl, + bytes memory accessControlInitializer, ICurator.Listing[] memory listings ) external returns (address curator) { if (renderer == address(0)) { renderer = defaultMetadataRenderer; } + if (accessControl == address(0)) { + accessControl = defaultAccessControl; + } curator = address( new ERC1967Proxy( @@ -69,11 +82,12 @@ contract CuratorFactory is ICuratorFactory, UUPS, Ownable, CuratorFactoryStorage curationManager, name, symbol, - curationPass, initialPause, curationLimit, renderer, rendererInitializer, + accessControl, + accessControlInitializer, listings ) ) diff --git a/src/CuratorStorageV1.sol b/src/CuratorStorageV1.sol index 212065e..7178a78 100644 --- a/src/CuratorStorageV1.sol +++ b/src/CuratorStorageV1.sol @@ -2,9 +2,8 @@ pragma solidity 0.8.15; import { ICurator } from "./interfaces/ICurator.sol"; -import { IERC721Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; import { IMetadataRenderer } from "./interfaces/IMetadataRenderer.sol"; - +import { IAccessControlRegistry } from "onchain/interfaces/IAccessControlRegistry.sol"; /** @notice Curator storage variables contract. @@ -17,9 +16,8 @@ abstract contract CuratorStorageV1 is ICurator { /// @notice Standard ERC721 symbol for the curator contract string internal contractSymbol; - /// Curation pass as an ERC721 that allows other users to curate. - /// @notice Address to ERC721 with `balanceOf` function. - IERC721Upgradeable public curationPass; + /// @notice Address of the accessControl contract + IAccessControlRegistry public accessControl; /// Stores virtual mapping array length parameters /// @notice Array total size (total size) diff --git a/src/interfaces/ICurator.sol b/src/interfaces/ICurator.sol index 66f65ea..63aadde 100644 --- a/src/interfaces/ICurator.sol +++ b/src/interfaces/ICurator.sol @@ -59,9 +59,8 @@ interface ICurator { /// @notice Emitted when a listing is removed event ListingRemoved(address indexed curator, Listing listing); - /// @notice The curation pass has been updated for the curation contract - /// @dev Any users that have already curated something still can delete their curation. - event CurationPassUpdated(address indexed owner, address curationPass); + /// @notice A new accessControl is set + event SetAccessControl(address); /// @notice A new renderer is set event SetRenderer(address); @@ -78,12 +77,6 @@ interface ICurator { /// @notice This contract is scheduled to be frozen event ScheduledFreeze(uint256 timestamp); - /// @notice Pass is required to manage curation but not held by attempted updater. - error PASS_REQUIRED(); - - /// @notice Only the curator of a listing (or owner) can manage that curation - error ONLY_CURATOR(); - /// @notice Wrong curator for the listing when attempting to access the listing. error WRONG_CURATOR_FOR_LISTING(address setCurator, address expectedCurator); @@ -115,11 +108,12 @@ interface ICurator { address _owner, string memory _name, string memory _symbol, - address _curationPass, bool _pause, uint256 _curationLimit, address _renderer, bytes memory _rendererInitializer, + address _accessControl, + bytes memory _accessControlInitializer, Listing[] memory _initialListings ) external; } diff --git a/src/interfaces/ICuratorFactory.sol b/src/interfaces/ICuratorFactory.sol index 36cb0cf..2460559 100644 --- a/src/interfaces/ICuratorFactory.sol +++ b/src/interfaces/ICuratorFactory.sol @@ -12,6 +12,8 @@ interface ICuratorFactory { event RegisteredUpgradePath(address implFrom, address implTo); /// @notice Emitted when a new metadata renderer is set event HasNewMetadataRenderer(address); + /// @notice Emitted when a new accessControl is set + event HasNewAccessControl(address); /// @notice Getter to determine if a contract upgrade path is valid. function isValidUpgrade(address baseImpl, address newImpl) external view returns (bool); diff --git a/src/interfaces/ICuratorInfo.sol b/src/interfaces/ICuratorInfo.sol index a4e5a94..fe9c7a9 100644 --- a/src/interfaces/ICuratorInfo.sol +++ b/src/interfaces/ICuratorInfo.sol @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import { IAccessControlRegistry } from "onchain/interfaces/IAccessControlRegistry.sol"; interface ICuratorInfo { function name() external view returns (string memory); - function curationPass() external view returns (IERC721Metadata); + function accessControl() external view returns (IAccessControlRegistry); function owner() external view returns (address); } diff --git a/src/renderer/SVGMetadataRenderer.sol b/src/renderer/SVGMetadataRenderer.sol index 2a02053..ff659ba 100644 --- a/src/renderer/SVGMetadataRenderer.sol +++ b/src/renderer/SVGMetadataRenderer.sol @@ -4,7 +4,7 @@ pragma solidity 0.8.15; import { Strings } from "@openzeppelin/contracts/utils/Strings.sol"; import { IMetadataRenderer } from "../interfaces/IMetadataRenderer.sol"; -import { ICuratorInfo, IERC721Metadata } from "../interfaces/ICuratorInfo.sol"; +import { ICuratorInfo } from "../interfaces/ICuratorInfo.sol"; import { IZoraDrop } from "../interfaces/IZoraDrop.sol"; import { ICurator } from "../interfaces/ICurator.sol"; @@ -119,9 +119,9 @@ contract SVGMetadataRenderer is IMetadataRenderer { ICuratorInfo curation = ICuratorInfo(msg.sender); MetadataBuilder.JSONItem[] memory items = new MetadataBuilder.JSONItem[](3); - string memory curationName = "Untitled NFT"; + string memory curationName = "Untitled Access Control"; - try curation.curationPass().name() returns (string memory result) { + try curation.accessControl().name() returns (string memory result) { curationName = result; } catch {} @@ -137,10 +137,7 @@ contract SVGMetadataRenderer is IMetadataRenderer { "The curation pass for this NFT is ", curationName, "\\n\\nThese NFTs only mark curations and are non-transferrable." - "\\n\\nView or manage this curation at: " - "https://public---assembly.com/curation/", - Strings.toHexString(msg.sender), - "\\n\\nA project of public assembly." + "\\n\\nA project of Public Assembly." ); items[1].quote = true; items[2].key = MetadataJSONKeys.keyImage; @@ -219,9 +216,7 @@ contract SVGMetadataRenderer is IMetadataRenderer { Strings.toHexString(listing.curator), "\\n\\nTo remove this curation, burn the NFT. " "\\n\\nThis NFT is non-transferrable. " - "\\n\\nView or manage this curation at: " - "https://public---assembly.com/curation/", - Strings.toHexString(msg.sender) + "\\n\\nA project of Public Assembly." ); items[1].quote = true; items[2].key = MetadataJSONKeys.keyImage;