Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first pass at new editions contract #80

Merged
merged 34 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
367d273
first pass at new editions contract
campionfellin Feb 8, 2024
21dbbbf
update tests
campionfellin Feb 9, 2024
c3b46eb
more refactor and tests
campionfellin Feb 9, 2024
1b79791
fix: full coverage
campionfellin Feb 9, 2024
7db09e8
fix: enforce max supply more than 0
campionfellin Feb 12, 2024
28382de
unchecked i++ for gas savings
campionfellin Feb 12, 2024
8c12e9c
fix: key off of creatorCore
campionfellin Feb 12, 2024
f091027
++i instead of i++
campionfellin Feb 12, 2024
0f3dc19
fix: dont instantiate i or j as 0
campionfellin Feb 12, 2024
4aec090
fix: check instance exists when updating tokenURI
campionfellin Feb 12, 2024
e9655c0
feat: getInstance function
campionfellin Feb 12, 2024
7a8f8a4
feat: allow minting to recipients on creation
campionfellin Feb 12, 2024
82cbd3e
fix: make function instanceExists
campionfellin Feb 12, 2024
2c58f0c
fix: helper function for minting
campionfellin Feb 12, 2024
92568f3
fix: ++j
campionfellin Feb 12, 2024
75d2b36
test minting while creating
campionfellin Feb 12, 2024
5033844
tests for instance exists
campionfellin Feb 12, 2024
97af782
fix: remove instance checks function
campionfellin Feb 12, 2024
224f3db
fix: better way of reverting
campionfellin Feb 12, 2024
93e5a26
fix: rename private method
campionfellin Feb 12, 2024
b6977f1
feat: make deploy script and ownership transfer
campionfellin Feb 12, 2024
95914b0
fix: remove initial owner
campionfellin Feb 12, 2024
1909ba6
fix: update createSeries and add more requires
campionfellin Feb 12, 2024
93cda23
more test coverage
campionfellin Feb 12, 2024
e77d612
more tests
campionfellin Feb 12, 2024
42b588e
udpate contracts and tests with new Recipient struct
campionfellin Feb 13, 2024
bc58d19
chore: another test for incorrect supply path
campionfellin Feb 13, 2024
b5642bd
fix: logic in _mintTokens
campionfellin Feb 13, 2024
59e7aa4
refactor to support v3 and fix tests
wwhchung Feb 13, 2024
fa0d89c
add another test
wwhchung Feb 13, 2024
86b7cec
chore: more tests for tokenURI
campionfellin Feb 13, 2024
1736179
hit another toomanyrequested error case
campionfellin Feb 13, 2024
1aef08f
fix: mock and no mock testing
campionfellin Feb 13, 2024
79b5ac6
fix: save instance id for v2 contracts
campionfellin Feb 13, 2024
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
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);
campionfellin marked this conversation as resolved.
Show resolved Hide resolved
} 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
Loading