diff --git a/src/test/tokenerc20-BTT/initialize/initialize.t.sol b/src/test/tokenerc20-BTT/initialize/initialize.t.sol new file mode 100644 index 000000000..6dae4df15 --- /dev/null +++ b/src/test/tokenerc20-BTT/initialize/initialize.t.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function eip712NameHash() external view returns (bytes32) { + return _EIP712NameHash(); + } + + function eip712VersionHash() external view returns (bytes32) { + return _EIP712VersionHash(); + } +} + +contract TokenERC20Test_Initialize is BaseTest { + address public implementation; + address public proxy; + + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + } + + function test_initialize_initializingImplementation() public { + vm.expectRevert("Initializable: contract is already initialized"); + TokenERC20(implementation).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + modifier whenNotImplementation() { + _; + } + + function test_initialize_proxyAlreadyInitialized() public whenNotImplementation { + vm.expectRevert("Initializable: contract is already initialized"); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + modifier whenProxyNotInitialized() { + proxy = address(new TWProxy(implementation, "")); + _; + } + + function test_initialize_exceedsMaxBps() public whenNotImplementation whenProxyNotInitialized { + vm.expectRevert("exceeds MAX_BPS"); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + uint128(MAX_BPS) + 1 // platformFeeBps greater than MAX_BPS + ); + } + + modifier whenPlatformFeeBpsWithinMaxBps() { + _; + } + + function test_initialize() public whenNotImplementation whenProxyNotInitialized whenPlatformFeeBpsWithinMaxBps { + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + + // check state + MyTokenERC20 tokenContract = MyTokenERC20(proxy); + + assertEq(tokenContract.eip712NameHash(), keccak256(bytes(NAME))); + assertEq(tokenContract.eip712VersionHash(), keccak256(bytes("1"))); + + address[] memory _trustedForwarders = forwarders(); + for (uint256 i = 0; i < _trustedForwarders.length; i++) { + assertTrue(tokenContract.isTrustedForwarder(_trustedForwarders[i])); + } + + assertEq(tokenContract.name(), NAME); + assertEq(tokenContract.symbol(), SYMBOL); + assertEq(tokenContract.contractURI(), CONTRACT_URI); + + (address _platformFeeRecipient, uint16 _platformFeeBps) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_platformFeeRecipient, platformFeeRecipient); + + assertEq(tokenContract.primarySaleRecipient(), saleRecipient); + + assertTrue(tokenContract.hasRole(bytes32(0x00), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + assertTrue(tokenContract.hasRole(keccak256("TRANSFER_ROLE"), address(0))); + assertTrue(tokenContract.hasRole(keccak256("MINTER_ROLE"), deployer)); + } + + function test_initialize_event_RoleGranted_DefaultAdmin() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _defaultAdminRole = bytes32(0x00); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_defaultAdminRole, deployer, deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + function test_initialize_event_RoleGranted_MinterRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _minterRole = keccak256("MINTER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_minterRole, deployer, deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + function test_initialize_event_RoleGranted_TransferRole() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, deployer, deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } + + function test_initialize_event_RoleGranted_TransferRole_AddressZero() + public + whenNotImplementation + whenProxyNotInitialized + whenPlatformFeeBpsWithinMaxBps + { + bytes32 _transferRole = keccak256("TRANSFER_ROLE"); + vm.prank(deployer); + vm.expectEmit(true, true, true, false); + emit RoleGranted(_transferRole, address(0), deployer); + MyTokenERC20(proxy).initialize( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ); + } +} diff --git a/src/test/tokenerc20-BTT/initialize/initialize.tree b/src/test/tokenerc20-BTT/initialize/initialize.tree new file mode 100644 index 000000000..a3ead7790 --- /dev/null +++ b/src/test/tokenerc20-BTT/initialize/initialize.tree @@ -0,0 +1,34 @@ +initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _primarySaleRecipient, + address _platformFeeRecipient + uint256 _platformFeeBps, +) +├── when initializing the implementation contract (not proxy) +│ └── it should revert ✅ +└── when it is a proxy to the implementation + └── when it is already initialized + │ └── it should revert ✅ + └── when it is not initialized + └── when platformFeeBps is greater than MAX_BPS + │ └── it should revert ✅ + └── when platformFeeBps is less than or equal to MAX_BPS + └── it should correctly set EIP712 name hash and version hash ✅ + └── it should set trustedForwarder mapping to true for all addresses in `_trustedForwarders` ✅ + └── it should set _name and _symbol to `_name` and `_symbol` param values respectively ✅ + └── it should set contractURI to `_contractURI` param value ✅ + └── it should set platformFeeRecipient and platformFeeBps as `_platformFeeRecipient` and `_platformFeeBps` respectively ✅ + └── it should set primary sale recipient as `_saleRecipient` param value ✅ + └── it should grant 0x00 (DEFAULT_ADMIN_ROLE) to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant MINTER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to `_defaultAdmin` address ✅ + └── it should emit RoleGranted event ✅ + └── it should grant TRANSFER_ROLE to address(0) ✅ + └── it should emit RoleGranted event ✅ + diff --git a/src/test/tokenerc20-BTT/mint-to/mintTo.t.sol b/src/test/tokenerc20-BTT/mint-to/mintTo.t.sol new file mode 100644 index 000000000..ad5095640 --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-to/mintTo.t.sol @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_MintTo is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + uint256 public amount; + + MyTokenERC20 internal tokenContract; + + event TokensMinted(address indexed mintedTo, uint256 quantityMinted); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + amount = 100; + } + + function test_mintTo_notMinterRole() public { + vm.prank(caller); + vm.expectRevert("not minter."); + tokenContract.mintTo(recipient, amount); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), caller); + _; + } + + function test_mintTo() public whenMinterRole { + // mint + vm.prank(caller); + tokenContract.mintTo(recipient, amount); + + // check state after + assertEq(tokenContract.balanceOf(recipient), amount); + } + + function test_mintTo_TokensMintedEvent() public whenMinterRole { + vm.prank(caller); + vm.expectEmit(true, false, false, true); + emit TokensMinted(recipient, amount); + tokenContract.mintTo(recipient, amount); + } +} diff --git a/src/test/tokenerc20-BTT/mint-to/mintTo.tree b/src/test/tokenerc20-BTT/mint-to/mintTo.tree new file mode 100644 index 000000000..33bb14c7e --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-to/mintTo.tree @@ -0,0 +1,7 @@ +mintTo(address to, uint256 amount) +├── when caller doesn't have MINTER_ROLE + │ └── it should revert ✅ + └── when caller has MINTER_ROLE + └── it should mint `amount` to `to` ✅ + └── it should emit TokensMinted event ✅ + diff --git a/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.t.sol b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.t.sol new file mode 100644 index 000000000..5a5810ec8 --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.t.sol @@ -0,0 +1,450 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function setMintedUID(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract ReentrantContract { + fallback() external payable { + TokenERC20.MintRequest memory _mintrequest; + bytes memory _signature; + MyTokenERC20(msg.sender).mintWithSignature(_mintrequest, _signature); + } +} + +contract TokenERC20Test_MintWithSignature is BaseTest { + address public implementation; + address public proxy; + address public caller; + address public recipient; + + MyTokenERC20 internal tokenContract; + + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC20.MintRequest _mintrequest; + + event TokensMintedWithSignature( + address indexed signer, + address indexed mintedTo, + TokenERC20.MintRequest mintRequest + ); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + caller = getActor(1); + recipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + + 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)) + ); + + // construct default mintrequest + _mintrequest.to = recipient; + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + + erc20.mint(deployer, 1_000 ether); + vm.deal(deployer, 1_000 ether); + erc20.mint(caller, 1_000 ether); + vm.deal(caller, 1_000 ether); + + vm.startPrank(deployer); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + + vm.startPrank(caller); + erc20.approve(address(tokenContract), type(uint256).max); + vm.stopPrank(); + } + + function signMintRequest(TokenERC20.MintRequest memory _request, uint256 _privateKey) + internal + view + 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; + } + + function test_mintWithSignature_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_mintWithSignature_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedUID(_mintrequest, _signature); + + // pass the same UID mintrequest again + vm.prank(caller); + vm.expectRevert("invalid signature"); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenUidNotUsed() { + _; + } + + function test_mintWithSignature_invalidStartTimestamp() public whenMinterRole whenUidNotUsed { + _mintrequest.validityStartTimestamp = uint128(block.timestamp + 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidStartTimestamp() { + _; + } + + function test_mintWithSignature_invalidEndTimestamp() public whenMinterRole whenUidNotUsed whenValidStartTimestamp { + _mintrequest.validityEndTimestamp = uint128(block.timestamp - 1); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("request expired"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenValidEndTimestamp() { + _; + } + + function test_mintWithSignature_recipientAddressZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + { + _mintrequest.to = address(0); + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("recipient undefined"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenRecipientAddressNotZero() { + _; + } + + function test_mintWithSignature_zeroQuantity() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + { + _mintrequest.quantity = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("zero quantity"); + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + modifier whenNotZeroQuantity() { + _mintrequest.quantity = 100; + _; + } + + // ================== + // ======= Test branch: when mint price is zero + // ================== + + function test_mintWithSignature_zeroPrice_msgValueNonZero() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.price = 0; + + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("!Value"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + modifier whenMsgValueZero() { + _; + } + + function test_mintWithSignature_zeroPrice() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.balanceOf(recipient), _mintrequest.quantity); + } + + function test_mintWithSignature_zeroPrice_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 0; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _mintrequest); + tokenContract.mintWithSignature(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: when mint price is not zero + // ================== + + function test_mintWithSignature_nonZeroPrice_nativeToken_incorrectMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + uint256 incorrectTotalPrice = (_mintrequest.price) + 1; + + vm.expectRevert("must send total price."); + vm.prank(caller); + tokenContract.mintWithSignature{ value: incorrectTotalPrice }(_mintrequest, _signature); + } + + modifier whenCorrectMsgValue() { + _; + } + + function test_mintWithSignature_nonZeroPrice_nativeToken() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.balanceOf(recipient), _mintrequest.quantity); + + uint256 _platformFee = (_mintrequest.price * platformFeeBps) / 10_000; + uint256 _saleProceeds = _mintrequest.price - _platformFee; + assertEq(caller.balance, 1000 ether - _mintrequest.price); + + (address _platformFeeRecipient, ) = tokenContract.getPlatformFeeInfo(); + assertEq(_platformFeeRecipient.balance, _platformFee); + assertEq(tokenContract.primarySaleRecipient().balance, _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_nativeToken_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenCorrectMsgValue + { + _mintrequest.price = 10; + _mintrequest.currency = NATIVE_TOKEN; + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _mintrequest); + tokenContract.mintWithSignature{ value: _mintrequest.price }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_nonZeroMsgValue() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.expectRevert("msg value not zero"); + vm.prank(caller); + tokenContract.mintWithSignature{ value: 1 }(_mintrequest, _signature); + } + + function test_mintWithSignature_nonZeroPrice_ERC20() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // mint + vm.prank(caller); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + + // check state after + assertEq(tokenContract.balanceOf(recipient), _mintrequest.quantity); + + uint256 _platformFee = (_mintrequest.price * platformFeeBps) / 10_000; + uint256 _saleProceeds = _mintrequest.price - _platformFee; + assertEq(erc20.balanceOf(caller), 1000 ether - _mintrequest.price); + (address _platformFeeRecipient, ) = tokenContract.getPlatformFeeInfo(); + assertEq(erc20.balanceOf(_platformFeeRecipient), _platformFee); + assertEq(erc20.balanceOf(tokenContract.primarySaleRecipient()), _saleProceeds); + } + + function test_mintWithSignature_nonZeroPrice_ERC20_TokensMintedWithSignatureEvent() + public + whenMinterRole + whenUidNotUsed + whenValidStartTimestamp + whenValidEndTimestamp + whenRecipientAddressNotZero + whenNotZeroQuantity + whenMsgValueZero + { + _mintrequest.price = 10; + _mintrequest.currency = address(erc20); + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + vm.prank(caller); + vm.expectEmit(true, true, false, true); + emit TokensMintedWithSignature(signer, _mintrequest.to, _mintrequest); + tokenContract.mintWithSignature{ value: 0 }(_mintrequest, _signature); + } + + // ================== + // ======= Test branch: other cases + // ================== +} diff --git a/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.tree b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.tree new file mode 100644 index 000000000..d7cfcdcab --- /dev/null +++ b/src/test/tokenerc20-BTT/mint-with-signature/mintWithSignature.tree @@ -0,0 +1,51 @@ +mintWithSignature(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should revert ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should revert ✅ + └── when `_req.uid` has not been used + └── when `_req.validityStartTimestamp` is greater than block timestamp + │ └── it should revert ✅ + └── when `_req.validityStartTimestamp` is less than or equal to block timestamp + └── when `_req.validityEndTimestamp` is less than block timestamp + │ └── it should revert ✅ + └── when `_req.validityEndTimestamp` is greater than or equal to block timestamp + └── when `_req.to` is address(0) + │ └── it should revert ✅ + └── when `_req.to` is not address(0) + ├── when `_req.quantity` is zero + │ └── it should revert ✅ + └── when `_req.quantity` is not zero + │ + │ // case: price is zero + └── when `_req.price` is zero + │ └── when msg.value is not zero + │ │ └── it should revert ✅ + │ └── when msg.value is zero + │ └── it should mint `amount` to `to` ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + │ + │ // case: price is not zero + └── when `_req.price` is not zero + └── when currency is native token + │ └── when msg.value is not equal to total price + │ │ └── it should revert ✅ + │ └── when msg.value is equal to total price + │ └── it should mint `amount` to `to` ✅ + │ └── (transfer to sale recipient) ✅ + │ └── (transfer to fee recipient) ✅ + │ └── it should emit TokensMintedWithSignature event ✅ + └── when currency is some ERC20 token + └── when msg.value is not zero + │ └── it should revert ✅ + └── when msg.value is zero + └── it should mint `amount` to `to` ✅ + └── (transfer to sale recipient) ✅ + └── (transfer to fee recipient) ✅ + └── it should emit TokensMintedWithSignature event ✅ + +// other cases + + + diff --git a/src/test/tokenerc20-BTT/other-functions/other.t.sol b/src/test/tokenerc20-BTT/other-functions/other.t.sol new file mode 100644 index 000000000..4b0049e06 --- /dev/null +++ b/src/test/tokenerc20-BTT/other-functions/other.t.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; +import { IStaking20 } from "contracts/extension/interface/IStaking20.sol"; + +import "@openzeppelin/contracts-upgradeable/access/IAccessControlEnumerableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/access/IAccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/introspection/IERC165Upgradeable.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function beforeTokenTransfer( + address from, + address to, + uint256 amount + ) external { + _beforeTokenTransfer(from, to, amount); + } + + function mint(address account, uint256 amount) external { + _mint(account, amount); + } + + function burn(address account, uint256 amount) external { + _burn(account, amount); + } +} + +contract TokenERC20Test_OtherFunctions is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC20 public tokenContract; + address internal caller; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + caller = getActor(3); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + } + + function test_contractType() public { + assertEq(tokenContract.contractType(), bytes32("TokenERC20")); + } + + function test_contractVersion() public { + assertEq(tokenContract.contractVersion(), uint8(1)); + } + + function test_beforeTokenTransfer_restricted_notTransferRole() public { + vm.prank(deployer); + tokenContract.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("transfers restricted."); + tokenContract.beforeTokenTransfer(caller, address(0x123), 100); + } + + modifier whenTransferRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("TRANSFER_ROLE"), caller); + _; + } + + function test_beforeTokenTransfer_restricted() public whenTransferRole { + tokenContract.beforeTokenTransfer(caller, address(0x123), 100); + } + + function test_mint() public { + tokenContract.mint(caller, 100); + assertEq(tokenContract.balanceOf(caller), 100); + } + + function test_burn() public { + tokenContract.mint(caller, 100); + assertEq(tokenContract.balanceOf(caller), 100); + + tokenContract.burn(caller, 60); + assertEq(tokenContract.balanceOf(caller), 40); + } +} diff --git a/src/test/tokenerc20-BTT/other-functions/other.tree b/src/test/tokenerc20-BTT/other-functions/other.tree new file mode 100644 index 000000000..57a1466a8 --- /dev/null +++ b/src/test/tokenerc20-BTT/other-functions/other.tree @@ -0,0 +1,20 @@ +contractType() +├── it should return bytes32("TokenERC20") ✅ + +contractVersion() +├── it should return uint8(1) ✅ + +_beforeTokenTransfers( + address from, + address to, + uint256 amount +) +├── when transfers are restricted (i.e. address(0) doesn't have transfer role, or from-to addresses are not address(0) + └── when from and to don't have transfer role + │ └── it should revert ✅ + +_mint(address account, uint256 amount) +├── it should mint amount to account ✅ + +_burn(address account, uint256 amount) +├── it should mint amount from account ✅ diff --git a/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.t.sol b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.t.sol new file mode 100644 index 000000000..29257093a --- /dev/null +++ b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.t.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_SetContractURI is BaseTest { + address public implementation; + address public proxy; + address internal caller; + string internal _contractURI; + + MyTokenERC20 internal tokenContract; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + caller = getActor(1); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + _contractURI = "ipfs://contracturi"; + } + + function test_setContractURI_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setContractURI(_contractURI); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setContractURI_empty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(""); + + // get contract uri + assertEq(tokenContract.contractURI(), ""); + } + + function test_setContractURI_notEmpty() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setContractURI(_contractURI); + + // get contract uri + assertEq(tokenContract.contractURI(), _contractURI); + } +} diff --git a/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.tree b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.tree new file mode 100644 index 000000000..8fc480b19 --- /dev/null +++ b/src/test/tokenerc20-BTT/set-contract-uri/setContractURI.tree @@ -0,0 +1,8 @@ +setContractURI(string calldata _uri) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── when `uri` is empty + │ └── it should update contract URI to empty string ✅ + └── when `uri` is not empty + └── it should update contract URI to `_uri` ✅ \ No newline at end of file diff --git a/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol new file mode 100644 index 000000000..c36ddc43d --- /dev/null +++ b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.t.sol @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_SetPlatformFeeInfo is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _platformFeeRecipient; + uint256 internal _platformFeeBps; + + MyTokenERC20 internal tokenContract; + + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + caller = getActor(1); + _platformFeeRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + } + + function test_setPlatformFeeInfo_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPlatformFeeInfo_exceedMaxBps() public whenCallerAuthorized { + _platformFeeBps = 10_001; + vm.prank(address(caller)); + vm.expectRevert("exceeds MAX_BPS"); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + modifier whenNotExceedMaxBps() { + _platformFeeBps = 500; + _; + } + + function test_setPlatformFeeInfo() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + + // get platform fee info + (address _recipient, uint16 _bps) = tokenContract.getPlatformFeeInfo(); + assertEq(_recipient, _platformFeeRecipient); + assertEq(_bps, uint16(_platformFeeBps)); + } + + function test_setPlatformFeeInfo_event() public whenCallerAuthorized whenNotExceedMaxBps { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + tokenContract.setPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } +} diff --git a/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.tree b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.tree new file mode 100644 index 000000000..dcef9965e --- /dev/null +++ b/src/test/tokenerc20-BTT/set-platform-fee-info/setPlatformFeeInfo.tree @@ -0,0 +1,10 @@ +setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + ├── when `_platformFeeBps` is greater than MAX_BPS + │ └── it should revert ✅ + └── when `_platformFeeBps` is less than or equal to MAX_BPS + └── it should update platform fee recipient ✅ + └── it should update platform fee bps ✅ + └── it should emit PlatformFeeInfoUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol new file mode 100644 index 000000000..06cac7206 --- /dev/null +++ b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.t.sol @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 {} + +contract TokenERC20Test_SetPrimarySaleRecipient is BaseTest { + address public implementation; + address public proxy; + address internal caller; + address internal _primarySaleRecipient; + + MyTokenERC20 internal tokenContract; + + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + caller = getActor(1); + _primarySaleRecipient = getActor(2); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + } + + function test_setPrimarySaleRecipient_callerNotAuthorized() public { + vm.prank(address(caller)); + vm.expectRevert( + abi.encodePacked( + "AccessControl: account ", + TWStrings.toHexString(uint160(caller), 20), + " is missing role ", + TWStrings.toHexString(uint256(0), 32) + ) + ); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } + + modifier whenCallerAuthorized() { + vm.prank(deployer); + tokenContract.grantRole(bytes32(0x00), caller); + _; + } + + function test_setPrimarySaleRecipient() public whenCallerAuthorized { + vm.prank(address(caller)); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + + // get primary sale recipient info + assertEq(tokenContract.primarySaleRecipient(), _primarySaleRecipient); + } + + function test_setPrimarySaleRecipient_event() public whenCallerAuthorized { + vm.prank(address(caller)); + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(_primarySaleRecipient); + tokenContract.setPrimarySaleRecipient(_primarySaleRecipient); + } +} diff --git a/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree new file mode 100644 index 000000000..230035a07 --- /dev/null +++ b/src/test/tokenerc20-BTT/set-primary-sale-recipient/setPrimarySaleRecipient.tree @@ -0,0 +1,6 @@ +setPrimarySaleRecipient(address _saleRecipient) +├── when caller not authorized + │ └── it should revert ✅ + └── when caller is authorized + └── it should update primary sale recipient ✅ + └── it should emit PrimarySaleRecipientUpdated event ✅ \ No newline at end of file diff --git a/src/test/tokenerc20-BTT/verify/verify.t.sol b/src/test/tokenerc20-BTT/verify/verify.t.sol new file mode 100644 index 000000000..1b3753431 --- /dev/null +++ b/src/test/tokenerc20-BTT/verify/verify.t.sol @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "../../utils/BaseTest.sol"; + +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +contract MyTokenERC20 is TokenERC20 { + function setMintedUID(MintRequest calldata _req, bytes calldata _signature) external { + verifyRequest(_req, _signature); + } +} + +contract TokenERC20Test_Verify is BaseTest { + address public implementation; + address public proxy; + + MyTokenERC20 internal tokenContract; + bytes32 internal typehashMintRequest; + bytes32 internal nameHash; + bytes32 internal versionHash; + bytes32 internal typehashEip712; + bytes32 internal domainSeparator; + + TokenERC20.MintRequest _mintrequest; + + function setUp() public override { + super.setUp(); + + // Deploy implementation. + implementation = address(new MyTokenERC20()); + + // Deploy proxy pointing to implementaion. + vm.prank(deployer); + proxy = address( + new TWProxy( + implementation, + abi.encodeCall( + TokenERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ) + ); + + tokenContract = MyTokenERC20(proxy); + + 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)) + ); + + // construct default mintrequest + _mintrequest.to = address(123); + _mintrequest.primarySaleRecipient = saleRecipient; + _mintrequest.quantity = 100; + _mintrequest.price = 0; + _mintrequest.currency = address(0); + _mintrequest.validityStartTimestamp = 0; + _mintrequest.validityEndTimestamp = 2000; + _mintrequest.uid = bytes32(0); + } + + function signMintRequest(TokenERC20.MintRequest memory _request, uint256 _privateKey) + internal + view + 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; + } + + function test_verify_notMinterRole() public { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenMinterRole() { + vm.prank(deployer); + tokenContract.grantRole(keccak256("MINTER_ROLE"), signer); + _; + } + + function test_verify_invalidUID() public whenMinterRole { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + // set state with this mintrequest and signature, marking the UID as used + tokenContract.setMintedUID(_mintrequest, _signature); + + // pass the same UID mintrequest again + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertFalse(_isValid); + assertEq(_recoveredSigner, signer); + } + + modifier whenUidNotUsed() { + _; + } + + function test_verify() public whenMinterRole whenUidNotUsed { + bytes memory _signature = signMintRequest(_mintrequest, privateKey); + + (bool _isValid, address _recoveredSigner) = tokenContract.verify(_mintrequest, _signature); + + assertTrue(_isValid); + assertEq(_recoveredSigner, signer); + } +} diff --git a/src/test/tokenerc20-BTT/verify/verify.tree b/src/test/tokenerc20-BTT/verify/verify.tree new file mode 100644 index 000000000..c160faa0f --- /dev/null +++ b/src/test/tokenerc20-BTT/verify/verify.tree @@ -0,0 +1,12 @@ +verify(MintRequest calldata _req, bytes calldata _signature) +├── when signer doesn't have MINTER_ROLE +│ └── it should return false ✅ +│ └── it should return recovered signer equal to the actual signer of the request ✅ +└── when signer has MINTER_ROLE + └── when `_req.uid` has already been used + │ └── it should return false ✅ + │ └── it should return recovered signer equal to the actual signer of the request ✅ + └── when `_req.uid` has not been used + └── it should return true ✅ + └── it should return recovered signer equal to the actual signer of the request ✅ +