Skip to content

Commit

Permalink
Merge pull request #364 from morpho-org/feat/wrapper
Browse files Browse the repository at this point in the history
Add Wrapper Bundler
  • Loading branch information
MerlinEgalite authored Nov 9, 2023
2 parents 259ad01 + 91b77a3 commit d5afc42
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 1 deletion.
56 changes: 56 additions & 0 deletions src/ERC20WrapperBundler.sol
Original file line number Diff line number Diff line change
@@ -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 [email protected]
/// @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);
}
}
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

0 comments on commit d5afc42

Please sign in to comment.