diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3887fb49b..81bdc28dd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -43,6 +43,8 @@ jobs: run: | forge coverage --report lcov lcov --remove lcov.info -o lcov.info 'src/test/**' + lcov --remove lcov.info -o lcov.info 'contracts/external-deps/**' + lcov --remove lcov.info -o lcov.info 'contracts/eip/**' forge test - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 diff --git a/contracts/extension/BurnToClaim.sol b/contracts/extension/BurnToClaim.sol index cf5ae639c..0d1490c56 100644 --- a/contracts/extension/BurnToClaim.sol +++ b/contracts/extension/BurnToClaim.sol @@ -17,6 +17,8 @@ abstract contract BurnToClaim is IBurnToClaim { function setBurnToClaimInfo(BurnToClaimInfo calldata _burnToClaimInfo) external virtual { require(_canSetBurnToClaim(), "Not authorized."); + require(_burnToClaimInfo.originContractAddress != address(0), "Origin contract not set."); + require(_burnToClaimInfo.currency != address(0), "Currency not set."); burnToClaimInfo = _burnToClaimInfo; } @@ -30,12 +32,15 @@ abstract contract BurnToClaim is IBurnToClaim { if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC721) { require(_quantity == 1, "Invalid amount"); - require(IERC721(_burnToClaimInfo.originContractAddress).ownerOf(_tokenId) == _tokenOwner); + require(IERC721(_burnToClaimInfo.originContractAddress).ownerOf(_tokenId) == _tokenOwner, "!Owner"); } else if (_burnToClaimInfo.tokenType == IBurnToClaim.TokenType.ERC1155) { uint256 _eligible1155TokenId = _burnToClaimInfo.tokenId; - require(_tokenId == _eligible1155TokenId || _eligible1155TokenId == type(uint256).max); - require(IERC1155(_burnToClaimInfo.originContractAddress).balanceOf(_tokenOwner, _tokenId) >= _quantity); + require(_tokenId == _eligible1155TokenId, "Invalid token Id"); + require( + IERC1155(_burnToClaimInfo.originContractAddress).balanceOf(_tokenOwner, _tokenId) >= _quantity, + "!Balance" + ); } // TODO: check if additional verification steps are required / override in main contract diff --git a/contracts/extension/PlatformFee.sol b/contracts/extension/PlatformFee.sol index d3152a7e5..5f3bdca9d 100644 --- a/contracts/extension/PlatformFee.sol +++ b/contracts/extension/PlatformFee.sol @@ -35,7 +35,7 @@ abstract contract PlatformFee is IPlatformFee { return (platformFeeRecipient, flatPlatformFee); } - /// @dev Returns the platform fee bps and recipient. + /// @dev Returns the platform fee type. function getPlatformFeeType() public view returns (PlatformFeeType) { return platformFeeType; } @@ -61,6 +61,9 @@ abstract contract PlatformFee is IPlatformFee { if (_platformFeeBps > 10_000) { revert("Exceeds max bps"); } + if (_platformFeeRecipient == address(0)) { + revert("Invalid recipient"); + } platformFeeBps = uint16(_platformFeeBps); platformFeeRecipient = _platformFeeRecipient; diff --git a/contracts/extension/PrimarySale.sol b/contracts/extension/PrimarySale.sol index 598645c93..551607e13 100644 --- a/contracts/extension/PrimarySale.sol +++ b/contracts/extension/PrimarySale.sol @@ -38,6 +38,9 @@ abstract contract PrimarySale is IPrimarySale { /// @dev Lets a contract admin set the recipient for all primary sales. function _setupPrimarySaleRecipient(address _saleRecipient) internal { + if (_saleRecipient == address(0)) { + revert("Invalid recipient"); + } recipient = _saleRecipient; emit PrimarySaleRecipientUpdated(_saleRecipient); } diff --git a/contracts/extension/upgradeable/PlatformFee.sol b/contracts/extension/upgradeable/PlatformFee.sol index 89b4fd32b..6b9e9ef21 100644 --- a/contracts/extension/upgradeable/PlatformFee.sol +++ b/contracts/extension/upgradeable/PlatformFee.sol @@ -19,6 +19,10 @@ library PlatformFeeStorage { address platformFeeRecipient; /// @dev The % of primary sales collected as platform fees. uint16 platformFeeBps; + /// @dev Fee type variants: percentage fee and flat fee + IPlatformFee.PlatformFeeType platformFeeType; + /// @dev The flat amount collected by the contract as fees on primary sales. + uint256 flatPlatformFee; } function data() internal pure returns (Data storage data_) { @@ -44,6 +48,16 @@ abstract contract PlatformFee is IPlatformFee { return (_platformFeeStorage().platformFeeRecipient, uint16(_platformFeeStorage().platformFeeBps)); } + /// @dev Returns the platform fee bps and recipient. + function getFlatPlatformFeeInfo() public view returns (address, uint256) { + return (_platformFeeStorage().platformFeeRecipient, _platformFeeStorage().flatPlatformFee); + } + + /// @dev Returns the platform fee type. + function getPlatformFeeType() public view returns (PlatformFeeType) { + return _platformFeeStorage().platformFeeType; + } + /** * @notice Updates the platform fee recipient and bps. * @dev Caller should be authorized to set platform fee info. @@ -75,6 +89,38 @@ abstract contract PlatformFee is IPlatformFee { emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); } + /// @notice Lets a module admin set a flat fee on primary sales. + function setFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) external { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + + _setupFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } + + /// @dev Sets a flat fee on primary sales. + function _setupFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) internal { + _platformFeeStorage().flatPlatformFee = _flatFee; + _platformFeeStorage().platformFeeRecipient = _platformFeeRecipient; + + emit FlatPlatformFeeUpdated(_platformFeeRecipient, _flatFee); + } + + /// @notice Lets a module admin set platform fee type. + function setPlatformFeeType(PlatformFeeType _feeType) external { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + _setupPlatformFeeType(_feeType); + } + + /// @dev Sets platform fee type. + function _setupPlatformFeeType(PlatformFeeType _feeType) internal { + _platformFeeStorage().platformFeeType = _feeType; + + emit PlatformFeeTypeUpdated(_feeType); + } + /// @dev Returns the PlatformFee storage. function _platformFeeStorage() internal pure returns (PlatformFeeStorage.Data storage data) { data = PlatformFeeStorage.data(); diff --git a/contracts/legacy-contracts/extension/PlatformFee_V1.sol b/contracts/legacy-contracts/extension/PlatformFee_V1.sol new file mode 100644 index 000000000..9014b7dca --- /dev/null +++ b/contracts/legacy-contracts/extension/PlatformFee_V1.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IPlatformFee_V1.sol"; + +/** + * @title Platform Fee + * @notice Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about platform fees, if desired. + */ + +abstract contract PlatformFee is IPlatformFee { + /// @dev The address that receives all platform fees from all sales. + address private platformFeeRecipient; + + /// @dev The % of primary sales collected as platform fees. + uint16 private platformFeeBps; + + /// @dev Fee type variants: percentage fee and flat fee + PlatformFeeType private platformFeeType; + + /// @dev The flat amount collected by the contract as fees on primary sales. + uint256 private flatPlatformFee; + + /// @dev Returns the platform fee recipient and bps. + function getPlatformFeeInfo() public view override returns (address, uint16) { + return (platformFeeRecipient, uint16(platformFeeBps)); + } + + /// @dev Returns the platform fee bps and recipient. + function getFlatPlatformFeeInfo() public view returns (address, uint256) { + return (platformFeeRecipient, flatPlatformFee); + } + + /// @dev Returns the platform fee bps and recipient. + function getPlatformFeeType() public view returns (PlatformFeeType) { + return platformFeeType; + } + + /** + * @notice Updates the platform fee recipient and bps. + * @dev Caller should be authorized to set platform fee info. + * See {_canSetPlatformFeeInfo}. + * Emits {PlatformFeeInfoUpdated Event}; See {_setupPlatformFeeInfo}. + * + * @param _platformFeeRecipient Address to be set as new platformFeeRecipient. + * @param _platformFeeBps Updated platformFeeBps. + */ + function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external override { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + _setupPlatformFeeInfo(_platformFeeRecipient, _platformFeeBps); + } + + /// @dev Sets the platform fee recipient and bps + function _setupPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) internal { + if (_platformFeeBps > 10_000) { + revert("Exceeds max bps"); + } + + platformFeeBps = uint16(_platformFeeBps); + platformFeeRecipient = _platformFeeRecipient; + + emit PlatformFeeInfoUpdated(_platformFeeRecipient, _platformFeeBps); + } + + /// @notice Lets a module admin set a flat fee on primary sales. + function setFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) external { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + + _setupFlatPlatformFeeInfo(_platformFeeRecipient, _flatFee); + } + + /// @dev Sets a flat fee on primary sales. + function _setupFlatPlatformFeeInfo(address _platformFeeRecipient, uint256 _flatFee) internal { + flatPlatformFee = _flatFee; + platformFeeRecipient = _platformFeeRecipient; + + emit FlatPlatformFeeUpdated(_platformFeeRecipient, _flatFee); + } + + /// @notice Lets a module admin set platform fee type. + function setPlatformFeeType(PlatformFeeType _feeType) external { + if (!_canSetPlatformFeeInfo()) { + revert("Not authorized"); + } + _setupPlatformFeeType(_feeType); + } + + /// @dev Sets platform fee type. + function _setupPlatformFeeType(PlatformFeeType _feeType) internal { + platformFeeType = _feeType; + + emit PlatformFeeTypeUpdated(_feeType); + } + + /// @dev Returns whether platform fee info can be set in the given execution context. + function _canSetPlatformFeeInfo() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/PrimarySale_V1.sol b/contracts/legacy-contracts/extension/PrimarySale_V1.sol new file mode 100644 index 000000000..8536c9724 --- /dev/null +++ b/contracts/legacy-contracts/extension/PrimarySale_V1.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +import "./interface/IPrimarySale_V1.sol"; + +/** + * @title Primary Sale + * @notice Thirdweb's `PrimarySale` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about + * primary sales, if desired. + */ + +abstract contract PrimarySale is IPrimarySale { + /// @dev The address that receives all primary sales value. + address private recipient; + + /// @dev Returns primary sale recipient address. + function primarySaleRecipient() public view override returns (address) { + return recipient; + } + + /** + * @notice Updates primary sale recipient. + * @dev Caller should be authorized to set primary sales info. + * See {_canSetPrimarySaleRecipient}. + * Emits {PrimarySaleRecipientUpdated Event}; See {_setupPrimarySaleRecipient}. + * + * @param _saleRecipient Address to be set as new recipient of primary sales. + */ + function setPrimarySaleRecipient(address _saleRecipient) external override { + if (!_canSetPrimarySaleRecipient()) { + revert("Not authorized"); + } + _setupPrimarySaleRecipient(_saleRecipient); + } + + /// @dev Lets a contract admin set the recipient for all primary sales. + function _setupPrimarySaleRecipient(address _saleRecipient) internal { + recipient = _saleRecipient; + emit PrimarySaleRecipientUpdated(_saleRecipient); + } + + /// @dev Returns whether primary sale recipient can be set in the given execution context. + function _canSetPrimarySaleRecipient() internal view virtual returns (bool); +} diff --git a/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol b/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol new file mode 100644 index 000000000..1a1fc778a --- /dev/null +++ b/contracts/legacy-contracts/extension/interface/IPlatformFee_V1.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * Thirdweb's `PlatformFee` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of platform fee and the platform fee basis points, and lets the inheriting contract perform conditional logic + * that uses information about platform fees, if desired. + */ + +interface IPlatformFee { + /// @dev Fee type variants: percentage fee and flat fee + enum PlatformFeeType { + Bps, + Flat + } + + /// @dev Returns the platform fee bps and recipient. + function getPlatformFeeInfo() external view returns (address, uint16); + + /// @dev Lets a module admin update the fees on primary sales. + function setPlatformFeeInfo(address _platformFeeRecipient, uint256 _platformFeeBps) external; + + /// @dev Emitted when fee on primary sales is updated. + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + + /// @dev Emitted when the flat platform fee is updated. + event FlatPlatformFeeUpdated(address platformFeeRecipient, uint256 flatFee); + + /// @dev Emitted when the platform fee type is updated. + event PlatformFeeTypeUpdated(PlatformFeeType feeType); +} diff --git a/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol b/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol new file mode 100644 index 000000000..6ca726842 --- /dev/null +++ b/contracts/legacy-contracts/extension/interface/IPrimarySale_V1.sol @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +/// @author thirdweb + +/** + * Thirdweb's `Primary` is a contract extension to be used with any base contract. It exposes functions for setting and reading + * the recipient of primary sales, and lets the inheriting contract perform conditional logic that uses information about + * primary sales, if desired. + */ + +interface IPrimarySale { + /// @dev The adress that receives all primary sales value. + function primarySaleRecipient() external view returns (address); + + /// @dev Lets a module admin set the default recipient of all primary sales. + function setPrimarySaleRecipient(address _saleRecipient) external; + + /// @dev Emitted when a new sale recipient is set. + event PrimarySaleRecipientUpdated(address indexed recipient); +} diff --git a/contracts/prebuilts/account/utils/AccountCore.sol b/contracts/prebuilts/account/utils/AccountCore.sol index c59cf8cac..d271cb650 100644 --- a/contracts/prebuilts/account/utils/AccountCore.sol +++ b/contracts/prebuilts/account/utils/AccountCore.sol @@ -75,7 +75,17 @@ contract AccountCore is IAccountCore, Initializable, Multicall, BaseAccount, Acc return entrypointContract; } - /// @notice Returns whether a signer is authorized to perform transactions using the wallet. + /** + @notice Returns whether a signer is authorized to perform transactions using the account. + Validity of the signature is based upon signer permission start/end timestamps, txn target, and txn value. + Account admins will always return true, and signers with address(0) as the only approved target will skip target checks. + + @param _signer The signer to check. + @param _userOp The user operation to check. + + @return Whether the signer is authorized to perform the transaction. + */ + /* solhint-disable*/ function isValidSigner(address _signer, UserOperation calldata _userOp) public view virtual returns (bool) { // First, check if the signer is an admin. @@ -102,6 +112,7 @@ contract AccountCore is IAccountCore, Initializable, Multicall, BaseAccount, Acc // if address(0) is the only approved target, set isWildCard to true (wildcard approved). bool isWildCard = approvedTargets.length() == 1 && approvedTargets.at(0) == address(0); + // checking target and value for `execute` if (sig == AccountExtension.execute.selector) { // Extract the `target` and `value` arguments from the calldata for `execute`. (address target, uint256 value) = decodeExecuteCalldata(_userOp.callData); @@ -115,12 +126,14 @@ contract AccountCore is IAccountCore, Initializable, Multicall, BaseAccount, Acc } } - // Check if the value is within the allowed range and if the target is approved. + // Check if the value is within the allowed range. if (permissions.nativeTokenLimitPerTransaction < value) { // Account: value too high OR Account: target not approved. return false; } - } else if (sig == AccountExtension.executeBatch.selector) { + } + // checking target and value for `executeBatch` + else if (sig == AccountExtension.executeBatch.selector) { // Extract the `target` and `value` array arguments from the calldata for `executeBatch`. (address[] memory targets, uint256[] memory values, ) = decodeExecuteBatchCalldata(_userOp.callData); @@ -134,7 +147,7 @@ contract AccountCore is IAccountCore, Initializable, Multicall, BaseAccount, Acc } } - // For each target+value pair, check if the value is within the allowed range and if the target is approved. + // For each target+value pair, check if the value is within the allowed range. for (uint256 i = 0; i < targets.length; i++) { if (permissions.nativeTokenLimitPerTransaction < values[i]) { // Account: value too high OR Account: target not approved. diff --git a/contracts/prebuilts/signature-drop/SignatureDrop.sol b/contracts/prebuilts/signature-drop/SignatureDrop.sol index 13f65fb7b..98b464fe3 100644 --- a/contracts/prebuilts/signature-drop/SignatureDrop.sol +++ b/contracts/prebuilts/signature-drop/SignatureDrop.sol @@ -28,9 +28,9 @@ import "../../lib/CurrencyTransferLib.sol"; // ========== Features ========== import "../../extension/ContractMetadata.sol"; -import "../../extension/PlatformFee.sol"; +import "../../legacy-contracts/extension/PlatformFee_V1.sol"; import "../../extension/Royalty.sol"; -import "../../extension/PrimarySale.sol"; +import "../../legacy-contracts/extension/PrimarySale_V1.sol"; import "../../extension/Ownable.sol"; import "../../extension/DelayedReveal.sol"; import "../../extension/LazyMint.sol"; diff --git a/foundry.toml b/foundry.toml index 406a17df9..8a646b446 100644 --- a/foundry.toml +++ b/foundry.toml @@ -2,7 +2,7 @@ solc-version = "0.8.12" #auto_detect_solc = false cache = true -evm_version = 'london' +evm_version = 'shanghai' force = false gas_reports = [ "DropERC721Benchmark", diff --git a/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.t.sol b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.t.sol new file mode 100644 index 000000000..c6c3e681b --- /dev/null +++ b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20BeforeClaim is DropERC20 { + bytes private emptyBytes = bytes(""); + + function harness_beforeClaim(uint256 quantity, AllowlistProof calldata _proof) public view { + _beforeClaim(address(0), quantity, address(0), 0, _proof, emptyBytes); + } +} + +contract DropERC20Test_beforeClaim is BaseTest { + address public dropImp; + HarnessDropERC20BeforeClaim public proxy; + + uint256 private mintQty; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20BeforeClaim()); + proxy = HarnessDropERC20BeforeClaim(address(new TWProxy(dropImp, initializeData))); + } + + modifier setMaxTotalSupply() { + vm.prank(deployer); + proxy.setMaxTotalSupply(100); + _; + } + + modifier qtyExceedMaxTotalSupply() { + mintQty = 101; + _; + } + + function test_revert_MaxSupplyExceeded() public setMaxTotalSupply qtyExceedMaxTotalSupply { + DropERC20.AllowlistProof memory proof; + vm.expectRevert("exceed max total supply."); + proxy.harness_beforeClaim(mintQty, proof); + } +} diff --git a/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.tree b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.tree new file mode 100644 index 000000000..f1cf867a4 --- /dev/null +++ b/src/test/drop/drop-erc20/_beforeClaim/_beforeClaim.tree @@ -0,0 +1,10 @@ +function _beforeClaim( + address, + uint256 _quantity, + address, + uint256, + AllowlistProof calldata, + bytes memory +) +└── when maxTotalSupply does not equal to 0 and totalSupply() + _quantity is greater than _maxTotalSupply + └── it should revert ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.t.sol b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.t.sol new file mode 100644 index 000000000..2b17cdd08 --- /dev/null +++ b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.t.sol @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20CanSet is DropERC20 { + function canSetPlatformFeeInfo() external view returns (bool) { + return _canSetPlatformFeeInfo(); + } + + function canSetPrimarySaleRecipient() external view returns (bool) { + return _canSetPrimarySaleRecipient(); + } + + function canSetContractURI() external view returns (bool) { + return _canSetContractURI(); + } + + function canSetClaimConditions() external view returns (bool) { + return _canSetClaimConditions(); + } +} + +contract DropERC20Test_canSet is BaseTest { + address public dropImp; + + HarnessDropERC20CanSet public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20CanSet()); + proxy = HarnessDropERC20CanSet(address(new TWProxy(dropImp, initializeData))); + } + + modifier callerHasDefaultAdminRole() { + vm.startPrank(deployer); + _; + } + + modifier callerDoesNotHaveDefaultAdminRole() { + _; + } + + function test_canSetPlatformFee_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetPlatformFeeInfo(); + assertEq(status, true); + } + + function test_canSetPlatformFee_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetPlatformFeeInfo(); + assertEq(status, false); + } + + function test_canSetPrimarySaleRecipient_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetPrimarySaleRecipient(); + assertEq(status, true); + } + + function test_canSetPrimarySaleRecipient_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetPrimarySaleRecipient(); + assertEq(status, false); + } + + function test_canSetContractURI_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetContractURI(); + assertEq(status, true); + } + + function test_canSetContractURI_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetContractURI(); + assertEq(status, false); + } + + function test_canSetClaimConditions_returnTrue() public callerHasDefaultAdminRole { + bool status = proxy.canSetClaimConditions(); + assertEq(status, true); + } + + function test_canSetClaimConditions_returnFalse() public callerDoesNotHaveDefaultAdminRole { + bool status = proxy.canSetClaimConditions(); + assertEq(status, false); + } +} diff --git a/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.tree b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.tree new file mode 100644 index 000000000..2f2da72e4 --- /dev/null +++ b/src/test/drop/drop-erc20/_canSetFunctions/_canSetFunctions.tree @@ -0,0 +1,23 @@ +function _canSetPlatformFeeInfo() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetPrimarySaleRecipient() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetContractURI() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ + +function _canSetClaimConditions() +├── when caller has DEFAULT_ADMIN_ROLE +│ └── it should return true ✅ +└── when caller does not have DEFAULT_ADMIN_ROLE + └── it should return false ✅ diff --git a/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.t.sol b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.t.sol new file mode 100644 index 000000000..b78b3eed9 --- /dev/null +++ b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.t.sol @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20CollectPriceOnClaim is DropERC20 { + function harness_collectPrice( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken + ) public payable { + _collectPriceOnClaim(_primarySaleRecipient, _quantityToClaim, _currency, _pricePerToken); + } +} + +contract DropERC20Test_collectPrice is BaseTest { + address public dropImp; + HarnessDropERC20CollectPriceOnClaim public proxy; + + address private currency; + address private primarySaleRecipient; + uint256 private msgValue; + uint256 private pricePerToken; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20CollectPriceOnClaim()); + proxy = HarnessDropERC20CollectPriceOnClaim(address(new TWProxy(dropImp, initializeData))); + } + + modifier pricePerTokenZero() { + _; + } + + modifier pricePerTokenNotZero() { + pricePerToken = 1 ether; + _; + } + + modifier msgValueZero() { + _; + } + + modifier msgValueNotZero() { + msgValue = 1 ether; + _; + } + + modifier valuePriceMismatch() { + msgValue = 1 ether; + pricePerToken = 2 ether; + _; + } + + modifier primarySaleRecipientZeroAddress() { + primarySaleRecipient = address(0); + _; + } + + modifier primarySaleRecipientNotZeroAddress() { + primarySaleRecipient = address(0x0999); + _; + } + + modifier currencyNativeToken() { + currency = NATIVE_TOKEN; + _; + } + + modifier currencyNotNativeToken() { + currency = address(erc20); + _; + } + + function test_revert_pricePerTokenZeroMsgValueNotZero() public pricePerTokenZero msgValueNotZero { + vm.expectRevert("!Value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + } + + function test_revert_nativeCurrencyTotalPriceZero() public pricePerTokenNotZero msgValueZero currencyNativeToken { + vm.expectRevert("quantity too low"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 0, currency, pricePerToken); + } + + function test_revert_nativeCurrencyValuePriceMismatch() public currencyNativeToken valuePriceMismatch { + vm.expectRevert("Invalid msg value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + } + + function test_revert_erc20ValuePriceMismatch() public currencyNotNativeToken valuePriceMismatch { + vm.expectRevert("Invalid msg value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + } + + function test_state_nativeCurrency() + public + currencyNativeToken + pricePerTokenNotZero + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + uint256 beforeBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + uint256 beforeBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(primarySaleRecipient).balance; + uint256 afterBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + + uint256 platformFeeVal = (msgValue * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + } + + function test_revert_erc20_msgValueNotZero() + public + currencyNotNativeToken + msgValueNotZero + primarySaleRecipientNotZeroAddress + { + vm.expectRevert("!Value"); + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, msgValue, currency, pricePerToken); + } + + function test_state_erc20() public currencyNotNativeToken pricePerTokenNotZero primarySaleRecipientNotZeroAddress { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(proxy), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + uint256 beforeBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + proxy.harness_collectPrice(primarySaleRecipient, pricePerToken, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(primarySaleRecipient); + uint256 afterBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + uint256 platformFeeVal = (pricePerToken * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + } + + function test_state_erc20StoredPrimarySaleRecipient() + public + currencyNotNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + address storedPrimarySaleRecipient = proxy.primarySaleRecipient(); + + erc20.mint(address(this), pricePerToken); + ERC20(erc20).approve(address(proxy), pricePerToken); + uint256 beforeBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + uint256 beforeBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + proxy.harness_collectPrice(primarySaleRecipient, pricePerToken, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = erc20.balanceOf(storedPrimarySaleRecipient); + uint256 afterBalancePlatformFeeRecipient = erc20.balanceOf(platformFeeRecipient); + + uint256 platformFeeVal = (pricePerToken * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = 1 ether - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + } + + function test_state_nativeCurrencyStoredPrimarySaleRecipient() + public + currencyNativeToken + pricePerTokenNotZero + primarySaleRecipientZeroAddress + msgValueNotZero + { + (address platformFeeRecipient, uint16 platformFeeBps) = proxy.getPlatformFeeInfo(); + address storedPrimarySaleRecipient = proxy.primarySaleRecipient(); + + uint256 beforeBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + uint256 beforeBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + + proxy.harness_collectPrice{ value: msgValue }(primarySaleRecipient, 1 ether, currency, pricePerToken); + + uint256 afterBalancePrimarySaleRecipient = address(storedPrimarySaleRecipient).balance; + uint256 afterBalancePlatformFeeRecipient = address(platformFeeRecipient).balance; + + uint256 platformFeeVal = (msgValue * platformFeeBps) / MAX_BPS; + uint256 primarySaleRecipientVal = msgValue - platformFeeVal; + + assertEq(beforeBalancePrimarySaleRecipient + primarySaleRecipientVal, afterBalancePrimarySaleRecipient); + assertEq(beforeBalancePlatformFeeRecipient + platformFeeVal, afterBalancePlatformFeeRecipient); + } +} diff --git a/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.tree b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.tree new file mode 100644 index 000000000..933ae6877 --- /dev/null +++ b/src/test/drop/drop-erc20/_collectPriceOnClaim/_collectPriceOnClaim.tree @@ -0,0 +1,44 @@ +function _collectPriceOnClaim( + address _primarySaleRecipient, + uint256 _quantityToClaim, + address _currency, + uint256 _pricePerToken +) +├── when _pricePerToken is equal to zero +│ ├── when msg.value does not equal to zero +│ │ └── it should revert ✅ +│ └── when msg.value is equal to zero +│ └── it should return ✅ +└── when _pricePerToken is not equal to zero + ├── when _primarySaleRecipient is equal to address(0) + │ ├── when totalPrice is equal to zero + │ │ └── it should revert ✅ + │ └── when total price is not equal to zero + │ ├── when currency is native token + │ │ ├── when msg.value does not equal totalPrice + │ │ │ └── it should revert ✅ + │ │ └── when msg.value does equal totalPrice + │ │ ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + │ │ └── totalPrice - platformFees should be transfered to primarySaleRecipient() ✅ + │ └── when currency is not native token + │ ├── when msg.value is not equal to zero + │ │ └── it should revert ✅ + │ └── when msg.value is equal to zero + │ ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + │ └── totalPrice - platformFees should be transfered to primarySaleRecipient() ✅ + └── when _primarySaleRecipient is not equal to address(0) + ├── when totalPrice is equal to zero + │ └── it should revert ✅ + └── when total price is not equal to zero + ├── when currency is not native token + │ ├── when msg.value does not equal totalPrice + │ │ └── it should revert ✅ + │ └── when msg.value does equal totalPrice + │ ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + │ └── totalPrice - platformFees should be transfered to _primarySaleRecipient ✅ + └── when currency is not native token + ├── when msg.value is not equal to zero + │ └── it should revert ✅ + └── when msg.value is equal to zero + ├── platformFees (totalPrice * platformFeeBps / MAX_BPS) should be transfered to platformFeeRecipient ✅ + └── totalPrice - platformFees should be transfered to _primarySaleRecipient ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/initialize/initialize.t.sol b/src/test/drop/drop-erc20/initialize/initialize.t.sol new file mode 100644 index 000000000..74714744e --- /dev/null +++ b/src/test/drop/drop-erc20/initialize/initialize.t.sol @@ -0,0 +1,233 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC20Test_initializer is BaseTest { + DropERC20 public newDropContract; + + event ContractURIUpdated(string prevURI, string newURI); + event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender); + event PlatformFeeInfoUpdated(address indexed platformFeeRecipient, uint256 platformFeeBps); + event PrimarySaleRecipientUpdated(address indexed recipient); + + function setUp() public override { + super.setUp(); + } + + modifier platformFeeBPSTooHigh() { + platformFeeBps = 10001; + _; + } + + function test_state() public { + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + + newDropContract = DropERC20(getContract("DropERC20")); + (address _platformFeeRecipient, uint128 _platformFeeBps) = newDropContract.getPlatformFeeInfo(); + address _saleRecipient = newDropContract.primarySaleRecipient(); + + for (uint256 i = 0; i < forwarders().length; i++) { + assertEq(newDropContract.isTrustedForwarder(forwarders()[i]), true); + } + + assertEq(newDropContract.name(), NAME); + assertEq(newDropContract.symbol(), SYMBOL); + assertEq(newDropContract.contractURI(), CONTRACT_URI); + assertEq(_platformFeeRecipient, platformFeeRecipient); + assertEq(_platformFeeBps, platformFeeBps); + assertEq(_saleRecipient, saleRecipient); + } + + function test_revert_PlatformFeeBPSTooHigh() public platformFeeBPSTooHigh { + vm.expectRevert("Exceeds max bps"); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_ContractURIUpdated() public { + vm.expectEmit(false, false, false, true); + emit ContractURIUpdated("", CONTRACT_URI); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_RoleGrantedDefaultAdminRole() public { + bytes32 role = bytes32(0x00); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_RoleGrantedTransferRole() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, deployer, 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_RoleGrantedTransferRoleZeroAddress() public { + bytes32 role = keccak256("TRANSFER_ROLE"); + vm.expectEmit(true, true, true, false); + emit RoleGranted(role, address(0), 0xf29D12e4c9d593D2887EEDDe076bBe39EDf3cD0F); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_PlatformFeeInfoUpdated() public { + vm.expectEmit(true, false, false, true); + emit PlatformFeeInfoUpdated(platformFeeRecipient, platformFeeBps); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_event_PrimarySaleRecipientUpdated() public { + vm.expectEmit(true, false, false, false); + emit PrimarySaleRecipientUpdated(saleRecipient); + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + } + + function test_roleCheck() public { + deployContractProxy( + "DropERC20", + abi.encodeCall( + DropERC20.initialize, + ( + deployer, + NAME, + SYMBOL, + CONTRACT_URI, + forwarders(), + saleRecipient, + platformFeeRecipient, + platformFeeBps + ) + ) + ); + + newDropContract = DropERC20(getContract("DropERC20")); + + assertEq(newDropContract.hasRole(bytes32(0x00), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), deployer), true); + assertEq(newDropContract.hasRole(keccak256("TRANSFER_ROLE"), address(0)), true); + } +} diff --git a/src/test/drop/drop-erc20/initialize/intialize.tree b/src/test/drop/drop-erc20/initialize/intialize.tree new file mode 100644 index 000000000..6731bc81e --- /dev/null +++ b/src/test/drop/drop-erc20/initialize/intialize.tree @@ -0,0 +1,33 @@ +function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _saleRecipient, + address _platformFeeRecipient, + uint128 _platformFeeBps +) +├── when _trustedForwarders.length > 0 +│ └── it should set _trustedForwarder[_trustedForwarders[i]] as true for each address in _trustedForwarders ✅ +├── it should set contractURI as _contractURI ✅ +├── it should emit ContractURIUpdated with the parameters: prevURI, _uri ✅ +├── it should set _defaultAdmin as the owner of the contract ✅ +├── it should emit OwnerUpdated with the parameters: _prevOwner, _defaultAdmin ✅ +├── it should assign the role DEFAULT_ADMIN_ROLE to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: DEFAULT_ADMIN_ROLE, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to _defaultAdmin ✅ +├── it should emit RoleGranted with the parameters: _transferRole, _defaultAdmin, msg.sender ✅ +├── it should assign the role _transferRole to address(0) ✅ +├── it should emit RoleGranted with the parameters: _transferRole, address(0), msg.sender ✅ +├── when _platformFeeBps is greater than 10_000 +│ └── it should revert ✅ +├── when _platformFeeBps is less than or equal to 10_000 +│ ├── it should set platformFeeBps to uint16(_platformFeeBps); ✅ +│ ├── it should set platformFeeRecipient to _platformFeeRecipient ✅ +│ └── it should emit PlatformFeeInfoUpdated with the following parameters: _platformFeeRecipient, _platformFeeBps ✅ +├── it should set recipient as _primarySaleRecipient ✅ +├── it should emit PrimarySaleRecipientUpdated with the parameters _primarySaleRecipient ✅ +├── it should set transferRole as keccak256("TRANSFER_ROLE")✅ +├── it should set _name as _name ✅ +└── it should set _symbol as _symbol ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/miscellaneous/miscellaneous.t.sol b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.t.sol new file mode 100644 index 000000000..c484a97b8 --- /dev/null +++ b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.t.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; +import { TWProxy } from "contracts/infra/TWProxy.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract HarnessDropERC20Misc is DropERC20 { + bytes32 private transferRole = keccak256("TRANSFER_ROLE"); + + function msgData() public view returns (bytes memory) { + return _msgData(); + } + + function transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) public returns (uint256) { + return _transferTokensOnClaim(_to, _quantityBeingClaimed); + } + + function beforeTokenTransfer( + address from, + address to, + uint256 amount + ) public { + _beforeTokenTransfer(from, to, amount); + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } + + function burn(address from, uint256 amount) public { + _burn(from, amount); + } + + function hasTransferRole(address _account) public view returns (bool) { + return hasRole(transferRole, _account); + } +} + +contract DropERC20Test_misc is BaseTest { + address public dropImp; + HarnessDropERC20Misc public proxy; + + function setUp() public override { + super.setUp(); + + bytes memory initializeData = abi.encodeCall( + DropERC20.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), saleRecipient, platformFeeRecipient, platformFeeBps) + ); + + dropImp = address(new HarnessDropERC20Misc()); + proxy = HarnessDropERC20Misc(address(new TWProxy(dropImp, initializeData))); + } + + function test_contractType_returnValue() public { + assertEq(proxy.contractType(), "DropERC20"); + } + + function test_contractVersion_returnValue() public { + assertEq(proxy.contractVersion(), uint8(4)); + } + + function test_msgData_returnValue() public { + bytes memory msgData = proxy.msgData(); + bytes4 expectedData = proxy.msgData.selector; + assertEq(bytes4(msgData), expectedData); + } + + function test_state_transferTokensOnClaim() public { + uint256 initialBalance = proxy.balanceOf(deployer); + uint256 quantityBeingClaimed = 1; + proxy.transferTokensOnClaim(deployer, quantityBeingClaimed); + assertEq(proxy.balanceOf(deployer), initialBalance + quantityBeingClaimed); + } + + function test_returnValue_transferTokensOnClaim() public { + uint256 quantityBeingClaimed = 1; + uint256 returnValue = proxy.transferTokensOnClaim(deployer, quantityBeingClaimed); + assertEq(returnValue, 0); + } + + function test_beforeTokenTransfer_revert_addressZeroNoTransferRole() public { + vm.prank(deployer); + proxy.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + vm.expectRevert("transfers restricted."); + proxy.beforeTokenTransfer(address(0x01), address(0x02), 1); + } + + function test_beforeTokenTransfer_doesNotRevert_addressZeroNoTransferRole_burnMint() public { + vm.prank(deployer); + proxy.revokeRole(keccak256("TRANSFER_ROLE"), address(0)); + proxy.beforeTokenTransfer(address(0), address(0x02), 1); + proxy.beforeTokenTransfer(address(0x01), address(0), 1); + } + + function test_state_mint() public { + uint256 initialBalance = proxy.balanceOf(deployer); + uint256 amount = 1; + proxy.mint(deployer, amount); + assertEq(proxy.balanceOf(deployer), initialBalance + amount); + } + + function test_state_burn() public { + proxy.mint(deployer, 1); + uint256 initialBalance = proxy.balanceOf(deployer); + uint256 amount = 1; + proxy.burn(deployer, amount); + assertEq(proxy.balanceOf(deployer), initialBalance - amount); + } + + function test_transfer_drop() public { + //deal erc20 drop to address(0x1) + deal(address(proxy), address(0x1), 1); + vm.prank(address(0x1)); + proxy.transfer(address(0x2), 1); + assertEq(proxy.balanceOf(address(0x2)), 1); + } +} diff --git a/src/test/drop/drop-erc20/miscellaneous/miscellaneous.tree b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.tree new file mode 100644 index 000000000..61c35271e --- /dev/null +++ b/src/test/drop/drop-erc20/miscellaneous/miscellaneous.tree @@ -0,0 +1,30 @@ +function contractType() +└── it should return bytes32("DropERC20") ✅ + +function contractVersion() +└── it should return uint8(4) ✅ + +function _mint(address account, uint256 amount) +└── it should mint amount tokens to account ✅ + +function _burn(address account, uint256 amount) +└── it should burn amount tokens from account ✅ + +function _afterTokenTransfer( + address from, + address to, + uint256 amount +) +└── it should call _afterTokenTransfer logic from ERC20VotesUpgradeable + +function _msgData() +└── it should return msg.data ✅ + +function _beforeTokenTransfer(address from, address to, uint256 amount) +└── when address(0) does not have transferRole and from does not equal address(0) and from does not equal address(0) + └── when from does not have transfer role and to does not have transferRole + └── it should revert ✅ + +function _transferTokensOnClaim(address _to, uint256 _quantityBeingClaimed) +├── it should mint _quantityBeingClaimed tokens to _to ✅ +└── it should return 0 ✅ \ No newline at end of file diff --git a/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.t.sol b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.t.sol new file mode 100644 index 000000000..1f99ef019 --- /dev/null +++ b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.t.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { DropERC20 } from "contracts/prebuilts/drop/DropERC20.sol"; + +// Test imports +import "../../../utils/BaseTest.sol"; + +contract DropERC20Test_setMaxTotalSupply is BaseTest { + event MaxTotalSupplyUpdated(uint256 maxTotalSupply); + + DropERC20 public drop; + + function setUp() public override { + super.setUp(); + + drop = DropERC20(getContract("DropERC20")); + } + + modifier callerHasDefaultAdminRole() { + vm.startPrank(deployer); + _; + } + + modifier callerDoesNotHaveDefaultAdminRole() { + _; + } + + function test_revert_doesNotHaveAdminRole() public callerDoesNotHaveDefaultAdminRole { + bytes32 role = bytes32(0x00); + vm.expectRevert( + abi.encodePacked( + "Permissions: account ", + TWStrings.toHexString(uint160(address(this)), 20), + " is missing role ", + TWStrings.toHexString(uint256(role), 32) + ) + ); + drop.setMaxTotalSupply(0); + } + + function test_state_callerHasDefaultAdminRole() public callerHasDefaultAdminRole { + drop.setMaxTotalSupply(100); + assertEq(drop.maxTotalSupply(), 100); + } + + function test_event_callerHasDefaultAdminRole() public callerHasDefaultAdminRole { + vm.expectEmit(false, false, false, true); + emit MaxTotalSupplyUpdated(100); + drop.setMaxTotalSupply(100); + } +} diff --git a/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.tree b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.tree new file mode 100644 index 000000000..a8b23e9ca --- /dev/null +++ b/src/test/drop/drop-erc20/setMaxTotalSupply/setMaxTotalSupply.tree @@ -0,0 +1,6 @@ +function setMaxTotalSupply(uint256 _maxTotalSupply) +├── when the caller does not have DEFAULT_ADMIN_ROLE +│ └── it should revert ✅ +└── when the caller does have DEFAULT_ADMIN_ROLE + ├── it should set maxTotalSupply as _maxTotalSupply ✅ + └── it should emit MaxTotalSupplyUpdated with the parameters _maxTotalSupply ✅ \ No newline at end of file