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

Token gated minter #6

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
89 changes: 89 additions & 0 deletions src/token-gated-minter/TokenGatedMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import {IERC165} from "forge-std/interfaces/IERC165.sol";
import {IERC721} from "forge-std/interfaces/IERC721.sol";
import {IERC721Drop} from "zora-drops-contracts/interfaces/IERC721Drop.sol";

contract TokenGatedMinter {
struct TokenGate {
uint256 mintPrice;
uint256 mintLimitPerToken;
}

// token gate contract address => token gate details
mapping(address => TokenGate) public tokenGates;

// token gate address => token gate tokenId => was used for minting
mapping(address => mapping(uint256 => bool)) public tokenUsedForMinting;

address payable public immutable mintingToken;

modifier onlyTokenAdmin() {
require(
jgeary marked this conversation as resolved.
Show resolved Hide resolved
IERC721Drop(mintingToken).isAdmin(msg.sender),
"TokenGatedMinter: not token admin"
);
_;
}

constructor(address payable _mintingToken) {
mintingToken = _mintingToken;
jgeary marked this conversation as resolved.
Show resolved Hide resolved
}

function setTokenGate(
address _token,
uint256 _mintPrice,
uint256 _mintLimitPerToken
) external onlyTokenAdmin {
if (_token == address(0)) {
return;
jgeary marked this conversation as resolved.
Show resolved Hide resolved
}
if (_mintLimitPerToken == 0) {
delete tokenGates[_token];
jgeary marked this conversation as resolved.
Show resolved Hide resolved
return;
}
tokenGates[_token] = TokenGate({
mintPrice: _mintPrice,
mintLimitPerToken: _mintLimitPerToken
});
}

function mintWithAllowedTokens(
address _tokenGate,
uint256 _amountToMint,
jgeary marked this conversation as resolved.
Show resolved Hide resolved
uint256[] calldata _tokenIds
) external payable {
require(_amountToMint > 0, "TokenGatedMinter: must mint at least 1");
require(_tokenIds.length > 0, "TokenGatedMinter: must provide tokens");
require(
_amountToMint <=
_tokenIds.length * tokenGates[_tokenGate].mintLimitPerToken,
"TokenGatedMinter: mint limit exceeded"
);
require(
_amountToMint >
(_tokenIds.length - 1) *
tokenGates[_tokenGate].mintLimitPerToken,
"TokenGatedMinter: too many tokens provided"
);
require(
msg.value == _amountToMint * tokenGates[_tokenGate].mintPrice,
"TokenGatedMinter: wrong price"
);
for (uint256 i = 0; i < _tokenIds.length; i++) {
require(
!tokenUsedForMinting[_tokenGate][_tokenIds[i]],
"TokenGatedMinter: token already used to mint"
);
require(
IERC721(_tokenGate).ownerOf(_tokenIds[i]) == msg.sender,
"TokenGatedMinter: not token owner"
);
tokenUsedForMinting[_tokenGate][_tokenIds[i]] = true;
}
IERC721Drop(mintingToken).adminMint(msg.sender, _amountToMint);
jgeary marked this conversation as resolved.
Show resolved Hide resolved
(bool sent, ) = mintingToken.call{value: msg.value}("");
require(sent, "TokenGatedMinter: failed to send ether");
}
}
190 changes: 190 additions & 0 deletions test/token-gated-minter/TokenGatedMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "forge-std/Test.sol";
import {ERC20PresetMinterPauser} from "@openzeppelin/contracts/token/ERC20/presets/ERC20PresetMinterPauser.sol";
import {ERC721PresetMinterPauserAutoId} from "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";
import {IERC721Drop} from "zora-drops-contracts/interfaces/IERC721Drop.sol";
import {ERC721Drop} from "zora-drops-contracts/ERC721Drop.sol";
import {ERC721DropProxy} from "zora-drops-contracts/ERC721DropProxy.sol";
import {FactoryUpgradeGate} from "zora-drops-contracts/FactoryUpgradeGate.sol";
import {IZoraFeeManager} from "zora-drops-contracts/interfaces/IZoraFeeManager.sol";

import {TokenGatedMinter} from "../../src/token-gated-minter/TokenGatedMinter.sol";
import {MockRenderer} from "../utils/MockRenderer.sol";

contract TokenGatedMinterModuleTest is Test {
using stdStorage for StdStorage;

ERC721Drop impl;
ERC721Drop drop;
TokenGatedMinter minter;

address DROP_OWNER = address(0x1);

function setUp() public {
impl = new ERC721Drop(
IZoraFeeManager(address(0x0)),
address(0x0),
FactoryUpgradeGate(address(0x0))
);
}

modifier withDropAndTokenGatedMinter() {
MockRenderer mockRenderer = new MockRenderer();

drop = ERC721Drop(
payable(
address(
new ERC721DropProxy(
address(impl),
abi.encodeWithSelector(
ERC721Drop.initialize.selector,
"Source NFT",
"SRC",
DROP_OWNER,
DROP_OWNER,
10,
10,
IERC721Drop.SalesConfiguration({
publicSaleStart: 0,
publicSaleEnd: 0,
presaleStart: 0,
presaleEnd: 0,
publicSalePrice: 0,
maxSalePurchasePerAddress: 0,
presaleMerkleRoot: 0x0
}),
mockRenderer,
""
)
)
)
)
);
minter = new TokenGatedMinter(payable(address(drop)));

vm.startPrank(DROP_OWNER);
drop.grantRole(drop.MINTER_ROLE(), address(minter));

vm.stopPrank();

_;
}

function test_onlyAdminCanSetOrDeleteTokenGate()
public
withDropAndTokenGatedMinter
{
ERC721PresetMinterPauserAutoId dummyToken = new ERC721PresetMinterPauserAutoId(
"Dummy",
"DUM",
""
);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: not token admin");
minter.setTokenGate(address(dummyToken), 0.1 ether, 2);

vm.startPrank(DROP_OWNER);
minter.setTokenGate(address(dummyToken), 0.1 ether, 2);
(uint256 mintPrice, uint256 mintLimitPerToken) = minter.tokenGates(
address(dummyToken)
);

assertEq(mintPrice, 0.1 ether);
assertEq(mintLimitPerToken, 2);

drop.grantRole(drop.DEFAULT_ADMIN_ROLE(), address(0x1234));
vm.stopPrank();

vm.prank(address(0x1234));
minter.setTokenGate(address(dummyToken), 0.2 ether, 0);
(mintPrice, mintLimitPerToken) = minter.tokenGates(address(dummyToken));
assertEq(mintPrice, 0);
assertEq(mintLimitPerToken, 0);
}

function test_mintWithAllowedTokens() public withDropAndTokenGatedMinter {
ERC721PresetMinterPauserAutoId dummyToken = new ERC721PresetMinterPauserAutoId(
"Dummy",
"DUM",
""
);
dummyToken.mint(address(0x1234));
dummyToken.mint(address(0x1234));
vm.deal(address(0x1234), 1 ether);

vm.prank(DROP_OWNER);
minter.setTokenGate(address(dummyToken), 0.1 ether, 2);

uint256[] memory tokenIds = new uint256[](2);
tokenIds[0] = 0;
tokenIds[1] = 1;
vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: must mint at least 1");
minter.mintWithAllowedTokens(address(dummyToken), 0, tokenIds);

uint256[] memory emptyTokenIds = new uint256[](0);
vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: must provide tokens");
minter.mintWithAllowedTokens{value: 0.1 ether}(
address(dummyToken),
1,
emptyTokenIds
);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: mint limit exceeded");
minter.mintWithAllowedTokens{value: 0.5 ether}(
address(dummyToken),
5,
tokenIds
);

uint256[] memory tooManyTokenIds = new uint256[](5);
vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: too many tokens provided");
minter.mintWithAllowedTokens{value: 0.1 ether}(
address(dummyToken),
1,
tooManyTokenIds
);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: wrong price");
minter.mintWithAllowedTokens{value: 0.3 ether}(
address(dummyToken),
4,
tokenIds
);

dummyToken.mint(address(0x999));
tokenIds[1] = 2;
vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: not token owner");
minter.mintWithAllowedTokens{value: 0.4 ether}(
address(dummyToken),
4,
tokenIds
);
tokenIds[1] = 1;

vm.prank(address(0x1234));
minter.mintWithAllowedTokens{value: 0.4 ether}(
address(dummyToken),
4,
tokenIds
);
assertEq(drop.balanceOf(address(0x1234)), 4);
assertEq(address(drop).balance, 0.4 ether);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: token already used to mint");
minter.mintWithAllowedTokens{value: 0.4 ether}(
address(dummyToken),
4,
tokenIds
);
}
}