Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/onchain
Submodule onchain added at 252d72
3 changes: 2 additions & 1 deletion remappings.txt
Original file line number Diff line number Diff line change
@@ -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/
@openzeppelin/contracts/=node_modules/@openzeppelin/contracts/
onchain/=lib/onchain/tokenized-access-control/src
107 changes: 65 additions & 42 deletions src/Curator.sol
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// 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";
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
Expand All @@ -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();
Expand All @@ -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();
}

Expand All @@ -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);
Expand Down Expand Up @@ -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);
}

Expand All @@ -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) {
Expand All @@ -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);
}

Expand All @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down
20 changes: 17 additions & 3 deletions src/CuratorFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -69,11 +82,12 @@ contract CuratorFactory is ICuratorFactory, UUPS, Ownable, CuratorFactoryStorage
curationManager,
name,
symbol,
curationPass,
initialPause,
curationLimit,
renderer,
rendererInitializer,
accessControl,
accessControlInitializer,
listings
)
)
Expand Down
8 changes: 3 additions & 5 deletions src/CuratorStorageV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
14 changes: 4 additions & 10 deletions src/interfaces/ICurator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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;
}
2 changes: 2 additions & 0 deletions src/interfaces/ICuratorFactory.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading