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

supply royalty #118

Merged
merged 10 commits into from
Jun 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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()));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have comments for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its @tbtstl 's whiteboard magic, would be great if you had any notes on it tyson.

if (totalRoyaltyMints > 0) {
_mintNFTs(royaltyRecipient, totalRoyaltyMints);
jgeary marked this conversation as resolved.
Show resolved Hide resolved
}
}

function updateRoyaltyMintSchedule(uint32 newSchedule) external onlyAdmin {
jgeary marked this conversation as resolved.
Show resolved Hide resolved
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);
jgeary marked this conversation as resolved.
Show resolved Hide resolved

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