Skip to content

Commit

Permalink
Fix issue where batch merkle claims could be blocked
Browse files Browse the repository at this point in the history
  • Loading branch information
neokry committed Dec 29, 2023
1 parent e81cfce commit fc48915
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 4 deletions.
28 changes: 25 additions & 3 deletions src/minters/MerkleReserveMinter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ contract MerkleReserveMinter {
/// @dev Invalid amount of tokens to claim
error INVALID_CLAIM_COUNT();

/// @dev Could not mint token
error ERROR_MINTING_TOKEN(uint256 tokenId);

/// @dev No tokens could be minted
error NO_TOKENS_MINTED();

/// @dev Merkle proof for claim is invalid
/// @param mintTo Address to mint to
/// @param merkleProof Merkle proof for token
Expand Down Expand Up @@ -145,11 +151,13 @@ contract MerkleReserveMinter {
revert MINT_NOT_STARTED();
}

// Check value sent
if (msg.value < _getTotalFeesForMint(settings.pricePerToken, claimCount)) {
// Ensure value sent matches total fees
if (msg.value != _getTotalFeesForMint(settings.pricePerToken, claimCount)) {
revert INVALID_VALUE();
}

uint256 mintCount;

// Mint tokens
unchecked {
for (uint256 i = 0; i < claimCount; ++i) {
Expand All @@ -162,10 +170,24 @@ contract MerkleReserveMinter {
}

// Only allowing reserved tokens to be minted for this strategy
IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId);
try IToken(tokenContract).mintFromReserveTo(claim.mintTo, claim.tokenId) {
// Track mint count to ensure at least one token is minted
mintCount++;
} catch {
// we don't want to handle refunds so revert if user is paying for tokens and a token is already minted
// if a user is not paying for tokens ignore the error and allow them to continue minting
if (settings.pricePerToken > 0) {
revert ERROR_MINTING_TOKEN(claim.tokenId);
}
}
}
}

// Revert if no tokens were minted
if (mintCount == 0) {
revert NO_TOKENS_MINTED();
}

// Distribute fees if minting fees for this collection are set (Builder DAO fee does not apply to free mints)
if (settings.pricePerToken > 0) {
_distributeFees(tokenContract, claimCount);
Expand Down
164 changes: 163 additions & 1 deletion test/MerkleReserveMinter.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,42 @@ contract MerkleReserveMinterTest is NounsBuilderTest {
assertEq(token.ownerOf(5), claimer1);
}

function testRevert_NoTokensMinted() public {
deployAltMock(20);

bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b);

MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({
mintStart: 0,
mintEnd: uint64(block.timestamp + 1000),
pricePerToken: 0 ether,
merkleRoot: root
});

vm.prank(address(founder));
minter.setMintSettings(address(token), settings);

(uint64 mintStart, uint64 mintEnd, uint64 pricePerToken, bytes32 merkleRoot) = minter.allowedMerkles(address(token));

TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true });
TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1);
minters[0] = params;
vm.prank(address(founder));
token.updateMinters(minters);

vm.prank(address(minter));
token.mintFromReserveTo(founder, 5);

bytes32[] memory proof = new bytes32[](1);
proof[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273);

MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](1);
claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof });

vm.expectRevert(abi.encodeWithSignature("NO_TOKENS_MINTED()"));
minter.mintFromReserve(address(token), claims);
}

function test_MintFlowSetFromToken() public {
bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b);

Expand Down Expand Up @@ -205,6 +241,128 @@ contract MerkleReserveMinterTest is NounsBuilderTest {
assertEq(address(treasury).balance, fees - minter.BUILDER_DAO_FEE() * claims.length);
}

function test_MintFlowSkipAlreadyMintedTokens() public {
deployAltMock(20);

bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b);

MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({
mintStart: 0,
mintEnd: uint64(block.timestamp + 1000),
pricePerToken: 0,
merkleRoot: root
});

vm.prank(address(founder));
minter.setMintSettings(address(token), settings);

TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true });
TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1);
minters[0] = params;
vm.prank(address(founder));
token.updateMinters(minters);

bytes32[] memory proof1 = new bytes32[](1);
proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273);

bytes32[] memory proof2 = new bytes32[](1);
proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2);

MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2);
claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 });
claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 });

vm.prank(address(minter));
token.mintFromReserveTo(founder, 5);

minter.mintFromReserve(address(token), claims);

assertEq(token.ownerOf(5), founder);
assertEq(token.ownerOf(6), claimer2);
}

function test_MultipleNoTokensMinted() public {
deployAltMock(20);

bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b);

MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({
mintStart: 0,
mintEnd: uint64(block.timestamp + 1000),
pricePerToken: 0,
merkleRoot: root
});

vm.prank(address(founder));
minter.setMintSettings(address(token), settings);

TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true });
TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1);
minters[0] = params;
vm.prank(address(founder));
token.updateMinters(minters);

bytes32[] memory proof1 = new bytes32[](1);
proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273);

bytes32[] memory proof2 = new bytes32[](1);
proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2);

MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2);
claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 });
claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 });

vm.startPrank(address(minter));
token.mintFromReserveTo(founder, 5);
token.mintFromReserveTo(founder, 6);
vm.stopPrank();

vm.expectRevert(abi.encodeWithSignature("NO_TOKENS_MINTED()"));
minter.mintFromReserve(address(token), claims);
}

function testRevert_TokenAlreadyMinted() public {
deployAltMock(20);

bytes32 root = bytes32(0x5e0da80989496579de029b8ad2f9c234e8de75f5487035210bfb7676e386af8b);

MerkleReserveMinter.MerkleMinterSettings memory settings = MerkleReserveMinter.MerkleMinterSettings({
mintStart: 0,
mintEnd: uint64(block.timestamp + 1000),
pricePerToken: 0.5 ether,
merkleRoot: root
});

vm.prank(address(founder));
minter.setMintSettings(address(token), settings);

TokenTypesV2.MinterParams memory params = TokenTypesV2.MinterParams({ minter: address(minter), allowed: true });
TokenTypesV2.MinterParams[] memory minters = new TokenTypesV2.MinterParams[](1);
minters[0] = params;
vm.prank(address(founder));
token.updateMinters(minters);

bytes32[] memory proof1 = new bytes32[](1);
proof1[0] = bytes32(0xd77d6d8eeae66a03ce8ecdba82c6a0ce9cff76f7a4a6bc2bdc670680d3714273);

bytes32[] memory proof2 = new bytes32[](1);
proof2[0] = bytes32(0x1845cf6ae7e4ea2bf7813e2b8bc2c114d32bd93817b2f113543c4e0ebc1f38d2);

MerkleReserveMinter.MerkleClaim[] memory claims = new MerkleReserveMinter.MerkleClaim[](2);
claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 });
claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 });

uint256 fees = minter.getTotalFeesForMint(address(token), claims.length);

vm.prank(address(minter));
token.mintFromReserveTo(claimer1, 5);

vm.deal(claimer1, fees);
vm.prank(claimer1);
vm.expectRevert(abi.encodeWithSignature("ERROR_MINTING_TOKEN(uint256)", 5));
minter.mintFromReserve{ value: fees }(address(token), claims);
}

function testRevert_InvalidValue() public {
deployAltMock(20);

Expand Down Expand Up @@ -236,10 +394,14 @@ contract MerkleReserveMinterTest is NounsBuilderTest {
claims[0] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer1, tokenId: 5, merkleProof: proof1 });
claims[1] = MerkleReserveMinter.MerkleClaim({ mintTo: claimer2, tokenId: 6, merkleProof: proof2 });

vm.deal(claimer1, 1 ether);
vm.deal(claimer1, 2.5 ether);
vm.prank(claimer1);
vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()"));
minter.mintFromReserve{ value: 0.5 ether }(address(token), claims);

vm.prank(claimer1);
vm.expectRevert(abi.encodeWithSignature("INVALID_VALUE()"));
minter.mintFromReserve{ value: 2 ether }(address(token), claims);
}

function testRevert_MintNotStarted() public {
Expand Down

0 comments on commit fc48915

Please sign in to comment.