Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Wrapper Bundler #364

Merged
merged 27 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4fdce5a
feat: add erc20 wrapper bundler
MerlinEgalite Oct 21, 2023
247e133
chore: shut warnings in config
MerlinEgalite Oct 21, 2023
895a1c6
test: add tests
MerlinEgalite Oct 21, 2023
179b3df
fix: wrapper bundler
MerlinEgalite Oct 21, 2023
d1ba076
docs: add natspecs
MerlinEgalite Oct 21, 2023
bf7c35d
fix: deposit for initiator
MerlinEgalite Oct 23, 2023
9a95e90
refactor: safe approve from solmate
MerlinEgalite Oct 23, 2023
f33171a
refactor: remove the check on address 0
MerlinEgalite Oct 24, 2023
6bde5b8
refactor: rename wrapper functions
MerlinEgalite Oct 24, 2023
b526a7f
feat: add erc20 wrapper bundler
MerlinEgalite Oct 21, 2023
8b7df76
test: add tests
MerlinEgalite Oct 21, 2023
67cfe8d
fix: wrapper bundler
MerlinEgalite Oct 21, 2023
3ab2465
docs: add natspecs
MerlinEgalite Oct 21, 2023
239c002
fix: deposit for initiator
MerlinEgalite Oct 23, 2023
ac81e9a
refactor: safe approve from solmate
MerlinEgalite Oct 23, 2023
521a408
refactor: remove the check on address 0
MerlinEgalite Oct 24, 2023
7b16d8f
refactor: rename wrapper functions
MerlinEgalite Oct 24, 2023
38730ba
Merge branch 'feat/wrapper' of github.com:morpho-labs/morpho-blue-bun…
MerlinEgalite Nov 8, 2023
b24bcc0
Merge branch 'review-cantina' of github.com:morpho-labs/morpho-blue-b…
MerlinEgalite Nov 8, 2023
300d519
feat: use approve max
MerlinEgalite Nov 8, 2023
e342807
Merge branch 'fix/prevent-reentrancy' of github.com:morpho-labs/morph…
MerlinEgalite Nov 8, 2023
f77392b
feat: protect functions
MerlinEgalite Nov 8, 2023
200a477
docs: apply suggestions
MerlinEgalite Nov 9, 2023
a55d2ed
style: fix formatting
MerlinEgalite Nov 9, 2023
1cd7365
feat: check min balace
MerlinEgalite Nov 9, 2023
7330ef3
docs: apply other suggestions
MerlinEgalite Nov 9, 2023
91b77a3
docs: add descriptive comment
MerlinEgalite Nov 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/ERC20WrapperBundler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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.
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved
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.
Jean-Grimal marked this conversation as resolved.
Show resolved Hide resolved
/// @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);
}
}
14 changes: 7 additions & 7 deletions src/MorphoBundler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler {
/// @param assets The amount of assets to supply. Pass `type(uint256).max` to supply the bundler's loan asset
/// balance.
/// @param shares The amount of shares to mint.
/// @param slippageAmount The minimum amount of shares to mint in exchange for `assets` when it is used.
/// @param slippageAmount The minimum amount of supply shares to mint in exchange for `assets` when it is used.
/// The maximum amount of assets to deposit in exchange for `shares` otherwise.
/// @param onBehalf The address that will own the increased supply position.
/// @param data Arbitrary data to pass to the `onMorphoSupply` callback. Pass empty data if not needed.
Expand Down Expand Up @@ -138,7 +138,7 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler {
/// @param marketParams The Morpho market to borrow assets from.
/// @param assets The amount of assets to borrow.
/// @param shares The amount of shares to mint.
/// @param slippageAmount The maximum amount of shares to mint in exchange for `assets` when it is used.
/// @param slippageAmount The maximum amount of borrow shares to mint in exchange for `assets` when it is used.
/// The minimum amount of assets to borrow in exchange for `shares` otherwise.
/// @param receiver The address that will receive the borrowed assets.
function morphoBorrow(
Expand All @@ -157,12 +157,12 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler {

/// @notice Repays `assets` of the loan asset on behalf of `onBehalf`.
/// @dev Either `assets` or `shares` should be zero. Most usecases should rely on `assets` as an input so the
/// bundler is guaranteed to have `assets` tokens pulled from its balance, but the possibility to mint a specific
/// bundler is guaranteed to have `assets` tokens pulled from its balance, but the possibility to burn a specific
/// amount of shares is given for full compatibility and precision.
/// @param marketParams The Morpho market to repay assets to.
/// @param assets The amount of assets to repay. Pass `type(uint256).max` to repay the bundler's loan asset balance.
/// @param shares The amount of shares to burn.
/// @param slippageAmount The minimum amount of shares to mint in exchange for `assets` when it is used.
/// @param slippageAmount The minimum amount of borrow shares to burn in exchange for `assets` when it is used.
/// The maximum amount of assets to deposit in exchange for `shares` otherwise.
/// @param onBehalf The address of the owner of the debt position.
/// @param data Arbitrary data to pass to the `onMorphoRepay` callback. Pass empty data if not needed.
Expand Down Expand Up @@ -191,14 +191,14 @@ abstract contract MorphoBundler is BaseBundler, IMorphoBundler {

/// @notice Withdraws `assets` of the loan asset on behalf of the initiator.
/// @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 mint a specific amount of shares is
/// initiator is guaranteed to withdraw `assets` tokens, but the possibility to burn a specific amount of shares is
/// given for full compatibility and precision.
/// @dev Initiator must have previously authorized the bundler to act on their behalf on Morpho.
/// @param marketParams The Morpho market to withdraw assets from.
/// @param assets The amount of assets to withdraw.
/// @param shares The amount of shares to burn.
/// @param slippageAmount The minimum amount of shares to mint in exchange for `assets` when it is used.
/// The maximum amount of assets to deposit in exchange for `shares` otherwise.
/// @param slippageAmount The minimum amount of supply shares to burn in exchange for `assets` when it is used.
/// The maximum amount of assets to withdraw in exchange for `shares` otherwise.
/// @param receiver The address that will receive the withdrawn assets.
function morphoWithdraw(
MarketParams calldata marketParams,
Expand Down
4 changes: 3 additions & 1 deletion src/ethereum/EthereumBundler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,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
Expand All @@ -25,7 +26,8 @@ contract EthereumBundler is
WNativeBundler,
EthereumStEthBundler,
UrdBundler,
MorphoBundler
MorphoBundler,
ERC20WrapperBundler
{
/* CONSTRUCTOR */

Expand Down
17 changes: 17 additions & 0 deletions src/mocks/ERC20WrapperMock.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
7 changes: 7 additions & 0 deletions src/mocks/bundlers/ERC20WrapperBundlerMock.sol
Original file line number Diff line number Diff line change
@@ -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 {}
101 changes: 101 additions & 0 deletions test/forge/ERC20WrapperBundlerLocalTest.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
15 changes: 15 additions & 0 deletions test/forge/helpers/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down
Loading