-
Notifications
You must be signed in to change notification settings - Fork 163
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: morpho blue flash loan asset manager
- Loading branch information
1 parent
b859070
commit e1a53a1
Showing
6 changed files
with
370 additions
and
0 deletions.
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
17 changes: 17 additions & 0 deletions
17
contracts/external-interfaces/IMorphoBlueFlashLoanCallback.sol
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 @@ | ||
/* | ||
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; | ||
} |
30 changes: 30 additions & 0 deletions
30
.../smart-accounts/morpho-blue-flash-loan-asset-manager/IMorphoBlueFlashLoanAssetManager.sol
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,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; | ||
} |
133 changes: 133 additions & 0 deletions
133
...mart-accounts/morpho-blue-flash-loan-asset-manager/MorphoBlueFlashLoanAssetManagerLib.sol
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,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; | ||
} | ||
} |
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
187 changes: 187 additions & 0 deletions
187
tests/tests/persistent/smart-accounts/MorphoBlueFlashLoanAssetManagerLib.t.sol
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,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); | ||
} | ||
} |