diff --git a/src/BaseBundler.sol b/src/BaseBundler.sol index 416b9fc6..ca7b7536 100644 --- a/src/BaseBundler.sol +++ b/src/BaseBundler.sol @@ -24,6 +24,17 @@ abstract contract BaseBundler is IMulticall { /// @dev Also prevents interacting with the bundler outside of an initiated execution context. address private _initiator = UNSET_INITIATOR; + /* MODIFIERS */ + + /// @dev Prevents a function to be called outside an initiated `multicall` context and protects a function from + /// being called by an unauthorized sender inside an initiated multicall context. + modifier protected() { + require(_initiator != UNSET_INITIATOR, ErrorsLib.UNINITIATED); + require(_isSenderAuthorized(), ErrorsLib.UNAUTHORIZED_SENDER); + + _; + } + /* PUBLIC */ /// @notice Returns the address of the initiator of the multicall transaction. @@ -60,11 +71,6 @@ abstract contract BaseBundler is IMulticall { } } - /// @dev Checks that the contract is in an initiated execution context. - function _checkInitiated() internal view { - require(_initiator != UNSET_INITIATOR, ErrorsLib.UNINITIATED); - } - /// @dev Bubbles up the revert reason / custom error encoded in `returnData`. /// @dev Assumes `returnData` is the return data of any kind of failing CALL to a contract. function _revert(bytes memory returnData) internal pure { @@ -76,6 +82,12 @@ abstract contract BaseBundler is IMulticall { } } + /// @dev Returns whether the sender of the call is authorized. + /// @dev Assumes to be inside a properly initiated `multicall` context. + function _isSenderAuthorized() internal view virtual returns (bool) { + return msg.sender == _initiator; + } + /// @dev Gives the max approval to `spender` to spend the given `asset` if not already approved. /// @dev Assumes that `type(uint256).max` is large enough to never have to increase the allowance again. function _approveMaxTo(address asset, address spender) internal { diff --git a/src/ERC20WrapperBundler.sol b/src/ERC20WrapperBundler.sol new file mode 100644 index 00000000..2066a2fd --- /dev/null +++ b/src/ERC20WrapperBundler.sol @@ -0,0 +1,56 @@ +// 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 {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"; + +/// @title ERC20WrapperBundler +/// @author Morpho Labs +/// @custom:contact security@morpho.org +/// @notice Enables the wrapping and unwrapping of ERC20 tokens. The largest usecase is to wrap permissionless tokens to +/// their permissioned counterparts and access permissioned markets on Morpho Blue. Permissioned tokens can be built +/// using: https://github.com/morpho-org/erc20-permissioned +abstract contract ERC20WrapperBundler is BaseBundler { + using SafeTransferLib for ERC20; + + /* WRAPPER ACTIONS */ + + /// @notice Deposits underlying tokens and mints the corresponding amount of wrapped tokens to the initiator. + /// @dev Wraps tokens on behalf of the initiator to make sure they are able to receive and transfer wrapped tokens. + /// @dev Wrapped tokens must be transferred to the bundler afterwards to perform additional actions. + /// @dev Initiator must have previously transferred their tokens to the bundler. + /// @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. Pass `type(uint256).max` to deposit the bundler's + /// balance. + function erc20WrapperDepositFor(address wrapper, uint256 amount) external protected { + ERC20 underlying = ERC20(address(ERC20Wrapper(wrapper).underlying())); + + amount = Math.min(amount, underlying.balanceOf(address(this))); + + require(amount != 0, ErrorsLib.ZERO_AMOUNT); + + _approveMaxTo(address(underlying), wrapper); + ERC20Wrapper(wrapper).depositFor(initiator(), amount); + } + + /// @notice Burns a number of wrapped tokens and withdraws the corresponding number of underlying tokens. + /// @dev Initiator must have previously transferred their wrapped tokens to the bundler. + /// @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. Pass `type(uint256).max` to burn the bundler's balance. + function erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external protected { + require(account != address(0), ErrorsLib.ZERO_ADDRESS); + + amount = Math.min(amount, ERC20(wrapper).balanceOf(address(this))); + + require(amount != 0, ErrorsLib.ZERO_AMOUNT); + + ERC20Wrapper(wrapper).withdrawTo(account, amount); + } +} diff --git a/src/ERC4626Bundler.sol b/src/ERC4626Bundler.sol index 3d22205f..79776092 100644 --- a/src/ERC4626Bundler.sol +++ b/src/ERC4626Bundler.sol @@ -20,13 +20,16 @@ abstract contract ERC4626Bundler is BaseBundler { /// @notice Mints the given amount of `shares` on the given ERC4626 `vault`, on behalf of `receiver`. /// @dev Initiator must have previously transferred their assets to the bundler. - /// @dev Warning: `vault` can re-enter the bundler flow. /// @dev Assumes the given `vault` implements EIP-4626. /// @param vault The address of the vault. /// @param shares The amount of shares to mint. Pass `type(uint256).max` to mint max. /// @param maxAssets The maximum amount of assets to deposit in exchange for `shares`. /// @param receiver The address to which shares will be minted. - function erc4626Mint(address vault, uint256 shares, uint256 maxAssets, address receiver) external payable { + function erc4626Mint(address vault, uint256 shares, uint256 maxAssets, address receiver) + external + payable + protected + { require(receiver != address(0), ErrorsLib.ZERO_ADDRESS); /// Do not check `receiver != address(this)` to allow the bundler to receive the vault's shares. @@ -42,13 +45,16 @@ abstract contract ERC4626Bundler is BaseBundler { /// @notice Deposits the given amount of `assets` on the given ERC4626 `vault`, on behalf of `receiver`. /// @dev Initiator must have previously transferred their assets to the bundler. - /// @dev Warning: `vault` can re-enter the bundler flow. /// @dev Assumes the given `vault` implements EIP-4626. /// @param vault The address of the vault. /// @param assets The amount of assets to deposit. Pass `type(uint256).max` to deposit max. /// @param minShares The minimum amount of shares to mint in exchange for `assets`. /// @param receiver The address to which shares will be minted. - function erc4626Deposit(address vault, uint256 assets, uint256 minShares, address receiver) external payable { + function erc4626Deposit(address vault, uint256 assets, uint256 minShares, address receiver) + external + payable + protected + { require(receiver != address(0), ErrorsLib.ZERO_ADDRESS); /// Do not check `receiver != address(this)` to allow the bundler to receive the vault's shares. @@ -67,14 +73,16 @@ abstract contract ERC4626Bundler is BaseBundler { /// @notice Withdraws the given amount of `assets` from the given ERC4626 `vault`, transferring assets to /// `receiver`. - /// @notice Warning: should only be called via the bundler's `multicall` function. - /// @dev Warning: `vault` can re-enter the bundler flow. /// @dev Assumes the given `vault` implements EIP-4626. /// @param vault The address of the vault. /// @param assets The amount of assets to withdraw. Pass `type(uint256).max` to withdraw max. /// @param maxShares The maximum amount of shares to redeem in exchange for `assets`. /// @param receiver The address that will receive the withdrawn assets. - function erc4626Withdraw(address vault, uint256 assets, uint256 maxShares, address receiver) external payable { + function erc4626Withdraw(address vault, uint256 assets, uint256 maxShares, address receiver) + external + payable + protected + { require(receiver != address(0), ErrorsLib.ZERO_ADDRESS); /// Do not check `receiver != address(this)` to allow the bundler to receive the underlying asset. @@ -89,14 +97,16 @@ abstract contract ERC4626Bundler is BaseBundler { } /// @notice Redeems the given amount of `shares` from the given ERC4626 `vault`, transferring assets to `receiver`. - /// @notice Warning: should only be called via the bundler's `multicall` function. - /// @dev Warning: `vault` can re-enter the bundler flow. /// @dev Assumes the given `vault` implements EIP-4626. /// @param vault The address of the vault. /// @param shares The amount of shares to burn. Pass `type(uint256).max` to redeem max. /// @param minAssets The minimum amount of assets to withdraw in exchange for `shares`. /// @param receiver The address that will receive the withdrawn assets. - function erc4626Redeem(address vault, uint256 shares, uint256 minAssets, address receiver) external payable { + function erc4626Redeem(address vault, uint256 shares, uint256 minAssets, address receiver) + external + payable + protected + { require(receiver != address(0), ErrorsLib.ZERO_ADDRESS); /// Do not check `receiver != address(this)` to allow the bundler to receive the underlying asset. diff --git a/src/MorphoBundler.sol b/src/MorphoBundler.sol index fb152c89..185f57e8 100644 --- a/src/MorphoBundler.sol +++ b/src/MorphoBundler.sol @@ -61,7 +61,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { Authorization calldata authorization, Signature calldata signature, bool skipRevert - ) external payable { + ) external payable protected { try MORPHO.setAuthorizationWithSig(authorization, signature) {} catch (bytes memory returnData) { if (!skipRevert) _revert(returnData); @@ -89,7 +89,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { uint256 slippageAmount, address onBehalf, bytes calldata data - ) external payable { + ) external payable protected { // Do not check `onBehalf` against the zero address as it's done at Morpho's level. require(onBehalf != address(this), ErrorsLib.BUNDLER_ADDRESS); @@ -117,7 +117,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { uint256 assets, address onBehalf, bytes calldata data - ) external payable { + ) external payable protected { // Do not check `onBehalf` against the zero address as it's done at Morpho's level. require(onBehalf != address(this), ErrorsLib.BUNDLER_ADDRESS); @@ -131,7 +131,6 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { } /// @notice Borrows `assets` of the loan asset on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the /// initiator is guaranteed to borrow `assets` tokens, but the possibility to mint a specific amount of shares is /// given for full compatibility and precision. @@ -148,7 +147,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { uint256 shares, uint256 slippageAmount, address receiver - ) external payable { + ) external payable protected { (uint256 borrowedAssets, uint256 borrowedShares) = MORPHO.borrow(marketParams, assets, shares, initiator(), receiver); @@ -174,7 +173,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { uint256 slippageAmount, address onBehalf, bytes calldata data - ) external payable { + ) external payable protected { // Do not check `onBehalf` against the zero address as it's done at Morpho's level. require(onBehalf != address(this), ErrorsLib.BUNDLER_ADDRESS); @@ -191,7 +190,6 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { } /// @notice Withdraws `assets` of the loan asset on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the /// initiator is guaranteed to withdraw `assets` tokens, but the possibility to burn a specific amount of shares is /// given for full compatibility and precision. @@ -208,7 +206,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { uint256 shares, uint256 slippageAmount, address receiver - ) external payable { + ) external payable protected { (uint256 withdrawnAssets, uint256 withdrawnShares) = MORPHO.withdraw(marketParams, assets, shares, initiator(), receiver); @@ -217,7 +215,6 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { } /// @notice Withdraws `assets` of the collateral asset on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Initiator must have previously authorized the bundler to act on their behalf on Morpho. /// @param marketParams The Morpho market to withdraw collateral from. /// @param assets The amount of collateral to withdraw. @@ -225,6 +222,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { function morphoWithdrawCollateral(MarketParams calldata marketParams, uint256 assets, address receiver) external payable + protected { MORPHO.withdrawCollateral(marketParams, assets, initiator(), receiver); } @@ -245,7 +243,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { uint256 repaidShares, uint256 maxRepaidAssets, bytes memory data - ) external payable { + ) external payable protected { _approveMaxTo(marketParams.loanToken, address(MORPHO)); (, uint256 repaidAssets) = MORPHO.liquidate(marketParams, borrower, seizedAssets, repaidShares, data); @@ -257,7 +255,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { /// @param token The address of the token to flash loan. /// @param assets The amount of assets to flash loan. /// @param data Arbitrary data to pass to the `onMorphoFlashLoan` callback. - function morphoFlashLoan(address token, uint256 assets, bytes calldata data) external payable { + function morphoFlashLoan(address token, uint256 assets, bytes calldata data) external payable protected { _approveMaxTo(token, address(MORPHO)); MORPHO.flashLoan(token, assets, data); @@ -267,8 +265,13 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler { /// @dev Triggers `_multicall` logic during a callback. function _callback(bytes calldata data) internal { - _checkInitiated(); + require(msg.sender == address(MORPHO), ErrorsLib.UNAUTHORIZED_SENDER); _multicall(abi.decode(data, (bytes[]))); } + + /// @inheritdoc BaseBundler + function _isSenderAuthorized() internal view virtual override returns (bool) { + return super._isSenderAuthorized() || msg.sender == address(MORPHO); + } } diff --git a/src/Permit2Bundler.sol b/src/Permit2Bundler.sol index d1596c43..2a63c5a7 100644 --- a/src/Permit2Bundler.sol +++ b/src/Permit2Bundler.sol @@ -18,15 +18,14 @@ abstract contract Permit2Bundler is BaseBundler { /* ACTIONS */ /// @notice Permits and performs a transfer from the initiator to the recipient via Permit2. - /// @notice Warning: should only be called via the bundler's `multicall` function. - /// @notice User must have given sufficient allowance to the Permit2 contract to manage his tokens. - /// @dev Warning: `permit.permitted.token` can re-enter the bundler flow. + /// @notice User must have given sufficient allowance to the Permit2 contract to manage their tokens. /// @dev Pass `permit.permitted.amount = type(uint256).max` to transfer all. /// @param permit The `PermitTransferFrom` struct. /// @param signature The signature. function permit2TransferFrom(ISignatureTransfer.PermitTransferFrom memory permit, bytes memory signature) external payable + protected { address initiator = initiator(); uint256 amount = Math.min(permit.permitted.amount, ERC20(permit.permitted.token).balanceOf(initiator)); diff --git a/src/PermitBundler.sol b/src/PermitBundler.sol index 53555cb8..af24da66 100644 --- a/src/PermitBundler.sol +++ b/src/PermitBundler.sol @@ -12,8 +12,6 @@ import {BaseBundler} from "./BaseBundler.sol"; abstract contract PermitBundler is BaseBundler { /// @notice Permits the given `amount` of `asset` from sender to be spent by the bundler via EIP-2612 Permit with /// the given `deadline` & EIP-712 signature's `v`, `r` & `s`. - /// @notice Warning: should only be called via the bundler's `multicall` function. - /// @dev Warning: `asset` can re-enter the bundler flow. /// @param asset The address of the token to be permitted. /// @param amount The amount of `asset` to be permitted. /// @param deadline The deadline of the approval. @@ -24,6 +22,7 @@ abstract contract PermitBundler is BaseBundler { function permit(address asset, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s, bool skipRevert) external payable + protected { try IERC20Permit(asset).permit(initiator(), address(this), amount, deadline, v, r, s) {} catch (bytes memory returnData) { diff --git a/src/StEthBundler.sol b/src/StEthBundler.sol index 19bb059e..450593f4 100644 --- a/src/StEthBundler.sol +++ b/src/StEthBundler.sol @@ -44,7 +44,7 @@ abstract contract StEthBundler is BaseBundler { /// @param amount The amount of ETH to stake. Pass `type(uint256).max` to stake all. /// @param minShares The minimum amount of shares to mint in exchange for `amount`. /// @param referral The address of the referral regarding the Lido Rewards-Share Program. - function stakeEth(uint256 amount, uint256 minShares, address referral) external payable { + function stakeEth(uint256 amount, uint256 minShares, address referral) external payable protected { amount = Math.min(amount, address(this).balance); require(amount != 0, ErrorsLib.ZERO_AMOUNT); @@ -57,7 +57,7 @@ abstract contract StEthBundler is BaseBundler { /// @notice wstETH tokens are received by the bundler and should be used afterwards. /// @dev Initiator must have previously transferred their stETH tokens to the bundler. /// @param amount The amount of stEth to wrap. Pass `type(uint256).max` to wrap all. - function wrapStEth(uint256 amount) external payable { + function wrapStEth(uint256 amount) external payable protected { amount = Math.min(amount, ERC20(ST_ETH).balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); @@ -69,7 +69,7 @@ abstract contract StEthBundler is BaseBundler { /// @notice stETH tokens are received by the bundler and should be used afterwards. /// @dev Initiator must have previously transferred their wstETH tokens to the bundler. /// @param amount The amount of wstEth to unwrap. Pass `type(uint256).max` to unwrap all. - function unwrapStEth(uint256 amount) external payable { + function unwrapStEth(uint256 amount) external payable protected { amount = Math.min(amount, ERC20(WST_ETH).balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); diff --git a/src/TransferBundler.sol b/src/TransferBundler.sol index 934a8ed8..f890aacd 100644 --- a/src/TransferBundler.sol +++ b/src/TransferBundler.sol @@ -19,11 +19,10 @@ abstract contract TransferBundler is BaseBundler { /// @notice Transfers the minimum between the given `amount` and the bundler's balance of native asset from the /// bundler to `recipient`. - /// @dev Warning: `recipient` can re-enter the bundler flow. /// @param recipient The address that will receive the native tokens. /// @param amount The amount of native tokens to transfer from the initiator. Pass `type(uint256).max` to transfer /// the initiator's balance. - function nativeTransfer(address recipient, uint256 amount) external payable { + function nativeTransfer(address recipient, uint256 amount) external payable protected { require(recipient != address(0), ErrorsLib.ZERO_ADDRESS); require(recipient != address(this), ErrorsLib.BUNDLER_ADDRESS); @@ -36,11 +35,10 @@ abstract contract TransferBundler is BaseBundler { /// @notice Transfers the minimum between the given `amount` and the bundler's balance of `asset` from the bundler /// to `recipient`. - /// @dev Warning: `asset` can re-enter the bundler flow. /// @param asset The address of the ERC20 token to transfer. /// @param recipient The address that will receive the tokens. /// @param amount The amount of `asset` to transfer. Pass `type(uint256).max` to transfer the bundler's balance. - function erc20Transfer(address asset, address recipient, uint256 amount) external payable { + function erc20Transfer(address asset, address recipient, uint256 amount) external payable protected { require(recipient != address(0), ErrorsLib.ZERO_ADDRESS); require(recipient != address(this), ErrorsLib.BUNDLER_ADDRESS); @@ -52,13 +50,11 @@ abstract contract TransferBundler is BaseBundler { } /// @notice Transfers the given `amount` of `asset` from sender to this contract via ERC20 transferFrom. - /// @notice User must have given sufficient allowance to the Bundler to manage his tokens. - /// @notice Warning: should only be called via the bundler's `multicall` function. - /// @dev Warning: `asset` can re-enter the bundler flow. + /// @notice User must have given sufficient allowance to the Bundler to manage their tokens. /// @param asset The address of the ERC20 token to transfer. /// @param amount The amount of `asset` to transfer from the initiator. Pass `type(uint256).max` to transfer the /// initiator's balance. - function erc20TransferFrom(address asset, uint256 amount) external payable { + function erc20TransferFrom(address asset, uint256 amount) external payable protected { address initiator = initiator(); amount = Math.min(amount, ERC20(asset).balanceOf(initiator)); diff --git a/src/UrdBundler.sol b/src/UrdBundler.sol index 62176367..911f59e8 100644 --- a/src/UrdBundler.sol +++ b/src/UrdBundler.sol @@ -14,7 +14,6 @@ import {BaseBundler} from "./BaseBundler.sol"; /// @notice Bundler that allows to claim token rewards on the Universal Rewards Distributor. abstract contract UrdBundler is BaseBundler { /// @notice Claims `amount` of `reward` on behalf of `account` on the given rewards distributor, using `proof`. - /// @dev Warning: `distributor` can re-enter the bundler flow. /// @dev Assumes the given distributor implements IUniversalRewardsDistributor. /// @param distributor The address of the reward distributor contract. /// @param account The address of the owner of the rewards (also the address that will receive the rewards). @@ -29,7 +28,7 @@ abstract contract UrdBundler is BaseBundler { uint256 amount, bytes32[] calldata proof, bool skipRevert - ) external payable { + ) external payable protected { require(account != address(0), ErrorsLib.ZERO_ADDRESS); require(account != address(this), ErrorsLib.BUNDLER_ADDRESS); diff --git a/src/WNativeBundler.sol b/src/WNativeBundler.sol index 724c93ea..5c053ba2 100644 --- a/src/WNativeBundler.sol +++ b/src/WNativeBundler.sol @@ -39,11 +39,10 @@ abstract contract WNativeBundler is BaseBundler { /* ACTIONS */ /// @notice Wraps the given `amount` of the native token to wNative. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @notice Wrapped native tokens are received by the bundler and should be used afterwards. /// @dev Initiator must have previously transferred their native tokens to the bundler. /// @param amount The amount of native token to wrap. Pass `type(uint256).max` to wrap all. - function wrapNative(uint256 amount) external payable { + function wrapNative(uint256 amount) external payable protected { amount = Math.min(amount, address(this).balance); require(amount != 0, ErrorsLib.ZERO_AMOUNT); @@ -52,11 +51,10 @@ abstract contract WNativeBundler is BaseBundler { } /// @notice Unwraps the given `amount` of wNative to the native token. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @notice Unwrapped native tokens are received by the bundler and should be used afterwards. /// @dev Initiator must have previously transferred their wrapped native tokens to the bundler. /// @param amount The amount of wrapped native token to unwrap. Pass `type(uint256).max` to unwrap all. - function unwrapNative(uint256 amount) external payable { + function unwrapNative(uint256 amount) external payable protected { amount = Math.min(amount, ERC20(WRAPPED_NATIVE).balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); diff --git a/src/ethereum/EthereumBundler.sol b/src/ethereum/EthereumBundler.sol index d77289d7..367d99ba 100644 --- a/src/ethereum/EthereumBundler.sol +++ b/src/ethereum/EthereumBundler.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.21; import {MainnetLib} from "./libraries/MainnetLib.sol"; +import {BaseBundler} from "../BaseBundler.sol"; import {TransferBundler} from "../TransferBundler.sol"; import {EthereumPermitBundler} from "./EthereumPermitBundler.sol"; import {Permit2Bundler} from "../Permit2Bundler.sol"; @@ -11,6 +12,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,9 +26,17 @@ contract EthereumBundler is WNativeBundler, EthereumStEthBundler, UrdBundler, - MorphoBundler + MorphoBundler, + ERC20WrapperBundler { /* CONSTRUCTOR */ constructor(address morpho) WNativeBundler(MainnetLib.WETH) MorphoBundler(morpho) {} + + /* INTERNAL */ + + /// @inheritdoc MorphoBundler + function _isSenderAuthorized() internal view override(BaseBundler, MorphoBundler) returns (bool) { + return MorphoBundler._isSenderAuthorized(); + } } diff --git a/src/ethereum/EthereumPermitBundler.sol b/src/ethereum/EthereumPermitBundler.sol index 491358e4..6ea128ce 100644 --- a/src/ethereum/EthereumPermitBundler.sol +++ b/src/ethereum/EthereumPermitBundler.sol @@ -14,7 +14,6 @@ import {PermitBundler} from "../PermitBundler.sol"; abstract contract EthereumPermitBundler is PermitBundler { /// @notice Permits DAI from sender to be spent by the bundler with the given `nonce`, `expiry` & EIP-712 /// signature's `v`, `r` & `s`. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @param nonce The nonce of the signed message. /// @param expiry The expiry of the signed message. /// @param allowed Whether the initiator gives the bundler infinite Dai approval or not. @@ -25,6 +24,7 @@ abstract contract EthereumPermitBundler is PermitBundler { function permitDai(uint256 nonce, uint256 expiry, bool allowed, uint8 v, bytes32 r, bytes32 s, bool skipRevert) external payable + protected { try IDaiPermit(MainnetLib.DAI).permit(initiator(), address(this), nonce, expiry, allowed, v, r, s) {} catch (bytes memory returnData) { diff --git a/src/ethereum/migration/AaveV2EthereumMigrationBundler.sol b/src/ethereum/migration/AaveV2EthereumMigrationBundler.sol index 48645b17..9566c732 100644 --- a/src/ethereum/migration/AaveV2EthereumMigrationBundler.sol +++ b/src/ethereum/migration/AaveV2EthereumMigrationBundler.sol @@ -3,7 +3,9 @@ pragma solidity 0.8.21; import {MainnetLib} from "../libraries/MainnetLib.sol"; +import {BaseBundler} from "../../BaseBundler.sol"; import {EthereumStEthBundler} from "../EthereumStEthBundler.sol"; +import {MigrationBundler} from "../../migration/MigrationBundler.sol"; import {AaveV2MigrationBundler} from "../../migration/AaveV2MigrationBundler.sol"; /// @title AaveV2EthereumMigrationBundler @@ -14,4 +16,11 @@ contract AaveV2EthereumMigrationBundler is EthereumStEthBundler, AaveV2Migration /* CONSTRUCTOR */ constructor(address morpho) AaveV2MigrationBundler(morpho, MainnetLib.AAVE_V2_POOL) {} + + /* INTERNAL */ + + /// @inheritdoc MigrationBundler + function _isSenderAuthorized() internal view override(BaseBundler, MigrationBundler) returns (bool) { + return MigrationBundler._isSenderAuthorized(); + } } diff --git a/src/goerli/GoerliBundler.sol b/src/goerli/GoerliBundler.sol index f7966f2a..6c3e175f 100644 --- a/src/goerli/GoerliBundler.sol +++ b/src/goerli/GoerliBundler.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.21; import {GoerliLib} from "./libraries/GoerliLib.sol"; +import {BaseBundler} from "../BaseBundler.sol"; import {TransferBundler} from "../TransferBundler.sol"; import {PermitBundler} from "../PermitBundler.sol"; import {Permit2Bundler} from "../Permit2Bundler.sol"; @@ -29,4 +30,11 @@ contract GoerliBundler is /* CONSTRUCTOR */ constructor(address morpho) WNativeBundler(GoerliLib.WETH) StEthBundler(GoerliLib.WST_ETH) MorphoBundler(morpho) {} + + /* INTERNAL */ + + /// @inheritdoc MorphoBundler + function _isSenderAuthorized() internal view override(BaseBundler, MorphoBundler) returns (bool) { + return MorphoBundler._isSenderAuthorized(); + } } diff --git a/src/libraries/ErrorsLib.sol b/src/libraries/ErrorsLib.sol index 4b0cdac3..ba4d26e4 100644 --- a/src/libraries/ErrorsLib.sol +++ b/src/libraries/ErrorsLib.sol @@ -8,11 +8,14 @@ pragma solidity ^0.8.0; library ErrorsLib { /* STANDARD BUNDLERS */ + /// @dev Thrown when a call is attempted while the bundler is not in an initiated execution context. + string internal constant UNINITIATED = "uninitiated"; + /// @dev Thrown when a multicall is attempted while the bundler in an initiated execution context. string internal constant ALREADY_INITIATED = "already initiated"; - /// @dev Thrown when a call is attempted while the bundler is not in an initiated execution context. - string internal constant UNINITIATED = "uninitiated"; + /// @dev Thrown when a call is attempted from an unauthorized sender. + string internal constant UNAUTHORIZED_SENDER = "unauthorized sender"; /// @dev Thrown when a call is attempted with a zero address as input. string internal constant ZERO_ADDRESS = "zero address"; @@ -31,9 +34,6 @@ library ErrorsLib { /* MIGRATION BUNDLERS */ - /// @dev Thrown when only the wrapped native token or the native cToken can send ETH to the migration bundler. - string internal constant UNAUTHORIZED_SENDER = "unauthorized sender"; - /// @dev Thrown when an action ends up minting/burning more shares than a given slippage. string internal constant SLIPPAGE_EXCEEDED = "slippage exceeded"; } diff --git a/src/migration/AaveV2MigrationBundler.sol b/src/migration/AaveV2MigrationBundler.sol index 442078b0..3f338184 100644 --- a/src/migration/AaveV2MigrationBundler.sol +++ b/src/migration/AaveV2MigrationBundler.sol @@ -31,13 +31,11 @@ contract AaveV2MigrationBundler is MigrationBundler { /* ACTIONS */ /// @notice Repays `amount` of `asset` on AaveV2, on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Initiator must have previously transferred their assets to the bundler. - /// @dev Warning: `asset` can re-enter the bundler flow. /// @param asset The address of the token to repay. /// @param amount The amount of `asset` to repay. Pass `type(uint256).max` to repay the bundler's `asset` balance. /// @param interestRateMode The interest rate mode of the position. - function aaveV2Repay(address asset, uint256 amount, uint256 interestRateMode) external payable { + function aaveV2Repay(address asset, uint256 amount, uint256 interestRateMode) external payable protected { if (amount != type(uint256).max) amount = Math.min(amount, ERC20(asset).balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); @@ -52,7 +50,7 @@ contract AaveV2MigrationBundler is MigrationBundler { /// @dev Initiator must have previously transferred their aTokens to the bundler. /// @param asset The address of the token to withdraw. /// @param amount The amount of `asset` to withdraw. Pass `type(uint256).max` to withdraw all. - function aaveV2Withdraw(address asset, uint256 amount) external payable { + function aaveV2Withdraw(address asset, uint256 amount) external payable protected { AAVE_V2_POOL.withdraw(asset, amount, address(this)); } } diff --git a/src/migration/AaveV3MigrationBundler.sol b/src/migration/AaveV3MigrationBundler.sol index 79e2c5c8..e8c578d0 100644 --- a/src/migration/AaveV3MigrationBundler.sol +++ b/src/migration/AaveV3MigrationBundler.sol @@ -30,13 +30,11 @@ contract AaveV3MigrationBundler is MigrationBundler { /* ACTIONS */ /// @notice Repays `amount` of `asset` on AaveV3, on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Initiator must have previously transferred their assets to the bundler. - /// @dev Warning: `asset` can re-enter the bundler flow. /// @param asset The address of the token to repay. /// @param amount The amount of `asset` to repay. Pass `type(uint256).max` to repay the bundler's `asset` balance. /// @param interestRateMode The interest rate mode of the position. - function aaveV3Repay(address asset, uint256 amount, uint256 interestRateMode) external payable { + function aaveV3Repay(address asset, uint256 amount, uint256 interestRateMode) external payable protected { if (amount != type(uint256).max) amount = Math.min(amount, ERC20(asset).balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); @@ -51,7 +49,7 @@ contract AaveV3MigrationBundler is MigrationBundler { /// @dev Initiator must have previously transferred their aTokens to the bundler. /// @param asset The address of the token to withdraw. /// @param amount The amount of `asset` to withdraw. Pass `type(uint256).max` to withdraw all. - function aaveV3Withdraw(address asset, uint256 amount) external payable { + function aaveV3Withdraw(address asset, uint256 amount) external payable protected { AAVE_V3_POOL.withdraw(asset, amount, address(this)); } } diff --git a/src/migration/AaveV3OptimizerMigrationBundler.sol b/src/migration/AaveV3OptimizerMigrationBundler.sol index e7de05a0..6edaffdd 100644 --- a/src/migration/AaveV3OptimizerMigrationBundler.sol +++ b/src/migration/AaveV3OptimizerMigrationBundler.sol @@ -30,13 +30,11 @@ contract AaveV3OptimizerMigrationBundler is MigrationBundler { /* ACTIONS */ /// @notice Repays `amount` of `underlying` on the AaveV3 Optimizer, on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Initiator must have previously transferred their assets to the bundler. - /// @dev Warning: `underlying` can re-enter the bundler flow. /// @param underlying The address of the underlying asset to repay. /// @param amount The amount of `underlying` to repay. Pass `type(uint256).max` to repay the bundler's `underlying` /// balance. - function aaveV3OptimizerRepay(address underlying, uint256 amount) external payable { + function aaveV3OptimizerRepay(address underlying, uint256 amount) external payable protected { if (amount != type(uint256).max) amount = Math.min(amount, ERC20(underlying).balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); @@ -47,31 +45,32 @@ contract AaveV3OptimizerMigrationBundler is MigrationBundler { } /// @notice Withdraws `amount` of `underlying` on the AaveV3 Optimizer, on behalf of the initiator`. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @notice Withdrawn assets are received by the bundler and should be used afterwards. /// @dev Initiator must have previously approved the bundler to manage their AaveV3 Optimizer position. /// @param underlying The address of the underlying asset to withdraw. /// @param amount The amount of `underlying` to withdraw. Pass `type(uint256).max` to withdraw all. /// @param maxIterations The maximum number of iterations allowed during the matching process. If it is less than /// `_defaultIterations.withdraw`, the latter will be used. Pass 0 to fallback to the `_defaultIterations.withdraw`. - function aaveV3OptimizerWithdraw(address underlying, uint256 amount, uint256 maxIterations) external payable { + function aaveV3OptimizerWithdraw(address underlying, uint256 amount, uint256 maxIterations) + external + payable + protected + { AAVE_V3_OPTIMIZER.withdraw(underlying, amount, initiator(), address(this), maxIterations); } /// @notice Withdraws `amount` of `underlying` used as collateral on the AaveV3 Optimizer, on behalf of the /// initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @notice Withdrawn assets are received by the bundler and should be used afterwards. /// @dev Initiator must have previously approved the bundler to manage their AaveV3 Optimizer position. /// @param underlying The address of the underlying asset to withdraw. /// @param amount The amount of `underlying` to withdraw. Pass `type(uint256).max` to withdraw all. - function aaveV3OptimizerWithdrawCollateral(address underlying, uint256 amount) external payable { + function aaveV3OptimizerWithdrawCollateral(address underlying, uint256 amount) external payable protected { AAVE_V3_OPTIMIZER.withdrawCollateral(underlying, amount, initiator(), address(this)); } /// @notice Approves the bundler to act on behalf of the initiator on the AaveV3 Optimizer, given a signed EIP-712 /// approval message. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @param isApproved Whether the bundler is allowed to manage the initiator's position or not. /// @param nonce The nonce of the signed message. /// @param deadline The deadline of the signed message. @@ -83,7 +82,7 @@ contract AaveV3OptimizerMigrationBundler is MigrationBundler { uint256 deadline, Signature calldata signature, bool skipRevert - ) external payable { + ) external payable protected { try AAVE_V3_OPTIMIZER.approveManagerWithSig(initiator(), address(this), isApproved, nonce, deadline, signature) {} catch (bytes memory returnData) { if (!skipRevert) _revert(returnData); diff --git a/src/migration/CompoundV2MigrationBundler.sol b/src/migration/CompoundV2MigrationBundler.sol index 2dfca1d3..fab8e5fd 100644 --- a/src/migration/CompoundV2MigrationBundler.sol +++ b/src/migration/CompoundV2MigrationBundler.sol @@ -7,6 +7,7 @@ import {ICToken} from "./interfaces/ICToken.sol"; import {Math} from "../../lib/morpho-utils/src/math/Math.sol"; import {ErrorsLib} from "../libraries/ErrorsLib.sol"; +import {BaseBundler} from "../BaseBundler.sol"; import {WNativeBundler} from "../WNativeBundler.sol"; import {MigrationBundler, ERC20} from "./MigrationBundler.sol"; @@ -32,12 +33,10 @@ contract CompoundV2MigrationBundler is WNativeBundler, MigrationBundler { /* ACTIONS */ /// @notice Repays `amount` of `cToken`'s underlying asset, on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Initiator must have previously transferred their assets to the bundler. - /// @dev Warning: `cToken` can re-enter the bundler flow. /// @param cToken The address of the cToken contract /// @param amount The amount of `cToken` to repay. Pass `type(uint256).max` to repay all (except for cETH). - function compoundV2Repay(address cToken, uint256 amount) external payable { + function compoundV2Repay(address cToken, uint256 amount) external payable protected { if (cToken == C_ETH) { amount = Math.min(amount, address(this).balance); @@ -60,15 +59,21 @@ contract CompoundV2MigrationBundler is WNativeBundler, MigrationBundler { /// @notice Redeems `amount` of `cToken` from CompoundV2. /// @notice Withdrawn assets are received by the bundler and should be used afterwards. /// @dev Initiator must have previously transferred their cTokens to the bundler. - /// @dev Warning: `cToken` can re-enter the bundler flow. /// @param cToken The address of the cToken contract /// @param amount The amount of `cToken` to redeem. Pass `type(uint256).max` to redeem the bundler's `cToken` /// balance. - function compoundV2Redeem(address cToken, uint256 amount) external payable { + function compoundV2Redeem(address cToken, uint256 amount) external payable protected { amount = Math.min(amount, ERC20(cToken).balanceOf(address(this))); require(amount != 0, ErrorsLib.ZERO_AMOUNT); ICToken(cToken).redeem(amount); } + + /* INTERNAL */ + + /// @inheritdoc MigrationBundler + function _isSenderAuthorized() internal view override(BaseBundler, MigrationBundler) returns (bool) { + return MigrationBundler._isSenderAuthorized(); + } } diff --git a/src/migration/CompoundV3MigrationBundler.sol b/src/migration/CompoundV3MigrationBundler.sol index 9c055a0c..e7eca18b 100644 --- a/src/migration/CompoundV3MigrationBundler.sol +++ b/src/migration/CompoundV3MigrationBundler.sol @@ -21,13 +21,11 @@ contract CompoundV3MigrationBundler is MigrationBundler { /* ACTIONS */ /// @notice Repays `amount` on the CompoundV3 `instance`, on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @dev Initiator must have previously transferred their assets to the bundler. - /// @dev Warning: `instance` can re-enter the bundler flow. /// @dev Assumes the given `instance` is a CompoundV3 instance. /// @param instance The address of the CompoundV3 instance to call. /// @param amount The amount of `asset` to repay. Pass `type(uint256).max` to repay all. - function compoundV3Repay(address instance, uint256 amount) external payable { + function compoundV3Repay(address instance, uint256 amount) external payable protected { address initiator = initiator(); address asset = ICompoundV3(instance).baseToken(); @@ -43,15 +41,13 @@ contract CompoundV3MigrationBundler is MigrationBundler { } /// @notice Withdraws `amount` of `asset` from the CompoundV3 `instance`, on behalf of the initiator. - /// @notice Warning: should only be called via the bundler's `multicall` function. /// @notice Withdrawn assets are received by the bundler and should be used afterwards. /// @dev Initiator must have previously approved the bundler to manage their CompoundV3 position. - /// @dev Warning: `instance` can re-enter the bundler flow. /// @dev Assumes the given `instance` is a CompoundV3 instance. /// @param instance The address of the CompoundV3 instance to call. /// @param asset The address of the token to withdraw. /// @param amount The amount of `asset` to withdraw. Pass `type(uint256).max` to withdraw all. - function compoundV3WithdrawFrom(address instance, address asset, uint256 amount) external payable { + function compoundV3WithdrawFrom(address instance, address asset, uint256 amount) external payable protected { address initiator = initiator(); uint256 balance = asset == ICompoundV3(instance).baseToken() ? ICompoundV3(instance).balanceOf(initiator) @@ -66,8 +62,6 @@ contract CompoundV3MigrationBundler is MigrationBundler { /// @notice Approves the bundler to act on behalf of the initiator on the CompoundV3 `instance`, given a signed /// EIP-712 approval message. - /// @notice Warning: should only be called via the bundler's `multicall` function. - /// @dev Warning: `instance` can re-enter the bundler flow. /// @dev Assumes the given `instance` is a CompoundV3 instance. /// @param instance The address of the CompoundV3 instance to call. /// @param isAllowed Whether the bundler is allowed to manage the initiator's position or not. @@ -86,7 +80,7 @@ contract CompoundV3MigrationBundler is MigrationBundler { bytes32 r, bytes32 s, bool skipRevert - ) external payable { + ) external payable protected { try ICompoundV3(instance).allowBySig(initiator(), address(this), isAllowed, nonce, expiry, v, r, s) {} catch (bytes memory returnData) { if (!skipRevert) _revert(returnData); diff --git a/src/migration/MigrationBundler.sol b/src/migration/MigrationBundler.sol index daf7fdfb..3bd77f00 100644 --- a/src/migration/MigrationBundler.sol +++ b/src/migration/MigrationBundler.sol @@ -3,6 +3,7 @@ pragma solidity 0.8.21; import {SafeTransferLib, ERC20} from "../../lib/solmate/src/utils/SafeTransferLib.sol"; +import {BaseBundler} from "../BaseBundler.sol"; import {TransferBundler} from "../TransferBundler.sol"; import {PermitBundler} from "../PermitBundler.sol"; import {Permit2Bundler} from "../Permit2Bundler.sol"; @@ -19,4 +20,11 @@ abstract contract MigrationBundler is TransferBundler, PermitBundler, Permit2Bun /* CONSTRUCTOR */ constructor(address morpho) MorphoBundler(morpho) {} + + /* INTERNAL */ + + /// @inheritdoc MorphoBundler + function _isSenderAuthorized() internal view virtual override(BaseBundler, MorphoBundler) returns (bool) { + return MorphoBundler._isSenderAuthorized(); + } } 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/src/mocks/bundlers/MorphoBundlerMock.sol b/src/mocks/bundlers/MorphoBundlerMock.sol index 74916fb3..0f57a5f2 100644 --- a/src/mocks/bundlers/MorphoBundlerMock.sol +++ b/src/mocks/bundlers/MorphoBundlerMock.sol @@ -1,9 +1,13 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; -import "../../MorphoBundler.sol"; import "../../TransferBundler.sol"; +import "../../MorphoBundler.sol"; contract MorphoBundlerMock is TransferBundler, MorphoBundler { constructor(address morpho) MorphoBundler(morpho) {} + + function _isSenderAuthorized() internal view override(BaseBundler, MorphoBundler) returns (bool) { + return MorphoBundler._isSenderAuthorized(); + } } diff --git a/test/forge/ERC20WrapperBundlerLocalTest.sol b/test/forge/ERC20WrapperBundlerLocalTest.sol new file mode 100644 index 00000000..09aa15b0 --- /dev/null +++ b/test/forge/ERC20WrapperBundlerLocalTest.sol @@ -0,0 +1,101 @@ +// 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 testErc20WrapperDepositFor(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_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)"); + assertEq(loanWrapper.balanceOf(RECEIVER), amount, "loanWrapper.balanceOf(RECEIVER)"); + } + + function testErc20WrapperDepositForZeroAmount() public { + bundle.push(_erc20WrapperDepositFor(address(loanWrapper), 0)); + + vm.expectRevert(bytes(ErrorsLib.ZERO_AMOUNT)); + bundler.multicall(bundle); + } + + function testErc20WrapperWithdrawTo(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 testErc20WrapperWithdrawToAll(uint256 amount, uint256 inputAmount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + inputAmount = bound(inputAmount, amount, type(uint256).max); + + loanWrapper.setBalance(address(bundler), amount); + loanToken.setBalance(address(loanWrapper), amount); + + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, inputAmount)); + + bundler.multicall(bundle); + + assertEq(loanWrapper.balanceOf(address(bundler)), 0, "loanWrapper.balanceOf(bundler)"); + assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); + } + + function testErc20WrapperWithdrawToAccountZeroAddress(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 testErc20WrapperWithdrawToZeroAmount() public { + bundle.push(_erc20WrapperWithdrawTo(address(loanWrapper), RECEIVER, 0)); + + 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); + } +} diff --git a/test/forge/ERC4626BundlerLocalTest.sol b/test/forge/ERC4626BundlerLocalTest.sol index 6d4d8f3f..4dc4b1b3 100644 --- a/test/forge/ERC4626BundlerLocalTest.sol +++ b/test/forge/ERC4626BundlerLocalTest.sol @@ -47,6 +47,11 @@ contract ERC4626BundlerLocalTest is LocalTest { bundler.multicall(bundle); } + function test4626DepositUninitiated(uint256 assets) public { + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + ERC4626BundlerMock(address(bundler)).erc4626Withdraw(address(vault), assets, 0, RECEIVER); + } + function testErc4626WithdrawZeroAdressVault(uint256 assets) public { bundle.push(_erc4626Withdraw(address(0), assets, type(uint256).max, RECEIVER)); @@ -61,6 +66,11 @@ contract ERC4626BundlerLocalTest is LocalTest { bundler.multicall(bundle); } + function test4626RedeemUninitiated(uint256 shares) public { + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + ERC4626BundlerMock(address(bundler)).erc4626Redeem(address(vault), shares, 0, RECEIVER); + } + function testErc4626RedeemZeroAdressVault(uint256 shares) public { bundle.push(_erc4626Redeem(address(0), shares, 0, RECEIVER)); diff --git a/test/forge/MorphoBundlerLocalTest.sol b/test/forge/MorphoBundlerLocalTest.sol index 61034957..e6dcf27f 100644 --- a/test/forge/MorphoBundlerLocalTest.sol +++ b/test/forge/MorphoBundlerLocalTest.sol @@ -215,6 +215,11 @@ contract MorphoBundlerLocalTest is LocalTest { _testSupplyCollateral(amount, onBehalf); } + function testWithdrawUninitiated(uint256 withdrawnShares) public { + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + MorphoBundlerMock(address(bundler)).morphoWithdraw(marketParams, 0, withdrawnShares, 0, RECEIVER); + } + function testWithdraw(uint256 privateKey, uint256 amount, uint256 withdrawnShares) public { address user; (privateKey, user) = _boundPrivateKey(privateKey); @@ -247,6 +252,11 @@ contract MorphoBundlerLocalTest is LocalTest { assertEq(morpho.borrowShares(id, user), 0, "borrowShares(user)"); } + function testBorrowUnititiated(uint256 borrowedAssets) public { + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + MorphoBundlerMock(address(bundler)).morphoBorrow(marketParams, borrowedAssets, 0, type(uint256).max, RECEIVER); + } + function _testSupplyCollateralBorrow(address user, uint256 amount, uint256 collateralAmount) internal { assertEq(collateralToken.balanceOf(RECEIVER), 0, "collateral.balanceOf(RECEIVER)"); assertEq(loanToken.balanceOf(RECEIVER), amount, "loan.balanceOf(RECEIVER)"); @@ -316,6 +326,11 @@ contract MorphoBundlerLocalTest is LocalTest { _testSupplyCollateralBorrow(user, amount, collateralAmount); } + function testWithdrawCollateralUninitiated(uint256 collateralAmount) public { + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + MorphoBundlerMock(address(bundler)).morphoWithdrawCollateral(marketParams, collateralAmount, RECEIVER); + } + function _testRepayWithdrawCollateral(address user, uint256 collateralAmount) internal { assertEq(collateralToken.balanceOf(RECEIVER), collateralAmount, "collateral.balanceOf(RECEIVER)"); assertEq(loanToken.balanceOf(RECEIVER), 0, "loan.balanceOf(RECEIVER)"); diff --git a/test/forge/PermitBundlerLocalTest.sol b/test/forge/PermitBundlerLocalTest.sol index b3722cf6..fdce7bd1 100644 --- a/test/forge/PermitBundlerLocalTest.sol +++ b/test/forge/PermitBundlerLocalTest.sol @@ -35,6 +35,13 @@ contract PermitBundlerLocalTest is LocalTest { assertEq(permitToken.allowance(user, address(bundler)), amount, "allowance(user, bundler)"); } + function testPermitUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + PermitBundlerMock(address(bundler)).permit(address(loanToken), amount, SIGNATURE_DEADLINE, 0, 0, 0, true); + } + function testPermitRevert(uint256 amount, uint256 privateKey, uint256 deadline) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); deadline = bound(deadline, block.timestamp, type(uint48).max); diff --git a/test/forge/TransferBundlerLocalTest.sol b/test/forge/TransferBundlerLocalTest.sol index 3b3d0dbf..683d86d2 100644 --- a/test/forge/TransferBundlerLocalTest.sol +++ b/test/forge/TransferBundlerLocalTest.sol @@ -93,6 +93,13 @@ contract TransferBundlerLocalTest is LocalTest { assertEq(loanToken.balanceOf(USER), 0, "loan.balanceOf(USER)"); } + function testTransferFromUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + TransferBundlerMock(address(bundler)).erc20TransferFrom(address(loanToken), amount); + } + function testTranferFromZeroAddress(uint256 amount) public { amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); diff --git a/test/forge/ethereum/EthereumPermitBundlerEthereumTest.sol b/test/forge/ethereum/EthereumPermitBundlerEthereumTest.sol index 30fde51e..97384f01 100644 --- a/test/forge/ethereum/EthereumPermitBundlerEthereumTest.sol +++ b/test/forge/ethereum/EthereumPermitBundlerEthereumTest.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0-or-later pragma solidity ^0.8.0; +import {ErrorsLib} from "../../../src/libraries/ErrorsLib.sol"; + import {DaiPermit} from "../helpers/SigUtils.sol"; import "../../../src/mocks/bundlers/ethereum/EthereumPermitBundlerMock.sol"; @@ -32,6 +34,11 @@ contract EthereumPermitBundlerEthereumTest is EthereumTest { assertEq(ERC20(DAI).allowance(user, address(bundler)), type(uint256).max, "allowance(user, bundler)"); } + function testPermitDaiUninitiated() public { + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + EthereumPermitBundlerMock(address(bundler)).permitDai(0, SIGNATURE_DEADLINE, true, 0, 0, 0, true); + } + function testPermitDaiRevert(uint256 privateKey, uint256 expiry) public { expiry = bound(expiry, block.timestamp, type(uint48).max); privateKey = bound(privateKey, 1, type(uint160).max); diff --git a/test/forge/ethereum/Permit2BundlerEthereumTest.sol b/test/forge/ethereum/Permit2BundlerEthereumTest.sol index 659951d4..bd9641d2 100644 --- a/test/forge/ethereum/Permit2BundlerEthereumTest.sol +++ b/test/forge/ethereum/Permit2BundlerEthereumTest.sol @@ -40,6 +40,14 @@ contract Permit2BundlerEthereumTest is EthereumTest { assertEq(ERC20(marketParams.loanToken).balanceOf(address(bundler)), amount, "loan.balanceOf(bundler)"); } + function testPermtestPermit2TransferFromUninitiated() public { + ISignatureTransfer.PermitTransferFrom memory permit; + bytes memory signature; + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + Permit2BundlerMock(address(bundler)).permit2TransferFrom(permit, signature); + } + function testPermit2TransferFromZeroAmount(uint256 seed, uint256 privateKey, uint256 amount) public { privateKey = bound(privateKey, 1, type(uint160).max); amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); diff --git a/test/forge/ethereum/migration/AaveV2EthereumMigrationBundlerEthereumTest.sol b/test/forge/ethereum/migration/AaveV2EthereumMigrationBundlerEthereumTest.sol index b417f5c4..049d78b9 100644 --- a/test/forge/ethereum/migration/AaveV2EthereumMigrationBundlerEthereumTest.sol +++ b/test/forge/ethereum/migration/AaveV2EthereumMigrationBundlerEthereumTest.sol @@ -30,6 +30,13 @@ contract AaveV2EthereumMigrationBundlerEthereumTest is EthereumMigrationTest { bundler = new AaveV2EthereumMigrationBundler(address(morpho)); } + function testAaveV2RepayUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + AaveV2EthereumMigrationBundler(address(bundler)).aaveV2Repay(marketParams.loanToken, amount, 1); + } + function testAaveV2RepayZeroAmount() public { bundle.push(_aaveV2Repay(marketParams.loanToken, 0)); diff --git a/test/forge/ethereum/migration/AaveV3MigrationBundlerEthereumTest.sol b/test/forge/ethereum/migration/AaveV3MigrationBundlerEthereumTest.sol index efddac63..a03ad97f 100644 --- a/test/forge/ethereum/migration/AaveV3MigrationBundlerEthereumTest.sol +++ b/test/forge/ethereum/migration/AaveV3MigrationBundlerEthereumTest.sol @@ -31,6 +31,13 @@ contract AaveV3MigrationBundlerEthereumTest is EthereumMigrationTest { vm.label(address(bundler), "Aave V3 Migration Bundler"); } + function testAaveV3RepayUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + AaveV3MigrationBundler(address(bundler)).aaveV3Repay(marketParams.loanToken, amount, 1); + } + function testAaveV3RepayZeroAmount() public { bundle.push(_aaveV3Repay(marketParams.loanToken, 0)); diff --git a/test/forge/ethereum/migration/AaveV3OptimizerMigrationBundlerEthereumTest.sol b/test/forge/ethereum/migration/AaveV3OptimizerMigrationBundlerEthereumTest.sol index 38027880..aaf18519 100644 --- a/test/forge/ethereum/migration/AaveV3OptimizerMigrationBundlerEthereumTest.sol +++ b/test/forge/ethereum/migration/AaveV3OptimizerMigrationBundlerEthereumTest.sol @@ -32,6 +32,13 @@ contract AaveV3OptimizerMigrationBundlerEthereumTest is EthereumMigrationTest { bundler = new AaveV3OptimizerMigrationBundler(address(morpho), address(AAVE_V3_OPTIMIZER)); } + function testAaveV3OptimizerRepayUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + AaveV3OptimizerMigrationBundler(address(bundler)).aaveV3OptimizerRepay(marketParams.loanToken, amount); + } + function testAaveV3Optimizer3RepayZeroAmount() public { bundle.push(_aaveV3OptimizerRepay(marketParams.loanToken, 0)); @@ -176,6 +183,35 @@ contract AaveV3OptimizerMigrationBundlerEthereumTest is EthereumMigrationTest { _assertVaultSupplierPosition(supplied, user, address(bundler)); } + function testAaveV3OptimizerApproveManagerUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + MA3Signature memory sig; + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + AaveV3OptimizerMigrationBundler(address(bundler)).aaveV3OptimizerApproveManagerWithSig( + true, 0, SIGNATURE_DEADLINE, sig, false + ); + } + + function testAaveV3OptimizerWithdrawUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + AaveV3OptimizerMigrationBundler(address(bundler)).aaveV3OptimizerWithdraw( + marketParams.loanToken, amount, MAX_ITERATIONS + ); + } + + function testAaveV3OptimizerWithdrawCollateralUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + AaveV3OptimizerMigrationBundler(address(bundler)).aaveV3OptimizerWithdrawCollateral( + marketParams.loanToken, amount + ); + } + /* ACTIONS */ function _aaveV3OptimizerApproveManager( diff --git a/test/forge/ethereum/migration/CompoundV2EthBorrowableMigrationBundlerEthereumTest.sol b/test/forge/ethereum/migration/CompoundV2EthBorrowableMigrationBundlerEthereumTest.sol index 70ea4d4a..92388714 100644 --- a/test/forge/ethereum/migration/CompoundV2EthBorrowableMigrationBundlerEthereumTest.sol +++ b/test/forge/ethereum/migration/CompoundV2EthBorrowableMigrationBundlerEthereumTest.sol @@ -26,6 +26,13 @@ contract CompoundV2EthLoanMigrationBundlerEthereumTest is EthereumMigrationTest enteredMarkets.push(C_DAI_V2); } + function testCompoundV2RepayUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + CompoundV2MigrationBundler(payable(address(bundler))).compoundV2Repay(C_DAI_V2, amount); + } + function testCompoundV2RepayCEthZeroAmount() public { bundle.push(_compoundV2Repay(C_ETH_V2, 0)); diff --git a/test/forge/ethereum/migration/CompoundV3MigrationBundlerEthereumTest.sol b/test/forge/ethereum/migration/CompoundV3MigrationBundlerEthereumTest.sol index 0cae0695..354cd73e 100644 --- a/test/forge/ethereum/migration/CompoundV3MigrationBundlerEthereumTest.sol +++ b/test/forge/ethereum/migration/CompoundV3MigrationBundlerEthereumTest.sol @@ -24,6 +24,13 @@ contract CompoundV3MigrationBundlerEthereumTest is EthereumMigrationTest { bundler = new CompoundV3MigrationBundler(address(morpho)); } + function testCompoundV3RepayUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + CompoundV3MigrationBundler(address(bundler)).compoundV3Repay(C_WETH_V3, amount); + } + function testCompoundV3RepayZeroAmount() public { bundle.push(_compoundV3Repay(C_WETH_V3, 0)); @@ -137,6 +144,20 @@ contract CompoundV3MigrationBundlerEthereumTest is EthereumMigrationTest { _assertVaultSupplierPosition(supplied, user, address(bundler)); } + function testCompoundV3AllowUninitiated() public { + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + CompoundV3MigrationBundler(address(bundler)).compoundV3AllowBySig( + C_WETH_V3, true, 0, SIGNATURE_DEADLINE, 0, 0, 0, false + ); + } + + function testCompoundV3WithdrawFromUninitiated(uint256 amount) public { + amount = bound(amount, MIN_AMOUNT, MAX_AMOUNT); + + vm.expectRevert(bytes(ErrorsLib.UNINITIATED)); + CompoundV3MigrationBundler(address(bundler)).compoundV3WithdrawFrom(C_WETH_V3, marketParams.loanToken, amount); + } + /* ACTIONS */ function _compoundV3Allow( diff --git a/test/forge/helpers/BaseTest.sol b/test/forge/helpers/BaseTest.sol index 9dc57cf2..c3fa8539 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,20 @@ abstract contract BaseTest is Test { return abi.encodeCall(TransferBundler.erc20TransferFrom, (asset, amount)); } + /* ERC20 WRAPPER ACTIONS */ + + function _erc20WrapperDepositFor(address asset, uint256 amount) internal pure returns (bytes memory) { + return abi.encodeCall(ERC20WrapperBundler.erc20WrapperDepositFor, (asset, amount)); + } + + function _erc20WrapperWithdrawTo(address asset, address account, uint256 amount) + internal + pure + returns (bytes memory) + { + return abi.encodeCall(ERC20WrapperBundler.erc20WrapperWithdrawTo, (asset, account, amount)); + } + /* ERC4626 ACTIONS */ function _erc4626Mint(address vault, uint256 shares, uint256 maxAssets, address receiver)