From f00bd85007bfba978204c4283a96a9bcd3a5e159 Mon Sep 17 00:00:00 2001 From: Rohan Kulkarni Date: Wed, 25 Oct 2023 16:13:23 -0400 Subject: [PATCH] feat: creator reward recipient per token (#278) will deploy and add changeset once we're good on reviews --- .changeset/weak-planets-love.md | 5 + packages/1155-contracts/addresses/1.json | 1 + packages/1155-contracts/addresses/10.json | 1 + packages/1155-contracts/addresses/420.json | 1 + packages/1155-contracts/addresses/5.json | 1 + .../1155-contracts/addresses/7777777.json | 1 + packages/1155-contracts/addresses/8453.json | 1 + packages/1155-contracts/addresses/84531.json | 1 + packages/1155-contracts/addresses/999.json | 7 +- .../script/ZoraDeployerBase.sol | 1 + .../src/nft/ZoraCreator1155Impl.sol | 25 +++- .../factory/ZoraCreator1155Factory_Fork.t.sol | 24 ---- .../test/nft/ZoraCreator1155.t.sol | 133 ++++++++++++++++++ .../ZoraCreator1155PremintExecutor.t.sol | 34 ----- 14 files changed, 169 insertions(+), 67 deletions(-) create mode 100644 .changeset/weak-planets-love.md diff --git a/.changeset/weak-planets-love.md b/.changeset/weak-planets-love.md new file mode 100644 index 000000000..c9283c653 --- /dev/null +++ b/.changeset/weak-planets-love.md @@ -0,0 +1,5 @@ +--- +"@zoralabs/zora-1155-contracts": minor +--- + +Creator reward recipient can now be defined on a token by token basis. This allows for multiple creators to collaborate on a contract and each to receive rewards for the token they created. The royaltyRecipient storage field is now used to determine the creator reward recipient for each token. If that's not set for a token, it falls back to use the contract wide fundsRecipient. diff --git a/packages/1155-contracts/addresses/1.json b/packages/1155-contracts/addresses/1.json index 1266e8c7c..93bdf96b6 100644 --- a/packages/1155-contracts/addresses/1.json +++ b/packages/1155-contracts/addresses/1.json @@ -1,6 +1,7 @@ { "CONTRACT_1155_IMPL": "0x8e90D8cfc0CA66EA143930E4c5F7E31Bf16F722b", "CONTRACT_1155_IMPL_VERSION": "2.0.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", "FACTORY_IMPL": "0x55B53DBE22859d538E3b44DD06C9FAE292409E3c", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", diff --git a/packages/1155-contracts/addresses/10.json b/packages/1155-contracts/addresses/10.json index 9c145a19a..50d580aac 100644 --- a/packages/1155-contracts/addresses/10.json +++ b/packages/1155-contracts/addresses/10.json @@ -1,6 +1,7 @@ { "CONTRACT_1155_IMPL": "0xF3a46845548bE811Ce37e65153563f4a0AaEbe31", "CONTRACT_1155_IMPL_VERSION": "2.0.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", "FACTORY_IMPL": "0xF7e49F97E82cc38ACd82E303F37Fe046f5a190B5", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x3678862f04290E565cCA2EF163BAeb92Bb76790C", diff --git a/packages/1155-contracts/addresses/420.json b/packages/1155-contracts/addresses/420.json index acd6caebf..b0074748a 100644 --- a/packages/1155-contracts/addresses/420.json +++ b/packages/1155-contracts/addresses/420.json @@ -1,6 +1,7 @@ { "CONTRACT_1155_IMPL": "0xAF5A4F6F6640734d7D000321Bb27De40D4Ae91f6", "CONTRACT_1155_IMPL_VERSION": "2.0.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", "FACTORY_IMPL": "0x7B59c0378F540c0356A5DAEF7574255A7C74EC76", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", diff --git a/packages/1155-contracts/addresses/5.json b/packages/1155-contracts/addresses/5.json index 509a8bcfe..b3c402156 100644 --- a/packages/1155-contracts/addresses/5.json +++ b/packages/1155-contracts/addresses/5.json @@ -1,6 +1,7 @@ { "CONTRACT_1155_IMPL": "0x455c9D3188A3Cd94aCDE8E5Ec90cA92FC10805EA", "CONTRACT_1155_IMPL_VERSION": "2.0.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", "FACTORY_IMPL": "0x9074Ae399235d26B56e3aF1331b033366E4FE072", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", diff --git a/packages/1155-contracts/addresses/7777777.json b/packages/1155-contracts/addresses/7777777.json index 0441dfdeb..f4260bda7 100644 --- a/packages/1155-contracts/addresses/7777777.json +++ b/packages/1155-contracts/addresses/7777777.json @@ -1,6 +1,7 @@ { "CONTRACT_1155_IMPL": "0xFc40F9BFE1289F27F89BAEfb4DB97CC2D8eF6a38", "CONTRACT_1155_IMPL_VERSION": "2.0.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", "FACTORY_IMPL": "0xC7598f8eAA1455f5b2B3f206A9af55B2BA248e3E", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", diff --git a/packages/1155-contracts/addresses/8453.json b/packages/1155-contracts/addresses/8453.json index 4bd17fb19..134d8842a 100644 --- a/packages/1155-contracts/addresses/8453.json +++ b/packages/1155-contracts/addresses/8453.json @@ -1,6 +1,7 @@ { "CONTRACT_1155_IMPL": "0x314E552b55DFbDfD4d76623E1D45E5056723998B", "CONTRACT_1155_IMPL_VERSION": "2.0.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", "FACTORY_IMPL": "0xC6899816663891D7493939d74d83cb7f2BBcBB16", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", diff --git a/packages/1155-contracts/addresses/84531.json b/packages/1155-contracts/addresses/84531.json index b7effc731..44b3dbae7 100644 --- a/packages/1155-contracts/addresses/84531.json +++ b/packages/1155-contracts/addresses/84531.json @@ -1,6 +1,7 @@ { "CONTRACT_1155_IMPL": "0xD66B730aA3B4921356Fc56907D22e65CA9F4ff58", "CONTRACT_1155_IMPL_VERSION": "2.0.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", "FACTORY_IMPL": "0xF482C51346f3c77673dc619F243Eb8B09E9A954E", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", diff --git a/packages/1155-contracts/addresses/999.json b/packages/1155-contracts/addresses/999.json index 2dfccad6d..b9a1bad61 100644 --- a/packages/1155-contracts/addresses/999.json +++ b/packages/1155-contracts/addresses/999.json @@ -1,7 +1,8 @@ { - "CONTRACT_1155_IMPL": "0x486709A6BeDBD8476d7bCF73F1ab42579A1A7d78", - "CONTRACT_1155_IMPL_VERSION": "2.0.0", - "FACTORY_IMPL": "0x366A36c10E1C851dcfA7804fB313dEA8E3488335", + "CONTRACT_1155_IMPL": "0xCE00c75B9807A2aA87B2297cA7Dc1C0190137D6F", + "CONTRACT_1155_IMPL_VERSION": "2.1.0", + "UPGRADE_GATE": "0xbC50029836A59A4E5e1Bb8988272F46ebA0F9900", + "FACTORY_IMPL": "0x15ba66e376856F3F6FE53dE9eeAb10dEF10E8C92", "FACTORY_PROXY": "0x777777C338d93e2C7adf08D102d45CA7CC4Ed021", "FIXED_PRICE_SALE_STRATEGY": "0x04E2516A2c207E84a1839755675dfd8eF6302F0a", "MERKLE_MINT_SALE_STRATEGY": "0xf48172CA3B6068B20eE4917Eb27b5472f1f272C7", diff --git a/packages/1155-contracts/script/ZoraDeployerBase.sol b/packages/1155-contracts/script/ZoraDeployerBase.sol index c29760b7c..f50b59852 100644 --- a/packages/1155-contracts/script/ZoraDeployerBase.sol +++ b/packages/1155-contracts/script/ZoraDeployerBase.sol @@ -34,6 +34,7 @@ abstract contract ZoraDeployerBase is ScriptDeploymentConfig, DeterministicDeplo vm.serializeAddress(deploymentJsonKey, FACTORY_IMPL, deployment.factoryImpl); vm.serializeAddress(deploymentJsonKey, PREMINTER_PROXY, deployment.preminterProxy); vm.serializeAddress(deploymentJsonKey, PREMINTER_IMPL, deployment.preminterImpl); + vm.serializeAddress(deploymentJsonKey, UPGRADE_GATE, deployment.upgradeGate); deploymentJson = vm.serializeAddress(deploymentJsonKey, FACTORY_PROXY, deployment.factoryProxy); console2.log(deploymentJson); } diff --git a/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol b/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol index d87c22557..221eff944 100644 --- a/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol +++ b/packages/1155-contracts/src/nft/ZoraCreator1155Impl.sol @@ -405,7 +405,7 @@ contract ZoraCreator1155Impl is uint256 ethValueSent = _handleRewardsAndGetValueSent( msg.value, quantity, - getCreatorRewardRecipient(), + getCreatorRewardRecipient(tokenId), createReferrals[tokenId], address(0), firstMinters[tokenId] @@ -437,7 +437,7 @@ contract ZoraCreator1155Impl is uint256 ethValueSent = _handleRewardsAndGetValueSent( msg.value, quantity, - getCreatorRewardRecipient(), + getCreatorRewardRecipient(tokenId), createReferrals[tokenId], mintReferral, firstMinters[tokenId] @@ -453,10 +453,23 @@ contract ZoraCreator1155Impl is return TOTAL_REWARD_PER_MINT; } - /// @notice Get the creator reward recipient address - /// @dev The creator is not enforced to set a funds recipient address, so in that case the reward would be claimable by creator's contract - function getCreatorRewardRecipient() public view returns (address payable) { - return config.fundsRecipient != address(0) ? config.fundsRecipient : payable(address(this)); + /// @notice Get the creator reward recipient address for a specific token. + /// @param tokenId The token id to get the creator reward recipient for + /// @dev Returns the royalty recipient address for the token if set; otherwise uses the fundsRecipient. + /// If both are not set, this contract will be set as the recipient, and an account with + /// `PERMISSION_BIT_FUNDS_MANAGER` will be able to withdraw via the `withdrawRewards` function. + function getCreatorRewardRecipient(uint256 tokenId) public view returns (address) { + address royaltyRecipient = getRoyalties(tokenId).royaltyRecipient; + + if (royaltyRecipient != address(0)) { + return royaltyRecipient; + } + + if (config.fundsRecipient != address(0)) { + return config.fundsRecipient; + } + + return address(this); } /// @notice Set a metadata renderer for a token diff --git a/packages/1155-contracts/test/factory/ZoraCreator1155Factory_Fork.t.sol b/packages/1155-contracts/test/factory/ZoraCreator1155Factory_Fork.t.sol index e45761254..c331d0355 100644 --- a/packages/1155-contracts/test/factory/ZoraCreator1155Factory_Fork.t.sol +++ b/packages/1155-contracts/test/factory/ZoraCreator1155Factory_Fork.t.sol @@ -149,28 +149,4 @@ contract ZoraCreator1155FactoryForkTest is ForkDeploymentConfig, Test { testTheFork(forkTestChains[i]); } } - - // this is a temporary test to simulate the upgrade to the correct factory implementation - // on zora goerli. it can be deleted post upgrade - function test_fork_zoraGoerli_factoryUpgradeCanMint() external { - // create and select the fork, which will be used for all subsequent calls - // it will also affect the current block chain id based on the rpc url returned - vm.createSelectFork(vm.rpcUrl("zora_goerli")); - - Deployment memory deployment = getDeployment(); - - address factoryAddress = deployment.factoryProxy; - ZoraCreator1155FactoryImpl factory = ZoraCreator1155FactoryImpl(factoryAddress); - - vm.prank(factory.owner()); - - factory.upgradeTo(deployment.factoryImpl); - - // sanity check - check minters match config - assertEq(address(factory.merkleMinter()), deployment.merkleMintSaleStrategy); - assertEq(address(factory.fixedPriceMinter()), deployment.fixedPriceSaleStrategy); - assertEq(address(factory.redeemMinterFactory()), deployment.redeemMinterFactory); - - mintTokenAtFork(factory); - } } diff --git a/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol b/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol index 4370496dc..448f09415 100644 --- a/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol +++ b/packages/1155-contracts/test/nft/ZoraCreator1155.t.sol @@ -77,6 +77,19 @@ contract ZoraCreator1155Test is Test { address internal zora; event Purchased(address indexed sender, address indexed minter, uint256 indexed tokenId, uint256 quantity, uint256 value); + event RewardsDeposit( + address indexed creator, + address indexed createReferral, + address indexed mintReferral, + address firstMinter, + address zora, + address from, + uint256 creatorReward, + uint256 createReferralReward, + uint256 mintReferralReward, + uint256 firstMinterReward, + uint256 zoraReward + ); function setUp() external { creator = makeAddr("creator"); @@ -1135,6 +1148,126 @@ contract ZoraCreator1155Test is Test { assertEq(protocolRewards.balanceOf(zora), settings.zoraReward + settings.createReferralReward); } + function test_SetCreatorRewardRecipientForToken() public { + address collaborator = makeAddr("collaborator"); + uint256 quantity = 100; + + init(); + + vm.prank(admin); + uint256 tokenId = target.setupNewToken("test", quantity); + + address creatorRewardRecipient; + + creatorRewardRecipient = target.getCreatorRewardRecipient(tokenId); + + ICreatorRoyaltiesControl.RoyaltyConfiguration memory newRoyaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, collaborator); + + vm.prank(admin); + target.updateRoyaltiesForToken(tokenId, newRoyaltyConfig); + + creatorRewardRecipient = target.getCreatorRewardRecipient(tokenId); + + assertEq(creatorRewardRecipient, collaborator); + + vm.prank(admin); + target.addPermission(tokenId, address(simpleMinter), adminRole); + + RewardsSettings memory settings = target.computeFreeMintRewards(quantity); + + uint256 totalReward = target.computeTotalReward(quantity); + vm.deal(collector, totalReward); + + vm.prank(collector); + vm.expectEmit(true, true, true, true); + emit RewardsDeposit( + collaborator, + zora, + zora, + collaborator, + zora, + address(target), + settings.creatorReward, + settings.createReferralReward, + settings.mintReferralReward, + settings.firstMinterReward, + settings.zoraReward + ); + target.mintWithRewards{value: totalReward}(simpleMinter, tokenId, quantity, abi.encode(recipient), address(0)); + + assertEq(protocolRewards.balanceOf(collaborator), settings.creatorReward + settings.firstMinterReward); + } + + function test_CreatorRewardRecipientConditionalAddress() public { + ICreatorRoyaltiesControl.RoyaltyConfiguration memory royaltyConfig; + address creatorRewardRecipient; + + address collaborator = makeAddr("collaborator"); + uint256 quantity = 100; + + init(); + + vm.prank(admin); + uint256 tokenId = target.setupNewToken("test", quantity); + + (, , address contractFundsRecipient, , , ) = target.config(); + + creatorRewardRecipient = target.getCreatorRewardRecipient(tokenId); + assertEq(creatorRewardRecipient, contractFundsRecipient); + + royaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, collaborator); + vm.prank(admin); + target.updateRoyaltiesForToken(tokenId, royaltyConfig); + + creatorRewardRecipient = target.getCreatorRewardRecipient(tokenId); + assertEq(creatorRewardRecipient, collaborator); + + royaltyConfig = ICreatorRoyaltiesControl.RoyaltyConfiguration(0, 0, address(0)); + vm.prank(admin); + target.updateRoyaltiesForToken(tokenId, royaltyConfig); + + vm.prank(admin); + target.setFundsRecipient(payable(address(0))); + + creatorRewardRecipient = target.getCreatorRewardRecipient(tokenId); + assertEq(creatorRewardRecipient, address(target)); + } + + function test_ContractAsCreatorRewardRecipientFallback() public { + uint256 quantity = 100; + + init(); + + vm.startPrank(admin); + uint256 tokenId = target.setupNewToken("test", quantity); + + target.setFundsRecipient(payable(address(0))); + + target.addPermission(tokenId, address(simpleMinter), adminRole); + vm.stopPrank(); + + RewardsSettings memory settings = target.computeFreeMintRewards(quantity); + + uint256 totalReward = target.computeTotalReward(quantity); + vm.deal(collector, totalReward); + + address creatorRewardRecipient = target.getCreatorRewardRecipient(tokenId); + + vm.prank(collector); + target.mintWithRewards{value: totalReward}(simpleMinter, tokenId, quantity, abi.encode(recipient), address(0)); + + assertEq(creatorRewardRecipient, address(target)); + + uint256 creatorRewardBalance = settings.creatorReward + settings.firstMinterReward; + assertEq(protocolRewards.balanceOf(address(target)), creatorRewardBalance); + + vm.prank(admin); + target.withdrawRewards(admin, creatorRewardBalance); + + assertEq(admin.balance, creatorRewardBalance); + assertEq(protocolRewards.balanceOf(address(target)), 0); + } + function testRevert_WrongValueForSale(uint256 quantity, uint256 salePrice) public { vm.assume(quantity > 0 && quantity < 1_000_000); vm.assume(salePrice > 0 && salePrice < 10 ether); diff --git a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol index d5c3bf87a..71a8fe1fd 100644 --- a/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol +++ b/packages/1155-contracts/test/premint/ZoraCreator1155PremintExecutor.t.sol @@ -292,40 +292,6 @@ contract ZoraCreator1155PreminterTest is ForkDeploymentConfig, Test { } } - // this is a temporary test to simulate the upcoming upgrade - function test_fork_zoraGoerli_afterUpgradeCanPremint() external { - vm.createSelectFork(vm.rpcUrl("zora_goerli")); - - Deployment memory deployment = getDeployment(); - - factory = ZoraCreator1155FactoryImpl(deployment.factoryProxy); - - console2.log("factory upgrade target:", deployment.factoryProxy); - bytes memory factoryProxyUpgradeCall = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, deployment.factoryImpl); - console2.log("factory upgrade call:", vm.toString(factoryProxyUpgradeCall)); - - console2.log("preminter upgrade target:", deployment.preminterProxy); - bytes memory preminterProxyUpgradeCall = abi.encodeWithSelector(UUPSUpgradeable.upgradeTo.selector, deployment.preminterImpl); - console2.log("preminter upgrade call:", vm.toString(preminterProxyUpgradeCall)); - - vm.prank(factory.owner()); - // lets call it as if we were calling from a safe: - deployment.factoryProxy.call(factoryProxyUpgradeCall); - - // override test storage to point to proxy - preminter = ZoraCreator1155PremintExecutorImpl(deployment.preminterProxy); - - vm.prank(preminter.owner()); - // preminter impl was already created with correct factory, were just upgrading it now - deployment.preminterProxy.call(preminterProxyUpgradeCall); - - assertEq(address(preminter.zora1155Factory()), address(factory)); - - preminterCanMintTokens(); - - // lets console.log these upgrades - } - function test_signatureForSameContractandUid_shouldMintExistingToken() external { // 1. Make contract creation params