diff --git a/src/test/token/TokenERC20.t.sol b/src/test/token/TokenERC20.t.sol new file mode 100644 index 000000000..eee96d6f3 --- /dev/null +++ b/src/test/token/TokenERC20.t.sol @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { TokenERC20 } from "contracts/token/TokenERC20.sol"; + +// Test imports +import "contracts/lib/TWStrings.sol"; +import "../utils/BaseTest.sol"; +import "@openzeppelin/contracts-upgradeable/utils/StringsUpgradeable.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + +contract TokenERC20Test is BaseTest { + using StringsUpgradeable for uint256; + + event TokensMinted(address indexed mintedTo, uint256 quantityMinted); + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + TokenERC20.MintRequest mintRequest + ); + + event PrimarySaleRecipientUpdated(address indexed recipient); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + TokenERC20 public tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + bytes32 internal permitTypehash; + + bytes private emptyEncodedBytes = abi.encode("", ""); + + TokenERC20.MintRequest _mintrequest; + bytes _signature; + + address internal deployerSigner; + address internal recipient; + + using stdStorage for StdStorage; + + function setUp() public override { + super.setUp(); + deployerSigner = signer; + recipient = address(0x123); + tokenContract = TokenERC20(getContract("TokenERC20")); + + erc20.mint(deployerSigner, 1_000); + vm.deal(deployerSigner, 1_000); + + erc20.mint(recipient, 1_000); + vm.deal(recipient, 1_000); + + typehashMintRequest = keccak256( + "MintRequest(address to,address primarySaleRecipient,uint256 quantity,uint256 price,address currency,uint128 validityStartTimestamp,uint128 validityEndTimestamp,bytes32 uid)" + ); + nameHash = keccak256(bytes(NAME)); + versionHash = keccak256(bytes("1")); + typehashEip712 = keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ); + domainSeparator = keccak256( + abi.encode(typehashEip712, nameHash, versionHash, block.chainid, address(tokenContract)) + ); + permitTypehash = keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 1000; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + _signature = signMintRequest(_mintrequest, privateKey); + } + + function signMintRequest(TokenERC20.MintRequest memory _request, uint256 _privateKey) + internal + returns (bytes memory) + { + bytes memory encodedRequest = abi.encode( + typehashMintRequest, + _request.to, + _request.primarySaleRecipient, + _request.quantity, + _request.price, + _request.currency, + _request.validityStartTimestamp, + _request.validityEndTimestamp, + _request.uid + ); + bytes32 structHash = keccak256(encodedRequest); + bytes32 typedDataHash = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(_privateKey, typedDataHash); + bytes memory sig = abi.encodePacked(r, s, v); + + return sig; + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintWithSignature` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintWithSignature_ZeroPrice() public { + vm.warp(1000); + + // initial balances and state + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + } + + function test_state_mintWithSignature_NonZeroPrice_ERC20() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(erc20); + _signature = signMintRequest(_mintrequest, privateKey); + + // approve erc20 tokens to tokenContract + vm.prank(recipient); + erc20.approve(address(tokenContract), _mintrequest.price); + + // initial balances and state + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + uint256 erc20BalanceOfSeller = erc20.balanceOf(address(saleRecipient)); + uint256 erc20BalanceOfRecipient = erc20.balanceOf(address(recipient)); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check erc20 balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; + assertEq(erc20.balanceOf(recipient), erc20BalanceOfRecipient - _mintrequest.price); + assertEq(erc20.balanceOf(address(saleRecipient)), erc20BalanceOfSeller + _mintrequest.price - _platformFees); + } + + function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { + vm.warp(1000); + + // update mintrequest data + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + // initial balances and state + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + uint256 etherBalanceOfSeller = address(saleRecipient).balance; + uint256 etherBalanceOfRecipient = address(recipient).balance; + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + + // check state after minting + assertEq(tokenContract.totalSupply(), currentTotalSupply + _mintrequest.quantity); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _mintrequest.quantity); + + // check balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; + assertEq(address(recipient).balance, etherBalanceOfRecipient - _mintrequest.price); + assertEq(address(saleRecipient).balance, etherBalanceOfSeller + _mintrequest.price - _platformFees); + } + + function test_revert_mintWithSignature_MustSendTotalPrice() public { + vm.warp(1000); + + _mintrequest.price = 1; + _mintrequest.currency = address(NATIVE_TOKEN); + _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(recipient); + vm.expectRevert("must send total price."); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_InvalidSignature() public { + vm.warp(1000); + + uint256 incorrectKey = 3456; + _signature = signMintRequest(_mintrequest, incorrectKey); + + vm.prank(recipient); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_revert_mintWithSignature_RequestExpired() public { + _signature = signMintRequest(_mintrequest, privateKey); + + // warp time more out of range + vm.warp(3000); + + vm.prank(recipient); + vm.expectRevert("request expired"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + function test_event_mintWithSignature() public { + vm.warp(1000); + + vm.expectEmit(true, true, true, true); + emit TokensMintedWithSignature(deployerSigner, recipient, _mintrequest); + + // mint with signature + vm.prank(recipient); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `mintTo` + //////////////////////////////////////////////////////////////*/ + + function test_state_mintTo() public { + uint256 _amount = 100; + + uint256 currentTotalSupply = tokenContract.totalSupply(); + uint256 currentBalanceOfRecipient = tokenContract.balanceOf(recipient); + + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _amount); + + assertEq(tokenContract.totalSupply(), currentTotalSupply + _amount); + assertEq(tokenContract.balanceOf(recipient), currentBalanceOfRecipient + _amount); + } + + function test_revert_mintTo_NotAuthorized() public { + uint256 _amount = 100; + + vm.expectRevert("not minter."); + vm.prank(address(0x1)); + tokenContract.mintTo(recipient, _amount); + } + + function test_event_mintTo() public { + uint256 _amount = 100; + + vm.expectEmit(true, true, true, true); + emit TokensMinted(recipient, _amount); + + // mint + vm.prank(deployerSigner); + tokenContract.mintTo(recipient, _amount); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: primary sale + //////////////////////////////////////////////////////////////*/ + + function test_state_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + address recipient = tokenContract.primarySaleRecipient(); + assertEq(recipient, _primarySaleRecipient); + } + + function test_revert_setPrimarySaleRecipient_NotAuthorized() public { + address _primarySaleRecipient = address(0x123); + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + TWStrings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + function test_event_setPrimarySaleRecipient() public { + address _primarySaleRecipient = address(0x123); + + vm.expectEmit(true, true, true, true); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + + vm.prank(deployerSigner); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: platform fee + //////////////////////////////////////////////////////////////*/ + + function test_state_setPlatformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + (address recipient, uint16 bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeRecipient, recipient); + assertEq(_platformFeeBps, bps); + } + + function test_revert_setPlatformFeeInfo_ExceedsMaxBps() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 10001; + + vm.expectRevert("bps <= 10000."); + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + function test_revert_setPlatformFeeInfo_NotAuthorized() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + TWStrings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setPlatformFeeInfo(address(1), 1000); + } + + function test_event_platformFeeInfo() public { + address _platformFeeRecipient = address(0x123); + uint256 _platformFeeBps = 1000; + + vm.expectEmit(true, true, true, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + + vm.prank(deployerSigner); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: contract metadata + //////////////////////////////////////////////////////////////*/ + + function test_state_setContractURI() public { + string memory uri = "uri_string"; + + vm.prank(deployerSigner); + tokenContract.setContractURI(uri); + + string memory _contractURI = tokenContract.contractURI(); + + assertEq(_contractURI, uri); + } + + function test_revert_setContractURI_NotAuthorized() public { + bytes32 role = tokenContract.DEFAULT_ADMIN_ROLE(); + + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(address(0x1)), 20), + " is missing role ", + TWStrings.toHexString(uint256(role), 32) + ) + ); + vm.prank(address(0x1)); + tokenContract.setContractURI(""); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: pause/unpause + //////////////////////////////////////////////////////////////*/ + + function test_state_pause() public { + vm.prank(deployerSigner); + tokenContract.pause(); + + assertEq(tokenContract.paused(), true); + + vm.expectRevert("Pausable: paused"); + vm.prank(deployerSigner); + tokenContract.pause(); + } + + function test_revert_pause_NotAuthorized() public { + vm.expectRevert("not pauser."); + vm.prank(address(0x1)); + tokenContract.pause(); + } + + function test_state_unpause() public { + vm.prank(deployerSigner); + tokenContract.pause(); + + assertEq(tokenContract.paused(), true); + + vm.prank(deployerSigner); + tokenContract.unpause(); + + assertFalse(tokenContract.paused()); + } + + function test_revert_unpause_NotAuthorized() public { + vm.expectRevert("not pauser."); + vm.prank(address(0x1)); + tokenContract.unpause(); + } +} diff --git a/src/test/token/TokenERC721.t.sol b/src/test/token/TokenERC721.t.sol index d3c8b9377..e9c09e1bd 100644 --- a/src/test/token/TokenERC721.t.sol +++ b/src/test/token/TokenERC721.t.sol @@ -163,8 +163,9 @@ contract TokenERC721Test is BaseTest { assertEq(tokenContract.ownerOf(nextTokenId), recipient); // check erc20 balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; assertEq(erc20.balanceOf(recipient), erc20BalanceOfRecipient - _mintrequest.price); - assertEq(erc20.balanceOf(address(saleRecipient)), erc20BalanceOfSeller + _mintrequest.price); + assertEq(erc20.balanceOf(address(saleRecipient)), erc20BalanceOfSeller + _mintrequest.price - _platformFees); } function test_state_mintWithSignature_NonZeroPrice_NativeToken() public { @@ -195,8 +196,9 @@ contract TokenERC721Test is BaseTest { assertEq(tokenContract.ownerOf(nextTokenId), recipient); // check erc20 balances after minting + uint256 _platformFees = (_mintrequest.price * platformFeeBps) / MAX_BPS; assertEq(address(recipient).balance, etherBalanceOfRecipient - _mintrequest.price); - assertEq(address(saleRecipient).balance, etherBalanceOfSeller + _mintrequest.price); + assertEq(address(saleRecipient).balance, etherBalanceOfSeller + _mintrequest.price - _platformFees); } function test_revert_mintWithSignature_MustSendTotalPrice() public {