diff --git a/packages/taikoon/contracts/IMinimalBlacklist.sol b/packages/taikoon/contracts/IMinimalBlacklist.sol new file mode 100644 index 0000000000..af184cd430 --- /dev/null +++ b/packages/taikoon/contracts/IMinimalBlacklist.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/// @title Minimal Blacklist Interface +/// @dev Mainnet blacklist: 0x97044531D0fD5B84438499A49629488105Dc58e6 +interface IMinimalBlacklist { + function isBlacklisted(address _account) external view returns (bool); +} diff --git a/packages/taikoon/contracts/MerkleWhitelist.sol b/packages/taikoon/contracts/MerkleWhitelist.sol index 5c50b0759a..e88881354c 100644 --- a/packages/taikoon/contracts/MerkleWhitelist.sol +++ b/packages/taikoon/contracts/MerkleWhitelist.sol @@ -8,6 +8,7 @@ import { Ownable2StepUpgradeable } from import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; import { ContextUpgradeable } from "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; +import { IMinimalBlacklist } from "./IMinimalBlacklist.sol"; /// @title MerkleWhitelist /// @dev Merkle Tree Whitelist @@ -19,12 +20,15 @@ contract MerkleWhitelist is ContextUpgradeable, UUPSUpgradeable, Ownable2StepUpg error MINTS_EXCEEDED(); error INVALID_PROOF(); error INVALID_TOKEN_AMOUNT(); + error ADDRESS_BLACKLISTED(); /// @notice Merkle Tree Root bytes32 public root; /// @notice Tracker for minted leaves mapping(bytes32 leaf => bool hasMinted) public minted; - + /// @notice Blackist address + IMinimalBlacklist public blacklist; + /// @notice Gap for upgrade safety uint256[48] private __gap; /// @custom:oz-upgrades-unsafe-allow constructor @@ -32,10 +36,23 @@ contract MerkleWhitelist is ContextUpgradeable, UUPSUpgradeable, Ownable2StepUpg _disableInitializers(); } + /// @notice Update the blacklist address + /// @param _blacklist The new blacklist address + function updateBlacklist(IMinimalBlacklist _blacklist) external onlyOwner { + blacklist = _blacklist; + } + /// @notice Contract initializer /// @param _root Merkle Tree root - function initialize(address _owner, bytes32 _root) external initializer { - __MerkleWhitelist_init(_owner, _root); + function initialize( + address _owner, + bytes32 _root, + IMinimalBlacklist _blacklist + ) + external + initializer + { + __MerkleWhitelist_init(_owner, _root, _blacklist); } /// @notice Check if a wallet can free mint @@ -43,6 +60,7 @@ contract MerkleWhitelist is ContextUpgradeable, UUPSUpgradeable, Ownable2StepUpg /// @param _maxMints Max amount of free mints /// @return Whether the wallet can mint function canMint(address _minter, uint256 _maxMints) public view returns (bool) { + if (blacklist.isBlacklisted(_minter)) revert ADDRESS_BLACKLISTED(); bytes32 _leaf = leaf(_minter, _maxMints); return !minted[_leaf]; } @@ -57,10 +75,18 @@ contract MerkleWhitelist is ContextUpgradeable, UUPSUpgradeable, Ownable2StepUpg /// @notice Internal initializer /// @param _root Merkle Tree root - function __MerkleWhitelist_init(address _owner, bytes32 _root) internal initializer { + function __MerkleWhitelist_init( + address _owner, + bytes32 _root, + IMinimalBlacklist _blacklist + ) + internal + initializer + { _transferOwnership(_owner == address(0) ? msg.sender : _owner); __Context_init(); root = _root; + blacklist = _blacklist; } /// @notice Update the merkle tree's root diff --git a/packages/taikoon/contracts/TaikoonToken.sol b/packages/taikoon/contracts/TaikoonToken.sol index bde8faa689..fb0de495e1 100644 --- a/packages/taikoon/contracts/TaikoonToken.sol +++ b/packages/taikoon/contracts/TaikoonToken.sol @@ -7,6 +7,7 @@ import { ERC721EnumerableUpgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721EnumerableUpgradeable.sol"; import { MerkleWhitelist } from "./MerkleWhitelist.sol"; +import { IMinimalBlacklist } from "./IMinimalBlacklist.sol"; /// @title TaikoonToken /// @dev The Taikoons ERC-721 token @@ -17,6 +18,7 @@ contract TaikoonToken is ERC721EnumerableUpgradeable, MerkleWhitelist { // Base URI required to interact with IPFS string private _baseURIExtended; + /// @notice Gap for upgrade safety uint256[48] private __gap; error MAX_MINTS_EXCEEDED(); @@ -30,13 +32,14 @@ contract TaikoonToken is ERC721EnumerableUpgradeable, MerkleWhitelist { function initialize( address _owner, string memory _rootURI, - bytes32 _merkleRoot + bytes32 _merkleRoot, + IMinimalBlacklist _blacklistAddress ) external initializer { __ERC721_init("Taikoon", "TKOON"); - __MerkleWhitelist_init(_owner, _merkleRoot); + __MerkleWhitelist_init(_owner, _merkleRoot, _blacklistAddress); _baseURIExtended = _rootURI; } diff --git a/packages/taikoon/script/sol/Deploy.s.sol b/packages/taikoon/script/sol/Deploy.s.sol index b66028f57f..be749eca9f 100644 --- a/packages/taikoon/script/sol/Deploy.s.sol +++ b/packages/taikoon/script/sol/Deploy.s.sol @@ -43,7 +43,12 @@ contract DeployScript is Script { // deploy token with empty root address impl = address(new TaikoonToken()); address proxy = address( - new ERC1967Proxy(impl, abi.encodeCall(TaikoonToken.initialize, (owner, baseURI, root))) + new ERC1967Proxy( + impl, + abi.encodeCall( + TaikoonToken.initialize, (owner, baseURI, root, utils.getBlacklist()) + ) + ) ); TaikoonToken token = TaikoonToken(proxy); diff --git a/packages/taikoon/script/sol/Utils.s.sol b/packages/taikoon/script/sol/Utils.s.sol index 753e02b384..ed0a5c6d76 100644 --- a/packages/taikoon/script/sol/Utils.s.sol +++ b/packages/taikoon/script/sol/Utils.s.sol @@ -3,6 +3,8 @@ pragma solidity 0.8.24; import { Script, console } from "forge-std/src/Script.sol"; import "forge-std/src/StdJson.sol"; +import { IMinimalBlacklist } from "../../contracts/IMinimalBlacklist.sol"; +import { MockBlacklist } from "../../test/Blacklist.sol"; contract UtilsScript is Script { using stdJson for string; @@ -57,5 +59,17 @@ contract UtilsScript is Script { return vm.envString("IPFS_BASE_URI"); } + function getBlacklist() public returns (IMinimalBlacklist blacklistAddress) { + if (block.chainid == 1) { + // mainnet blacklist address + blacklistAddress = IMinimalBlacklist(0x97044531D0fD5B84438499A49629488105Dc58e6); + } else { + // deploy a mock blacklist otherwise + blacklistAddress = IMinimalBlacklist(new MockBlacklist()); + } + + return blacklistAddress; + } + function run() public { } } diff --git a/packages/taikoon/test/Blacklist.sol b/packages/taikoon/test/Blacklist.sol new file mode 100644 index 0000000000..05f74425d7 --- /dev/null +++ b/packages/taikoon/test/Blacklist.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { IMinimalBlacklist } from "../contracts/IMinimalBlacklist.sol"; +// Blacklist contract mock + +contract MockBlacklist is IMinimalBlacklist { + address[] public blacklist; + + constructor() { + // hardhat accounts, #5 to #9 + blacklist.push(0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc); + blacklist.push(0x976EA74026E726554dB657fA54763abd0C3a0aa9); + blacklist.push(0x14dC79964da2C08b23698B3D3cc7Ca32193d9955); + blacklist.push(0x23618e81E3f5cdF7f54C3d65f7FBc0aBf5B21E8f); + blacklist.push(0xa0Ee7A142d267C1f36714E4a8F75612F20a79720); + } + + function isBlacklisted(address _address) public view returns (bool) { + for (uint256 i = 0; i < blacklist.length; i++) { + if (blacklist[i] == _address) { + return true; + } + } + return false; + } +} diff --git a/packages/taikoon/test/MerkleWhitelist.t.sol b/packages/taikoon/test/MerkleWhitelist.t.sol index 60232521a8..8f6061f493 100644 --- a/packages/taikoon/test/MerkleWhitelist.t.sol +++ b/packages/taikoon/test/MerkleWhitelist.t.sol @@ -6,6 +6,7 @@ import "forge-std/src/StdJson.sol"; import { Merkle } from "murky/Merkle.sol"; import { MerkleWhitelist } from "../contracts/MerkleWhitelist.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { UtilsScript } from "../script/sol/Utils.s.sol"; /// @custom:oz-upgrades-from MerkleWhitelist contract MerkleWhitelistForTest is MerkleWhitelist { @@ -20,6 +21,7 @@ contract MerkleWhitelistForTest is MerkleWhitelist { contract MerkleWhitelistTest is Test { Merkle tree; + UtilsScript public utils; using stdJson for string; @@ -38,6 +40,8 @@ contract MerkleWhitelistTest is Test { address[3] minters = [address(0x1), address(0x2), address(0x3)]; function setUp() public { + utils = new UtilsScript(); + utils.setUp(); vm.startBroadcast(owner); tree = new Merkle(); @@ -51,7 +55,10 @@ contract MerkleWhitelistTest is Test { address impl = address(new MerkleWhitelistForTest()); address proxy = address( - new ERC1967Proxy(impl, abi.encodeCall(MerkleWhitelist.initialize, (address(0), root))) + new ERC1967Proxy( + impl, + abi.encodeCall(MerkleWhitelist.initialize, (address(0), root, utils.getBlacklist())) + ) ); whitelist = MerkleWhitelistForTest(proxy); @@ -113,4 +120,11 @@ contract MerkleWhitelistTest is Test { vm.stopBroadcast(); } + + function test_revert_canMint_blacklisted() public { + address blacklisted = + vm.addr(0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6); + vm.expectRevert(); + whitelist.canMint(blacklisted, MAX_MINTS); + } } diff --git a/packages/taikoon/test/TaikoonToken.t.sol b/packages/taikoon/test/TaikoonToken.t.sol index 5dadd1c422..d7286a6681 100644 --- a/packages/taikoon/test/TaikoonToken.t.sol +++ b/packages/taikoon/test/TaikoonToken.t.sol @@ -6,8 +6,11 @@ import { Test } from "forge-std/src/Test.sol"; import { TaikoonToken } from "../contracts/TaikoonToken.sol"; import { Merkle } from "murky/Merkle.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { UtilsScript } from "../script/sol/Utils.s.sol"; contract TaikoonTokenTest is Test { + UtilsScript public utils; + TaikoonToken public token; address public owner = vm.addr(0x5); @@ -20,6 +23,8 @@ contract TaikoonTokenTest is Test { Merkle tree = new Merkle(); function setUp() public { + utils = new UtilsScript(); + utils.setUp(); // create whitelist merkle tree vm.startBroadcast(owner); bytes32 root = tree.getRoot(leaves); @@ -28,7 +33,10 @@ contract TaikoonTokenTest is Test { address impl = address(new TaikoonToken()); address proxy = address( new ERC1967Proxy( - impl, abi.encodeCall(TaikoonToken.initialize, (address(0), "ipfs://", root)) + impl, + abi.encodeCall( + TaikoonToken.initialize, (address(0), "ipfs://", root, utils.getBlacklist()) + ) ) ); @@ -97,4 +105,14 @@ contract TaikoonTokenTest is Test { assertEq(token.balanceOf(owner), 5); assertEq(tokenIds.length, 5); } + + function test_revert_mint_blacklisted() public { + address blacklisted = + vm.addr(0x2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6); + vm.startBroadcast(blacklisted); + bytes32[] memory fakeProof = tree.getProof(leaves, 0); + vm.expectRevert(); + token.mint(fakeProof, MAX_MINTS); + vm.stopBroadcast(); + } } diff --git a/packages/taikoon/test/Upgradeable.t.sol b/packages/taikoon/test/Upgradeable.t.sol index 0f2b42d87c..b67f0875f5 100644 --- a/packages/taikoon/test/Upgradeable.t.sol +++ b/packages/taikoon/test/Upgradeable.t.sol @@ -6,12 +6,15 @@ import { TaikoonToken } from "../contracts/TaikoonToken.sol"; import { Merkle } from "murky/Merkle.sol"; import { MerkleMintersScript } from "../script/sol/MerkleMinters.s.sol"; import "forge-std/src/StdJson.sol"; +import { UtilsScript } from "../script/sol/Utils.s.sol"; import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; contract UpgradeableTest is Test { using stdJson for string; + UtilsScript public utils; + TaikoonToken public token; address public owner = vm.addr(0x5); @@ -26,6 +29,8 @@ contract UpgradeableTest is Test { Merkle tree = new Merkle(); function setUp() public { + utils = new UtilsScript(); + utils.setUp(); // create whitelist merkle tree vm.startPrank(owner); bytes32 root = tree.getRoot(leaves); @@ -34,7 +39,10 @@ contract UpgradeableTest is Test { address impl = address(new TaikoonToken()); address proxy = address( new ERC1967Proxy( - impl, abi.encodeCall(TaikoonToken.initialize, (address(0), "ipfs://", root)) + impl, + abi.encodeCall( + TaikoonToken.initialize, (address(0), "ipfs://", root, utils.getBlacklist()) + ) ) );