-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #364 from morpho-org/feat/wrapper
Add Wrapper Bundler
- Loading branch information
Showing
6 changed files
with
199 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters