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 #309

Closed
wants to merge 22 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 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
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
13 changes: 7 additions & 6 deletions config/ConfigLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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[]));

Expand Down
52 changes: 52 additions & 0 deletions src/ERC20WrapperBundler.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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.
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.
/// @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 erc20WrapperDepositFor(address wrapper, uint256 amount) external {
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.
underlying.safeApprove(wrapper, 0);
underlying.safeApprove(wrapper, amount);
ERC20Wrapper(wrapper).depositFor(initiator(), amount);
}

/// @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 erc20WrapperWithdrawTo(address wrapper, address account, uint256 amount) external {
require(account != address(0), ErrorsLib.ZERO_ADDRESS);
require(amount != 0, ErrorsLib.ZERO_AMOUNT);

ERC20Wrapper(wrapper).withdrawTo(account, amount);
MerlinEgalite marked this conversation as resolved.
Show resolved Hide resolved
}
}
4 changes: 3 additions & 1 deletion src/ethereum/EthereumBundler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,7 +25,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 {}
72 changes: 72 additions & 0 deletions test/forge/ERC20WrapperBundlerLocalTest.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// 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 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);
}
}
15 changes: 15 additions & 0 deletions test/forge/helpers/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -108,6 +109,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, address receiver) internal pure returns (bytes memory) {
Expand Down
Loading