Skip to content

Commit

Permalink
feat: morpho blue flash loan asset manager
Browse files Browse the repository at this point in the history
  • Loading branch information
SeanJCasey authored Nov 22, 2024
1 parent b859070 commit e1a53a1
Show file tree
Hide file tree
Showing 6 changed files with 370 additions and 0 deletions.
2 changes: 2 additions & 0 deletions contracts/external-interfaces/IMorphoBlue.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ interface IMorphoBlue {
uint128 fee;
}

function flashLoan(address _token, uint256 _assets, bytes calldata _data) external;

function supply(
MarketParams memory _marketParams,
uint256 _assets,
Expand Down
17 changes: 17 additions & 0 deletions contracts/external-interfaces/IMorphoBlueFlashLoanCallback.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
This file is part of the Enzyme Protocol.
(c) Enzyme Foundation <security@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;

/// @title IMorphoBlueFlashLoanCallback Interface
/// @author Enzyme Foundation <security@enzyme.finance>
interface IMorphoBlueFlashLoanCallback {
function onMorphoFlashLoan(uint256 _assets, bytes calldata _data) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-License-Identifier: GPL-3.0

/*
This file is part of the Enzyme Protocol.
(c) Enzyme Foundation <security@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/

pragma solidity >=0.6.0 <0.9.0;

/// @title IMorphoBlueFlashLoanAssetManager Interface
/// @author Enzyme Foundation <security@enzyme.finance>
interface IMorphoBlueFlashLoanAssetManager {
struct Call {
address target;
bytes data;
}

struct ForwardData {
address borrowedAssetAddress;
Call[] calls;
}

function flashLoan(address _assetAddress, uint256 _amount, Call[] calldata _calls) external;

function init(address _owner, address _borrowedAssetsRecipient) external;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// SPDX-License-Identifier: GPL-3.0

/*
This file is part of the Enzyme Protocol.
(c) Enzyme Foundation <security@enzyme.finance>
For the full license information, please view the LICENSE
file that was distributed with this source code.
*/

pragma solidity 0.8.19;

import {Address} from "openzeppelin-solc-0.8/utils/Address.sol";
import {IERC20} from "../../../external-interfaces/IERC20.sol";
import {IMorphoBlue} from "../../../external-interfaces/IMorphoBlue.sol";
import {IMorphoBlueFlashLoanCallback} from "../../../external-interfaces/IMorphoBlueFlashLoanCallback.sol";
import {WrappedSafeERC20 as SafeERC20} from "../../../utils/0.8.19/open-zeppelin/WrappedSafeERC20.sol";
import {IMorphoBlueFlashLoanAssetManager} from "./IMorphoBlueFlashLoanAssetManager.sol";

/// @title MorphoBlueFlashLoanAssetManagerLib Contract
/// @author Enzyme Foundation <security@enzyme.finance>
/// @notice An asset manager contract for executing flash loans on Morpho Blue
/// @dev Intended as implementation contract for a proxy.
/// Must add this contract instance as an asset manager on the intended Enzyme vault.
contract MorphoBlueFlashLoanAssetManagerLib is IMorphoBlueFlashLoanAssetManager, IMorphoBlueFlashLoanCallback {
using SafeERC20 for IERC20;

IMorphoBlue public immutable MORPHO;

// `owner`: The authorized caller of this contract instance
address internal owner;
// `borrowedAssetsRecipient`: The address where all borrowed assets are transferred. Generally the VaultProxy.
address internal borrowedAssetsRecipient;

error MorphoBlueFlashLoanAssetManager__FlashLoan__Unauthorized();
error MorphoBlueFlashLoanAssetManager__Init__AlreadyInitialized();
error MorphoBlueFlashLoanAssetManager__OnMorphoFlashLoan__UnauthorizedCaller();
error MorphoBlueFlashLoanAssetManager__OnMorphoFlashLoan__UnauthorizedInitiator();

event BorrowedAssetsRecipientSet(address borrowedAssetsRecipient);
event OwnerSet(address owner);

constructor(address _morphoBlueAddress) {
MORPHO = IMorphoBlue(_morphoBlueAddress);
}

/// @notice Initializes the contract
/// @param _owner The owner (authorized caller) of the contract
/// @param _borrowedAssetsRecipient The recipient of the flash loan borrowed assets
function init(address _owner, address _borrowedAssetsRecipient) external {
if (getOwner() != address(0)) revert MorphoBlueFlashLoanAssetManager__Init__AlreadyInitialized();

__setOwner(_owner);
__setBorrowedAssetsRecipient(_borrowedAssetsRecipient);
}

/// @notice Executes a flash loan on Morpho Blue
/// @param _assetAddress The asset to borrow
/// @param _amount The amount to borrow
/// @param _calls Call[] items to execute during the flash loan
function flashLoan(address _assetAddress, uint256 _amount, Call[] calldata _calls) external override {
if (msg.sender != getOwner()) revert MorphoBlueFlashLoanAssetManager__FlashLoan__Unauthorized();

MORPHO.flashLoan({
_token: _assetAddress,
_assets: _amount,
_data: abi.encode(ForwardData({borrowedAssetAddress: _assetAddress, calls: _calls}))
});
}

/// @dev Helper to set `borrowedAssetsRecipient`
function __setBorrowedAssetsRecipient(address _borrowedAssetsRecipient) internal {
borrowedAssetsRecipient = _borrowedAssetsRecipient;

emit BorrowedAssetsRecipientSet(_borrowedAssetsRecipient);
}

/// @dev Helper to set `owner`
function __setOwner(address _owner) internal {
owner = _owner;

emit OwnerSet(_owner);
}

//==================================================================================================================
// IMorphoBlueFlashLoanCallback
//==================================================================================================================

/// @notice Required callback function for Morpho Blue flash loans
function onMorphoFlashLoan(uint256 _amount, bytes calldata _data) external {
// Only Morpho can call directly, and it only does so to the requesting user's contract
if (msg.sender != address(MORPHO)) {
revert MorphoBlueFlashLoanAssetManager__OnMorphoFlashLoan__UnauthorizedCaller();
}

// Decode forwarded data
ForwardData memory forwardData = abi.decode(_data, (ForwardData));
IERC20 asset = IERC20(forwardData.borrowedAssetAddress);
Call[] memory calls = forwardData.calls;

// Send full balance of borrowed asset to recipient.
// Leaving 0-balance makes calculating repayment amount to transfer simpler,
// and prevents griefing by sending surplus assets here.
asset.safeTransfer(getBorrowedAssetsRecipient(), asset.balanceOf(address(this)));

// Execute calls.
// The final `Call[]` items should transfer exact "asset + premium" amounts to this contract to repay the loan.
for (uint256 i; i < calls.length; i++) {
Call memory call = calls[i];

Address.functionCall({target: call.target, data: call.data});
}

asset.safeApprove(address(MORPHO), _amount);
}

//==================================================================================================================
// Storage getters
//==================================================================================================================

/// @notice Gets the recipient of the flash loan borrowed assets
/// @return borrowedAssetsRecipient_ The recipient
function getBorrowedAssetsRecipient() public view returns (address borrowedAssetsRecipient_) {
return borrowedAssetsRecipient;
}

/// @notice Gets the owner (authorized caller) of the contract
/// @return owner_ The owner
function getOwner() public view returns (address owner_) {
return owner;
}
}
1 change: 1 addition & 0 deletions tests/interfaces/interfaces.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ ISingleAssetRedemptionQueueLib.sol: SingleAssetRedemptionQueueLib.abi.json

# Smart accounts
IAaveV3FlashLoanAssetManager.sol: AaveV3FlashLoanAssetManagerLib.abi.json
IMorphoBlueFlashLoanAssetManager.sol: MorphoBlueFlashLoanAssetManagerLib.abi.json
IMultiCallAccountMixin.sol: MultiCallAccountMixin.abi.json
IMultiCallAccountMixinHarness.sol: MultiCallAccountMixinHarness.abi.json
ISharePriceThrottledAssetManagerLib.sol: SharePriceThrottledAssetManagerLib.abi.json
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.19;

import {IERC20 as IERC20Prod} from "contracts/external-interfaces/IERC20.sol";
import {WrappedSafeERC20 as SafeERC20Prod} from "contracts/utils/0.8.19/open-zeppelin/WrappedSafeERC20.sol";

import {IntegrationTest} from "tests/bases/IntegrationTest.sol";
import {IERC20} from "tests/interfaces/external/IERC20.sol";
import {IMorphoBlue} from "tests/interfaces/external/IMorphoBlue.sol";
import {IMorphoBlueFlashLoanAssetManager} from "tests/interfaces/internal/IMorphoBlueFlashLoanAssetManager.sol";

address constant ETHEREUM_MORPHO_BLUE = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;

abstract contract TestBase is IntegrationTest {
// Events: MorphoBlueFlashLoanAssetManager
event BorrowedAssetsRecipientSet(address borrowedAssetsRecipient);
event OwnerSet(address owner);
// Events: MockVault
event MockVaultTransferAsset(address assetAddress, address target, uint256 amount);

IMorphoBlue morpho;

address accountOwner = makeAddr("AccountOwner");
MockVault vault;
IMorphoBlueFlashLoanAssetManager morphoFlashLoanAssetManager;

IERC20 borrowAsset;

function __initialize(address _morphoBlueAddress, address _borrowAssetAddress, uint256 _chainId) internal {
setUpNetworkEnvironment({_chainId: _chainId});

morpho = IMorphoBlue(_morphoBlueAddress);
borrowAsset = IERC20(_borrowAssetAddress);

// Deploy a mock vault to use as (1) the borrowed assets recipient and (2) target of Call[] items
vault = new MockVault();

// Deploy the account
morphoFlashLoanAssetManager = __deployAccount({_morphoBlueAddress: ETHEREUM_MORPHO_BLUE});

// Initialize the account (as a proxy would)
morphoFlashLoanAssetManager.init({_owner: accountOwner, _borrowedAssetsRecipient: address(vault)});
}

// DEPLOYMENT

function __deployAccount(address _morphoBlueAddress) internal returns (IMorphoBlueFlashLoanAssetManager account_) {
return IMorphoBlueFlashLoanAssetManager(
deployCode("MorphoBlueFlashLoanAssetManagerLib.sol", abi.encode(_morphoBlueAddress))
);
}

// HELPERS

function __flashLoan(uint256 _amount) internal {
IMorphoBlueFlashLoanAssetManager.Call[] memory calls = new IMorphoBlueFlashLoanAssetManager.Call[](1);
calls[0] = IMorphoBlueFlashLoanAssetManager.Call({
target: address(vault),
data: abi.encodeWithSelector(vault.transferAsset.selector, borrowAsset, morphoFlashLoanAssetManager, _amount)
});

vm.prank(accountOwner);
morphoFlashLoanAssetManager.flashLoan({_assetAddress: address(borrowAsset), _amount: _amount, _calls: calls});
}

// TESTS

function test_init_failsWithAlreadyInitialized() public {
address testInitOwner = makeAddr("TestInitOwner");
address testInitBorrowedAssetsRecipient = makeAddr("TestInitBorrowedAssetsRecipient");

// Already initialized during setup, so a 2nd call should fail
vm.expectRevert(
IMorphoBlueFlashLoanAssetManager.MorphoBlueFlashLoanAssetManager__Init__AlreadyInitialized.selector
);
morphoFlashLoanAssetManager.init({
_owner: testInitOwner,
_borrowedAssetsRecipient: testInitBorrowedAssetsRecipient
});
}

function test_init_success() public {
address testInitOwner = makeAddr("TestInitOwner");
address testInitBorrowedAssetsRecipient = makeAddr("TestInitBorrowedAssetsRecipient");

// Deploy a fresh account, without initializing
IMorphoBlueFlashLoanAssetManager testInitAccount = __deployAccount({_morphoBlueAddress: address(morpho)});

// Pre-assert expected events
expectEmit(address(testInitAccount));
emit OwnerSet(testInitOwner);
expectEmit(address(testInitAccount));
emit BorrowedAssetsRecipientSet(testInitBorrowedAssetsRecipient);

// Initialize the new account
testInitAccount.init({_owner: testInitOwner, _borrowedAssetsRecipient: testInitBorrowedAssetsRecipient});

// Assert stored values
assertEq(testInitAccount.getOwner(), testInitOwner, "Unexpected owner");
assertEq(
testInitAccount.getBorrowedAssetsRecipient(),
testInitBorrowedAssetsRecipient,
"Unexpected borrowedAssetsRecipient"
);
}

function test_flashLoan_failsWithUnauthorizedCaller() public {
address randomCaller = makeAddr("RandomCaller");

vm.expectRevert(
IMorphoBlueFlashLoanAssetManager.MorphoBlueFlashLoanAssetManager__FlashLoan__Unauthorized.selector
);
vm.prank(randomCaller);
morphoFlashLoanAssetManager.flashLoan({
_assetAddress: address(0),
_amount: 0,
_calls: new IMorphoBlueFlashLoanAssetManager.Call[](0)
});
}

function test_flashLoan_success() public {
// Start the vault and the asset manager contract with a balance of the asset to borrow
uint256 preVaultBalance = 123;
uint256 preMorphoFlashLoanAssetManagerBalance = 456;
uint256 borrowAmount = 789;
increaseTokenBalance({_token: borrowAsset, _to: address(vault), _amount: preVaultBalance});
increaseTokenBalance({
_token: borrowAsset,
_to: address(morphoFlashLoanAssetManager),
_amount: preMorphoFlashLoanAssetManagerBalance
});

// Assert that the correct amount is borrowed based on Vault's repayment amount
expectEmit(address(vault));
emit MockVaultTransferAsset(address(borrowAsset), address(morphoFlashLoanAssetManager), borrowAmount);

__flashLoan({_amount: borrowAmount});

// Vault balance should now include the pre-tx asset manager contract surplus
assertEq(
borrowAsset.balanceOf(address(vault)),
preVaultBalance + preMorphoFlashLoanAssetManagerBalance,
"Incorrect remainder in vault"
);
// Nothing should remain in the asset manager contract
assertEq(
borrowAsset.balanceOf(address(morphoFlashLoanAssetManager)),
0,
"Non-zero remainder in asset manager contract"
);
}

function test_onMorphoFlashLoan_failsWithUnauthorizedCaller() public {
address randomCaller = makeAddr("RandomCaller");

vm.expectRevert(
IMorphoBlueFlashLoanAssetManager
.MorphoBlueFlashLoanAssetManager__OnMorphoFlashLoan__UnauthorizedCaller
.selector
);
vm.prank(randomCaller);
morphoFlashLoanAssetManager.onMorphoFlashLoan({_amount: 0, _data: new bytes(0)});
}
}

contract TestEthereum is TestBase {
function setUp() public override {
// Use USDT because it has annoying behavior
__initialize({
_morphoBlueAddress: ETHEREUM_MORPHO_BLUE,
_borrowAssetAddress: ETHEREUM_USDT,
_chainId: ETHEREUM_CHAIN_ID
});
}
}

contract MockVault {
using SafeERC20Prod for IERC20Prod;

event MockVaultTransferAsset(address assetAddress, address target, uint256 amount);

function transferAsset(address _assetAddress, address _target, uint256 _amount) external {
IERC20Prod(_assetAddress).safeTransfer(_target, _amount);

emit MockVaultTransferAsset(_assetAddress, _target, _amount);
}
}

0 comments on commit e1a53a1

Please sign in to comment.