diff --git a/.gitmodules b/.gitmodules index 9296efd..359112e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "lib/openzeppelin-contracts-upgradeable"] path = lib/openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "lib/royalty-registry-solidity"] + path = lib/royalty-registry-solidity + url = https://github.com/manifoldxyz/royalty-registry-solidity diff --git a/Makefile b/Makefile index 2784ffb..fddbbb8 100644 --- a/Makefile +++ b/Makefile @@ -12,20 +12,30 @@ remove: # Install the modules install: - forge install foundry-rs/forge-std - forge install OpenZeppelin/openzeppelin-contracts@v4.8.3 - forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v4.8.3 + forge install foundry-rs/forge-std --no-commit + forge install OpenZeppelin/openzeppelin-contracts@v4.8.3 --no-commit + forge install OpenZeppelin/openzeppelin-contracts-upgradeable@v4.8.3 --no-commit + forge install manifoldxyz/royalty-registry-solidity --no-commit # Updatee the modules update: remove install # Builds build: - forge fmt && forge clean && forge build --optimize --optimizer-runs 2000 + forge fmt && forge clean && forge build # Tests -test_suite: +compiler_test: forge test --use 0.8.17 forge test --use 0.8.18 forge test --use 0.8.19 - forge test --use 0.8.20 \ No newline at end of file + forge test --use 0.8.20 + +quick_test: + forge test --fuzz-runs 512 + +gas_test: + forge test --gas-report + +fuzz_test: + forge test --fuzz-runs 10000 \ No newline at end of file diff --git a/README.md b/README.md index 2436935..afee23a 100644 --- a/README.md +++ b/README.md @@ -23,4 +23,4 @@ This codebase is provided on an "as is" and "as available" basis. We do not give any warranties and will not be liable for any loss incurred through any use of this codebase. ## License -This code is copyright Transient Labs, Inc 2022 and is licensed under the MIT license. \ No newline at end of file +This code is copyright Transient Labs, Inc 2023 and is licensed under the MIT license. \ No newline at end of file diff --git a/foundry.toml b/foundry.toml index 1c71788..709186c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,21 +1,12 @@ [profile.default] -src = 'src' # the source directory -test = 'test' # the test directory -out = 'out' # the output directory (for artifacts) -libs = ['lib'] # a list of library directories -libraries = [] # a list of deployed libraries to link against -cache = true # whether to cache builds or not -force = false # whether to ignore the cache (clean build) -evm_version = 'london' # the evm version (by hardfork name) -#solc_version = '0.8.17' # override for the solc version (setting this ignores `auto_detect_solc`) -auto_detect_solc = true # enable auto-detection of the appropriate solc version to use -offline = false # disable downloading of missing solc version(s) -optimizer = true # enable or disable the solc optimizer -optimizer_runs = 2000 # the number of optimizer runs -verbosity = 3 # the verbosity of tests -ffi = false # whether to enable ffi or not -gas_reports = ["*"] -fs_permissions = [{ access = 'read', path = './test/file_utils'}] - -[fuzz] -runs = 10000 \ No newline at end of file +src = 'src' +test = 'test' +out = 'out' +libs = ['lib'] +auto_detect_solc = true +optimizer = true +optimizer_runs = 20000 +verbosity = 3 +wrap_comments = true +gas_reports = ["OwnableAccessControl", "EIP2981TL", "OwnableAccessControlUpgradeable", "EIP2981TLUpgradeable", "TransferHelper", "RoyaltyPayoutHelper", "RoyaltyPayoutHelperUpgradeable"] +fs_permissions = [{ access = "read", path = "./"}] \ No newline at end of file diff --git a/lib/forge-std b/lib/forge-std index e8a047e..74cfb77 160000 --- a/lib/forge-std +++ b/lib/forge-std @@ -1 +1 @@ -Subproject commit e8a047e3f40f13fa37af6fe14e6e06283d9a060e +Subproject commit 74cfb77e308dd188d2f58864aaf44963ae6b88b1 diff --git a/lib/royalty-registry-solidity b/lib/royalty-registry-solidity new file mode 160000 index 0000000..e5369fc --- /dev/null +++ b/lib/royalty-registry-solidity @@ -0,0 +1 @@ +Subproject commit e5369fc79279ce2e4c6ea2eb5914df51e89e8bd8 diff --git a/remappings.txt b/remappings.txt index fe7dfe8..43c8980 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,3 +1,14 @@ -forge-std/=lib/forge-std/src +@manifoldxyz/libraries-solidity/=lib/royalty-registry-solidity/lib/libraries-solidity/ +@openzeppelin/contracts-upgradeable/=lib/royalty-registry-solidity/lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/contracts/=lib/royalty-registry-solidity/lib/openzeppelin-contracts/contracts/ +create2-helpers/=lib/royalty-registry-solidity/lib/create2-helpers/ +create2-scripts/=lib/royalty-registry-solidity/lib/create2-helpers/script/ +ds-test/=lib/forge-std/lib/ds-test/src/ +forge-std/=lib/forge-std/src/ +libraries-solidity/=lib/royalty-registry-solidity/lib/libraries-solidity/contracts/ +openzeppelin-contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/ +openzeppelin-contracts/=lib/openzeppelin-contracts/ +openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ openzeppelin/=lib/openzeppelin-contracts/contracts/ -openzeppelin-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ \ No newline at end of file +royalty-registry-solidity/=lib/royalty-registry-solidity/contracts +tl-sol-tools/=src/ \ No newline at end of file diff --git a/src/access/OwnableAccessControl.sol b/src/access/OwnableAccessControl.sol index 39b1cb4..0b709a0 100644 --- a/src/access/OwnableAccessControl.sol +++ b/src/access/OwnableAccessControl.sol @@ -24,7 +24,7 @@ error NotRoleOrOwner(bytes32 role); /// @dev by default, only the owner can grant roles but by inheriting, but you /// may allow other roles to grant roles by using the internal helper. /// @author transientlabs.xyz -/// @custom:version 2.2.2 +/// @custom:last-updated 2.2.2 abstract contract OwnableAccessControl is Ownable { /*////////////////////////////////////////////////////////////////////////// State Variables diff --git a/src/payments/IWETH.sol b/src/payments/IWETH.sol new file mode 100644 index 0000000..f38b643 --- /dev/null +++ b/src/payments/IWETH.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; + +interface IWETH is IERC20 { + function deposit() external payable; +} diff --git a/src/payments/RoyaltyPayoutHelper.sol b/src/payments/RoyaltyPayoutHelper.sol new file mode 100644 index 0000000..f502132 --- /dev/null +++ b/src/payments/RoyaltyPayoutHelper.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {TransferHelper} from "./TransferHelper.sol"; +import {IRoyaltyEngineV1} from "royalty-registry-solidity/IRoyaltyEngineV1.sol"; + +/*////////////////////////////////////////////////////////////////////////// + Royalty Payout Helper +//////////////////////////////////////////////////////////////////////////*/ + +/// @title Royalty Payout Helper +/// @notice Abstract contract to help payout royalties using the Royalty Registry +/// @author transientlabs.xyz +/// @custom:last-updated 2.3.0 +abstract contract RoyaltyPayoutHelper is TransferHelper { + /*////////////////////////////////////////////////////////////////////////// + State Variables + //////////////////////////////////////////////////////////////////////////*/ + + address public weth; + IRoyaltyEngineV1 public royaltyEngine; + + /*////////////////////////////////////////////////////////////////////////// + Constructor + //////////////////////////////////////////////////////////////////////////*/ + + /// @param wethAddress - the init weth address + /// @param royaltyEngineAddress - the init royalty engine address + constructor(address wethAddress, address royaltyEngineAddress) { + weth = wethAddress; + royaltyEngine = IRoyaltyEngineV1(royaltyEngineAddress); + } + + /*////////////////////////////////////////////////////////////////////////// + Internal State Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to update the WETH address + /// @dev Care should be taken to ensure proper access control for this function + /// @param wethAddress The new WETH token address + function _setWethAddress(address wethAddress) internal { + weth = wethAddress; + } + + /// @notice Function to update the royalty engine address + /// @dev Care should be taken to ensure proper access control for this function + /// @param royaltyEngineAddress The new royalty engine address + function _setRoyaltyEngineAddress(address royaltyEngineAddress) internal { + royaltyEngine = IRoyaltyEngineV1(royaltyEngineAddress); + } + + /*////////////////////////////////////////////////////////////////////////// + Royalty Payout Function + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to payout royalties from the contract balance based on sale price + /// @dev if the call to the royalty engine reverts or if the return values are invalid, no payments are made + /// @dev if the sum of the royalty payouts is greater than the salePrice, the loop exits early for gas savings (this shouldn't happen in reality) + /// @dev if this is used in a call where tokens should be transferred from a sender, it is advisable to + /// first transfer the required amount to the contract and then call this function, as it will save on gas + /// @param token The contract address for the token + /// @param tokenId The token id + /// @param currency The address of the currency to send to recipients (null address == ETH) + /// @param salePrice The sale price for the token + /// @return remainingSale The amount left over in the sale after paying out royalties + function _payoutRoyalties(address token, uint256 tokenId, address currency, uint256 salePrice) + internal + returns (uint256 remainingSale) + { + remainingSale = salePrice; + try royaltyEngine.getRoyalty(token, tokenId, salePrice) returns ( + address payable[] memory recipients, uint256[] memory amounts + ) { + if (recipients.length != amounts.length) return remainingSale; + + for (uint256 i = 0; i < recipients.length; i++) { + if (amounts[i] > remainingSale) break; + remainingSale -= amounts[i]; + if (currency == address(0)) { + _safeTransferETH(recipients[i], amounts[i], weth); + } else { + _safeTransferERC20(recipients[i], currency, amounts[i]); + } + } + + return remainingSale; + } catch { + return remainingSale; + } + } +} diff --git a/src/payments/TransferHelper.sol b/src/payments/TransferHelper.sol new file mode 100644 index 0000000..5eb8ad7 --- /dev/null +++ b/src/payments/TransferHelper.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {SafeERC20} from "openzeppelin/token/ERC20/utils/SafeERC20.sol"; +import {IWETH, IERC20} from "./IWETH.sol"; + +/*////////////////////////////////////////////////////////////////////////// + Custom Errors +//////////////////////////////////////////////////////////////////////////*/ + +/// @dev ETH transfer failed +error ETHTransferFailed(); + +/// @dev Transferred too few ERC-20 tokens +error InsufficentERC20Transfer(); + +/*////////////////////////////////////////////////////////////////////////// + Transfer Helper +//////////////////////////////////////////////////////////////////////////*/ + +/// @title Transfer Helper +/// @notice Abstract contract that has helper function for sending ETH and ERC20's safely +/// @author transientlabs.xyz +/// @custom:last-updated 2.3.0 +abstract contract TransferHelper { + /*////////////////////////////////////////////////////////////////////////// + State Variables + //////////////////////////////////////////////////////////////////////////*/ + + using SafeERC20 for IERC20; + using SafeERC20 for IWETH; + + /*////////////////////////////////////////////////////////////////////////// + ETH Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to force transfer ETH + /// @dev On failure to send the ETH, the ETH is converted to WETH and sent + /// @dev Care should be taken to always pass the proper WETH address that adheres to IWETH + /// @param recipient The recipient of the ETH + /// @param amount The amount of ETH to send + /// @param weth The WETH token address + function _safeTransferETH(address recipient, uint256 amount, address weth) internal { + (bool success,) = recipient.call{value: amount}(""); + if (!success) { + IWETH token = IWETH(weth); + token.deposit{value: amount}(); + token.safeTransfer(recipient, amount); + } + } + + /*////////////////////////////////////////////////////////////////////////// + ERC-20 Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to safely transfer ERC-20 tokens from the contract, without checking for token tax + /// @dev Does not check if the sender has enough balance as that is handled by the token contract + /// @dev Does not check for token tax as that could lock up funds in the contract + /// @dev Reverts on failure to transfer + /// @param recipient The recipient of the ERC-20 token + /// @param currency The address of the ERC-20 token + /// @param amount The amount of ERC-20 to send + function _safeTransferERC20(address recipient, address currency, uint256 amount) internal { + IERC20(currency).safeTransfer(recipient, amount); + } + + /// @notice Function to safely transfer ERC-20 tokens from another address to a recipient + /// @dev Does not check if the sender has enough balance or allowance for this contract as that is handled by the token contract + /// @dev Reverts on failure to transfer + /// @dev Reverts if there is a token tax taken out + /// @param sender The sender of the tokens + /// @param recipient The recipient of the ERC-20 token + /// @param currency The address of the ERC-20 token + /// @param amount The amount of ERC-20 to send + function _safeTransferFromERC20(address sender, address recipient, address currency, uint256 amount) internal { + IERC20 token = IERC20(currency); + uint256 intialBalance = token.balanceOf(recipient); + token.safeTransferFrom(sender, recipient, amount); + uint256 finalBalance = token.balanceOf(recipient); + if (finalBalance - intialBalance < amount) revert InsufficentERC20Transfer(); + } +} diff --git a/src/royalties/EIP2981TL.sol b/src/royalties/EIP2981TL.sol index 7a4b284..c4f82fc 100644 --- a/src/royalties/EIP2981TL.sol +++ b/src/royalties/EIP2981TL.sol @@ -24,7 +24,7 @@ error MaxRoyaltyError(); /// @dev follows EIP-2981 (https://eips.ethereum.org/EIPS/eip-2981) /// @author transientlabs.xyz /// https://github.com/Transient-Labs/tl-sol-tools -/// @custom:version 2.2.2 +/// @custom:last-updated 2.2.2 abstract contract EIP2981TL is IEIP2981, ERC165 { /*////////////////////////////////////////////////////////////////////////// Royalty Struct diff --git a/src/upgradeable/access/OwnableAccessControlUpgradeable.sol b/src/upgradeable/access/OwnableAccessControlUpgradeable.sol index b2710d3..229c18b 100644 --- a/src/upgradeable/access/OwnableAccessControlUpgradeable.sol +++ b/src/upgradeable/access/OwnableAccessControlUpgradeable.sol @@ -25,7 +25,7 @@ error NotRoleOrOwner(bytes32 role); /// @dev by default, only the owner can grant roles but by inheriting, but you /// may allow other roles to grant roles by using the internal helper. /// @author transientlabs.xyz -/// @custom:version 2.2.2 +/// @custom:last-updated 2.2.2 abstract contract OwnableAccessControlUpgradeable is Initializable, OwnableUpgradeable { /*////////////////////////////////////////////////////////////////////////// State Variables diff --git a/src/upgradeable/payments/RoyaltyPayoutHelperUpgradeable.sol b/src/upgradeable/payments/RoyaltyPayoutHelperUpgradeable.sol new file mode 100644 index 0000000..bb011d0 --- /dev/null +++ b/src/upgradeable/payments/RoyaltyPayoutHelperUpgradeable.sol @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol"; +import {TransferHelper} from "../../payments/TransferHelper.sol"; +import {IRoyaltyEngineV1} from "royalty-registry-solidity/IRoyaltyEngineV1.sol"; + +/*////////////////////////////////////////////////////////////////////////// + Royalty Payout Helper +//////////////////////////////////////////////////////////////////////////*/ + +/// @title Royalty Payout Helper +/// @notice Abstract contract to help payout royalties using the Royalty Registry +/// @author transientlabs.xyz +/// @custom:last-updated 2.3.0 +abstract contract RoyaltyPayoutHelperUpgradeable is Initializable, TransferHelper { + /*////////////////////////////////////////////////////////////////////////// + State Variables + //////////////////////////////////////////////////////////////////////////*/ + + address public weth; + IRoyaltyEngineV1 public royaltyEngine; + + /*////////////////////////////////////////////////////////////////////////// + Initializer + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to initialize the contract + /// @param wethAddress - the init weth address + /// @param royaltyEngineAddress - the init royalty engine address + function __RoyaltyPayoutHelper_init(address wethAddress, address royaltyEngineAddress) internal onlyInitializing { + __RoyaltyPayoutHelper_init_unchained(wethAddress, royaltyEngineAddress); + } + + /// @notice unchained function to initialize the contract + /// @param wethAddress - the init weth address + /// @param royaltyEngineAddress - the init royalty engine address + function __RoyaltyPayoutHelper_init_unchained(address wethAddress, address royaltyEngineAddress) + internal + onlyInitializing + { + weth = wethAddress; + royaltyEngine = IRoyaltyEngineV1(royaltyEngineAddress); + } + + /*////////////////////////////////////////////////////////////////////////// + Internal State Functions + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to update the WETH address + /// @dev Care should be taken to ensure proper access control for this function + /// @param wethAddress The new WETH token address + function _setWethAddress(address wethAddress) internal { + weth = wethAddress; + } + + /// @notice Function to update the royalty engine address + /// @dev Care should be taken to ensure proper access control for this function + /// @param royaltyEngineAddress The new royalty engine address + function _setRoyaltyEngineAddress(address royaltyEngineAddress) internal { + royaltyEngine = IRoyaltyEngineV1(royaltyEngineAddress); + } + + /*////////////////////////////////////////////////////////////////////////// + Royalty Payout Function + //////////////////////////////////////////////////////////////////////////*/ + + /// @notice Function to payout royalties from the contract balance based on sale price + /// @dev if the call to the royalty engine reverts or if the return values are invalid, no payments are made + /// @dev if the sum of the royalty payouts is greater than the salePrice, the loop exits early for gas savings (this shouldn't happen in reality) + /// @dev if this is used in a call where tokens should be transferred from a sender, it is advisable to + /// first transfer the required amount to the contract and then call this function, as it will save on gas + /// @param token The contract address for the token + /// @param tokenId The token id + /// @param currency The address of the currency to send to recipients (null address == ETH) + /// @param salePrice The sale price for the token + /// @return remainingSale The amount left over in the sale after paying out royalties + function _payoutRoyalties(address token, uint256 tokenId, address currency, uint256 salePrice) + internal + returns (uint256 remainingSale) + { + remainingSale = salePrice; + try royaltyEngine.getRoyalty(token, tokenId, salePrice) returns ( + address payable[] memory recipients, uint256[] memory amounts + ) { + if (recipients.length != amounts.length) return remainingSale; + + for (uint256 i = 0; i < recipients.length; i++) { + if (amounts[i] > remainingSale) break; + remainingSale -= amounts[i]; + if (currency == address(0)) { + _safeTransferETH(recipients[i], amounts[i], weth); + } else { + _safeTransferERC20(recipients[i], currency, amounts[i]); + } + } + + return remainingSale; + } catch { + return remainingSale; + } + } + + /*////////////////////////////////////////////////////////////////////////// + Upgradeability Gap + //////////////////////////////////////////////////////////////////////////*/ + + /// @dev gap variable - see https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps + uint256[50] private _gap; +} diff --git a/src/upgradeable/royalties/EIP2981TLUpgradeable.sol b/src/upgradeable/royalties/EIP2981TLUpgradeable.sol index b1349cb..443ab9d 100644 --- a/src/upgradeable/royalties/EIP2981TLUpgradeable.sol +++ b/src/upgradeable/royalties/EIP2981TLUpgradeable.sol @@ -24,7 +24,7 @@ error MaxRoyaltyError(); /// while allowing for specific token overrides /// @dev follows EIP-2981 (https://eips.ethereum.org/EIPS/eip-2981) /// @author transientlabs.xyz -/// @custom:version 2.2.2 +/// @custom:last-updated 2.2.2 abstract contract EIP2981TLUpgradeable is IEIP2981, Initializable, ERC165Upgradeable { /*////////////////////////////////////////////////////////////////////////// Royalty Struct diff --git a/test/OwnableAccessControl.t.sol b/test/access/OwnableAccessControl.t.sol similarity index 98% rename from test/OwnableAccessControl.t.sol rename to test/access/OwnableAccessControl.t.sol index d4be2aa..d927b40 100644 --- a/test/OwnableAccessControl.t.sol +++ b/test/access/OwnableAccessControl.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {MockOwnableAccessControl} from "./mocks/MockOwnableAccessControl.sol"; -import {OwnableAccessControl, NotRoleOrOwner, NotSpecifiedRole} from "../src/access/OwnableAccessControl.sol"; +import {MockOwnableAccessControl} from "../utils/MockOwnableAccessControl.sol"; +import {OwnableAccessControl, NotRoleOrOwner, NotSpecifiedRole} from "tl-sol-tools/access/OwnableAccessControl.sol"; contract TestOwnableAccessControl is Test { MockOwnableAccessControl public mockContract; diff --git a/test/payments/RoyaltyPayoutHelper.t.sol b/test/payments/RoyaltyPayoutHelper.t.sol new file mode 100644 index 0000000..5fb0ad2 --- /dev/null +++ b/test/payments/RoyaltyPayoutHelper.t.sol @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {Receiver, RevertingReceiver} from "../utils/Receivers.sol"; +import {WETH9} from "../utils/WETH9.sol"; +import {MockERC20, MockERC20WithFee} from "../utils/MockERC20.sol"; +import {RoyaltyPayoutHelper, IRoyaltyEngineV1} from "tl-sol-tools/payments/RoyaltyPayoutHelper.sol"; +import {Strings} from "openzeppelin/utils/Strings.sol"; + +contract ExternalRoyaltyPayoutHelper is RoyaltyPayoutHelper { + + constructor(address wethAddress, address royaltyEngineAddress) RoyaltyPayoutHelper(wethAddress, royaltyEngineAddress) {} + + function setWethAddress(address wethAddress) external { + _setWethAddress(wethAddress); + } + + function setRoyaltyEngineAddress(address royaltyEngineAddress) external { + _setRoyaltyEngineAddress(royaltyEngineAddress); + } + + function payoutRoyalties(address token, uint256 tokenId, address currency, uint256 salePrice) external returns(uint256) { + return _payoutRoyalties(token, tokenId, currency, salePrice); + } +} + +contract TestRoyaltyPayoutHelper is Test { + + using Strings for uint256; + + ExternalRoyaltyPayoutHelper rph; + address weth; + address receiver; + address revertingReceiver; + MockERC20 erc20; + MockERC20WithFee erc20fee; + + address royaltyEngine = 0x0385603ab55642cb4Dd5De3aE9e306809991804f; + + address ben = address(0x0BEEF); + address chris = address(0xC0FFEE); + address david = address(0x1D1B); + + function setUp() public { + weth = address(new WETH9()); + receiver = address(new Receiver()); + revertingReceiver = address(new RevertingReceiver()); + erc20 = new MockERC20(ben); + erc20fee = new MockERC20WithFee(ben); + + rph = new ExternalRoyaltyPayoutHelper(weth, royaltyEngine); + } + + function testInit() public view { + assert(rph.weth() == weth); + assert(address(rph.royaltyEngine()) == royaltyEngine); + } + + function testUpdateWethAddress(address newWeth) public { + rph.setWethAddress(newWeth); + assert(rph.weth() == newWeth); + } + + function testUpdateRoyaltyEngine(address newRoyaltyEngine) public { + rph.setRoyaltyEngineAddress(newRoyaltyEngine); + assert(address(rph.royaltyEngine()) == newRoyaltyEngine); + } + + function testPayoutRoyaltiesRevertingQuery(uint256 salePrice) public { + vm.mockCallRevert( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + "fail fail" + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingSale == salePrice); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesUnequalLengthArrays(uint256 salePrice) public { + + address[] memory recipients = new address[](1); + recipients[0] = address(1); + uint256[] memory amounts = new uint256[](0); + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingSale == salePrice); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesZeroLengthArrays(uint256 salePrice) public { + + address[] memory recipients = new address[](0); + uint256[] memory amounts = new uint256[](0); + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingSale == salePrice); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesMoreThanSalePrice() public { + uint256 price = 1 ether; + address[] memory recipients = new address[](2); + recipients[0] = address(100); + recipients[1] = address(101); + uint256[] memory amounts = new uint256[](2); + amounts[0] = 0.9 ether; + amounts[1] = 0.2 ether; + + vm.deal(address(rph), price); + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), price); + assert(address(100).balance == 0.9 ether); + assert(remainingSale == 0.1 ether); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesETH(uint8 numRecipients, uint256 salePrice) public { + vm.assume(salePrice > 4); + vm.assume(numRecipients > 0); + vm.assume(salePrice >= numRecipients); + uint256 price = salePrice / numRecipients; + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + uint256 remainingAmount = salePrice; + for (uint256 i = 0; i < numRecipients; i++) { + remainingAmount -= price; + amounts[i] = price; + recipients[i] = makeAddr(i.toString()); + } + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + vm.deal(address(rph), salePrice); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingAmount == remainingSale); + for (uint256 i = 0; i < numRecipients; i++) { + assert(recipients[i].balance == price); + } + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesERC20(uint8 numRecipients, uint256 salePrice) public { + vm.assume(salePrice > 4); + vm.assume(numRecipients > 0); + vm.assume(salePrice >= numRecipients); + uint256 price = salePrice / numRecipients; + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + uint256 remainingAmount = salePrice; + for (uint256 i = 0; i < numRecipients; i++) { + remainingAmount -= price; + amounts[i] = price; + recipients[i] = makeAddr(i.toString()); + } + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + vm.prank(ben); + erc20.transfer(address(rph), salePrice); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(erc20), salePrice); + assert(remainingAmount == remainingSale); + for (uint256 i = 0; i < numRecipients; i++) { + assert(erc20.balanceOf(recipients[i]) == price); + } + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesERC20WithFee(uint8 numRecipients, uint128 salePrice) public { + vm.assume(salePrice > 4); + vm.assume(numRecipients > 0); + vm.assume(salePrice >= numRecipients); + uint256 price = uint256(salePrice) / numRecipients; + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + uint256 remainingAmount = salePrice; + for (uint256 i = 0; i < numRecipients; i++) { + remainingAmount -= price; + amounts[i] = price; + recipients[i] = makeAddr(i.toString()); + } + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + vm.prank(ben); + erc20fee.transfer(address(rph), uint256(salePrice)+1); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(erc20fee), uint256(salePrice)); + assert(remainingAmount == remainingSale); + for (uint256 i = 0; i < numRecipients; i++) { + assert(erc20fee.balanceOf(recipients[i]) == price-1); + } + + vm.clearMockedCalls(); + } + +} \ No newline at end of file diff --git a/test/payments/TransferHelper.t.sol b/test/payments/TransferHelper.t.sol new file mode 100644 index 0000000..f7fdd37 --- /dev/null +++ b/test/payments/TransferHelper.t.sol @@ -0,0 +1,136 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {Receiver, RevertingReceiver} from "../utils/Receivers.sol"; +import {WETH9} from "../utils/WETH9.sol"; +import {MockERC20, MockERC20WithFee} from "../utils/MockERC20.sol"; +import {TransferHelper, ETHTransferFailed, InsufficentERC20Transfer} from "tl-sol-tools/payments/TransferHelper.sol"; +import {IERC20} from "openzeppelin/token/ERC20/IERC20.sol"; + +contract ExternalTransferHelper is TransferHelper { + + function safeTransferETH(address recipient, uint256 amount, address weth) external { + _safeTransferETH(recipient, amount, weth); + } + + function safeTransferERC20(address recipient, address currency, uint256 amount) external { + _safeTransferERC20(recipient, currency, amount); + } + + function safeTransferFromERC20(address sender, address recipient, address currency, uint256 amount) external { + _safeTransferFromERC20(sender, recipient, currency, amount); + } +} + +contract TestTransferHelper is Test { + ExternalTransferHelper th; + address weth; + address receiver; + address revertingReceiver; + MockERC20 erc20; + MockERC20WithFee erc20fee; + + address ben = address(0x0BEEF); + address chris = address(0xC0FFEE); + address david = address(0x1D1B); + + function setUp() public { + th = new ExternalTransferHelper(); + weth = address(new WETH9()); + receiver = address(new Receiver()); + revertingReceiver = address(new RevertingReceiver()); + erc20 = new MockERC20(ben); + erc20fee = new MockERC20WithFee(ben); + } + + function testSafeTransferETH(address recipient, uint256 amount) public { + + vm.assume( + recipient.code.length == 0 && recipient > address(100) + ); + + // test contract receiver + vm.deal(address(th), amount); + uint256 b1 = receiver.balance; + th.safeTransferETH(receiver, amount, weth); + assert(receiver.balance - b1 == amount); + + // test recipient + vm.deal(address(th), amount); + uint256 b2 = recipient.balance; + th.safeTransferETH(recipient, amount, weth); + assert(recipient.balance - b2 == amount); + + // test reverting receiver + vm.deal(address(th), amount); + uint256 b3 = IERC20(weth).balanceOf(revertingReceiver); + th.safeTransferETH(revertingReceiver, amount, weth); + assert(IERC20(weth).balanceOf(revertingReceiver) - b3 == amount); + } + + function testSafeTransferERC20(address recipient, uint256 amount) public { + + vm.assume(recipient != address(0) && recipient != address(th) && amount > 0); + + // fund contract + vm.prank(ben); + erc20.transfer(address(th), amount); + + // test amount with regular ERC20 + uint256 b1 = erc20.balanceOf(recipient); + th.safeTransferERC20(recipient, address(erc20), amount); + assert(erc20.balanceOf(recipient) - b1 == amount); + + if (amount > 1) { + // fund contract + vm.prank(ben); + erc20fee.transfer(address(th), amount); + + // test amount with token tax ERC20 + uint256 b2 = erc20fee.balanceOf(recipient); + th.safeTransferERC20(recipient, address(erc20fee), amount-1); + assert(erc20fee.balanceOf(recipient) - b2 == amount-2); + } + } + + function testSafeTransferFromERC20(address recipient, uint256 amount) public { + vm.assume(recipient != address(0) && recipient != address(th) && amount > 0); + + // fund chris + vm.prank(ben); + erc20.transfer(chris, amount); + + // test failure for allowance + vm.expectRevert(); + th.safeTransferFromERC20(chris, recipient, address(erc20), amount); + + // give allowance + vm.prank(chris); + erc20.approve(address(th), amount); + + // test amount with regular ERC20 + uint256 b1 = erc20.balanceOf(recipient); + th.safeTransferFromERC20(chris, recipient, address(erc20), amount); + assert(erc20.balanceOf(recipient) - b1 == amount); + + if (amount > 1) { + // fund chris + vm.prank(ben); + erc20fee.transfer(chris, amount); + + // test failure for allowance + vm.expectRevert(); + th.safeTransferFromERC20(chris, recipient, address(erc20fee), amount-1); + + // give allowance + vm.prank(chris); + erc20fee.approve(address(th), amount-1); + + // test amount with token tax ERC20 + vm.expectRevert(InsufficentERC20Transfer.selector); + th.safeTransferFromERC20(chris, recipient, address(erc20fee), amount-1); + } + } + +} \ No newline at end of file diff --git a/test/EIP2981TL.t.sol b/test/royalties/EIP2981TL.t.sol similarity index 95% rename from test/EIP2981TL.t.sol rename to test/royalties/EIP2981TL.t.sol index b1418ff..4db9afd 100644 --- a/test/EIP2981TL.t.sol +++ b/test/royalties/EIP2981TL.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {MockEIP2981TL} from "./mocks/MockEIP2981TL.sol"; -import {ZeroAddressError, MaxRoyaltyError} from "../src/royalties/EIP2981TL.sol"; +import {MockEIP2981TL} from "../utils/MockEIP2981TL.sol"; +import {ZeroAddressError, MaxRoyaltyError} from "tl-sol-tools/royalties/EIP2981TL.sol"; contract TestEIP2981TL is Test { MockEIP2981TL public mockContract; diff --git a/test/upgradeable/OwnableAccessControlUpgradeable.t.sol b/test/upgradeable/access/OwnableAccessControlUpgradeable.t.sol similarity index 98% rename from test/upgradeable/OwnableAccessControlUpgradeable.t.sol rename to test/upgradeable/access/OwnableAccessControlUpgradeable.t.sol index 3f134e0..5154dc7 100644 --- a/test/upgradeable/OwnableAccessControlUpgradeable.t.sol +++ b/test/upgradeable/access/OwnableAccessControlUpgradeable.t.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {MockOwnableAccessControlUpgradeable} from "./mocks/MockOwnableAccessControlUpgradeable.sol"; +import {MockOwnableAccessControlUpgradeable} from "../../utils/MockOwnableAccessControlUpgradeable.sol"; import { OwnableAccessControlUpgradeable, NotRoleOrOwner, NotSpecifiedRole -} from "../../src/upgradeable/access/OwnableAccessControlUpgradeable.sol"; +} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; contract TestOwnableAccessControl is Test { MockOwnableAccessControlUpgradeable public mockContract; diff --git a/test/upgradeable/payments/RoyaltyPayoutHelperUpgradeable.t.sol b/test/upgradeable/payments/RoyaltyPayoutHelperUpgradeable.t.sol new file mode 100644 index 0000000..a999488 --- /dev/null +++ b/test/upgradeable/payments/RoyaltyPayoutHelperUpgradeable.t.sol @@ -0,0 +1,244 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import "forge-std/Test.sol"; +import {Receiver, RevertingReceiver} from "../../utils/Receivers.sol"; +import {WETH9} from "../../utils/WETH9.sol"; +import {MockERC20, MockERC20WithFee} from "../../utils/MockERC20.sol"; +import {RoyaltyPayoutHelperUpgradeable, IRoyaltyEngineV1} from "tl-sol-tools/upgradeable/payments/RoyaltyPayoutHelperUpgradeable.sol"; +import {Strings} from "openzeppelin/utils/Strings.sol"; +import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol"; + +contract ExternalRoyaltyPayoutHelper is Initializable, RoyaltyPayoutHelperUpgradeable { + + function initialize(address wethAddress, address royaltyEngineAddress) external initializer { + __RoyaltyPayoutHelper_init(wethAddress, royaltyEngineAddress); + } + + function setWethAddress(address wethAddress) external { + _setWethAddress(wethAddress); + } + + function setRoyaltyEngineAddress(address royaltyEngineAddress) external { + _setRoyaltyEngineAddress(royaltyEngineAddress); + } + + function payoutRoyalties(address token, uint256 tokenId, address currency, uint256 salePrice) external returns(uint256) { + return _payoutRoyalties(token, tokenId, currency, salePrice); + } +} + +contract TestRoyaltyPayoutHelperUpgradeable is Test { + + using Strings for uint256; + + ExternalRoyaltyPayoutHelper rph; + address weth; + address receiver; + address revertingReceiver; + MockERC20 erc20; + MockERC20WithFee erc20fee; + + address royaltyEngine = 0x0385603ab55642cb4Dd5De3aE9e306809991804f; + + address ben = address(0x0BEEF); + address chris = address(0xC0FFEE); + address david = address(0x1D1B); + + function setUp() public { + weth = address(new WETH9()); + receiver = address(new Receiver()); + revertingReceiver = address(new RevertingReceiver()); + erc20 = new MockERC20(ben); + erc20fee = new MockERC20WithFee(ben); + + rph = new ExternalRoyaltyPayoutHelper(); + rph.initialize(weth, royaltyEngine); + } + + function testInit() public view { + assert(rph.weth() == weth); + assert(address(rph.royaltyEngine()) == royaltyEngine); + } + + function testInitAgain() public { + vm.expectRevert(); + rph.initialize(weth, royaltyEngine); + } + + function testUpdateWethAddress(address newWeth) public { + rph.setWethAddress(newWeth); + assert(rph.weth() == newWeth); + } + + function testUpdateRoyaltyEngine(address newRoyaltyEngine) public { + rph.setRoyaltyEngineAddress(newRoyaltyEngine); + assert(address(rph.royaltyEngine()) == newRoyaltyEngine); + } + + function testPayoutRoyaltiesRevertingQuery(uint256 salePrice) public { + vm.mockCallRevert( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + "fail fail" + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingSale == salePrice); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesUnequalLengthArrays(uint256 salePrice) public { + + address[] memory recipients = new address[](1); + recipients[0] = address(1); + uint256[] memory amounts = new uint256[](0); + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingSale == salePrice); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesZeroLengthArrays(uint256 salePrice) public { + + address[] memory recipients = new address[](0); + uint256[] memory amounts = new uint256[](0); + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingSale == salePrice); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesMoreThanSalePrice() public { + uint256 price = 1 ether; + address[] memory recipients = new address[](2); + recipients[0] = address(100); + recipients[1] = address(101); + uint256[] memory amounts = new uint256[](2); + amounts[0] = 0.9 ether; + amounts[1] = 0.2 ether; + + vm.deal(address(rph), price); + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), price); + assert(address(100).balance == 0.9 ether); + assert(remainingSale == 0.1 ether); + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesETH(uint8 numRecipients, uint256 salePrice) public { + vm.assume(salePrice > 4); + vm.assume(numRecipients > 0); + vm.assume(salePrice >= numRecipients); + uint256 price = salePrice / numRecipients; + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + uint256 remainingAmount = salePrice; + for (uint256 i = 0; i < numRecipients; i++) { + remainingAmount -= price; + amounts[i] = price; + recipients[i] = makeAddr(i.toString()); + } + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + vm.deal(address(rph), salePrice); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(0), salePrice); + assert(remainingAmount == remainingSale); + for (uint256 i = 0; i < numRecipients; i++) { + assert(recipients[i].balance == price); + } + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesERC20(uint8 numRecipients, uint256 salePrice) public { + vm.assume(salePrice > 4); + vm.assume(numRecipients > 0); + vm.assume(salePrice >= numRecipients); + uint256 price = salePrice / numRecipients; + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + uint256 remainingAmount = salePrice; + for (uint256 i = 0; i < numRecipients; i++) { + remainingAmount -= price; + amounts[i] = price; + recipients[i] = makeAddr(i.toString()); + } + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + vm.prank(ben); + erc20.transfer(address(rph), salePrice); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(erc20), salePrice); + assert(remainingAmount == remainingSale); + for (uint256 i = 0; i < numRecipients; i++) { + assert(erc20.balanceOf(recipients[i]) == price); + } + + vm.clearMockedCalls(); + } + + function testPayoutRoyaltiesERC20WithFee(uint8 numRecipients, uint128 salePrice) public { + vm.assume(salePrice > 4); + vm.assume(numRecipients > 0); + vm.assume(salePrice >= numRecipients); + uint256 price = uint256(salePrice) / numRecipients; + address[] memory recipients = new address[](numRecipients); + uint256[] memory amounts = new uint256[](numRecipients); + uint256 remainingAmount = salePrice; + for (uint256 i = 0; i < numRecipients; i++) { + remainingAmount -= price; + amounts[i] = price; + recipients[i] = makeAddr(i.toString()); + } + + vm.mockCall( + royaltyEngine, + abi.encodeWithSelector(IRoyaltyEngineV1.getRoyalty.selector), + abi.encode(recipients, amounts) + ); + + vm.prank(ben); + erc20fee.transfer(address(rph), uint256(salePrice)+1); + + uint256 remainingSale = rph.payoutRoyalties(address(1), 1, address(erc20fee), uint256(salePrice)); + assert(remainingAmount == remainingSale); + for (uint256 i = 0; i < numRecipients; i++) { + assert(erc20fee.balanceOf(recipients[i]) == price-1); + } + + vm.clearMockedCalls(); + } + +} \ No newline at end of file diff --git a/test/upgradeable/EIP2981TLUpgradeable.t.sol b/test/upgradeable/royalties/EIP2981TLUpgradeable.t.sol similarity index 94% rename from test/upgradeable/EIP2981TLUpgradeable.t.sol rename to test/upgradeable/royalties/EIP2981TLUpgradeable.t.sol index 034916b..fdb507b 100644 --- a/test/upgradeable/EIP2981TLUpgradeable.t.sol +++ b/test/upgradeable/royalties/EIP2981TLUpgradeable.t.sol @@ -3,8 +3,8 @@ pragma solidity ^0.8.17; import "forge-std/Test.sol"; -import {MockEIP2981TLUpgradeable} from "./mocks/MockEIP2981TLUpgradeable.sol"; -import {ZeroAddressError, MaxRoyaltyError} from "../../src/upgradeable/royalties/EIP2981TLUpgradeable.sol"; +import {MockEIP2981TLUpgradeable} from "../../utils/MockEIP2981TLUpgradeable.sol"; +import {ZeroAddressError, MaxRoyaltyError} from "tl-sol-tools/upgradeable/royalties/EIP2981TLUpgradeable.sol"; contract TestEIP2981TLUpgradeable is Test { MockEIP2981TLUpgradeable public mockContract; diff --git a/test/mocks/MockEIP2981TL.sol b/test/utils/MockEIP2981TL.sol similarity index 91% rename from test/mocks/MockEIP2981TL.sol rename to test/utils/MockEIP2981TL.sol index 0570ffd..2ab8ae1 100644 --- a/test/mocks/MockEIP2981TL.sol +++ b/test/utils/MockEIP2981TL.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.17; -import {EIP2981TL} from "../../src/royalties/EIP2981TL.sol"; +import {EIP2981TL} from "tl-sol-tools/royalties/EIP2981TL.sol"; contract MockEIP2981TL is EIP2981TL { constructor(address recipient, uint256 percentage) EIP2981TL(recipient, percentage) {} diff --git a/test/upgradeable/mocks/MockEIP2981TLUpgradeable.sol b/test/utils/MockEIP2981TLUpgradeable.sol similarity index 93% rename from test/upgradeable/mocks/MockEIP2981TLUpgradeable.sol rename to test/utils/MockEIP2981TLUpgradeable.sol index d440482..d056f61 100644 --- a/test/upgradeable/mocks/MockEIP2981TLUpgradeable.sol +++ b/test/utils/MockEIP2981TLUpgradeable.sol @@ -5,7 +5,7 @@ pragma solidity ^0.8.17; import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol"; -import {EIP2981TLUpgradeable} from "../../../src/upgradeable/royalties/EIP2981TLUpgradeable.sol"; +import {EIP2981TLUpgradeable} from "tl-sol-tools/upgradeable/royalties/EIP2981TLUpgradeable.sol"; contract MockEIP2981TLUpgradeable is Initializable, EIP2981TLUpgradeable { function initialize(address recipient, uint256 percentage) external initializer { diff --git a/test/utils/MockERC20.sol b/test/utils/MockERC20.sol new file mode 100644 index 0000000..ef02184 --- /dev/null +++ b/test/utils/MockERC20.sol @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +import {ERC20} from "openzeppelin/token/ERC20/ERC20.sol"; + +contract MockERC20 is ERC20 { + constructor(address mintReceiver) ERC20("Test Token", "TEST") { + _mint(mintReceiver, type(uint256).max); + } +} + +contract MockERC20WithFee is ERC20 { + constructor(address mintReceiver) ERC20("Test Token", "TEST") { + _mint(mintReceiver, type(uint256).max); + } + + function transfer(address to, uint256 value) public override returns (bool) { + address owner = _msgSender(); + _burn(owner, 1); + _transfer(owner, to, value-1); + return true; + } + + function transferFrom(address from, address to, uint256 value) public override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, value); + _burn(from, 1); + _transfer(from, to, value-1); + return true; + } +} \ No newline at end of file diff --git a/test/mocks/MockOwnableAccessControl.sol b/test/utils/MockOwnableAccessControl.sol similarity index 93% rename from test/mocks/MockOwnableAccessControl.sol rename to test/utils/MockOwnableAccessControl.sol index e61b345..db8ee02 100644 --- a/test/mocks/MockOwnableAccessControl.sol +++ b/test/utils/MockOwnableAccessControl.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.17; -import {OwnableAccessControl} from "../../src/access/OwnableAccessControl.sol"; +import {OwnableAccessControl} from "tl-sol-tools/access/OwnableAccessControl.sol"; contract MockOwnableAccessControl is OwnableAccessControl { uint256 public number; diff --git a/test/upgradeable/mocks/MockOwnableAccessControlUpgradeable.sol b/test/utils/MockOwnableAccessControlUpgradeable.sol similarity index 95% rename from test/upgradeable/mocks/MockOwnableAccessControlUpgradeable.sol rename to test/utils/MockOwnableAccessControlUpgradeable.sol index ff7fd6d..cf50760 100644 --- a/test/upgradeable/mocks/MockOwnableAccessControlUpgradeable.sol +++ b/test/utils/MockOwnableAccessControlUpgradeable.sol @@ -3,7 +3,7 @@ pragma solidity ^0.8.17; import {Initializable} from "openzeppelin-upgradeable/proxy/utils/Initializable.sol"; -import {OwnableAccessControlUpgradeable} from "../../../src/upgradeable/access/OwnableAccessControlUpgradeable.sol"; +import {OwnableAccessControlUpgradeable} from "tl-sol-tools/upgradeable/access/OwnableAccessControlUpgradeable.sol"; contract MockOwnableAccessControlUpgradeable is Initializable, OwnableAccessControlUpgradeable { uint256 public number; diff --git a/test/utils/Receivers.sol b/test/utils/Receivers.sol new file mode 100644 index 0000000..116eb26 --- /dev/null +++ b/test/utils/Receivers.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +contract Receiver { + event EthReceived(uint256 indexed amount); + + receive() external payable { + emit EthReceived(msg.value); + } +} + +contract RevertingReceiver { + receive() external payable { + revert("you shall not pass"); + } +} \ No newline at end of file diff --git a/test/utils/WETH9.sol b/test/utils/WETH9.sol new file mode 100644 index 0000000..0a993b9 --- /dev/null +++ b/test/utils/WETH9.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +contract WETH9 { + string public name = "Wrapped Ether"; + string public symbol = "WETH"; + uint8 public decimals = 18; + + event Approval( + address indexed src, + address indexed guy, + uint256 wad + ); + event Transfer( + address indexed src, + address indexed dst, + uint256 wad + ); + event Deposit(address indexed dst, uint256 wad); + event Withdrawal(address indexed src, uint256 wad); + + mapping(address => uint256) public balanceOf; + mapping(address => + mapping(address => uint256)) public allowance; + + receive() external payable { + deposit(); + } + + function deposit() public payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + function withdraw(uint256 wad) public { + require(balanceOf[msg.sender] >= wad); + balanceOf[msg.sender] -= wad; + payable(msg.sender).transfer(wad); + emit Withdrawal(msg.sender, wad); + } + + function totalSupply() public view returns (uint256) { + return address(this).balance; + } + + function approve( + address guy, + uint256 wad + ) public returns (bool) { + allowance[msg.sender][guy] = wad; + emit Approval(msg.sender, guy, wad); + return true; + } + + function transfer( + address dst, + uint256 wad + ) public returns (bool) { + return transferFrom(msg.sender, dst, wad); + } + + function transferFrom( + address src, + address dst, + uint256 wad + ) public returns (bool) { + require(balanceOf[src] >= wad); + + if ( + src != msg.sender && + allowance[src][msg.sender] != type(uint256).max + ) { + require(allowance[src][msg.sender] >= wad); + allowance[src][msg.sender] -= wad; + } + + balanceOf[src] -= wad; + balanceOf[dst] += wad; + + emit Transfer(src, dst, wad); + + return true; + } +} \ No newline at end of file