Skip to content

Commit

Permalink
Merge pull request #118 from ourzora/james/supply-royalty
Browse files Browse the repository at this point in the history
supply royalty
  • Loading branch information
kulkarohan committed Jun 15, 2023
2 parents 7812bd3 + 415375a commit d1f8797
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ cache
.env.*
dist/
.idea
broadcast
broadcast/
1 change: 1 addition & 0 deletions .storage-layout
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
| config | struct IERC721Drop.Configuration | 352 | 0 | 64 | src/ERC721Drop.sol:ERC721Drop |
| salesConfig | struct IERC721Drop.SalesConfiguration | 354 | 0 | 96 | src/ERC721Drop.sol:ERC721Drop |
| presaleMintsByAddress | mapping(address => uint256) | 357 | 0 | 32 | src/ERC721Drop.sol:ERC721Drop |
| royaltyMintSchedule | uint32 | 358 | 0 | 4 | src/ERC721Drop.sol:ERC721Drop |

=======================
➡ ERC721DropProxy
Expand Down
8 changes: 6 additions & 2 deletions addresses/5.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
{
"DROP_METADATA_RENDERER": "0x5956Fd16c4d8c4b4711F2551971aBB7c2F4aF677",
"ZORA_NFT_CREATOR_V1": "0xb4d319458E489825Cea8e25b5e43742BCdAFc6dd",
"ZORA_NFT_CREATOR_PROXY": "0xb9583D05Ba9ba8f7F14CCEe3Da10D2bc0A72f519",
"ZORA_ERC721_TRANSFER_HELPER": "0xd1adAF05575295710dE1145c3c9427c364A70a7f",
"ZORA_FEE_MANAGER": "0xCf5E957CA7b77EC16611992d79f4E179132ad98C",
"EDITION_METADATA_RENDERER": "0x2f5C21EF9DdFf9A1FE76a1c55dd5112fcf2EfD39",
"ERC721DROP_IMPL": "0x9dbc5D5Abc25460195e34fF42916eF6D183Cfafd",
"DROP_METADATA_RENDERER": "0x5956Fd16c4d8c4b4711F2551971aBB7c2F4aF677",
"ERC721_DROP": "0x2093eFB737C07324D8b6c5807910EFF45045D987",
"FACTORY_UPGRADE_GATE": "0x942C03C7afE5c8118BDB728Aa06d1b894B1cD9A8",
"ZORA_NFT_CREATOR_PROXY": "0xb9583D05Ba9ba8f7F14CCEe3Da10D2bc0A72f519",
"ZORA_NFT_CREATOR_V1_IMPL": "0x4328cbDAD668E81B475766520E1004e6688D2949"
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[profile.default]
solc_version = '0.8.17'
optimizer = true
optimizer_runs = 5000
optimizer_runs = 3000
via_ir = true
out = 'dist/artifacts'
test = 'test'
Expand Down
51 changes: 45 additions & 6 deletions src/ERC721Drop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {AccessControlUpgradeable} from "@openzeppelin/contracts-upgradeable/acce
import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import {MerkleProofUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/cryptography/MerkleProofUpgradeable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {MathUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/math/MathUpgradeable.sol";

import {IMetadataRenderer} from "./interfaces/IMetadataRenderer.sol";
import {IOperatorFilterRegistry} from "./interfaces/IOperatorFilterRegistry.sol";
Expand All @@ -35,6 +36,8 @@ import {FundsReceiver} from "./utils/FundsReceiver.sol";
import {Version} from "./utils/Version.sol";
import {PublicMulticall} from "./utils/PublicMulticall.sol";
import {ERC721DropStorageV1} from "./storage/ERC721DropStorageV1.sol";
import {ERC721DropStorageV2} from "./storage/ERC721DropStorageV2.sol";


/**
* @notice ZORA NFT Base contract for Drops and Editions
Expand All @@ -55,8 +58,9 @@ contract ERC721Drop is
PublicMulticall,
OwnableSkeleton,
FundsReceiver,
Version(12),
ERC721DropStorageV1
Version(13),
ERC721DropStorageV1,
ERC721DropStorageV2
{
/// @dev This is the max mint batch size for the optimized ERC721A mint contract
uint256 internal immutable MAX_MINT_BATCH_SIZE = 8;
Expand All @@ -83,6 +87,8 @@ contract ERC721Drop is
/// @notice Max royalty BPS
uint16 constant MAX_ROYALTY_BPS = 50_00;

uint8 constant SUPPLY_ROYALTY_FOR_EVERY_MINT = 1;

// /// @notice Empty string for blank comments
// string constant EMPTY_STRING = "";

Expand Down Expand Up @@ -440,7 +446,6 @@ contract ERC721Drop is
external
payable
nonReentrant
canMintTokens(quantity)
onlyPublicSaleActive
returns (uint256)
{
Expand All @@ -455,14 +460,16 @@ contract ERC721Drop is
external
payable
nonReentrant
canMintTokens(quantity)
onlyPublicSaleActive
returns (uint256)
{
return _handlePurchase(quantity, comment);
}

function _handlePurchase(uint256 quantity, string memory comment) internal returns (uint256) {
_mintSupplyRoyalty(quantity);
_requireCanMintQuantity(quantity);

uint256 salePrice = salesConfig.publicSalePrice;

if (msg.value != (salePrice + ZORA_MINT_FEE) * quantity) {
Expand Down Expand Up @@ -594,7 +601,6 @@ contract ERC721Drop is
external
payable
nonReentrant
canMintTokens(quantity)
onlyPresaleActive
returns (uint256)
{
Expand All @@ -617,7 +623,6 @@ contract ERC721Drop is
external
payable
nonReentrant
canMintTokens(quantity)
onlyPresaleActive
returns (uint256)
{
Expand All @@ -631,6 +636,9 @@ contract ERC721Drop is
bytes32[] calldata merkleProof,
string memory comment
) internal returns (uint256) {
_mintSupplyRoyalty(quantity);
_requireCanMintQuantity(quantity);

if (
!MerkleProofUpgradeable.verify(
merkleProof,
Expand Down Expand Up @@ -1267,6 +1275,37 @@ contract ERC721Drop is
emit MintFeePayout(zoraFee, ZORA_MINT_FEE_RECIPIENT, success);
}

function _requireCanMintQuantity(uint256 quantity) internal view {
if (quantity + _totalMinted() > config.editionSize) {
revert Mint_SoldOut();
}
}

function _mintSupplyRoyalty(uint256 mintQuantity) internal {
uint32 royaltySchedule = royaltyMintSchedule;
if (royaltySchedule == 0) {
return;
}

address royaltyRecipient = config.fundsRecipient;
if (royaltyRecipient == address(0)) {
return;
}

uint256 totalRoyaltyMints = (mintQuantity + (_totalMinted() % royaltySchedule)) / (royaltySchedule - 1);
totalRoyaltyMints = MathUpgradeable.min(totalRoyaltyMints, config.editionSize - (mintQuantity + _totalMinted()));
if (totalRoyaltyMints > 0) {
_mintNFTs(royaltyRecipient, totalRoyaltyMints);
}
}

function updateRoyaltyMintSchedule(uint32 newSchedule) external onlyAdmin {
if (newSchedule == SUPPLY_ROYALTY_FOR_EVERY_MINT) {
revert InvalidMintSchedule();
}
royaltyMintSchedule = newSchedule;
}

/// @notice ERC165 supports interface
/// @param interfaceId interface id to check if supported
function supportsInterface(bytes4 interfaceId)
Expand Down
2 changes: 2 additions & 0 deletions src/interfaces/IERC721Drop.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ interface IERC721Drop {
error Admin_InvalidUpgradeAddress(address proposedAddress);
/// @notice Unable to finalize an edition not marked as open (size set to uint64_max_value)
error Admin_UnableToFinalizeNotOpenEdition();
/// @notice Cannot reserve every mint for admin
error InvalidMintSchedule();

/// @notice Event emitted for mint fee payout
/// @param mintFeeAmount amount of the mint fee
Expand Down
6 changes: 6 additions & 0 deletions src/storage/ERC721DropStorageV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

contract ERC721DropStorageV2 {
uint32 public royaltyMintSchedule;
}
139 changes: 139 additions & 0 deletions test/ERC721Drop.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pragma solidity ^0.8.10;

import {Test} from "forge-std/Test.sol";
import {IERC721AUpgradeable} from "erc721a-upgradeable/IERC721AUpgradeable.sol";
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";

import {ERC721Drop} from "../src/ERC721Drop.sol";
import {DummyMetadataRenderer} from "./utils/DummyMetadataRenderer.sol";
Expand Down Expand Up @@ -977,6 +978,144 @@ contract ERC721DropTest is Test {
assertEq(dummyRenderer.someState(), "");
}

function test_SupplyRoyaltyMintScheduleCannotBeOne() public setupZoraNFTBase(100) {
vm.startPrank(DEFAULT_OWNER_ADDRESS);
vm.expectRevert(IERC721Drop.InvalidMintSchedule.selector);
zoraNFTBase.updateRoyaltyMintSchedule(1);
}

function test_SupplyRoyaltyPurchase(uint32 royaltyMintSchedule, uint32 editionSize, uint256 mintQuantity) public setupZoraNFTBase(editionSize) {
vm.assume(royaltyMintSchedule > 1 && royaltyMintSchedule <= editionSize && editionSize <= 100000 && mintQuantity > 0 && mintQuantity <= editionSize);
uint256 totalRoyaltyMintsForSale = editionSize / royaltyMintSchedule;
vm.assume(mintQuantity <= editionSize - totalRoyaltyMintsForSale);

vm.startPrank(DEFAULT_OWNER_ADDRESS);

zoraNFTBase.updateRoyaltyMintSchedule(royaltyMintSchedule);

zoraNFTBase.setSaleConfiguration({
publicSaleStart: 0,
publicSaleEnd: type(uint64).max,
presaleStart: 0,
presaleEnd: 0,
publicSalePrice: 0.1 ether,
maxSalePurchasePerAddress: editionSize,
presaleMerkleRoot: bytes32(0)
});
vm.stopPrank();

uint256 totalRoyaltyMintsForPurchase = mintQuantity / (royaltyMintSchedule - 1);
totalRoyaltyMintsForPurchase = Math.min(totalRoyaltyMintsForPurchase, editionSize - mintQuantity);
(, uint256 zoraFee) = zoraNFTBase.zoraFeeForAmount(mintQuantity);

uint256 paymentAmount = 0.1 ether * mintQuantity + zoraFee;
vm.deal(address(456), paymentAmount);

vm.startPrank(address(456));
zoraNFTBase.purchase{value: paymentAmount}(mintQuantity);

assertEq(zoraNFTBase.balanceOf(address(456)), mintQuantity);
assertEq(zoraNFTBase.balanceOf(DEFAULT_FUNDS_RECIPIENT_ADDRESS), totalRoyaltyMintsForPurchase);

vm.stopPrank();
}

function test_SupplyRoyaltyCleanNumbers() public setupZoraNFTBase(100) {
vm.startPrank(DEFAULT_OWNER_ADDRESS);

zoraNFTBase.updateRoyaltyMintSchedule(5);

zoraNFTBase.setSaleConfiguration({
publicSaleStart: 0,
publicSaleEnd: type(uint64).max,
presaleStart: 0,
presaleEnd: 0,
publicSalePrice: 0.1 ether,
maxSalePurchasePerAddress: 100,
presaleMerkleRoot: bytes32(0)
});
vm.stopPrank();

(, uint256 zoraFee) = zoraNFTBase.zoraFeeForAmount(80);
uint256 paymentAmount = 0.1 ether * 80 + zoraFee;
vm.deal(address(456), paymentAmount);

vm.startPrank(address(456));
zoraNFTBase.purchase{value: paymentAmount}(80);

assertEq(zoraNFTBase.balanceOf(address(456)), 80);
assertEq(zoraNFTBase.balanceOf(DEFAULT_FUNDS_RECIPIENT_ADDRESS), 20);

vm.stopPrank();
}

function test_SupplyRoyaltyEdgeCaseNumbers() public setupZoraNFTBase(137) {
vm.startPrank(DEFAULT_OWNER_ADDRESS);

zoraNFTBase.updateRoyaltyMintSchedule(3);

zoraNFTBase.setSaleConfiguration({
publicSaleStart: 0,
publicSaleEnd: type(uint64).max,
presaleStart: 0,
presaleEnd: 0,
publicSalePrice: 0.1 ether,
maxSalePurchasePerAddress: 92,
presaleMerkleRoot: bytes32(0)
});
vm.stopPrank();

(, uint256 zoraFee) = zoraNFTBase.zoraFeeForAmount(92);
uint256 paymentAmount = 0.1 ether * 92 + zoraFee;
vm.deal(address(456), paymentAmount);

vm.startPrank(address(456));
zoraNFTBase.purchase{value: paymentAmount}(92);

assertEq(zoraNFTBase.balanceOf(address(456)), 92);
assertEq(zoraNFTBase.balanceOf(DEFAULT_FUNDS_RECIPIENT_ADDRESS), 45);

vm.stopPrank();
}

function test_SupplyRoyaltyEdgeCaseNumbersOpenEdition() public setupZoraNFTBase(type(uint64).max) {
vm.startPrank(DEFAULT_OWNER_ADDRESS);

zoraNFTBase.updateRoyaltyMintSchedule(3);

zoraNFTBase.setSaleConfiguration({
publicSaleStart: 0,
publicSaleEnd: type(uint64).max,
presaleStart: 0,
presaleEnd: 0,
publicSalePrice: 0.1 ether,
maxSalePurchasePerAddress: 93,
presaleMerkleRoot: bytes32(0)
});
vm.stopPrank();

(, uint256 zoraFee) = zoraNFTBase.zoraFeeForAmount(92);
uint256 paymentAmount = 0.1 ether * 92 + zoraFee;
vm.deal(address(456), paymentAmount);

vm.startPrank(address(456));
zoraNFTBase.purchase{value: paymentAmount}(92);

assertEq(zoraNFTBase.balanceOf(address(456)), 92);
assertEq(zoraNFTBase.balanceOf(DEFAULT_FUNDS_RECIPIENT_ADDRESS), 46);

(, zoraFee) = zoraNFTBase.zoraFeeForAmount(1);
paymentAmount = 0.1 ether + zoraFee;
vm.deal(address(456), paymentAmount);

zoraNFTBase.purchase{value: paymentAmount}(1);

assertEq(zoraNFTBase.balanceOf(address(456)), 93);
assertEq(zoraNFTBase.balanceOf(DEFAULT_FUNDS_RECIPIENT_ADDRESS), 46);

vm.stopPrank();
}

function test_EIP165() public view {
require(zoraNFTBase.supportsInterface(0x01ffc9a7), "supports 165");
require(zoraNFTBase.supportsInterface(0x80ac58cd), "supports 721");
Expand Down

0 comments on commit d1f8797

Please sign in to comment.