diff --git a/.changeset/modern-plants-matter.md b/.changeset/modern-plants-matter.md new file mode 100644 index 000000000..116ca0135 --- /dev/null +++ b/.changeset/modern-plants-matter.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/zora-1155-contracts": patch +--- + +Add first minter payouts as chain sponsor diff --git a/.github/workflows/changesets-prerelease.yml b/.github/workflows/changesets-prerelease.yml index 4651b2f66..1cf414a2b 100644 --- a/.github/workflows/changesets-prerelease.yml +++ b/.github/workflows/changesets-prerelease.yml @@ -25,19 +25,8 @@ jobs: if: steps.check_pre.outputs.files_exists != 'true' run: echo "pre.json does not exist, enter prerelease mode with 'yarn changeset pre enter {prereleaseName}'"; exit 1 - - name: Install Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: "yarn" - - - name: Install project dependencies - run: yarn - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly + - name: Install node deps and founry + uses: ./.github/actions/setup_deps - name: Create Release Pull Request or Publish to npm id: changesets diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 5269b991c..5a124f7a3 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -15,19 +15,8 @@ jobs: - name: Checkout Repo uses: actions/checkout@v3 - - name: Install Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: "yarn" - - - name: Install project dependencies - run: yarn - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly + - name: Install node deps and founry + uses: ./.github/actions/setup_deps - name: Create Release Pull Request or Publish to npm id: changesets diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index adfe70c51..ca67d962c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -17,19 +17,8 @@ jobs: with: submodules: recursive - - name: Install Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: "yarn" - - - name: Install project dependencies - run: yarn - - - name: Install Foundry - uses: foundry-rs/foundry-toolchain@v1 - with: - version: nightly + - name: Install node deps and founry + uses: ./.github/actions/setup_deps - name: Run Forge coverage run: yarn run coverage diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4fd3219d4..4ce454415 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -6,14 +6,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - name: Use Node.js - uses: actions/setup-node@v3 - with: - node-version: 18 - cache: "yarn" - - name: Install project dependencies - run: yarn + - name: Install node deps and founry + uses: ./.github/actions/setup_deps - name: Run prettier run: yarn run prettier:check diff --git a/foundry.toml b/foundry.toml index ac19e9648..53b2757a6 100644 --- a/foundry.toml +++ b/foundry.toml @@ -20,6 +20,7 @@ via_ir = true [profile.fast_compilation] optimizer_runs = 50 +via_ir = false solc_version = '0.8.17' [rpc_endpoints] diff --git a/package/batchPublish.test.ts b/package/batchPublish.test.ts index f63a4c03b..2820e12c4 100644 --- a/package/batchPublish.test.ts +++ b/package/batchPublish.test.ts @@ -280,7 +280,7 @@ describe("ZoraCreator1155Preminter", () => { [collectorAccount] ); - const zoraMintFee = parseEther("0.0007777"); + const zoraMintFee = parseEther("0.000777"); const valueToSend = (BigInt(zoraMintFee) + createToken3Params.price) * quantityToMint; diff --git a/src/delegation/ZoraCreator1155PremintExecutorImpl.sol b/src/delegation/ZoraCreator1155PremintExecutorImpl.sol index 19e9f60a4..37069483c 100644 --- a/src/delegation/ZoraCreator1155PremintExecutorImpl.sol +++ b/src/delegation/ZoraCreator1155PremintExecutorImpl.sol @@ -70,7 +70,7 @@ contract ZoraCreator1155PremintExecutorImpl is Ownable2StepUpgradeable, UUPSUpgr // pass the signature and the premint config to the token contract to create the token. // The token contract will verify the signature and that the signer has permission to create a new token. // and then create and setup the token using the given token config. - newTokenId = tokenContract.delegateSetupNewToken(premintConfig, signature); + newTokenId = tokenContract.delegateSetupNewToken(premintConfig, signature, msg.sender); // if the executor would also like to mint: if (quantityToMint != 0) { diff --git a/src/interfaces/IZoraCreator1155.sol b/src/interfaces/IZoraCreator1155.sol index 1488def9c..cf00e8702 100644 --- a/src/interfaces/IZoraCreator1155.sol +++ b/src/interfaces/IZoraCreator1155.sol @@ -82,7 +82,7 @@ interface IZoraCreator1155 is IZoraCreator1155TypesV1, IZoraCreator1155Errors, I /// @param maxSupply maxSupply for the token, set to 0 for open edition function setupNewToken(string memory tokenURI, uint256 maxSupply) external returns (uint256 tokenId); - function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature) external returns (uint256 newTokenId); + function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature, address sender) external returns (uint256 newTokenId); function updateTokenURI(uint256 tokenId, string memory _newURI) external; diff --git a/src/nft/ZoraCreator1155Impl.sol b/src/nft/ZoraCreator1155Impl.sol index bcaae49ad..b2d83005b 100644 --- a/src/nft/ZoraCreator1155Impl.sol +++ b/src/nft/ZoraCreator1155Impl.sol @@ -371,13 +371,6 @@ contract ZoraCreator1155Impl is uint256 quantity, bytes memory data ) external nonReentrant onlyAdminOrRole(tokenId, PERMISSION_BIT_MINTER) { - // If this is the token's first mint: - if (firstMinters[tokenId] == address(0)) { - // Store the recipient address as the first minter - // Note: If the recipient is address(0) the tx will revert in the `_mint` call below - firstMinters[tokenId] = recipient; - } - // Mint the specified tokens _mint(recipient, tokenId, quantity, data); } @@ -407,9 +400,6 @@ contract ZoraCreator1155Impl is // Require admin from the minter to mint _requireAdminOrRole(address(minter), tokenId, PERMISSION_BIT_MINTER); - // Get the token's first minter - address firstMinter = _handleFirstMinter(tokenId, minterArguments); - // Get value sent and handle mint fee uint256 ethValueSent = _handleRewardsAndGetValueSent( msg.value, @@ -417,7 +407,7 @@ contract ZoraCreator1155Impl is getCreatorRewardRecipient(), createReferrals[tokenId], address(0), - firstMinter + firstMinters[tokenId] ); // Execute commands returned from minter @@ -442,9 +432,6 @@ contract ZoraCreator1155Impl is // Require admin from the minter to mint _requireAdminOrRole(address(minter), tokenId, PERMISSION_BIT_MINTER); - // Get the token's first minter - address firstMinter = _handleFirstMinter(tokenId, minterArguments); - // Get value sent and handle mint rewards uint256 ethValueSent = _handleRewardsAndGetValueSent( msg.value, @@ -452,7 +439,7 @@ contract ZoraCreator1155Impl is getCreatorRewardRecipient(), createReferrals[tokenId], mintReferral, - firstMinter + firstMinters[tokenId] ); // Execute commands returned from minter @@ -461,23 +448,6 @@ contract ZoraCreator1155Impl is emit Purchased(msg.sender, address(minter), tokenId, quantity, msg.value); } - /// @dev Get and/or set the first minter a token - function _handleFirstMinter(uint256 tokenId, bytes calldata data) internal returns (address) { - // If this is the first mint for the token: - if (firstMinters[tokenId] == address(0)) { - // Decode the address of the reward recipient - // Assume the first argument is an address - address rewardRecipient = abi.decode(data, (address)); - - // Store the address to lookup for future mints - firstMinters[tokenId] = rewardRecipient; - - return rewardRecipient; - } - - return firstMinters[tokenId]; - } - function mintFee() external pure returns (uint256) { return TOTAL_REWARD_PER_MINT; } @@ -792,7 +762,11 @@ contract ZoraCreator1155Impl is /// The signature must be created by an account with the PERMISSION_BIT_MINTER role on the contract. /// @param premintConfig configuration of token to be created /// @param signature EIP-712 Signature created on the premintConfig by an account with the PERMISSION_BIT_MINTER role on the contract. - function delegateSetupNewToken(PremintConfig calldata premintConfig, bytes calldata signature) public nonReentrant returns (uint256 newTokenId) { + function delegateSetupNewToken( + PremintConfig calldata premintConfig, + bytes calldata signature, + address sender + ) public nonReentrant returns (uint256 newTokenId) { // if a token has already been created for a premint config with this uid: if (delegatedTokenId[premintConfig.uid] != 0) { // return its token id @@ -817,6 +791,8 @@ contract ZoraCreator1155Impl is delegatedTokenId[premintConfig.uid] = newTokenId; + firstMinters[newTokenId] = sender; + // invoke setup actions for new token, to save contract size, first get them from an external lib bytes[] memory tokenSetupActions = PremintTokenSetup.makeSetupNewTokenCalls(newTokenId, creator, premintConfig.tokenConfig); diff --git a/test/nft/ZoraCreator1155.t.sol b/test/nft/ZoraCreator1155.t.sol index 5e2c7cc89..b951594f9 100644 --- a/test/nft/ZoraCreator1155.t.sol +++ b/test/nft/ZoraCreator1155.t.sol @@ -11,6 +11,8 @@ import {ITransferHookReceiver} from "../../src/interfaces/ITransferHookReceiver. import {Zora1155} from "../../src/proxies/Zora1155.sol"; import {ZoraCreatorFixedPriceSaleStrategy} from "../../src/minters/fixed-price/ZoraCreatorFixedPriceSaleStrategy.sol"; import {UpgradeGate} from "../../src/upgrades/UpgradeGate.sol"; +import {PremintConfig, TokenCreationConfig} from "../../src/delegation/ZoraCreator1155Attribution.sol"; +import {ZoraCreator1155Attribution} from "../../src/delegation/ZoraCreator1155Attribution.sol"; import {IZoraCreator1155Errors} from "../../src/interfaces/IZoraCreator1155Errors.sol"; import {IZoraCreator1155} from "../../src/interfaces/IZoraCreator1155.sol"; @@ -26,17 +28,25 @@ import {SimpleRenderer} from "../mock/SimpleRenderer.sol"; contract MockTransferHookReceiver is ITransferHookReceiver { mapping(uint256 => bool) public hasTransfer; - function onTokenTransferBatch(address, address, address, address, uint256[] memory ids, uint256[] memory, bytes memory) external { + function onTokenTransferBatch( + address target, + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) external { for (uint256 i = 0; i < ids.length; i++) { hasTransfer[ids[i]] = true; } } - function onTokenTransfer(address, address, address, address, uint256 id, uint256, bytes memory) external { + function onTokenTransfer(address target, address operator, address from, address to, uint256 id, uint256 amount, bytes memory data) external { hasTransfer[id] = true; } - function supportsInterface(bytes4 testInterface) external pure override returns (bool) { + function supportsInterface(bytes4 testInterface) external view override returns (bool) { return testInterface == type(ITransferHookReceiver).interfaceId; } } @@ -53,6 +63,7 @@ contract ZoraCreator1155Test is Test { UpgradeGate internal upgradeGate; address payable internal admin; + uint256 internal adminKey; address internal recipient; uint256 internal adminRole; uint256 internal minterRole; @@ -74,7 +85,9 @@ contract ZoraCreator1155Test is Test { createReferral = makeAddr("createReferral"); zora = makeAddr("zora"); - admin = payable(vm.addr(0x1)); + address adminAddress; + (adminAddress, adminKey) = makeAddrAndKey("admin"); + admin = payable(adminAddress); recipient = vm.addr(0x2); protocolRewards = new ProtocolRewards(); @@ -739,8 +752,8 @@ contract ZoraCreator1155Test is Test { (, , address fundsRecipient, , , ) = target.config(); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); - assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward); + assertEq(protocolRewards.balanceOf(recipient), 0); + assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward + settings.firstMinterReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward + settings.createReferralReward); } @@ -765,8 +778,8 @@ contract ZoraCreator1155Test is Test { (, , address fundsRecipient, , , ) = target.config(); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); - assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward); + assertEq(protocolRewards.balanceOf(recipient), 0); + assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward + settings.firstMinterReward); assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward); } @@ -792,8 +805,8 @@ contract ZoraCreator1155Test is Test { (, , address fundsRecipient, , , ) = target.config(); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); - assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward); + assertEq(protocolRewards.balanceOf(recipient), 0); + assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward + settings.firstMinterReward); assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.createReferralReward); } @@ -819,8 +832,8 @@ contract ZoraCreator1155Test is Test { (, , address fundsRecipient, , , ) = target.config(); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); - assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward); + assertEq(protocolRewards.balanceOf(recipient), 0); + assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward + settings.firstMinterReward); assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward); @@ -883,7 +896,7 @@ contract ZoraCreator1155Test is Test { assertEq(address(target).balance, totalSale); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(admin), settings.firstMinterReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward + settings.createReferralReward); } @@ -929,7 +942,7 @@ contract ZoraCreator1155Test is Test { assertEq(address(target).balance, totalSale); assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(admin), settings.firstMinterReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.createReferralReward); } @@ -973,7 +986,7 @@ contract ZoraCreator1155Test is Test { target.mintWithRewards{value: totalValue}(fixedPriceMinter, tokenId, quantity, abi.encode(recipient), address(0)); assertEq(address(target).balance, totalSale); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(admin), settings.firstMinterReward); assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward); } @@ -1018,7 +1031,7 @@ contract ZoraCreator1155Test is Test { target.mintWithRewards{value: totalValue}(fixedPriceMinter, tokenId, quantity, abi.encode(recipient), mintReferral); assertEq(address(target).balance, totalSale); - assertEq(protocolRewards.balanceOf(recipient), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(admin), settings.firstMinterReward); assertEq(protocolRewards.balanceOf(mintReferral), settings.mintReferralReward); assertEq(protocolRewards.balanceOf(createReferral), settings.createReferralReward); assertEq(protocolRewards.balanceOf(zora), settings.zoraReward); @@ -1057,36 +1070,75 @@ contract ZoraCreator1155Test is Test { target.mintWithRewards(fixedPriceMinter, tokenId, quantity, abi.encode(recipient), address(0)); } - function test_FirstMinterRewardReceivedOnConsecutiveMints(uint256 quantity) public { + function test_FirstMinterRewardReceivedOnConsecutiveMints(uint32 quantity) public { vm.assume(quantity > 0 && quantity < type(uint200).max); init(); - vm.prank(admin); - uint256 tokenId = target.setupNewToken("test", quantity * 2); + PremintConfig memory premintConfig = PremintConfig({ + tokenConfig: TokenCreationConfig({ + // Metadata URI for the created token + tokenURI: "", + // Max supply of the created token + maxSupply: type(uint64).max, + // Max tokens that can be minted for an address, 0 if unlimited + maxTokensPerAddress: type(uint64).max, + // Price per token in eth wei. 0 for a free mint. + pricePerToken: 0, + // The start time of the mint, 0 for immediate. Prevents signatures from being used until the start time. + mintStart: 0, + // The duration of the mint, starting from the first mint of this token. 0 for infinite + mintDuration: type(uint64).max - 1, + // RoyaltyMintSchedule for created tokens. Every nth token will go to the royalty recipient. + royaltyMintSchedule: 0, + // RoyaltyBPS for created tokens. The royalty amount in basis points for secondary sales. + royaltyBPS: 0, + // RoyaltyRecipient for created tokens. The address that will receive the royalty payments. + royaltyRecipient: address(0), + // Fixed price minter address + fixedPriceMinter: address(fixedPriceMinter) + }), + // Unique id of the token, used to ensure that multiple signatures can't be used to create the same intended token. + // only one signature per token id, scoped to the contract hash can be executed. + uid: 1, + // Version of this premint, scoped to the uid and contract. Not used for logic in the contract, but used externally to track the newest version + version: 1, + // If executing this signature results in preventing any signature with this uid from being minted. + deleted: false + }); + + address[] memory collectors = new address[](3); + collectors[0] = makeAddr("firstMinter"); + collectors[1] = makeAddr("collector1"); + collectors[2] = makeAddr("collector2"); + + uint256 chainId; + assembly { + chainId := chainid() + } - vm.prank(admin); - target.addPermission(tokenId, address(simpleMinter), adminRole); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(adminKey, ZoraCreator1155Attribution.premintHashedTypeDataV4(premintConfig, address(target), chainId)); - RewardsSettings memory settings = target.computeFreeMintRewards(quantity); + bytes memory signature = abi.encodePacked(r, s, v); - uint256 totalReward = target.computeTotalReward(quantity); - vm.deal(collector, totalReward); + vm.prank(collector); + uint256 tokenId = target.delegateSetupNewToken(premintConfig, signature, collectors[0]); - address firstMinter = makeAddr("firstMinter"); + RewardsSettings memory settings = target.computeFreeMintRewards(quantity); - vm.prank(collector); - target.mintWithRewards{value: totalReward}(simpleMinter, tokenId, quantity, abi.encode(firstMinter), address(0)); + uint256 totalReward = target.computeTotalReward(quantity); + vm.deal(collectors[1], totalReward); + vm.prank(collectors[1]); + target.mintWithRewards{value: totalReward}(fixedPriceMinter, tokenId, quantity, abi.encode(collectors[1]), address(0)); - assertEq(protocolRewards.balanceOf(firstMinter), settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(collectors[0]), settings.firstMinterReward); - address collector2 = makeAddr("collector2"); - vm.deal(collector2, totalReward); + vm.deal(collectors[2], totalReward); - vm.prank(collector2); - target.mintWithRewards{value: totalReward}(simpleMinter, tokenId, quantity, abi.encode(collector2), address(0)); + vm.prank(collectors[2]); + target.mintWithRewards{value: totalReward}(fixedPriceMinter, tokenId, quantity, abi.encode(collectors[2]), address(0)); - assertEq(protocolRewards.balanceOf(firstMinter), settings.firstMinterReward * 2); + assertEq(protocolRewards.balanceOf(collectors[0]), settings.firstMinterReward * 2); } function test_AssumeFirstMinterRecipientIsAddress(uint256 quantity) public { @@ -1105,16 +1157,19 @@ contract ZoraCreator1155Test is Test { uint256 totalReward = target.computeTotalReward(quantity); vm.deal(collector, totalReward); - uint256 rewardRecipient = 1234; + uint256 mintRecipient = 1234; + + address rewardRecipient = makeAddr("rewardRecipient"); vm.prank(collector); - target.mintWithRewards{value: totalReward}(simpleMinter, tokenId, quantity, abi.encode(rewardRecipient), address(0)); + target.mintWithRewards{value: totalReward}(simpleMinter, tokenId, quantity, abi.encode(mintRecipient), rewardRecipient); (, , address fundsRecipient, , , ) = target.config(); - assertEq(protocolRewards.balanceOf(address(uint160(rewardRecipient))), settings.firstMinterReward); - assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward); - assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.mintReferralReward + settings.createReferralReward); + assertEq(protocolRewards.balanceOf(address(uint160(mintRecipient))), 0); + assertEq(protocolRewards.balanceOf(rewardRecipient), settings.mintReferralReward); + assertEq(protocolRewards.balanceOf(fundsRecipient), settings.creatorReward + settings.firstMinterReward); + assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.createReferralReward); } function testRevert_WrongValueForSale(uint256 quantity, uint256 salePrice) public { diff --git a/test/premint/ZoraCreator1155PremintExecutor.t.sol b/test/premint/ZoraCreator1155PremintExecutor.t.sol index aeba75665..d70bd3bae 100644 --- a/test/premint/ZoraCreator1155PremintExecutor.t.sol +++ b/test/premint/ZoraCreator1155PremintExecutor.t.sol @@ -178,7 +178,7 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { // get the created contract, and make sure that tokens have been minted to the address assertEq(created1155Contract.balanceOf(premintExecutor, tokenId), 0); - assertEq(ZoraCreator1155Impl(contractAddress).firstMinters(tokenId), address(0)); + assertEq(ZoraCreator1155Impl(contractAddress).firstMinters(tokenId), address(premintExecutor)); } event CreatorAttribution(bytes32 structHash, string domainName, string version, address creator, bytes signature);