Skip to content

Commit

Permalink
first pass at new editions contract (#80)
Browse files Browse the repository at this point in the history
* first pass at new editions contract

Signed-off-by: Campion Fellin <[email protected]>

* update tests

Signed-off-by: Campion Fellin <[email protected]>

* more refactor and tests

Signed-off-by: Campion Fellin <[email protected]>

* fix: full coverage

Signed-off-by: Campion Fellin <[email protected]>

* fix: enforce max supply more than 0

Signed-off-by: Campion Fellin <[email protected]>

* unchecked i++ for gas savings

Signed-off-by: Campion Fellin <[email protected]>

* fix: key off of creatorCore

Signed-off-by: Campion Fellin <[email protected]>

* ++i instead of i++

Signed-off-by: Campion Fellin <[email protected]>

* fix: dont instantiate i or j as 0

Signed-off-by: Campion Fellin <[email protected]>

* fix: check instance exists when updating tokenURI

Signed-off-by: Campion Fellin <[email protected]>

* feat: getInstance function

Signed-off-by: Campion Fellin <[email protected]>

* feat: allow minting to recipients on creation

Signed-off-by: Campion Fellin <[email protected]>

* fix: make function instanceExists

Signed-off-by: Campion Fellin <[email protected]>

* fix: helper function for minting

Signed-off-by: Campion Fellin <[email protected]>

* fix: ++j

Signed-off-by: Campion Fellin <[email protected]>

* test minting while creating

Signed-off-by: Campion Fellin <[email protected]>

* tests for instance exists

Signed-off-by: Campion Fellin <[email protected]>

* fix: remove instance checks function

Signed-off-by: Campion Fellin <[email protected]>

* fix: better way of reverting

Signed-off-by: Campion Fellin <[email protected]>

* fix: rename private method

Signed-off-by: Campion Fellin <[email protected]>

* feat: make deploy script and ownership transfer

Signed-off-by: Campion Fellin <[email protected]>

* fix: remove initial owner

Signed-off-by: Campion Fellin <[email protected]>

* fix: update createSeries and add more requires

Signed-off-by: Campion Fellin <[email protected]>

* more test coverage

Signed-off-by: Campion Fellin <[email protected]>

* more tests

Signed-off-by: Campion Fellin <[email protected]>

* udpate contracts and tests with new Recipient struct

Signed-off-by: Campion Fellin <[email protected]>

* chore: another test for incorrect supply path

Signed-off-by: Campion Fellin <[email protected]>

* fix: logic in _mintTokens

Signed-off-by: Campion Fellin <[email protected]>

* refactor to support v3 and fix tests

* add another test

* chore: more tests for tokenURI

Signed-off-by: Campion Fellin <[email protected]>

* hit another toomanyrequested error case

Signed-off-by: Campion Fellin <[email protected]>

* fix: mock and no mock testing

Signed-off-by: Campion Fellin <[email protected]>

* fix: save instance id for v2 contracts

Signed-off-by: Campion Fellin <[email protected]>

---------

Signed-off-by: Campion Fellin <[email protected]>
Co-authored-by: Wilkins Chung <[email protected]>
  • Loading branch information
campionfellin and wwhchung authored Mar 11, 2024
1 parent 87e46f4 commit 66b794e
Show file tree
Hide file tree
Showing 4 changed files with 477 additions and 116 deletions.
35 changes: 19 additions & 16 deletions packages/manifold/contracts/edition/IManifoldERC721Edition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,40 +9,43 @@ pragma solidity ^0.8.0;
*/
interface IManifoldERC721Edition {

event SeriesCreated(address caller, address creator, uint256 series, uint256 maxSupply);
error InvalidEdition();
error InvalidInput();
error TooManyRequested();
error InvalidToken();

event SeriesCreated(address caller, address creatorCore, uint256 series, uint256 maxSupply);

struct Recipient {
address recipient;
uint16 count;
}

enum StorageProtocol { INVALID, NONE, ARWEAVE, IPFS }

/**
* @dev Create a new series. Returns the series id.
*/
function createSeries(address creator, uint256 maxSupply, string calldata prefix) external returns(uint256);

/**
* @dev Get the latest series created.
* @dev Create a new series. Returns the series id.
*/
function latestSeries(address creator) external view returns(uint256);
function createSeries(address creatorCore, uint256 instanceId, uint24 maxSupply_, StorageProtocol storageProtocol, string calldata location, Recipient[] memory recipients) external;

/**
* @dev Set the token uri prefix
*/
function setTokenURIPrefix(address creator, uint256 series, string calldata prefix) external;
function setTokenURI(address creatorCore, uint256 instanceId, StorageProtocol storageProtocol, string calldata location) external;

/**
* @dev Mint NFTs to a single recipient
*/
function mint(address creator, uint256 series, address recipient, uint16 count) external;

/**
* @dev Mint NFTS to the recipients
*/
function mint(address creator, uint256 series, address[] calldata recipients) external;
function mint(address creatorCore, uint256 instanceId, uint24 currentSupply, Recipient[] memory recipients) external;

/**
* @dev Total supply of editions
*/
function totalSupply(address creator, uint256 series) external view returns(uint256);
function totalSupply(address creatorCore, uint256 instanceId) external view returns(uint256);

/**
* @dev Max supply of editions
*/
function maxSupply(address creator, uint256 series) external view returns(uint256);
function maxSupply(address creatorCore, uint256 instanceId) external view returns(uint256);
}
226 changes: 163 additions & 63 deletions packages/manifold/contracts/edition/ManifoldERC721Edition.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import "@manifoldxyz/creator-core-solidity/contracts/extensions/ICreatorExtensio
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

import "../libraries/IERC721CreatorCoreVersion.sol";
import "./IManifoldERC721Edition.sol";

/**
Expand All @@ -25,105 +26,201 @@ contract ManifoldERC721Edition is CreatorExtension, ICreatorExtensionTokenURI, I
uint256 count;
}

mapping(address => mapping(uint256 => string)) _tokenPrefix;
mapping(address => mapping(uint256 => uint256)) _maxSupply;
mapping(address => mapping(uint256 => uint256)) _totalSupply;
struct EditionInfo {
uint8 contractVersion;
uint24 totalSupply;
uint24 maxSupply;
StorageProtocol storageProtocol;
string location;
}

string private constant ARWEAVE_PREFIX = "https://arweave.net/";
string private constant IPFS_PREFIX = "ipfs://";

uint256 private constant MAX_UINT_24 = 0xffffff;
uint256 private constant MAX_UINT_56 = 0xffffffffffffff;

mapping(address => mapping(uint256 => EditionInfo)) _editionInfo;
mapping(address => mapping(uint256 => IndexRange[])) _indexRanges;
mapping(address => uint256) _currentSeries;


mapping(address => uint256[]) _creatorInstanceIds;

/**
* @dev Only allows approved admins to call the specified function
*/
modifier creatorAdminRequired(address creator) {
require(IAdminControl(creator).isAdmin(msg.sender), "Must be owner or admin of creator contract");
if (!IAdminControl(creator).isAdmin(msg.sender)) revert("Must be owner or admin of creator contract");
_;
}

function supportsInterface(bytes4 interfaceId) public view virtual override(CreatorExtension, IERC165) returns (bool) {
return interfaceId == type(ICreatorExtensionTokenURI).interfaceId || interfaceId == type(IManifoldERC721Edition).interfaceId ||
CreatorExtension.supportsInterface(interfaceId);
return
interfaceId == type(ICreatorExtensionTokenURI).interfaceId ||
interfaceId == type(IManifoldERC721Edition).interfaceId ||
CreatorExtension.supportsInterface(interfaceId);
}

/**
* @dev See {IManifoldERC721Edition-totalSupply}.
* @dev See {IManifoldERC721Edition-createSeries}.
*/
function totalSupply(address creator, uint256 series) external view override returns(uint256) {
return _totalSupply[creator][series];
function createSeries(address creatorCore, uint256 instanceId, uint24 maxSupply_, StorageProtocol storageProtocol, string calldata location, Recipient[] memory recipients) external override creatorAdminRequired(creatorCore) {
if (instanceId == 0 ||
instanceId > MAX_UINT_56 ||
maxSupply_ == 0 ||
storageProtocol == StorageProtocol.INVALID ||
_editionInfo[creatorCore][instanceId].storageProtocol != StorageProtocol.INVALID
) revert InvalidInput();

uint8 creatorContractVersion;
try IERC721CreatorCoreVersion(creatorCore).VERSION() returns(uint256 version) {
require(version <= 255, "Unsupported contract version");
creatorContractVersion = uint8(version);
} catch {}

_editionInfo[creatorCore][instanceId] = EditionInfo({
maxSupply: maxSupply_,
totalSupply: 0,
contractVersion: creatorContractVersion,
storageProtocol: storageProtocol,
location: location
});

if (creatorContractVersion < 3) {
_creatorInstanceIds[creatorCore].push(instanceId);
}

emit SeriesCreated(msg.sender, creatorCore, instanceId, maxSupply_);

if (recipients.length > 0) _mintTokens(creatorCore, instanceId, _editionInfo[creatorCore][instanceId], recipients);
}


/**
* @dev See {IManifoldERC721Edition-maxSupply}.
* @dev See {IManifoldERC721Edition-totalSupply}.
*/
function maxSupply(address creator, uint256 series) external view override returns(uint256) {
return _maxSupply[creator][series];
function totalSupply(address creatorCore, uint256 instanceId) external view override returns(uint256) {
EditionInfo storage info = _getEditionInfo(creatorCore, instanceId);
return info.totalSupply;
}

/**
* @dev See {IManifoldERC721Edition-createSeries}.
* @dev See {IManifoldERC721Edition-maxSupply}.
*/
function createSeries(address creator, uint256 maxSupply_, string calldata prefix) external override creatorAdminRequired(creator) returns(uint256) {
_currentSeries[creator] += 1;
uint256 series = _currentSeries[creator];
_maxSupply[creator][series] = maxSupply_;
_tokenPrefix[creator][series] = prefix;
emit SeriesCreated(msg.sender, creator, series, maxSupply_);
return series;
function maxSupply(address creatorCore, uint256 instanceId) external view override returns(uint256) {
EditionInfo storage info = _getEditionInfo(creatorCore, instanceId);
return info.maxSupply;
}

/**
* @dev See {IManifoldERC721Edition-latestSeries}.
* See {IManifoldERC721Edition-setTokenURI}.
*/
function latestSeries(address creator) external view override returns(uint256) {
return _currentSeries[creator];
function setTokenURI(address creatorCore, uint256 instanceId, StorageProtocol storageProtocol, string calldata location) external override creatorAdminRequired(creatorCore) {
if (storageProtocol == StorageProtocol.INVALID) revert InvalidInput();
EditionInfo storage info = _getEditionInfo(creatorCore, instanceId);
info.storageProtocol = storageProtocol;
info.location = location;
}

/**
* See {IManifoldERC721Edition-setTokenURIPrefix}.
*/
function setTokenURIPrefix(address creator, uint256 series, string calldata prefix) external override creatorAdminRequired(creator) {
require(series > 0 && series <= _currentSeries[creator], "Invalid series");
_tokenPrefix[creator][series] = prefix;
function _getEditionInfo(address creatorCore, uint256 instanceId) private view returns(EditionInfo storage info) {
info = _editionInfo[creatorCore][instanceId];
if (info.storageProtocol == StorageProtocol.INVALID) revert InvalidEdition();
}

/**
* @dev See {ICreatorExtensionTokenURI-tokenURI}.
*/
function tokenURI(address creator, uint256 tokenId) external view override returns (string memory) {
(uint256 series, uint256 index) = _tokenSeriesAndIndex(creator, tokenId);
return string(abi.encodePacked(_tokenPrefix[creator][series], (index+1).toString()));
function tokenURI(address creatorCore, uint256 tokenId) external view override returns (string memory) {
uint8 creatorContractVersion;
try IERC721CreatorCoreVersion(creatorCore).VERSION() returns(uint256 version) {
require(version <= 255, "Unsupported contract version");
creatorContractVersion = uint8(version);
} catch {}

uint256 instanceId;
uint256 index;
if (creatorContractVersion >= 3) {
// Contract versions 3+ support storage of data with the token mint, so use that
uint80 tokenData = IERC721CreatorCore(creatorCore).tokenData(tokenId);
instanceId = uint56(tokenData >> 24);
if (instanceId == 0) revert InvalidToken();
index = uint256(tokenData & MAX_UINT_24);
} else {
(instanceId, index) = _tokenInstanceAndIndex(creatorCore, tokenId);
}

EditionInfo storage info = _getEditionInfo(creatorCore, instanceId);

string memory prefix = "";
if (info.storageProtocol == StorageProtocol.ARWEAVE) {
prefix = ARWEAVE_PREFIX;
} else if (info.storageProtocol == StorageProtocol.IPFS) {
prefix = IPFS_PREFIX;
}
return string(abi.encodePacked(prefix, info.location, (index+1).toString()));
}

/**
* @dev See {IManifoldERC721Edition-mint}.
*/
function mint(address creator, uint256 series, address recipient, uint16 count) external override nonReentrant creatorAdminRequired(creator) {
require(count > 0, "Invalid amount requested");
require(_totalSupply[creator][series]+count <= _maxSupply[creator][series], "Too many requested");

uint256[] memory tokenIds = IERC721CreatorCore(creator).mintExtensionBatch(recipient, count);
_updateIndexRanges(creator, series, tokenIds[0], count);
function mint(address creatorCore, uint256 instanceId, uint24 currentSupply, Recipient[] memory recipients) external override nonReentrant creatorAdminRequired(creatorCore) {
EditionInfo storage info = _getEditionInfo(creatorCore, instanceId);
if (currentSupply != info.totalSupply) revert InvalidInput();
_mintTokens(creatorCore, instanceId, info, recipients);
}

/**
* @dev See {IManifoldERC721Edition-mint}.
*/
function mint(address creator, uint256 series, address[] calldata recipients) external override nonReentrant creatorAdminRequired(creator) {
require(recipients.length > 0, "Invalid amount requested");
require(_totalSupply[creator][series]+recipients.length <= _maxSupply[creator][series], "Too many requested");

uint256 startIndex = IERC721CreatorCore(creator).mintExtension(recipients[0]);
for (uint256 i = 1; i < recipients.length;) {
IERC721CreatorCore(creator).mintExtension(recipients[i]);
unchecked{i++;}
function _mintTokens(address creatorCore, uint256 instanceId, EditionInfo storage info, Recipient[] memory recipients) private {
if (recipients.length == 0) revert InvalidInput();
if (info.totalSupply+1 > info.maxSupply) revert TooManyRequested();

if (info.contractVersion >= 3) {
uint16 count = 0;
uint24 totalSupply_ = info.totalSupply;
uint24 maxSupply_ = info.maxSupply;
uint256 newMintIndex = totalSupply_;
// Contract versions 3+ support storage of data with the token mint, so use that
// to avoid additional storage costs
for (uint256 i; i < recipients.length;) {
uint16 mintCount = recipients[i].count;
if (mintCount == 0) revert InvalidInput();
count += mintCount;
if (totalSupply_+count > maxSupply_) revert TooManyRequested();
uint80[] memory tokenDatas = new uint80[](mintCount);
for (uint256 j; j < mintCount;) {
tokenDatas[j] = uint56(instanceId) << 24 | uint24(newMintIndex+j);
unchecked { ++j; }
}
// Airdrop the tokens
IERC721CreatorCore(creatorCore).mintExtensionBatch(recipients[i].recipient, tokenDatas);

// Increment newMintIndex for the next airdrop
unchecked{ newMintIndex += mintCount; }

unchecked{ ++i; }
}
info.totalSupply += count;
} else {
uint256 startIndex;
uint16 count = 0;
uint256[] memory tokenIdResults;
uint24 totalSupply_ = info.totalSupply;
uint24 maxSupply_ = info.maxSupply;
for (uint256 i; i < recipients.length;) {
if (recipients[i].count == 0) revert InvalidInput();
count += recipients[i].count;
if (totalSupply_+count > maxSupply_) revert TooManyRequested();
tokenIdResults = IERC721CreatorCore(creatorCore).mintExtensionBatch(recipients[i].recipient, recipients[i].count);
if (i == 0) startIndex = tokenIdResults[0];
unchecked{++i;}
}
_updateIndexRanges(creatorCore, instanceId, info, startIndex, count);
}
_updateIndexRanges(creator, series, startIndex, recipients.length);
}

/**
* @dev Update the index ranges, which is used to figure out the index from a tokenId
*/
function _updateIndexRanges(address creator, uint256 series, uint256 startIndex, uint256 count) internal {
IndexRange[] storage indexRanges = _indexRanges[creator][series];
function _updateIndexRanges(address creatorCore, uint256 instanceId, EditionInfo storage info, uint256 startIndex, uint16 count) internal {
IndexRange[] storage indexRanges = _indexRanges[creatorCore][instanceId];
if (indexRanges.length == 0) {
indexRanges.push(IndexRange(startIndex, count));
} else {
Expand All @@ -134,27 +231,30 @@ contract ManifoldERC721Edition is CreatorExtension, ICreatorExtensionTokenURI, I
indexRanges.push(IndexRange(startIndex, count));
}
}
_totalSupply[creator][series] += count;
info.totalSupply += count;
}

/**
* @dev Index from tokenId
*/
function _tokenSeriesAndIndex(address creator, uint256 tokenId) internal view returns(uint256, uint256) {
require(_currentSeries[creator] > 0, "Invalid token");
for (uint series=1; series <= _currentSeries[creator]; series++) {
IndexRange[] memory indexRanges = _indexRanges[creator][series];
function _tokenInstanceAndIndex(address creatorCore, uint256 tokenId) internal view returns(uint256, uint256) {
// Go through all their series until we find the tokenId
for (uint256 i; i < _creatorInstanceIds[creatorCore].length;) {
uint256 instanceId = _creatorInstanceIds[creatorCore][i];
IndexRange[] memory indexRanges = _indexRanges[creatorCore][instanceId];
uint256 offset;
for (uint i = 0; i < indexRanges.length; i++) {
IndexRange memory currentIndex = indexRanges[i];
for (uint j; j < indexRanges.length;) {
IndexRange memory currentIndex = indexRanges[j];
if (tokenId < currentIndex.startIndex) break;
if (tokenId >= currentIndex.startIndex && tokenId < currentIndex.startIndex + currentIndex.count) {
return (series, tokenId - currentIndex.startIndex + offset);
return (instanceId, tokenId - currentIndex.startIndex + offset);
}
offset += currentIndex.count;
unchecked{++j;}
}
unchecked{++i;}
}
revert("Invalid token");
revert InvalidToken();
}

}
5 changes: 4 additions & 1 deletion packages/manifold/script/ManifoldERC721Edition.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import "../contracts/edition/ManifoldERC721Edition.sol";

contract DeployManifoldERC721Edition is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
// uint256 deployerPrivateKey = pk; // uncomment this when testing on goerli
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); // comment this out when testing on goerli
vm.startBroadcast(deployerPrivateKey);
// forge script scripts/ManifoldERC721Edition.s.sol --optimizer-runs 1000 --rpc-url <YOUR_NODE> --broadcast
// forge verify-contract --compiler-version 0.8.17 --optimizer-runs 1000 --chain sepolia <DEPLOYED_ADDRESS> contracts/edition/ManifoldERC721Edition.sol:ManifoldERC721Edition --constructor-args $(cast abi-encode "constructor(address)" "${INITIAL_OWNER}") --watch
new ManifoldERC721Edition{salt: 0x4d616e69666f6c6445524337323145646974696f6e4d616e69666f6c64455243}();
vm.stopBroadcast();
}
Expand Down
Loading

0 comments on commit 66b794e

Please sign in to comment.