From 8348ca80ded9ee765afcf687d04440c9614dfe0e Mon Sep 17 00:00:00 2001 From: Bernat Canal Garceran Date: Thu, 3 Oct 2024 16:43:32 +0200 Subject: [PATCH] feat(nfts): tbz s1 claim contracts (#18192) Co-authored-by: Daniel Wang <99078276+dantaik@users.noreply.github.com> --- .../trailblazers-airdrop/ERC20Airdrop.sol | 99 ++++++++++++ .../trailblazers-airdrop/MerkleClaimable.sol | 138 +++++++++++++++++ .../trailblazers-airdrop/hekla.json | 4 + .../trailblazers-airdrop/mainnet.json | 0 packages/nfts/package.json | 4 +- .../script/trailblazers-airdrop/Deploy.s.sol | 93 ++++++++++++ .../script/trailblazers-airdrop/Utils.s.sol | 78 ++++++++++ .../trailblazers-airdrop/ERC20Airdrop.t.sol | 141 ++++++++++++++++++ packages/nfts/test/util/MockTokens.sol | 15 ++ 9 files changed, 571 insertions(+), 1 deletion(-) create mode 100644 packages/nfts/contracts/trailblazers-airdrop/ERC20Airdrop.sol create mode 100644 packages/nfts/contracts/trailblazers-airdrop/MerkleClaimable.sol create mode 100644 packages/nfts/deployments/trailblazers-airdrop/hekla.json create mode 100644 packages/nfts/deployments/trailblazers-airdrop/mainnet.json create mode 100644 packages/nfts/script/trailblazers-airdrop/Deploy.s.sol create mode 100644 packages/nfts/script/trailblazers-airdrop/Utils.s.sol create mode 100644 packages/nfts/test/trailblazers-airdrop/ERC20Airdrop.t.sol diff --git a/packages/nfts/contracts/trailblazers-airdrop/ERC20Airdrop.sol b/packages/nfts/contracts/trailblazers-airdrop/ERC20Airdrop.sol new file mode 100644 index 0000000000..831abfad59 --- /dev/null +++ b/packages/nfts/contracts/trailblazers-airdrop/ERC20Airdrop.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/governance/utils/IVotes.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import "@openzeppelin/contracts-upgradeable/utils/PausableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@taiko/blacklist/IMinimalBlacklist.sol"; + +import "./MerkleClaimable.sol"; + +/// @title ERC20Airdrop +/// @notice Contract for managing Taiko token airdrop for eligible users. +/// @custom:security-contact security@taiko.xyz +contract ERC20Airdrop is MerkleClaimable, ReentrancyGuardUpgradeable, PausableUpgradeable { + using SafeERC20 for IERC20; + + /// @notice The address of the Taiko token contract. + IERC20 public token; + /// @notice Blackist address + IMinimalBlacklist public blacklist; + + /// @notice Event emitted when the blacklist is updated. + event BlacklistUpdated(address _blacklist); + /// @notice Errors + + error ADDRESS_BLACKLISTED(); + + uint256[48] private __gap; + + /// @notice Modifier to check if the address is not blacklisted. + /// @param _address The address to check. + modifier isNotBlacklisted(address _address) { + if (blacklist.isBlacklisted(_address)) revert ADDRESS_BLACKLISTED(); + _; + } + + /// @notice Initializes the contract. + /// @param _owner The owner of this contract. + /// @param _claimStart The start time of the claim period. + /// @param _claimEnd The end time of the claim period. + /// @param _merkleRoot The merkle root. + /// @param _token The address of the token contract. + function init( + address _owner, + uint64 _claimStart, + uint64 _claimEnd, + bytes32 _merkleRoot, + IERC20 _token, + address _blacklist + ) + external + initializer + { + __ReentrancyGuard_init(); + __Pausable_init(); + __MerkleClaimable_init(_claimStart, _claimEnd, _merkleRoot); + _transferOwnership(_owner == address(0) ? _msgSender() : _owner); + blacklist = IMinimalBlacklist(_blacklist); + token = _token; + } + + /// @notice Claims the airdrop for the user. + /// @param user The address of the user. + /// @param amount The amount of tokens to claim. + /// @param proof The merkle proof. + function claim( + address user, + uint256 amount, + bytes32[] calldata proof + ) + external + nonReentrant + isNotBlacklisted(user) + { + // Check if this can be claimed + _verifyClaim(abi.encode(user, amount), proof); + + // Transfer the tokens from contract + IERC20(token).transfer(user, amount); + } + + /// @notice Withdraw ERC20 tokens from the Vault + /// @param _token The ERC20 token address to withdraw + /// @dev Only the owner can execute this function + function withdrawERC20(IERC20 _token) external onlyOwner { + // If token address is address(0), use token + if (address(_token) == address(0)) { + _token = token; + } + // Transfer the tokens to owner + _token.transfer(owner(), _token.balanceOf(address(this))); + } + + /// @notice Internal method to authorize an upgrade + function _authorizeUpgrade(address) internal virtual override onlyOwner { } +} diff --git a/packages/nfts/contracts/trailblazers-airdrop/MerkleClaimable.sol b/packages/nfts/contracts/trailblazers-airdrop/MerkleClaimable.sol new file mode 100644 index 0000000000..3396bf998c --- /dev/null +++ b/packages/nfts/contracts/trailblazers-airdrop/MerkleClaimable.sol @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; + +import { UUPSUpgradeable } from + "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; +import { Ownable2StepUpgradeable } from + "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol"; +import { MerkleProof } from "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; +import { ContextUpgradeable } from + "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; + +/// @title MerkleClaimable +/// @notice Contract for managing Taiko token airdrop for eligible users +/// @custom:security-contact security@taiko.xyz +abstract contract MerkleClaimable is + ContextUpgradeable, + UUPSUpgradeable, + Ownable2StepUpgradeable +{ + /// @notice Mapping of hashes and their claim status + mapping(bytes32 hash => bool claimed) public isClaimed; + + /// @notice Merkle root of the tree + bytes32 public merkleRoot; + + /// @notice Unix timestamp for claim start + uint64 public claimStart; + + /// @notice Unix timestamp for claim end + uint64 public claimEnd; + + uint256[47] private __gap; + + /// @notice Event emitted when a claim is made + /// @param hash Hash of the claim + event Claimed(bytes32 hash); + + /// @notice Event emitted when config is changed + /// @param claimStart Unix timestamp for claim start + /// @param claimEnd Unix timestamp for claim end + /// @param merkleRoot Merkle root of the tree + event ConfigChanged(uint64 claimStart, uint64 claimEnd, bytes32 merkleRoot); + + /// @notice Errors + error CLAIM_NOT_ONGOING(); + error CLAIMED_ALREADY(); + error INVALID_PARAMS(); + error INVALID_PROOF(); + + /// @notice Modifier to check if the claim is ongoing + modifier ongoingClaim() { + if ( + merkleRoot == 0x0 || claimStart == 0 || claimEnd == 0 || claimStart > block.timestamp + || claimEnd < block.timestamp + ) revert CLAIM_NOT_ONGOING(); + _; + } + + /// @notice Set config parameters + /// @param _claimStart Unix timestamp for claim start + /// @param _claimEnd Unix timestamp for claim end + /// @param _merkleRoot Merkle root of the tree + function setConfig( + uint64 _claimStart, + uint64 _claimEnd, + bytes32 _merkleRoot + ) + external + onlyOwner + { + _setConfig(_claimStart, _claimEnd, _merkleRoot); + } + + /// @notice Initialize the contract + /// @param _claimStart Unix timestamp for claim start + /// @param _claimEnd Unix timestamp for claim end + /// @param _merkleRoot Merkle root of the tree + function __MerkleClaimable_init( + uint64 _claimStart, + uint64 _claimEnd, + bytes32 _merkleRoot + ) + internal + onlyInitializing + { + __Context_init(); + _setConfig(_claimStart, _claimEnd, _merkleRoot); + } + + /// @notice Verify an airdrop claim + /// @param data Data to be hashed + /// @param proof Merkle proof + function _verifyClaim(bytes memory data, bytes32[] calldata proof) internal ongoingClaim { + bytes32 hash = keccak256(abi.encode("CLAIM_TAIKO_AIRDROP", data)); + + if (isClaimed[hash]) revert CLAIMED_ALREADY(); + if (!_verifyMerkleProof(proof, merkleRoot, hash)) revert INVALID_PROOF(); + + isClaimed[hash] = true; + emit Claimed(hash); + } + + /// @notice Verify a Merkle proof + /// @param _proof Merkle proof + /// @param _merkleRoot Merkle root + /// @param _value Value to verify + /// @return Whether the proof is valid + function _verifyMerkleProof( + bytes32[] calldata _proof, + bytes32 _merkleRoot, + bytes32 _value + ) + internal + pure + virtual + returns (bool) + { + return MerkleProof.verify(_proof, _merkleRoot, _value); + } + + /// @notice Set config parameters + /// @param _claimStart Unix timestamp for claim start + /// @param _claimEnd Unix timestamp for claim end + /// @param _merkleRoot Merkle root of the tree + function _setConfig(uint64 _claimStart, uint64 _claimEnd, bytes32 _merkleRoot) private { + if (_claimStart > _claimEnd) revert INVALID_PARAMS(); + + claimStart = _claimStart; + claimEnd = _claimEnd; + merkleRoot = _merkleRoot; + emit ConfigChanged(_claimStart, _claimEnd, _merkleRoot); + } + + /// @notice Internal method to authorize an upgrade + function _authorizeUpgrade(address) internal virtual override onlyOwner { } +} diff --git a/packages/nfts/deployments/trailblazers-airdrop/hekla.json b/packages/nfts/deployments/trailblazers-airdrop/hekla.json new file mode 100644 index 0000000000..b0417106a8 --- /dev/null +++ b/packages/nfts/deployments/trailblazers-airdrop/hekla.json @@ -0,0 +1,4 @@ +{ + "ERC20Airdrop": "0x42DB7bE491a8933FaADbca4891dA2632D45e5CfC", + "MerkleRoot": "0xea5b2299e76b4860965e9059388d021145269c96b816b07a808ff391cd80753e" +} diff --git a/packages/nfts/deployments/trailblazers-airdrop/mainnet.json b/packages/nfts/deployments/trailblazers-airdrop/mainnet.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/nfts/package.json b/packages/nfts/package.json index f71dee9342..e0feeef624 100644 --- a/packages/nfts/package.json +++ b/packages/nfts/package.json @@ -32,7 +32,9 @@ "kbw:deploy:mainnet": "forge clean && pnpm compile && forge script script/party-ticket/sol/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --legacy --with-gas-price 30 ", "kbw:upgradeV2:hekla": "forge clean && pnpm compile && forge script script/party-ticket/sol/UpgradeV2.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", "kbw:upgradeV2:mainnet": "forge clean && pnpm compile && forge script script/party-ticket/sol/UpgradeV2.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast", - "pfp:deploy:hekla": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200" + "pfp:deploy:hekla": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "pfp:deploy:mainnet": "forge clean && pnpm compile && forge script script/profile/Deploy.s.sol --rpc-url https://rpc.mainnet.taiko.xyz --broadcast --gas-estimate-multiplier 200", + "tbz:airdrop:hekla": "forge clean && pnpm compile && forge script script/trailblazers-airdrop/Deploy.s.sol --rpc-url https://rpc.hekla.taiko.xyz --broadcast --gas-estimate-multiplier 200" }, "devDependencies": { "@types/node": "^20.11.30", diff --git a/packages/nfts/script/trailblazers-airdrop/Deploy.s.sol b/packages/nfts/script/trailblazers-airdrop/Deploy.s.sol new file mode 100644 index 0000000000..663ef57e3d --- /dev/null +++ b/packages/nfts/script/trailblazers-airdrop/Deploy.s.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { UtilsScript } from "./Utils.s.sol"; +import { Script, console } from "forge-std/src/Script.sol"; +import { Merkle } from "murky/Merkle.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { TrailblazersBadges } from "../../contracts/trailblazers-badges/TrailblazersBadges.sol"; +import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; +import { ERC20Airdrop } from "../../contracts/trailblazers-airdrop/ERC20Airdrop.sol"; +import { ERC20Mock } from "../../test/util/MockTokens.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; +import { MockBlacklist } from "../../test/util/Blacklist.sol"; + +contract DeployScript is Script { + UtilsScript public utils; + string public jsonLocation; + uint256 public deployerPrivateKey; + address public deployerAddress; + + // only used for production + IMinimalBlacklist blacklist = IMinimalBlacklist(0xe61E9034b5633977eC98E302b33e321e8140F105); + + ERC20Airdrop public airdrop; + uint256 constant TOTAL_AVAILABLE_FUNDS = 1000 ether; + + uint256 constant CLAIM_AMOUNT = 10 ether; + + // hekla test root + bytes32 public merkleRoot = 0xea5b2299e76b4860965e9059388d021145269c96b816b07a808ff391cd80753e; + + // rewards token + ERC20Upgradeable public erc20; + ERC20Mock public mockERC20; + // start and end times for the claim + uint64 constant CLAIM_DURATION = 1 days; + uint64 public CLAIM_START = uint64(block.timestamp); + uint64 public CLAIM_END = CLAIM_START + CLAIM_DURATION; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + + jsonLocation = utils.getContractJsonLocation(); + deployerPrivateKey = utils.getPrivateKey(); + deployerAddress = utils.getAddress(); + + vm.startBroadcast(deployerPrivateKey); + + if (block.chainid != 167_000) { + // not mainnet, create mock contracts + blacklist = new MockBlacklist(); + mockERC20 = new ERC20Mock(); + // mint the necessary funds + erc20 = ERC20Upgradeable(address(mockERC20)); + } + + vm.stopBroadcast(); + } + + function run() public { + string memory jsonRoot = "root"; + + vm.startBroadcast(deployerPrivateKey); + + // deploy token with empty root + address impl = address(new ERC20Airdrop()); + address proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + ERC20Airdrop.init, + (deployerAddress, CLAIM_START, CLAIM_END, merkleRoot, erc20, address(blacklist)) + ) + ) + ); + + airdrop = ERC20Airdrop(proxy); + + // mint the necessary funds on hekla + if (block.chainid != 167_000) { + mockERC20.mint(address(airdrop), TOTAL_AVAILABLE_FUNDS); + } + + console.log("Deployed ERC20Airdrop to:", address(airdrop)); + + vm.serializeBytes32(jsonRoot, "MerkleRoot", merkleRoot); + string memory finalJson = vm.serializeAddress(jsonRoot, "ERC20Airdrop", address(airdrop)); + vm.writeJson(finalJson, jsonLocation); + + vm.stopBroadcast(); + } +} diff --git a/packages/nfts/script/trailblazers-airdrop/Utils.s.sol b/packages/nfts/script/trailblazers-airdrop/Utils.s.sol new file mode 100644 index 0000000000..cb9b6269d5 --- /dev/null +++ b/packages/nfts/script/trailblazers-airdrop/Utils.s.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { Script, console } from "forge-std/src/Script.sol"; +import "forge-std/src/StdJson.sol"; +import { IMinimalBlacklist } from "@taiko/blacklist/IMinimalBlacklist.sol"; +import { MockBlacklist } from "../../test/util/Blacklist.sol"; + +contract UtilsScript is Script { + using stdJson for string; + + address public nounsTokenAddress; + + uint256 public chainId; + + string public lowercaseNetworkKey; + string public uppercaseNetworkKey; + + function setUp() public { + // load all network configs + chainId = block.chainid; + + if (chainId == 31_337) { + lowercaseNetworkKey = "localhost"; + uppercaseNetworkKey = "LOCALHOST"; + } else if (chainId == 17_000) { + lowercaseNetworkKey = "holesky"; + uppercaseNetworkKey = "HOLESKY"; + } else if (chainId == 167_001) { + lowercaseNetworkKey = "devnet"; + uppercaseNetworkKey = "DEVNET"; + } else if (chainId == 11_155_111) { + lowercaseNetworkKey = "sepolia"; + uppercaseNetworkKey = "SEPOLIA"; + } else if (chainId == 167_008) { + lowercaseNetworkKey = "katla"; + uppercaseNetworkKey = "KATLA"; + } else if (chainId == 167_000) { + lowercaseNetworkKey = "mainnet"; + uppercaseNetworkKey = "MAINNET"; + } else if (chainId == 167_009) { + lowercaseNetworkKey = "hekla"; + uppercaseNetworkKey = "HEKLA"; + } else { + revert("Unsupported chainId"); + } + } + + function getPrivateKey() public view returns (uint256) { + string memory lookupKey = string.concat(uppercaseNetworkKey, "_PRIVATE_KEY"); + return vm.envUint(lookupKey); + } + + function getAddress() public view returns (address) { + string memory lookupKey = string.concat(uppercaseNetworkKey, "_ADDRESS"); + return vm.envAddress(lookupKey); + } + + function getContractJsonLocation() public view returns (string memory) { + string memory root = vm.projectRoot(); + return + string.concat(root, "/deployments/trailblazers-airdrop/", lowercaseNetworkKey, ".json"); + } + + function getBlacklist() public view returns (IMinimalBlacklist blacklistAddress) { + if (block.chainid == 167_000) { + // mainnet blacklist address + blacklistAddress = IMinimalBlacklist(vm.envAddress("BLACKLIST_ADDRESS")); + } else { + // deploy a mock blacklist otherwise + blacklistAddress = IMinimalBlacklist(0xbdEd0D2bf404bdcBa897a74E6657f1f12e5C6fb6); + } + + return blacklistAddress; + } + + function run() public { } +} diff --git a/packages/nfts/test/trailblazers-airdrop/ERC20Airdrop.t.sol b/packages/nfts/test/trailblazers-airdrop/ERC20Airdrop.t.sol new file mode 100644 index 0000000000..e2dbaeb4d8 --- /dev/null +++ b/packages/nfts/test/trailblazers-airdrop/ERC20Airdrop.t.sol @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import { Test } from "forge-std/src/Test.sol"; + +import { ERC20Airdrop } from "../../contracts/trailblazers-airdrop/ERC20Airdrop.sol"; +import { Merkle } from "murky/Merkle.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; +import { UtilsScript } from "../../script/taikoon/sol/Utils.s.sol"; +import { MockBlacklist } from "../util/Blacklist.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { ERC20Mock } from "../util/MockTokens.sol"; + +contract ERC20AirdropTest is Test { + UtilsScript public utils; + + ERC20Airdrop public airdrop; + + address public owner = vm.addr(0x5); + + address[3] public minters = [ + vm.addr(0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80), + vm.addr(0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d), + vm.addr(0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a) + ]; + + bytes32[] public leaves = [ + bytes32(0xbe00dd3c5d43551e03bf9a60316bee19ede94bf34486c39398c4f9f3b309d7a3), + bytes32(0xa097cea9c873bd65b34c8d7d543e90ac1e18e5ec72c17cd95dedd0b52f02022e) + ]; + + Merkle tree = new Merkle(); + + address mintSigner; + uint256 mintSignerPk; + + /////////////////////////////// + uint64 constant CLAIM_START = 100; + uint64 constant CLAIM_END = 200; + + ERC20Mock public erc20; + + MockBlacklist public blacklist; + + uint256 constant TOTAL_AVAILABLE_FUNDS = 1000 ether; + + uint256 constant CLAIM_AMOUNT = 1 ether; + + function setUp() public { + utils = new UtilsScript(); + utils.setUp(); + // create whitelist merkle tree + vm.startBroadcast(owner); + + // mock tree + bytes32 merkleRoot = tree.getRoot(leaves); + + // deploy supplementary contracts + erc20 = new ERC20Mock(); + + blacklist = new MockBlacklist(); + + // deploy airdrop with empty root + address impl = address(new ERC20Airdrop()); + address proxy = address( + new ERC1967Proxy( + impl, + abi.encodeCall( + ERC20Airdrop.init, + (owner, CLAIM_START, CLAIM_END, merkleRoot, erc20, address(blacklist)) + ) + ) + ); + + airdrop = ERC20Airdrop(proxy); + + // fund the airdrop contract + erc20.mint(address(airdrop), TOTAL_AVAILABLE_FUNDS); + + vm.stopBroadcast(); + } + + function test_revert_claim_beforeClaimStart() public { + vm.warp(CLAIM_START - 1); + address user = minters[0]; + + bytes32[] memory proof = tree.getProof(leaves, 0); + + vm.prank(user); + vm.expectRevert(); + airdrop.claim(user, CLAIM_AMOUNT, proof); + } + + function test_revert_claim_afterClaimEnd() public { + vm.warp(CLAIM_END + 1); + address user = minters[0]; + + bytes32[] memory proof = tree.getProof(leaves, 0); + + vm.prank(user); + vm.expectRevert(); + airdrop.claim(user, CLAIM_AMOUNT, proof); + } + + function test_claim() public { + vm.warp(CLAIM_START + 1); + address user = minters[0]; + + bytes32[] memory proof = tree.getProof(leaves, 0); + + vm.prank(user); + airdrop.claim(user, CLAIM_AMOUNT, proof); + + assertEq(erc20.balanceOf(user), CLAIM_AMOUNT); + assertEq(erc20.balanceOf(address(airdrop)), TOTAL_AVAILABLE_FUNDS - CLAIM_AMOUNT); + } + + function test_revert_claim_twice() public { + test_claim(); + address user = minters[0]; + + bytes32[] memory proof = tree.getProof(leaves, 0); + + vm.prank(user); + vm.expectRevert(); + airdrop.claim(user, CLAIM_AMOUNT, proof); + } + + function test_revert_blacklisted_mint() public { + vm.warp(CLAIM_START + 1); + + address user = minters[0]; + blacklist.add(user); + + bytes32[] memory proof = tree.getProof(leaves, 0); + + vm.prank(user); + vm.expectRevert(); + airdrop.claim(user, CLAIM_AMOUNT, proof); + } +} diff --git a/packages/nfts/test/util/MockTokens.sol b/packages/nfts/test/util/MockTokens.sol index 7f7192475d..6143d1b37e 100644 --- a/packages/nfts/test/util/MockTokens.sol +++ b/packages/nfts/test/util/MockTokens.sol @@ -1,10 +1,25 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.24; +import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/token/ERC1155/ERC1155Upgradeable.sol"; import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +contract ERC20Mock is Initializable, ERC20Upgradeable { + function initialize(string memory name, string memory symbol) public initializer { + __ERC20_init(name, symbol); + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public { + _burn(from, amount); + } +} + contract ERC721Mock is Initializable, ERC721Upgradeable { function initialize(string memory name, string memory symbol) public initializer { __ERC721_init(name, symbol);