From 4fdce5a1947c664ec8bf0415b488767c5c6a342f Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 19:01:53 +0300 Subject: [PATCH 01/19] feat: add erc20 wrapper bundler --- src/ERC20WrapperBundler.sol | 35 ++++++++++++++++++++++++++++++++ src/ethereum/EthereumBundler.sol | 4 +++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/ERC20WrapperBundler.sol diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol new file mode 100644 index 00000000..28870f4d --- /dev/null +++ b/src/ERC20WrapperBundler.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.21; + +import {ErrorsLib} from "./libraries/ErrorsLib.sol"; +import {Math} from "../lib/morpho-utils/src/math/Math.sol"; + +import {BaseBundler} from "./BaseBundler.sol"; +import {ERC20Wrapper, ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; + +/// @title ERC20WrapperBundler +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Enables the wrapping and unwrapping of ERC20 tokens. +abstract contract ERC20WrapperBundler is BaseBundler { + /* WRAPPER ACTIONS */ + + function depositFor(address asset, address account, uint256 amount) external { + require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + require(account != address(0), ErrorsLib.ZERO_ADDRESS); + + amount = Math.min(amount, ERC20(asset).balanceOf(address(this))); + + require(amount != 0, ErrorsLib.ZERO_AMOUNT); + + ERC20Wrapper(asset).depositFor(account, amount); + } + + function withdrawTo(address asset, address account, uint256 amount) external { + require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + require(account != address(0), ErrorsLib.ZERO_ADDRESS); + require(amount != 0, ErrorsLib.ZERO_AMOUNT); + + ERC20Wrapper(asset).withdrawTo(account, amount); + } +} diff --git a/src/ethereum/EthereumBundler.sol b/src/ethereum/EthereumBundler.sol index d77289d7..745006b8 100644 --- a/src/ethereum/EthereumBundler.sol +++ b/src/ethereum/EthereumBundler.sol @@ -11,6 +11,7 @@ import {WNativeBundler} from "../WNativeBundler.sol"; import {EthereumStEthBundler} from "./EthereumStEthBundler.sol"; import {UrdBundler} from "../UrdBundler.sol"; import {MorphoBundler} from "../MorphoBundler.sol"; +import {ERC20WrapperBundler} from "../ERC20WrapperBundler.sol"; /// @title EthereumBundler /// @author Morpho Labs @@ -24,7 +25,8 @@ contract EthereumBundler is WNativeBundler, EthereumStEthBundler, UrdBundler, - MorphoBundler + MorphoBundler, + ERC20WrapperBundler { /* CONSTRUCTOR */ From 247e1335c1e518455e8372531bf9d778974dd4e3 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 19:04:58 +0300 Subject: [PATCH 02/19] chore: shut warnings in config --- config/ConfigLib.sol | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/config/ConfigLib.sol b/config/ConfigLib.sol index 572dafbc..989d255d 100644 --- a/config/ConfigLib.sol +++ b/config/ConfigLib.sol @@ -29,12 +29,13 @@ library ConfigLib { string internal constant WRAPPED_NATIVE_PATH = "$.wrappedNative"; string internal constant LSD_NATIVES_PATH = "$.lsdNatives"; - function getAddress(Config storage config, string memory key) internal returns (address) { + function getAddress(Config storage config, string memory key) internal view returns (address) { return config.json.readAddress(string.concat("$.", key)); } function getAddressArray(Config storage config, string[] memory keys) internal + view returns (address[] memory addresses) { addresses = new address[](keys.length); @@ -44,23 +45,23 @@ library ConfigLib { } } - function getChainId(Config storage config) internal returns (uint256) { + function getChainId(Config storage config) internal view returns (uint256) { return config.json.readUint(CHAIN_ID_PATH); } - function getForkBlockNumber(Config storage config) internal returns (uint256) { + function getForkBlockNumber(Config storage config) internal view returns (uint256) { return config.json.readUint(FORK_BLOCK_NUMBER_PATH); } - function getWrappedNative(Config storage config) internal returns (address) { + function getWrappedNative(Config storage config) internal view returns (address) { return getAddress(config, config.json.readString(WRAPPED_NATIVE_PATH)); } - function getLsdNatives(Config storage config) internal returns (address[] memory) { + function getLsdNatives(Config storage config) internal view returns (address[] memory) { return getAddressArray(config, config.json.readStringArray(LSD_NATIVES_PATH)); } - function getMarkets(Config storage config) internal returns (ConfigMarket[] memory markets) { + function getMarkets(Config storage config) internal view returns (ConfigMarket[] memory markets) { bytes memory encodedMarkets = config.json.parseRaw(MARKETS_PATH); RawConfigMarket[] memory rawMarkets = abi.decode(encodedMarkets, (RawConfigMarket[])); From 895a1c62107cd37a0600b46861c8404a95648f5e Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 20:07:36 +0300 Subject: [PATCH 03/19] test: add tests --- src/mocks/ERC20WrapperMock.sol | 17 ++++ .../bundlers/ERC20WrapperBundlerMock.sol | 7 ++ test/forge/ERC20WrapperBundlerLocalTest.sol | 98 +++++++++++++++++++ test/forge/helpers/BaseTest.sol | 19 ++++ 4 files changed, 141 insertions(+) create mode 100644 src/mocks/ERC20WrapperMock.sol create mode 100644 src/mocks/bundlers/ERC20WrapperBundlerMock.sol create mode 100644 test/forge/ERC20WrapperBundlerLocalTest.sol diff --git a/src/mocks/ERC20WrapperMock.sol b/src/mocks/ERC20WrapperMock.sol new file mode 100644 index 00000000..484af4f4 --- /dev/null +++ b/src/mocks/ERC20WrapperMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import { + IERC20, + ERC20Wrapper, + ERC20 +} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; + +contract ERC20WrapperMock is ERC20Wrapper { + constructor(IERC20 token, string memory _name, string memory _symbol) ERC20Wrapper(token) ERC20(_name, _symbol) {} + + function setBalance(address account, uint256 amount) external { + _burn(account, balanceOf(account)); + _mint(account, amount); + } +} diff --git a/src/mocks/bundlers/ERC20WrapperBundlerMock.sol b/src/mocks/bundlers/ERC20WrapperBundlerMock.sol new file mode 100644 index 00000000..d2b18e81 --- /dev/null +++ b/src/mocks/bundlers/ERC20WrapperBundlerMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../../TransferBundler.sol"; +import {ERC20WrapperBundler} from "../../ERC20WrapperBundler.sol"; + +contract ERC20WrapperBundlerMock is ERC20WrapperBundler, TransferBundler {} diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol new file mode 100644 index 00000000..3a19b00d --- /dev/null +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {ErrorsLib} from "../../src/libraries/ErrorsLib.sol"; + +import {ERC20WrapperBundlerMock} from "../../src/mocks/bundlers/ERC20WrapperBundlerMock.sol"; +import {ERC20WrapperMock} from "../../src/mocks/ERC20WrapperMock.sol"; + +import "./helpers/LocalTest.sol"; + +contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { + ERC20WrapperMock internal loanWrapper; + + function setUp() public override { + super.setUp(); + + bundler = new ERC20WrapperBundlerMock(); + + loanWrapper = new ERC20WrapperMock(loanToken, "Wrapped Loan Token", "WLT"); + } + + function testDepositFor(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, amount)); + + loanToken.setBalance(address(bundler), amount); + + bundler.multicall(bundle); + + assertEq(loanToken.balanceOf(address(bundler)), 0, "loan.balanceOf(bundler)"); + assertEq(loanWrapper.balanceOf(RECEIVER), amount, "loanWrapper.balanceOf(RECEIVER)"); + } + + function testDepositForAssetZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperDepositFor(address(0), RECEIVER, amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testDepositForAccountZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), address(0), amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testDepositForZeroAmount() public { + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, 0)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); + bundler.multicall(bundle); + } + + function testWithdrawTo(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + loanWrapper.setBalance(address(bundler), amount); + loanToken.setBalance(address(loanWrapper), amount); + + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, amount)); + + bundler.multicall(bundle); + + assertEq(loanWrapper.balanceOf(address(bundler)), 0, "loanWrapper.balanceOf(bundler)"); + assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); + } + + function testWithdrawToAssetZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperWithdrawTo(address(0), RECEIVER, amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testWithdrawToAccountZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), address(0), amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testWithdrawToZeroAmount() public { + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, 0)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); + bundler.multicall(bundle); + } +} diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index 7ba80a65..b9afb44e 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -25,6 +25,7 @@ import {TransferBundler} from "../../../src/TransferBundler.sol"; import {ERC4626Bundler} from "../../../src/ERC4626Bundler.sol"; import {UrdBundler} from "../../../src/UrdBundler.sol"; import {MorphoBundler} from "../../../src/MorphoBundler.sol"; +import {ERC20WrapperBundler} from "../../../src/ERC20WrapperBundler.sol"; import "../../../lib/forge-std/src/Test.sol"; import "../../../lib/forge-std/src/console2.sol"; @@ -108,6 +109,24 @@ abstract contract BaseTest is Test { return abi.encodeCall(TransferBundler.erc20TransferFrom, (asset, amount)); } + /* ERC20 WRAPPER ACTIONS */ + + function _erc20WrapperDepositFor(address asset, address account, uint256 amount) + internal + pure + returns (bytes memory) + { + return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, account, amount)); + } + + function _erc20WrapperWithdrawTo(address asset, address account, uint256 amount) + internal + pure + returns (bytes memory) + { + return abi.encodeCall(ERC20WrapperBundler.withdrawTo, (asset, account, amount)); + } + /* ERC4626 ACTIONS */ function _erc4626Mint(address vault, uint256 shares, address receiver) internal pure returns (bytes memory) { From 179b3dfc2716bb84f583fe76a942a5ddcbf87561 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 20:11:27 +0300 Subject: [PATCH 04/19] fix: wrapper bundler --- src/ERC20WrapperBundler.sol | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 28870f4d..c973e62c 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.21; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {Math} from "../lib/morpho-utils/src/math/Math.sol"; +import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {BaseBundler} from "./BaseBundler.sol"; -import {ERC20Wrapper, ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; +import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; /// @title ERC20WrapperBundler /// @author Morpho Labs @@ -14,22 +17,27 @@ import {ERC20Wrapper, ERC20} from "../lib/openzeppelin-contracts/contracts/token abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ - function depositFor(address asset, address account, uint256 amount) external { - require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + function depositFor(address wrapper, address account, uint256 amount) external { + require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); - amount = Math.min(amount, ERC20(asset).balanceOf(address(this))); + IERC20 underlying = ERC20Wrapper(wrapper).underlying(); + + amount = Math.min(amount, underlying.balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); - ERC20Wrapper(asset).depositFor(account, amount); + // Approve 0 first to comply with tokens that implement the anti frontrunning approval fix. + SafeERC20.safeApprove(underlying, wrapper, 0); + SafeERC20.safeApprove(underlying, wrapper, amount); + ERC20Wrapper(wrapper).depositFor(account, amount); } - function withdrawTo(address asset, address account, uint256 amount) external { - require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + function withdrawTo(address wrapper, address account, uint256 amount) external { + require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(amount != 0, ErrorsLib.ZERO_AMOUNT); - ERC20Wrapper(asset).withdrawTo(account, amount); + ERC20Wrapper(wrapper).withdrawTo(account, amount); } } From d1ba0769b1c6c99011bfe1e8155f48886abda89d Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 20:17:16 +0300 Subject: [PATCH 05/19] docs: add natspecs --- src/ERC20WrapperBundler.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index c973e62c..89cc0122 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -17,6 +17,11 @@ import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ + /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens. + /// @param wrapper The address of the ERC20 wrapper contract. + /// @param account The address of the account to mint the wrapped tokens to. + /// @param amount The amount of underlying tokens to deposit. + /// @dev Assumes that underlying tokens are already on the bundler. function depositFor(address wrapper, address account, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); @@ -33,6 +38,10 @@ abstract contract ERC20WrapperBundler is BaseBundler { ERC20Wrapper(wrapper).depositFor(account, amount); } + /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. + /// @param wrapper The address of the ERC20 wrapper contract. + /// @param account The address receiving the underlying tokens. + /// @param amount The amount of wrapped tokens to burn. function withdrawTo(address wrapper, address account, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); From bf7c35d9ee099e9116b06ba3037460eebe9a759d Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Mon, 23 Oct 2023 19:57:52 +0300 Subject: [PATCH 06/19] fix: deposit for initiator --- src/ERC20WrapperBundler.sol | 10 +++++----- test/forge/ERC20WrapperBundlerLocalTest.sol | 16 ++++------------ test/forge/helpers/BaseTest.sol | 8 ++------ 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 89cc0122..e26b7371 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -17,14 +17,14 @@ import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ - /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens. + /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens to the initiator. /// @param wrapper The address of the ERC20 wrapper contract. - /// @param account The address of the account to mint the wrapped tokens to. /// @param amount The amount of underlying tokens to deposit. /// @dev Assumes that underlying tokens are already on the bundler. - function depositFor(address wrapper, address account, uint256 amount) external { + /// @dev Deposits tokens "for" the `initiator` to conduct the permissionned check. Wrapped tokens must + /// be sent back to the bundler contract to perform additional actions. + function depositFor(address wrapper, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); - require(account != address(0), ErrorsLib.ZERO_ADDRESS); IERC20 underlying = ERC20Wrapper(wrapper).underlying(); @@ -35,7 +35,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { // Approve 0 first to comply with tokens that implement the anti frontrunning approval fix. SafeERC20.safeApprove(underlying, wrapper, 0); SafeERC20.safeApprove(underlying, wrapper, amount); - ERC20Wrapper(wrapper).depositFor(account, amount); + ERC20Wrapper(wrapper).depositFor(initiator(), amount); } /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol index 3a19b00d..ed18729d 100644 --- a/test/forge/ERC20WrapperBundlerLocalTest.sol +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -22,10 +22,11 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { function testDepositFor(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, amount)); + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), amount)); loanToken.setBalance(address(bundler), amount); + vm.prank(RECEIVER); bundler.multicall(bundle); assertEq(loanToken.balanceOf(address(bundler)), 0, "loan.balanceOf(bundler)"); @@ -35,23 +36,14 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { function testDepositForAssetZeroAddress(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - bundle.push(_erc20WrapperDepositFor(address(0), RECEIVER, amount)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - bundler.multicall(bundle); - } - - function testDepositForAccountZeroAddress(uint256 amount) public { - amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - - bundle.push(_erc20WrapperDepositFor(address(loanWrapper), address(0), amount)); + bundle.push(_erc20WrapperDepositFor(address(0), amount)); vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); bundler.multicall(bundle); } function testDepositForZeroAmount() public { - bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, 0)); + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), 0)); vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); bundler.multicall(bundle); diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index b9afb44e..4fb8a5ba 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -111,12 +111,8 @@ abstract contract BaseTest is Test { /* ERC20 WRAPPER ACTIONS */ - function _erc20WrapperDepositFor(address asset, address account, uint256 amount) - internal - pure - returns (bytes memory) - { - return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, account, amount)); + function _erc20WrapperDepositFor(address asset, uint256 amount) internal pure returns (bytes memory) { + return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, amount)); } function _erc20WrapperWithdrawTo(address asset, address account, uint256 amount) From 9a95e9079a479ee0ac9c1645d202dd902ecdd06e Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Mon, 23 Oct 2023 20:00:32 +0300 Subject: [PATCH 07/19] refactor: safe approve from solmate --- src/ERC20WrapperBundler.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index e26b7371..4f3965d6 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -5,7 +5,7 @@ import {IERC20} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC20. import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {Math} from "../lib/morpho-utils/src/math/Math.sol"; -import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeTransferLib, ERC20} from "../lib/solmate/src/utils/SafeTransferLib.sol"; import {BaseBundler} from "./BaseBundler.sol"; import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; @@ -15,6 +15,8 @@ import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ /// @custom:contact security@morpho.org /// @notice Enables the wrapping and unwrapping of ERC20 tokens. abstract contract ERC20WrapperBundler is BaseBundler { + using SafeTransferLib for ERC20; + /* WRAPPER ACTIONS */ /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens to the initiator. @@ -26,15 +28,15 @@ abstract contract ERC20WrapperBundler is BaseBundler { function depositFor(address wrapper, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); - IERC20 underlying = ERC20Wrapper(wrapper).underlying(); + ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); // Approve 0 first to comply with tokens that implement the anti frontrunning approval fix. - SafeERC20.safeApprove(underlying, wrapper, 0); - SafeERC20.safeApprove(underlying, wrapper, amount); + underlying.safeApprove(wrapper, 0); + underlying.safeApprove(wrapper, amount); ERC20Wrapper(wrapper).depositFor(initiator(), amount); } From f33171aaa1353e8e3bcce4c3fe016bacee28b1f7 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Tue, 24 Oct 2023 11:27:36 +0300 Subject: [PATCH 08/19] refactor: remove the check on address 0 --- src/ERC20WrapperBundler.sol | 12 ++++++------ test/forge/ERC20WrapperBundlerLocalTest.sol | 18 ------------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 4f3965d6..c4f05272 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -20,14 +20,13 @@ abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens to the initiator. - /// @param wrapper The address of the ERC20 wrapper contract. - /// @param amount The amount of underlying tokens to deposit. - /// @dev Assumes that underlying tokens are already on the bundler. /// @dev Deposits tokens "for" the `initiator` to conduct the permissionned check. Wrapped tokens must /// be sent back to the bundler contract to perform additional actions. + /// @dev Assumes that underlying tokens are already on the bundler. + /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. + /// @param wrapper The address of the ERC20 wrapper contract. + /// @param amount The amount of underlying tokens to deposit. function depositFor(address wrapper, uint256 amount) external { - require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); - ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); @@ -41,11 +40,12 @@ abstract contract ERC20WrapperBundler is BaseBundler { } /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. + /// @dev Assumes that wrapped tokens are already on the bundler. + /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. /// @param wrapper The address of the ERC20 wrapper contract. /// @param account The address receiving the underlying tokens. /// @param amount The amount of wrapped tokens to burn. function withdrawTo(address wrapper, address account, uint256 amount) external { - require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(amount != 0, ErrorsLib.ZERO_AMOUNT); diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol index ed18729d..fb1bc613 100644 --- a/test/forge/ERC20WrapperBundlerLocalTest.sol +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -33,15 +33,6 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanWrapper.balanceOf(RECEIVER), amount, "loanWrapper.balanceOf(RECEIVER)"); } - function testDepositForAssetZeroAddress(uint256 amount) public { - amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - - bundle.push(_erc20WrapperDepositFor(address(0), amount)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - bundler.multicall(bundle); - } - function testDepositForZeroAmount() public { bundle.push(_erc20WrapperDepositFor(address(loanWrapper), 0)); @@ -63,15 +54,6 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); } - function testWithdrawToAssetZeroAddress(uint256 amount) public { - amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - - bundle.push(_erc20WrapperWithdrawTo(address(0), RECEIVER, amount)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - bundler.multicall(bundle); - } - function testWithdrawToAccountZeroAddress(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); From 6bde5b8075f0820888114d7a93fc597e1b58d2a9 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Tue, 24 Oct 2023 11:30:31 +0300 Subject: [PATCH 09/19] refactor: rename wrapper functions --- src/ERC20WrapperBundler.sol | 6 ++---- test/forge/ERC20WrapperBundlerLocalTest.sol | 10 +++++----- test/forge/helpers/BaseTest.sol | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index c4f05272..f5e71200 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.21; -import {IERC20} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; - import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {Math} from "../lib/morpho-utils/src/math/Math.sol"; import {SafeTransferLib, ERC20} from "../lib/solmate/src/utils/SafeTransferLib.sol"; @@ -26,7 +24,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. /// @param wrapper The address of the ERC20 wrapper contract. /// @param amount The amount of underlying tokens to deposit. - function depositFor(address wrapper, uint256 amount) external { + function erc20WrapperDepositFor(address wrapper, uint256 amount) external { ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); @@ -45,7 +43,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @param wrapper The address of the ERC20 wrapper contract. /// @param account The address receiving the underlying tokens. /// @param amount The amount of wrapped tokens to burn. - function withdrawTo(address wrapper, address account, uint256 amount) external { + function erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external { require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(amount != 0, ErrorsLib.ZERO_AMOUNT); diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol index fb1bc613..e82c472e 100644 --- a/test/forge/ERC20WrapperBundlerLocalTest.sol +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -19,7 +19,7 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { loanWrapper = new ERC20WrapperMock(loanToken, "Wrapped Loan Token", "WLT"); } - function testDepositFor(uint256 amount) public { + function testErc20WrapperDepositFor(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); bundle.push(_erc20WrapperDepositFor(address(loanWrapper), amount)); @@ -33,14 +33,14 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanWrapper.balanceOf(RECEIVER), amount, "loanWrapper.balanceOf(RECEIVER)"); } - function testDepositForZeroAmount() public { + function testErc20WrapperDepositForZeroAmount() public { bundle.push(_erc20WrapperDepositFor(address(loanWrapper), 0)); vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); bundler.multicall(bundle); } - function testWithdrawTo(uint256 amount) public { + function testErc20WrapperWithdrawTo(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); loanWrapper.setBalance(address(bundler), amount); @@ -54,7 +54,7 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); } - function testWithdrawToAccountZeroAddress(uint256 amount) public { + function testErc20WrapperWithdrawToAccountZeroAddress(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), address(0), amount)); @@ -63,7 +63,7 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { bundler.multicall(bundle); } - function testWithdrawToZeroAmount() public { + function testErc20WrapperWithdrawToZeroAmount() public { bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, 0)); vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index 4fb8a5ba..27bec820 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -112,7 +112,7 @@ abstract contract BaseTest is Test { /* ERC20 WRAPPER ACTIONS */ function _erc20WrapperDepositFor(address asset, uint256 amount) internal pure returns (bytes memory) { - return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, amount)); + return abi.encodeCall(ERC20WrapperBundler.erc20WrapperDepositFor, (asset, amount)); } function _erc20WrapperWithdrawTo(address asset, address account, uint256 amount) @@ -120,7 +120,7 @@ abstract contract BaseTest is Test { pure returns (bytes memory) { - return abi.encodeCall(ERC20WrapperBundler.withdrawTo, (asset, account, amount)); + return abi.encodeCall(ERC20WrapperBundler.erc20WrapperWithdrawTo, (asset, account, amount)); } /* ERC4626 ACTIONS */ From b526a7fe35911c250d232c5208c5be02e5095d66 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 19:01:53 +0300 Subject: [PATCH 10/19] feat: add erc20 wrapper bundler --- src/ERC20WrapperBundler.sol | 35 ++++++++++++++++++++++++++++++++ src/ethereum/EthereumBundler.sol | 4 +++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 src/ERC20WrapperBundler.sol diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol new file mode 100644 index 00000000..28870f4d --- /dev/null +++ b/src/ERC20WrapperBundler.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity 0.8.21; + +import {ErrorsLib} from "./libraries/ErrorsLib.sol"; +import {Math} from "../lib/morpho-utils/src/math/Math.sol"; + +import {BaseBundler} from "./BaseBundler.sol"; +import {ERC20Wrapper, ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; + +/// @title ERC20WrapperBundler +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Enables the wrapping and unwrapping of ERC20 tokens. +abstract contract ERC20WrapperBundler is BaseBundler { + /* WRAPPER ACTIONS */ + + function depositFor(address asset, address account, uint256 amount) external { + require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + require(account != address(0), ErrorsLib.ZERO_ADDRESS); + + amount = Math.min(amount, ERC20(asset).balanceOf(address(this))); + + require(amount != 0, ErrorsLib.ZERO_AMOUNT); + + ERC20Wrapper(asset).depositFor(account, amount); + } + + function withdrawTo(address asset, address account, uint256 amount) external { + require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + require(account != address(0), ErrorsLib.ZERO_ADDRESS); + require(amount != 0, ErrorsLib.ZERO_AMOUNT); + + ERC20Wrapper(asset).withdrawTo(account, amount); + } +} diff --git a/src/ethereum/EthereumBundler.sol b/src/ethereum/EthereumBundler.sol index d77289d7..745006b8 100644 --- a/src/ethereum/EthereumBundler.sol +++ b/src/ethereum/EthereumBundler.sol @@ -11,6 +11,7 @@ import {WNativeBundler} from "../WNativeBundler.sol"; import {EthereumStEthBundler} from "./EthereumStEthBundler.sol"; import {UrdBundler} from "../UrdBundler.sol"; import {MorphoBundler} from "../MorphoBundler.sol"; +import {ERC20WrapperBundler} from "../ERC20WrapperBundler.sol"; /// @title EthereumBundler /// @author Morpho Labs @@ -24,7 +25,8 @@ contract EthereumBundler is WNativeBundler, EthereumStEthBundler, UrdBundler, - MorphoBundler + MorphoBundler, + ERC20WrapperBundler { /* CONSTRUCTOR */ From 8b7df764e22fe316f1bbf88f603d4dc77e11bb0a Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 20:07:36 +0300 Subject: [PATCH 11/19] test: add tests --- src/mocks/ERC20WrapperMock.sol | 17 ++++ .../bundlers/ERC20WrapperBundlerMock.sol | 7 ++ test/forge/ERC20WrapperBundlerLocalTest.sol | 98 +++++++++++++++++++ test/forge/helpers/BaseTest.sol | 19 ++++ 4 files changed, 141 insertions(+) create mode 100644 src/mocks/ERC20WrapperMock.sol create mode 100644 src/mocks/bundlers/ERC20WrapperBundlerMock.sol create mode 100644 test/forge/ERC20WrapperBundlerLocalTest.sol diff --git a/src/mocks/ERC20WrapperMock.sol b/src/mocks/ERC20WrapperMock.sol new file mode 100644 index 00000000..484af4f4 --- /dev/null +++ b/src/mocks/ERC20WrapperMock.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import { + IERC20, + ERC20Wrapper, + ERC20 +} from "../../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; + +contract ERC20WrapperMock is ERC20Wrapper { + constructor(IERC20 token, string memory _name, string memory _symbol) ERC20Wrapper(token) ERC20(_name, _symbol) {} + + function setBalance(address account, uint256 amount) external { + _burn(account, balanceOf(account)); + _mint(account, amount); + } +} diff --git a/src/mocks/bundlers/ERC20WrapperBundlerMock.sol b/src/mocks/bundlers/ERC20WrapperBundlerMock.sol new file mode 100644 index 00000000..d2b18e81 --- /dev/null +++ b/src/mocks/bundlers/ERC20WrapperBundlerMock.sol @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import "../../TransferBundler.sol"; +import {ERC20WrapperBundler} from "../../ERC20WrapperBundler.sol"; + +contract ERC20WrapperBundlerMock is ERC20WrapperBundler, TransferBundler {} diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol new file mode 100644 index 00000000..3a19b00d --- /dev/null +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +pragma solidity ^0.8.0; + +import {ErrorsLib} from "../../src/libraries/ErrorsLib.sol"; + +import {ERC20WrapperBundlerMock} from "../../src/mocks/bundlers/ERC20WrapperBundlerMock.sol"; +import {ERC20WrapperMock} from "../../src/mocks/ERC20WrapperMock.sol"; + +import "./helpers/LocalTest.sol"; + +contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { + ERC20WrapperMock internal loanWrapper; + + function setUp() public override { + super.setUp(); + + bundler = new ERC20WrapperBundlerMock(); + + loanWrapper = new ERC20WrapperMock(loanToken, "Wrapped Loan Token", "WLT"); + } + + function testDepositFor(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, amount)); + + loanToken.setBalance(address(bundler), amount); + + bundler.multicall(bundle); + + assertEq(loanToken.balanceOf(address(bundler)), 0, "loan.balanceOf(bundler)"); + assertEq(loanWrapper.balanceOf(RECEIVER), amount, "loanWrapper.balanceOf(RECEIVER)"); + } + + function testDepositForAssetZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperDepositFor(address(0), RECEIVER, amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testDepositForAccountZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), address(0), amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testDepositForZeroAmount() public { + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, 0)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); + bundler.multicall(bundle); + } + + function testWithdrawTo(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + loanWrapper.setBalance(address(bundler), amount); + loanToken.setBalance(address(loanWrapper), amount); + + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, amount)); + + bundler.multicall(bundle); + + assertEq(loanWrapper.balanceOf(address(bundler)), 0, "loanWrapper.balanceOf(bundler)"); + assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); + } + + function testWithdrawToAssetZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperWithdrawTo(address(0), RECEIVER, amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testWithdrawToAccountZeroAddress(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), address(0), amount)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); + bundler.multicall(bundle); + } + + function testWithdrawToZeroAmount() public { + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, 0)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); + bundler.multicall(bundle); + } +} diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index bf632c17..42c5a60a 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -31,6 +31,7 @@ import {TransferBundler} from "../../../src/TransferBundler.sol"; import {ERC4626Bundler} from "../../../src/ERC4626Bundler.sol"; import {UrdBundler} from "../../../src/UrdBundler.sol"; import {MorphoBundler} from "../../../src/MorphoBundler.sol"; +import {ERC20WrapperBundler} from "../../../src/ERC20WrapperBundler.sol"; import "../../../lib/forge-std/src/Test.sol"; import "../../../lib/forge-std/src/console2.sol"; @@ -114,6 +115,24 @@ abstract contract BaseTest is Test { return abi.encodeCall(TransferBundler.erc20TransferFrom, (asset, amount)); } + /* ERC20 WRAPPER ACTIONS */ + + function _erc20WrapperDepositFor(address asset, address account, uint256 amount) + internal + pure + returns (bytes memory) + { + return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, account, amount)); + } + + function _erc20WrapperWithdrawTo(address asset, address account, uint256 amount) + internal + pure + returns (bytes memory) + { + return abi.encodeCall(ERC20WrapperBundler.withdrawTo, (asset, account, amount)); + } + /* ERC4626 ACTIONS */ function _erc4626Mint(address vault, uint256 shares, address receiver) internal pure returns (bytes memory) { From 67cfe8d46748a0f945ff79c6feeb60a45d58f217 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 20:11:27 +0300 Subject: [PATCH 12/19] fix: wrapper bundler --- src/ERC20WrapperBundler.sol | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 28870f4d..c973e62c 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -1,11 +1,14 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.21; +import {IERC20} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; + import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {Math} from "../lib/morpho-utils/src/math/Math.sol"; +import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; import {BaseBundler} from "./BaseBundler.sol"; -import {ERC20Wrapper, ERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; +import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; /// @title ERC20WrapperBundler /// @author Morpho Labs @@ -14,22 +17,27 @@ import {ERC20Wrapper, ERC20} from "../lib/openzeppelin-contracts/contracts/token abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ - function depositFor(address asset, address account, uint256 amount) external { - require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + function depositFor(address wrapper, address account, uint256 amount) external { + require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); - amount = Math.min(amount, ERC20(asset).balanceOf(address(this))); + IERC20 underlying = ERC20Wrapper(wrapper).underlying(); + + amount = Math.min(amount, underlying.balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); - ERC20Wrapper(asset).depositFor(account, amount); + // Approve 0 first to comply with tokens that implement the anti frontrunning approval fix. + SafeERC20.safeApprove(underlying, wrapper, 0); + SafeERC20.safeApprove(underlying, wrapper, amount); + ERC20Wrapper(wrapper).depositFor(account, amount); } - function withdrawTo(address asset, address account, uint256 amount) external { - require(asset != address(0), ErrorsLib.ZERO_ADDRESS); + function withdrawTo(address wrapper, address account, uint256 amount) external { + require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(amount != 0, ErrorsLib.ZERO_AMOUNT); - ERC20Wrapper(asset).withdrawTo(account, amount); + ERC20Wrapper(wrapper).withdrawTo(account, amount); } } From 3ab2465c56547f5d6fa8ce860800e4d318a32de3 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Sat, 21 Oct 2023 20:17:16 +0300 Subject: [PATCH 13/19] docs: add natspecs --- src/ERC20WrapperBundler.sol | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index c973e62c..89cc0122 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -17,6 +17,11 @@ import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ + /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens. + /// @param wrapper The address of the ERC20 wrapper contract. + /// @param account The address of the account to mint the wrapped tokens to. + /// @param amount The amount of underlying tokens to deposit. + /// @dev Assumes that underlying tokens are already on the bundler. function depositFor(address wrapper, address account, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); @@ -33,6 +38,10 @@ abstract contract ERC20WrapperBundler is BaseBundler { ERC20Wrapper(wrapper).depositFor(account, amount); } + /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. + /// @param wrapper The address of the ERC20 wrapper contract. + /// @param account The address receiving the underlying tokens. + /// @param amount The amount of wrapped tokens to burn. function withdrawTo(address wrapper, address account, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); From 239c0029b669bad93b12f4a40102d18213ce7c4e Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Mon, 23 Oct 2023 19:57:52 +0300 Subject: [PATCH 14/19] fix: deposit for initiator --- src/ERC20WrapperBundler.sol | 10 +++++----- test/forge/ERC20WrapperBundlerLocalTest.sol | 16 ++++------------ test/forge/helpers/BaseTest.sol | 8 ++------ 3 files changed, 11 insertions(+), 23 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 89cc0122..e26b7371 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -17,14 +17,14 @@ import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ - /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens. + /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens to the initiator. /// @param wrapper The address of the ERC20 wrapper contract. - /// @param account The address of the account to mint the wrapped tokens to. /// @param amount The amount of underlying tokens to deposit. /// @dev Assumes that underlying tokens are already on the bundler. - function depositFor(address wrapper, address account, uint256 amount) external { + /// @dev Deposits tokens "for" the `initiator` to conduct the permissionned check. Wrapped tokens must + /// be sent back to the bundler contract to perform additional actions. + function depositFor(address wrapper, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); - require(account != address(0), ErrorsLib.ZERO_ADDRESS); IERC20 underlying = ERC20Wrapper(wrapper).underlying(); @@ -35,7 +35,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { // Approve 0 first to comply with tokens that implement the anti frontrunning approval fix. SafeERC20.safeApprove(underlying, wrapper, 0); SafeERC20.safeApprove(underlying, wrapper, amount); - ERC20Wrapper(wrapper).depositFor(account, amount); + ERC20Wrapper(wrapper).depositFor(initiator(), amount); } /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol index 3a19b00d..ed18729d 100644 --- a/test/forge/ERC20WrapperBundlerLocalTest.sol +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -22,10 +22,11 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { function testDepositFor(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, amount)); + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), amount)); loanToken.setBalance(address(bundler), amount); + vm.prank(RECEIVER); bundler.multicall(bundle); assertEq(loanToken.balanceOf(address(bundler)), 0, "loan.balanceOf(bundler)"); @@ -35,23 +36,14 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { function testDepositForAssetZeroAddress(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - bundle.push(_erc20WrapperDepositFor(address(0), RECEIVER, amount)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - bundler.multicall(bundle); - } - - function testDepositForAccountZeroAddress(uint256 amount) public { - amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - - bundle.push(_erc20WrapperDepositFor(address(loanWrapper), address(0), amount)); + bundle.push(_erc20WrapperDepositFor(address(0), amount)); vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); bundler.multicall(bundle); } function testDepositForZeroAmount() public { - bundle.push(_erc20WrapperDepositFor(address(loanWrapper), RECEIVER, 0)); + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), 0)); vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); bundler.multicall(bundle); diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index 42c5a60a..63530559 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -117,12 +117,8 @@ abstract contract BaseTest is Test { /* ERC20 WRAPPER ACTIONS */ - function _erc20WrapperDepositFor(address asset, address account, uint256 amount) - internal - pure - returns (bytes memory) - { - return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, account, amount)); + function _erc20WrapperDepositFor(address asset, uint256 amount) internal pure returns (bytes memory) { + return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, amount)); } function _erc20WrapperWithdrawTo(address asset, address account, uint256 amount) From ac81e9ab9b01320579a1ddd57a613269537166a7 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Mon, 23 Oct 2023 20:00:32 +0300 Subject: [PATCH 15/19] refactor: safe approve from solmate --- src/ERC20WrapperBundler.sol | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index e26b7371..4f3965d6 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -5,7 +5,7 @@ import {IERC20} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC20. import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {Math} from "../lib/morpho-utils/src/math/Math.sol"; -import {SafeERC20} from "../lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol"; +import {SafeTransferLib, ERC20} from "../lib/solmate/src/utils/SafeTransferLib.sol"; import {BaseBundler} from "./BaseBundler.sol"; import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Wrapper.sol"; @@ -15,6 +15,8 @@ import {ERC20Wrapper} from "../lib/openzeppelin-contracts/contracts/token/ERC20/ /// @custom:contact security@morpho.org /// @notice Enables the wrapping and unwrapping of ERC20 tokens. abstract contract ERC20WrapperBundler is BaseBundler { + using SafeTransferLib for ERC20; + /* WRAPPER ACTIONS */ /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens to the initiator. @@ -26,15 +28,15 @@ abstract contract ERC20WrapperBundler is BaseBundler { function depositFor(address wrapper, uint256 amount) external { require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); - IERC20 underlying = ERC20Wrapper(wrapper).underlying(); + ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); // Approve 0 first to comply with tokens that implement the anti frontrunning approval fix. - SafeERC20.safeApprove(underlying, wrapper, 0); - SafeERC20.safeApprove(underlying, wrapper, amount); + underlying.safeApprove(wrapper, 0); + underlying.safeApprove(wrapper, amount); ERC20Wrapper(wrapper).depositFor(initiator(), amount); } From 521a408af6081f948c2c78554ab350d9a91887ed Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Tue, 24 Oct 2023 11:27:36 +0300 Subject: [PATCH 16/19] refactor: remove the check on address 0 --- src/ERC20WrapperBundler.sol | 12 ++++++------ test/forge/ERC20WrapperBundlerLocalTest.sol | 18 ------------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 4f3965d6..c4f05272 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -20,14 +20,13 @@ abstract contract ERC20WrapperBundler is BaseBundler { /* WRAPPER ACTIONS */ /// @notice Deposits underlying tokens and mints the corresponding number of wrapped tokens to the initiator. - /// @param wrapper The address of the ERC20 wrapper contract. - /// @param amount The amount of underlying tokens to deposit. - /// @dev Assumes that underlying tokens are already on the bundler. /// @dev Deposits tokens "for" the `initiator` to conduct the permissionned check. Wrapped tokens must /// be sent back to the bundler contract to perform additional actions. + /// @dev Assumes that underlying tokens are already on the bundler. + /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. + /// @param wrapper The address of the ERC20 wrapper contract. + /// @param amount The amount of underlying tokens to deposit. function depositFor(address wrapper, uint256 amount) external { - require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); - ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); @@ -41,11 +40,12 @@ abstract contract ERC20WrapperBundler is BaseBundler { } /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. + /// @dev Assumes that wrapped tokens are already on the bundler. + /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. /// @param wrapper The address of the ERC20 wrapper contract. /// @param account The address receiving the underlying tokens. /// @param amount The amount of wrapped tokens to burn. function withdrawTo(address wrapper, address account, uint256 amount) external { - require(wrapper != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(amount != 0, ErrorsLib.ZERO_AMOUNT); diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol index ed18729d..fb1bc613 100644 --- a/test/forge/ERC20WrapperBundlerLocalTest.sol +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -33,15 +33,6 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanWrapper.balanceOf(RECEIVER), amount, "loanWrapper.balanceOf(RECEIVER)"); } - function testDepositForAssetZeroAddress(uint256 amount) public { - amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - - bundle.push(_erc20WrapperDepositFor(address(0), amount)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - bundler.multicall(bundle); - } - function testDepositForZeroAmount() public { bundle.push(_erc20WrapperDepositFor(address(loanWrapper), 0)); @@ -63,15 +54,6 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); } - function testWithdrawToAssetZeroAddress(uint256 amount) public { - amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); - - bundle.push(_erc20WrapperWithdrawTo(address(0), RECEIVER, amount)); - - vm.expectRevert(bytes(ErrorsLib.ZERO_ADDRESS)); - bundler.multicall(bundle); - } - function testWithdrawToAccountZeroAddress(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); From 7b16d8f17c9daf98f28c125bed3c23d4d1ed8383 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Tue, 24 Oct 2023 11:30:31 +0300 Subject: [PATCH 17/19] refactor: rename wrapper functions --- src/ERC20WrapperBundler.sol | 6 ++---- test/forge/ERC20WrapperBundlerLocalTest.sol | 10 +++++----- test/forge/helpers/BaseTest.sol | 4 ++-- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index c4f05272..f5e71200 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -1,8 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity 0.8.21; -import {IERC20} from "../lib/openzeppelin-contracts/contracts/interfaces/IERC20.sol"; - import {ErrorsLib} from "./libraries/ErrorsLib.sol"; import {Math} from "../lib/morpho-utils/src/math/Math.sol"; import {SafeTransferLib, ERC20} from "../lib/solmate/src/utils/SafeTransferLib.sol"; @@ -26,7 +24,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. /// @param wrapper The address of the ERC20 wrapper contract. /// @param amount The amount of underlying tokens to deposit. - function depositFor(address wrapper, uint256 amount) external { + function erc20WrapperDepositFor(address wrapper, uint256 amount) external { ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); @@ -45,7 +43,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @param wrapper The address of the ERC20 wrapper contract. /// @param account The address receiving the underlying tokens. /// @param amount The amount of wrapped tokens to burn. - function withdrawTo(address wrapper, address account, uint256 amount) external { + function erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external { require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(amount != 0, ErrorsLib.ZERO_AMOUNT); diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol index fb1bc613..e82c472e 100644 --- a/test/forge/ERC20WrapperBundlerLocalTest.sol +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -19,7 +19,7 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { loanWrapper = new ERC20WrapperMock(loanToken, "Wrapped Loan Token", "WLT"); } - function testDepositFor(uint256 amount) public { + function testErc20WrapperDepositFor(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); bundle.push(_erc20WrapperDepositFor(address(loanWrapper), amount)); @@ -33,14 +33,14 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanWrapper.balanceOf(RECEIVER), amount, "loanWrapper.balanceOf(RECEIVER)"); } - function testDepositForZeroAmount() public { + function testErc20WrapperDepositForZeroAmount() public { bundle.push(_erc20WrapperDepositFor(address(loanWrapper), 0)); vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); bundler.multicall(bundle); } - function testWithdrawTo(uint256 amount) public { + function testErc20WrapperWithdrawTo(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); loanWrapper.setBalance(address(bundler), amount); @@ -54,7 +54,7 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); } - function testWithdrawToAccountZeroAddress(uint256 amount) public { + function testErc20WrapperWithdrawToAccountZeroAddress(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), address(0), amount)); @@ -63,7 +63,7 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { bundler.multicall(bundle); } - function testWithdrawToZeroAmount() public { + function testErc20WrapperWithdrawToZeroAmount() public { bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, 0)); vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index 63530559..e9d8d66c 100644 --- a/test/forge/helpers/BaseTest.sol +++ b/test/forge/helpers/BaseTest.sol @@ -118,7 +118,7 @@ abstract contract BaseTest is Test { /* ERC20 WRAPPER ACTIONS */ function _erc20WrapperDepositFor(address asset, uint256 amount) internal pure returns (bytes memory) { - return abi.encodeCall(ERC20WrapperBundler.depositFor, (asset, amount)); + return abi.encodeCall(ERC20WrapperBundler.erc20WrapperDepositFor, (asset, amount)); } function _erc20WrapperWithdrawTo(address asset, address account, uint256 amount) @@ -126,7 +126,7 @@ abstract contract BaseTest is Test { pure returns (bytes memory) { - return abi.encodeCall(ERC20WrapperBundler.withdrawTo, (asset, account, amount)); + return abi.encodeCall(ERC20WrapperBundler.erc20WrapperWithdrawTo, (asset, account, amount)); } /* ERC4626 ACTIONS */ From 300d519349d80ed8af79931cba8b30f172c4901e Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Wed, 8 Nov 2023 11:58:34 +0300 Subject: [PATCH 18/19] feat: use approve max --- src/ERC20WrapperBundler.sol | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index f5e71200..14287f99 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -31,9 +31,7 @@ abstract contract ERC20WrapperBundler is BaseBundler { require(amount != 0, ErrorsLib.ZERO_AMOUNT); - // Approve 0 first to comply with tokens that implement the anti frontrunning approval fix. - underlying.safeApprove(wrapper, 0); - underlying.safeApprove(wrapper, amount); + _approveMaxTo(address(underlying), wrapper); ERC20Wrapper(wrapper).depositFor(initiator(), amount); } From f77392b52abe2f9f1262e979890385c54f806bc6 Mon Sep 17 00:00:00 2001 From: MerlinEgalite Date: Wed, 8 Nov 2023 12:11:12 +0300 Subject: [PATCH 19/19] feat: protect functions --- src/ERC20WrapperBundler.sol | 8 ++++---- test/forge/ERC20WrapperBundlerLocalTest.sol | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol index 14287f99..d1b79813 100644 --- a/src/ERC20WrapperBundler.sol +++ b/src/ERC20WrapperBundler.sol @@ -21,10 +21,10 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @dev Deposits tokens "for" the `initiator` to conduct the permissionned check. Wrapped tokens must /// be sent back to the bundler contract to perform additional actions. /// @dev Assumes that underlying tokens are already on the bundler. - /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. + /// @dev Assumes that `wrapper` implements the `ERC20Wrapper` interface. /// @param wrapper The address of the ERC20 wrapper contract. /// @param amount The amount of underlying tokens to deposit. - function erc20WrapperDepositFor(address wrapper, uint256 amount) external { + function erc20WrapperDepositFor(address wrapper, uint256 amount) external protected { ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); amount = Math.min(amount, underlying.balanceOf(address(this))); @@ -37,11 +37,11 @@ abstract contract ERC20WrapperBundler is BaseBundler { /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. /// @dev Assumes that wrapped tokens are already on the bundler. - /// @dev Assumes that `wrapper` is implements the `ERC20Wrapper` interface. + /// @dev Assumes that `wrapper` implements the `ERC20Wrapper` interface. /// @param wrapper The address of the ERC20 wrapper contract. /// @param account The address receiving the underlying tokens. /// @param amount The amount of wrapped tokens to burn. - function erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external { + function erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external protected { require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(amount != 0, ErrorsLib.ZERO_AMOUNT); diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol index e82c472e..a822332a 100644 --- a/test/forge/ERC20WrapperBundlerLocalTest.sol +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -69,4 +69,18 @@ contract ERC20WrapperBundlerBundlerLocalTest is LocalTest { vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); bundler.multicall(bundle); } + + function testErc20WrapperDepositForUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + ERC20WrapperBundler(address(bundler)).erc20WrapperDepositFor(address(loanWrapper), amount); + } + + function testErc20WrapperWithdrawToUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + ERC20WrapperBundler(address(bundler)).erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, amount); + } }