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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
cache/
.DS_Store
out/
broadcast/

.env
.env*
!.env.example
!.env.example
18 changes: 18 additions & 0 deletions script/token-gated-minter/DeployTokenGatedMinter.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "forge-std/Script.sol";

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

contract DeployTokenGatedMinter is Script {
function run() external {
uint256 key = vm.envUint("PRIVATE_KEY");

vm.startBroadcast(key);

new TokenGatedMinter();

vm.stopBroadcast();
}
}
110 changes: 110 additions & 0 deletions src/token-gated-minter/TokenGatedMinter.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 TokenGateDetails {
uint256 mintPrice;
uint256 maxQuantityPerGateToken;
}

// mint address => gate address => token gate details
mapping(address => mapping(address => TokenGateDetails)) public tokenGates;

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

event TokenGateUpdated(
address mintToken,
address gateToken,
uint256 mintPrice,
uint256 maxQuantityPerGateToken
);

event MintedUsingGatedTokens(
address recipient,
address mintToken,
uint256 numMinted,
address gateToken,
uint256[] gateTokenIds
);

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

function setTokenGate(
address _mintToken,
address _gateToken,
uint256 _mintPrice,
uint256 _maxQuantityPerGateToken
) external onlyTokenAdmin(_mintToken) {
require(_gateToken != address(0), "TokenGatedMinter: zero address");
tokenGates[_mintToken][_gateToken] = TokenGateDetails({
mintPrice: _mintPrice,
maxQuantityPerGateToken: _maxQuantityPerGateToken
});
emit TokenGateUpdated(
_mintToken,
_gateToken,
_mintPrice,
_maxQuantityPerGateToken
);
}

function mintWithGatedTokens(
address _mintToken,
address _gateToken,
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[_mintToken][_gateToken].maxQuantityPerGateToken,
"TokenGatedMinter: mint limit exceeded"
);
require(
_amountToMint >
(_tokenIds.length - 1) *
tokenGates[_mintToken][_gateToken].maxQuantityPerGateToken,
"TokenGatedMinter: too many gate tokens provided for mint amount"
);
require(
msg.value ==
_amountToMint * tokenGates[_mintToken][_gateToken].mintPrice,
"TokenGatedMinter: incorrect ETH amount sent"
);
for (uint256 i = 0; i < _tokenIds.length; i++) {
require(
!tokenWasUsedToMint[_mintToken][_gateToken][_tokenIds[i]],
"TokenGatedMinter: token already used to mint"
);
require(
IERC721(_gateToken).ownerOf(_tokenIds[i]) == msg.sender,
"TokenGatedMinter: not token owner"
);
tokenWasUsedToMint[_mintToken][_gateToken][_tokenIds[i]] = true;
}
emit MintedUsingGatedTokens(
msg.sender,
_mintToken,
_amountToMint,
_gateToken,
_tokenIds
);
IERC721Drop(_mintToken).adminMint(msg.sender, _amountToMint);
(bool sent, ) = _mintToken.call{value: msg.value}("");
require(sent, "TokenGatedMinter: failed to send ether");
}
}
208 changes: 208 additions & 0 deletions test/token-gated-minter/TokenGatedMinter.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
// 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();

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(drop), address(dummyToken), 0.1 ether, 2);

vm.startPrank(DROP_OWNER);
minter.setTokenGate(address(drop), address(dummyToken), 0.1 ether, 2);
(uint256 mintPrice, uint256 mintLimitPerToken) = minter.tokenGates(
address(drop),
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(drop), address(dummyToken), 0.2 ether, 0);
(mintPrice, mintLimitPerToken) = minter.tokenGates(
address(drop),
address(dummyToken)
);
assertEq(mintPrice, 0.2 ether);
assertEq(mintLimitPerToken, 0);
}

function test_mintWithGatedTokens() 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(drop), 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.mintWithGatedTokens(
address(drop),
address(dummyToken),
0,
tokenIds
);

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

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

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

vm.prank(address(0x1234));
vm.expectRevert("TokenGatedMinter: incorrect ETH amount sent");
minter.mintWithGatedTokens{value: 0.3 ether}(
address(drop),
address(dummyToken),
4,
tokenIds
);

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

vm.prank(address(0x1234));
minter.mintWithGatedTokens{value: 0.4 ether}(
address(drop),
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.mintWithGatedTokens{value: 0.4 ether}(
address(drop),
address(dummyToken),
4,
tokenIds
);
}
}