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
82 changes: 82 additions & 0 deletions src/token-gated-minter/TokenGatedMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

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

contract TokenGatedMinter {
struct TokenGate {
uint256 amount;
jgeary marked this conversation as resolved.
Show resolved Hide resolved
uint256 mintPrice;
uint256 mintLimit;
}

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

// minter address => token gate contract address => has minted
mapping(address => mapping(address => bool)) public hasMintedWithTokenGate;

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 _amount,
uint256 _mintPrice,
uint256 _mintLimit
) external onlyTokenAdmin {
if (_token == address(0)) {
return;
jgeary marked this conversation as resolved.
Show resolved Hide resolved
}
if (_amount == 0 || _mintLimit == 0) {
delete tokenGates[_token];
jgeary marked this conversation as resolved.
Show resolved Hide resolved
return;
}
tokenGates[_token] = TokenGate({
amount: _amount,
mintPrice: _mintPrice,
mintLimit: _mintLimit
});
}

function mintWithAllowedToken(address _tokenGate, uint256 _amountToMint)
external
payable
{
require(_amountToMint > 0, "TokenGatedMinter: must mint at least 1");
require(
_amountToMint <= tokenGates[_tokenGate].mintLimit,
"TokenGatedMinter: mint limit exceeded"
);
require(
msg.value == _amountToMint * tokenGates[_tokenGate].mintPrice,
"TokenGatedMinter: wrong price"
);
require(
ITokenBalance(_tokenGate).balanceOf(msg.sender) >=
tokenGates[_tokenGate].amount,
"TokenGatedMinter: token gate not met"
);
require(
!hasMintedWithTokenGate[msg.sender][_tokenGate],
jgeary marked this conversation as resolved.
Show resolved Hide resolved
"TokenGatedMinter: already minted"
);
hasMintedWithTokenGate[msg.sender][_tokenGate] = 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");
}
}
6 changes: 6 additions & 0 deletions src/utils/ITokenBalance.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

interface ITokenBalance {
function balanceOf(address account) external view returns (uint256);
}
204 changes: 204 additions & 0 deletions test/token-gated-minter/TokenGatedMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
// 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
{
ERC20PresetMinterPauser dummyToken = new ERC20PresetMinterPauser(
"Dummy",
"DUM"
);

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

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

assertEq(amount, 100);
assertEq(mintPrice, 0.1 ether);
assertEq(mintLimit, 2);

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

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

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

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

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: must mint at least 1");
minter.mintWithAllowedToken(address(dummyToken), 0);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: mint limit exceeded");
minter.mintWithAllowedToken{value: 0.3 ether}(address(dummyToken), 3);

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

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: token gate not met");
minter.mintWithAllowedToken{value: 0.2 ether}(address(dummyToken), 2);

dummyToken.mint(address(0x1234), 1);
vm.prank(address(0x1234));
minter.mintWithAllowedToken{value: 0.2 ether}(address(dummyToken), 2);

assertEq(
drop.balanceOf(address(0x1234)),
2,
"Should have minted 2 tokens for address(0x1234)"
);

assertEq(
address(drop).balance,
0.2 ether,
"Should have received 0.2 ether"
);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: already minted");
minter.mintWithAllowedToken{value: 0.1 ether}(address(dummyToken), 1);
}

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

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

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: must mint at least 1");
minter.mintWithAllowedToken(address(dummyToken), 0);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: mint limit exceeded");
minter.mintWithAllowedToken{value: 0.3 ether}(address(dummyToken), 3);

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

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: token gate not met");
minter.mintWithAllowedToken{value: 0.2 ether}(address(dummyToken), 2);

dummyToken.mint(address(0x1234));
vm.prank(address(0x1234));
minter.mintWithAllowedToken{value: 0.2 ether}(address(dummyToken), 2);

assertEq(
drop.balanceOf(address(0x1234)),
2,
"Should have minted 2 tokens for address(0x1234)"
);

assertEq(
address(drop).balance,
0.2 ether,
"Should have received 0.2 ether"
);

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: already minted");
minter.mintWithAllowedToken{value: 0.1 ether}(address(dummyToken), 1);
}
}