Skip to content

Commit

Permalink
Use _numberMinted on edition instead of tally on minter (#233)
Browse files Browse the repository at this point in the history
* Use _numberMinted on edition instead of tally on minter

* additional tests

* Add numberBurned and some comments

Co-authored-by: Vectorized <webby1111@hotmail.com>
  • Loading branch information
vigneshka and Vectorized committed Sep 19, 2022
1 parent 1298050 commit 041e5f2
Show file tree
Hide file tree
Showing 11 changed files with 126 additions and 85 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-wasps-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@soundxyz/sound-protocol": patch
---

Use \_numberMinted on edition instead of tally on minter
14 changes: 14 additions & 0 deletions contracts/core/SoundEditionV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,20 @@ contract SoundEditionV1 is ISoundEditionV1, ERC721AQueryableUpgradeable, ERC721A
return _nextTokenId();
}

/**
* @inheritdoc ISoundEditionV1
*/
function numberMinted(address owner) external view returns (uint256) {
return _numberMinted(owner);
}

/**
* @inheritdoc ISoundEditionV1
*/
function numberBurned(address owner) external view returns (uint256) {
return _numberBurned(owner);
}

/**
* @inheritdoc ISoundEditionV1
*/
Expand Down
14 changes: 14 additions & 0 deletions contracts/core/interfaces/ISoundEditionV1.sol
Original file line number Diff line number Diff line change
Expand Up @@ -523,6 +523,20 @@ interface ISoundEditionV1 is IERC721AUpgradeable, IERC2981Upgradeable {
*/
function nextTokenId() external view returns (uint256);

/**
* @dev Returns the number of tokens minted by `owner`.
* @param owner Address to query for number minted.
* @return The latest value.
*/
function numberMinted(address owner) external view returns (uint256);

/**
* @dev Returns the number of tokens burned by `owner`.
* @param owner Address to query for number burned.
* @return The latest value.
*/
function numberBurned(address owner) external view returns (uint256);

/**
* @dev Returns the total amount of tokens minted.
* @return The latest value.
Expand Down
19 changes: 5 additions & 14 deletions contracts/modules/EditionMaxMinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,6 @@ contract EditionMaxMinter is IEditionMaxMinter, BaseMinter {
*/
mapping(address => mapping(uint128 => EditionMintData)) internal _editionMintData;

/**
* @dev Number of tokens minted by each buyer address
* edition => mintId => buyer => mintedTallies
*/
mapping(address => mapping(uint256 => mapping(address => uint256))) public mintedTallies;

// =============================================================
// CONSTRUCTOR
// =============================================================
Expand Down Expand Up @@ -84,14 +78,11 @@ contract EditionMaxMinter is IEditionMaxMinter, BaseMinter {
EditionMintData storage data = _editionMintData[edition][mintId];

unchecked {
uint256 userMintedBalance = mintedTallies[edition][mintId][msg.sender];
// Check the additional quantity does not exceed the set maximum.
// If `quantity` is large enough to cause an overflow,
// `_mint` will give an out of gas error.
uint256 tally = userMintedBalance + quantity;
if (tally > data.maxMintablePerAccount) revert ExceedsMaxPerAccount();
// Update the minted tally for this account
mintedTallies[edition][mintId][msg.sender] = tally;
// Check the additional `requestedQuantity` does not exceed the maximum mintable per account.
uint256 numberMinted = ISoundEditionV1(edition).numberMinted(msg.sender);
// Won't overflow. The total number of tokens minted in `edition` won't exceed `type(uint32).max`,
// and `quantity` has 32 bits.
if (numberMinted + quantity > data.maxMintablePerAccount) revert ExceedsMaxPerAccount();
}

_mint(edition, mintId, quantity, affiliate);
Expand Down
20 changes: 6 additions & 14 deletions contracts/modules/MerkleDropMinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ISoundFeeRegistry } from "@core/interfaces/ISoundFeeRegistry.sol";
import { BaseMinter } from "@modules/BaseMinter.sol";
import { IMerkleDropMinter, EditionMintData, MintInfo } from "./interfaces/IMerkleDropMinter.sol";
import { IMinterModule } from "@core/interfaces/IMinterModule.sol";
import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol";

/**
* @title MerkleDropMinter
Expand All @@ -25,12 +26,6 @@ contract MerkleDropMinter is IMerkleDropMinter, BaseMinter {
*/
mapping(address => mapping(uint128 => EditionMintData)) internal _editionMintData;

/**
* @dev Number of tokens minted by each buyer address
* Maps: `edition` => `mintId` => `buyer` => value.
*/
mapping(address => mapping(uint128 => mapping(address => uint256))) public mintedTallies;

// =============================================================
// CONSTRUCTOR
// =============================================================
Expand Down Expand Up @@ -99,14 +94,11 @@ contract MerkleDropMinter is IMerkleDropMinter, BaseMinter {
if (!valid) revert InvalidMerkleProof();

unchecked {
uint256 userMintedBalance = mintedTallies[edition][mintId][msg.sender];
// Check the additional requestedQuantity does not exceed the set maximum.
// If `requestedQuantity` is large enough to cause an overflow,
// `_mint` will give an out of gas error.
uint256 tally = userMintedBalance + requestedQuantity;
if (tally > data.maxMintablePerAccount) revert ExceedsMaxPerAccount();
// Update the minted tally for this account
mintedTallies[edition][mintId][msg.sender] = tally;
// Check the additional `requestedQuantity` does not exceed the maximum mintable per account.
uint256 numberMinted = ISoundEditionV1(edition).numberMinted(msg.sender);
// Won't overflow. The total number of tokens minted in `edition` won't exceed `type(uint32).max`,
// and `quantity` has 32 bits.
if (numberMinted + requestedQuantity > data.maxMintablePerAccount) revert ExceedsMaxPerAccount();
}

_mint(edition, mintId, requestedQuantity, affiliate);
Expand Down
20 changes: 6 additions & 14 deletions contracts/modules/RangeEditionMinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ISoundFeeRegistry } from "@core/interfaces/ISoundFeeRegistry.sol";
import { IRangeEditionMinter, EditionMintData, MintInfo } from "./interfaces/IRangeEditionMinter.sol";
import { BaseMinter } from "./BaseMinter.sol";
import { IMinterModule } from "@core/interfaces/IMinterModule.sol";
import { ISoundEditionV1 } from "@core/interfaces/ISoundEditionV1.sol";

/*
* @title RangeEditionMinter
Expand All @@ -24,12 +25,6 @@ contract RangeEditionMinter is IRangeEditionMinter, BaseMinter {
*/
mapping(address => mapping(uint128 => EditionMintData)) internal _editionMintData;

/**
* @dev Number of tokens minted by each buyer address
* edition => mintId => buyer => mintedTallies
*/
mapping(address => mapping(uint256 => mapping(address => uint256))) public mintedTallies;

// =============================================================
// CONSTRUCTOR
// =============================================================
Expand Down Expand Up @@ -99,14 +94,11 @@ contract RangeEditionMinter is IRangeEditionMinter, BaseMinter {
data.totalMinted = _incrementTotalMinted(data.totalMinted, quantity, _maxMintable);

unchecked {
uint256 userMintedBalance = mintedTallies[edition][mintId][msg.sender];
// Check the additional quantity does not exceed the set maximum.
// If `quantity` is large enough to cause an overflow,
// `_mint` will give an out of gas error.
uint256 tally = userMintedBalance + quantity;
if (tally > data.maxMintablePerAccount) revert ExceedsMaxPerAccount();
// Update the minted tally for this account
mintedTallies[edition][mintId][msg.sender] = tally;
// Check the additional `requestedQuantity` does not exceed the maximum mintable per account.
uint256 numberMinted = ISoundEditionV1(edition).numberMinted(msg.sender);
// Won't overflow. The total number of tokens minted in `edition` won't exceed `type(uint32).max`,
// and `quantity` has 32 bits.
if (numberMinted + quantity > data.maxMintablePerAccount) revert ExceedsMaxPerAccount();
}

_mint(edition, mintId, quantity, affiliate);
Expand Down
13 changes: 0 additions & 13 deletions contracts/modules/interfaces/IMerkleDropMinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -223,19 +223,6 @@ interface IMerkleDropMinter is IMinterModule {
// PUBLIC / EXTERNAL VIEW FUNCTIONS
// =============================================================

/**
* @dev Returns the amount of minted tokens for `account` in `mintData`.
* @param edition Address of the edition.
* @param mintId Mint identifier.
* @param account Address of the account.
* @return tally The number of minted tokens for the account.
*/
function mintedTallies(
address edition,
uint128 mintId,
address account
) external view returns (uint256);

/**
* @dev Returns IMerkleDropMinter.MintInfo instance containing the full minter parameter set.
* @param edition The edition to get the mint instance for.
Expand Down
16 changes: 16 additions & 0 deletions tests/core/SoundEdition/mint.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ contract SoundEdition_mint is TestConfig {
edition.burn(TOKEN1_ID);

assert(edition.balanceOf(address(this)) == 0);
assert(edition.numberBurned(address(this)) == ONE_TOKEN);
assert(edition.totalSupply() == 0);

// Mint another token and assert that the attacker can't burn
Expand Down Expand Up @@ -476,4 +477,19 @@ contract SoundEdition_mint is TestConfig {
// Airdrop with `quantity` right at the limit is ok.
edition.airdrop(to, limit);
}

function test_numberMintedReturnsExpectedValue() public {
SoundEditionV1 edition = createGenericEdition();

address owner = address(12345);
edition.transferOwnership(owner);

assertTrue(edition.numberMinted(owner) == 0);

vm.prank(owner);
uint32 quantity = 10;
edition.mint(owner, quantity);

assertTrue(edition.numberMinted(owner) == quantity);
}
}
35 changes: 30 additions & 5 deletions tests/modules/EditionMaxMinter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,44 @@ contract EditionMaxMinterTests is TestConfig {
minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0));
}

function test_mintWhenOverMaxMintableDueToPreviousMintedReverts() public {
(SoundEditionV1 edition, EditionMaxMinter minter) = _createEditionAndMinter(3);
vm.warp(START_TIME);

address caller = getFundedAccount(1);

// have 2 previously minted
address owner = address(12345);
edition.transferOwnership(owner);
vm.prank(owner);
edition.mint(caller, 2);

// attempting to mint 2 more reverts
vm.prank(caller);
vm.expectRevert(IEditionMaxMinter.ExceedsMaxPerAccount.selector);
minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0));
}

function test_mintWhenMintablePerAccountIsSetAndSatisfied() public {
// Set max allowed per account to 2
(SoundEditionV1 edition, EditionMaxMinter minter) = _createEditionAndMinter(2);
// Set max allowed per account to 3
(SoundEditionV1 edition, EditionMaxMinter minter) = _createEditionAndMinter(3);

// Ensure we can mint the max allowed of 2 tokens
address caller = getFundedAccount(1);

// Set 1 previous mint
address owner = address(12345);
edition.transferOwnership(owner);
vm.prank(owner);
edition.mint(caller, 1);

// Ensure we can mint the max allowed of 2 tokens
vm.warp(START_TIME);
vm.prank(caller);
minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0));

assertEq(edition.balanceOf(caller), 2);
assertEq(edition.balanceOf(caller), 3);

assertEq(edition.totalMinted(), 2);
assertEq(edition.totalMinted(), 3);
}

function test_mintUpdatesValuesAndMintsCorrectly() public {
Expand Down
19 changes: 0 additions & 19 deletions tests/modules/MerkleDropMinter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -170,25 +170,6 @@ contract MerkleDropMinterTests is TestConfig {
minter.mint(address(edition), mintId, requestedQuantity, proof, address(0));
}

function test_canGetMintedTallyForAccount() public {
uint32 maxMintablePerAccount = 1;
(SoundEditionV1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(
0,
6,
maxMintablePerAccount
);
bytes32[] memory proof = m.getProof(leaves, 0);

vm.warp(START_TIME);
vm.prank(accounts[0]);

uint32 requestedQuantity = maxMintablePerAccount;
minter.mint(address(edition), mintId, requestedQuantity, proof, address(0));

uint256 mintedTally = minter.mintedTallies(address(edition), mintId, accounts[0]);
assertEq(mintedTally, 1);
}

function test_setPrice(uint96 price) public {
(SoundEditionV1 edition, MerkleDropMinter minter, uint128 mintId) = _createEditionAndMinter(0, 0, 1);

Expand Down
36 changes: 30 additions & 6 deletions tests/modules/RangeEditionMinter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -230,20 +230,44 @@ contract RangeEditionMinterTests is TestConfig {
minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0));
}

function test_mintWhenOverMaxMintableDueToPreviousMintedReverts() public {
(SoundEditionV1 edition, RangeEditionMinter minter) = _createEditionAndMinter(3);
vm.warp(START_TIME);

address caller = getFundedAccount(1);

// have 2 previously minted
address owner = address(12345);
edition.transferOwnership(owner);
vm.prank(owner);
edition.mint(caller, 2);

// attempting to mint 2 more reverts
vm.prank(caller);
vm.expectRevert(IRangeEditionMinter.ExceedsMaxPerAccount.selector);
minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0));
}

function test_mintWhenMintablePerAccountIsSetAndSatisfied() public {
// Set max allowed per account to 2
(SoundEditionV1 edition, RangeEditionMinter minter) = _createEditionAndMinter(2);
// Set max allowed per account to 3
(SoundEditionV1 edition, RangeEditionMinter minter) = _createEditionAndMinter(3);

// Ensure we can mint the max allowed of 2 tokens
address caller = getFundedAccount(1);

// Set 1 previous mint
address owner = address(12345);
edition.transferOwnership(owner);
vm.prank(owner);
edition.mint(caller, 1);

// Ensure we can mint the max allowed of 2 tokens
vm.warp(START_TIME);
vm.prank(caller);
minter.mint{ value: PRICE * 2 }(address(edition), MINT_ID, 2, address(0));

assertEq(edition.balanceOf(caller), 2);
assertEq(edition.balanceOf(caller), 3);

MintInfo memory data = minter.mintInfo(address(edition), MINT_ID);
assertEq(data.totalMinted, 2);
assertEq(edition.totalMinted(), 3);
}

function test_mintUpdatesValuesAndMintsCorrectly() public {
Expand Down

0 comments on commit 041e5f2

Please sign in to comment.