Skip to content

Commit

Permalink
Merge pull request #43 from alcueca/morpho-pendle
Browse files Browse the repository at this point in the history
morpho pendle
  • Loading branch information
ultrasecreth authored May 3, 2024
2 parents 5b3401b + 2ef12bf commit d61ab3e
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 1 deletion.
1 change: 1 addition & 0 deletions .forge-snapshots/MorphoPendle.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
558536
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ repayment approval.
| Gnosis Safe | | | 109223 | Variable | [GnosisSafeWrapper](src/gnosisSafe/GnosisSafeWrapper.sol) |
| MakerDAO | | | 313172 | 0 | [ERC3156Wrapper](src/erc3156/ERC3156Wrapper.sol) |
| MorphoBlue | 0xa0Cb4e1222d813D6e4dE79f2A7A0B7759209588F | Ethereum | 132114 | 0 | [MorphoBlueWrapper](src/morpho/MorphoBlueWrapper.sol) |
| MorphoBlue + Pendle | 0xE1f9e06006B2592dDe5b1bBA3ae2DD34DF557007 | Ethereum | 558536 | 0 | [MorphoPendleWrapper](src/pendle/MorphoPendleWrapper.sol) |
| Spark | 0x8cB701df93f2Dae295aE8D7beE5Aa7e4D40CB397 | Ethereum, Gnosis | 212569 | 0 | [AaveWrapper](src/aave/AaveWrapper.sol) |
| Uniswap v3 | 0x319300462C37AD2D4f26B584C2b67De51F51f289 | Arbitrum One, Optimism, Polygon, Ethereum | 94720 | Variable | [UniswapV3Wrapper](src/uniswapV3/UniswapV3Wrapper.sol) |
| Uniswap v3 + Pendle | 0xa353Fd50210786F0E038ddD574A21d0CCefb3163 | Arbitrum One | 497567 | Variable | [UniswapV3PendleWrapper](src/pendle/UniswapV3PendleWrapper.sol) |
Expand Down
27 changes: 27 additions & 0 deletions script/MorphoPendleDeploy.s.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <=0.9.0;

import { Script } from "forge-std/Script.sol";

import { console2 } from "forge-std/console2.sol";

import { MorphoPendleWrapper, IMorpho, IPendleRouterV3 } from "../src/pendle/MorphoPendleWrapper.sol";

/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting
contract MorphoPendleDeploy is Script {
bytes32 public constant SALT = keccak256("ultrasecr.eth");

function run() public {
console2.log("Deploying as %s", msg.sender);

IMorpho morpho = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb);
IPendleRouterV3 pendleRouter = IPendleRouterV3(0x00000000005BBB0EF59571E58418F9a4357b68A0);

console2.log("pendleRouter: %s", address(pendleRouter));
console2.log("morpho: %s", address(morpho));

vm.broadcast();
MorphoPendleWrapper wrapper = new MorphoPendleWrapper{ salt: SALT }(morpho, pendleRouter);
console2.log("MorphoPendleWrapper: %s", address(wrapper));
}
}
2 changes: 1 addition & 1 deletion src/morpho/MorphoBlueWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ contract MorphoBlueWrapper is BaseWrapper, IMorphoFlashLoanCallback {
return amount >= max ? type(uint256).max : 0;
}

function onMorphoFlashLoan(uint256 amount, bytes calldata params) external {
function onMorphoFlashLoan(uint256 amount, bytes calldata params) external override {
if (msg.sender != address(morpho)) revert NotMorpho();
(address asset, bytes memory data) = abi.decode(params, (address, bytes));

Expand Down
2 changes: 2 additions & 0 deletions src/pendle/AlgebraPendleWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ contract AlgebraPendleWrapper is BasePendleWrapper, IAlgebraFlashCallback, Acces

/// @inheritdoc IERC7399
function maxFlashLoan(address asset) public view returns (uint256) {
if (IPPrincipalToken(asset).isExpired()) return 0;

IERC20 underlying = IPPrincipalToken(asset).SY().yieldToken();
(IAlgebraPool pool,) = _pool(address(underlying));
uint256 poolBalance = underlying.balanceOf(address(pool));
Expand Down
1 change: 1 addition & 0 deletions src/pendle/BalancerPendleWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ contract BalancerPendleWrapper is BasePendleWrapper, IFlashLoanRecipient {

/// @inheritdoc IERC7399
function maxFlashLoan(address asset) public view returns (uint256) {
if (IPPrincipalToken(asset).isExpired()) return 0;
return IPPrincipalToken(asset).SY().yieldToken().balanceOf(address(balancer));
}

Expand Down
58 changes: 58 additions & 0 deletions src/pendle/MorphoPendleWrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-License-Identifier: MIT
// Thanks to ultrasecr.eth
pragma solidity ^0.8.19;

import { IMorphoFlashLoanCallback } from "../morpho/interfaces/IMorphoFlashLoanCallback.sol";
import { IMorpho } from "../morpho/interfaces/IMorpho.sol";

import { IPendleRouterV3 } from "./interfaces/IPendleRouterV3.sol";
import { IPPrincipalToken } from "./interfaces/IPPrincipalToken.sol";

import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { IERC7399, IERC20 } from "../BaseWrapper.sol";
import { BasePendleWrapper } from "./BasePendleWrapper.sol";

/// @dev Pendle Flash Lender that uses Morpho Pools as source of X liquidity,
/// then deposits X on Pendle to borrow whatever's necessary.
contract MorphoPendleWrapper is BasePendleWrapper, IMorphoFlashLoanCallback {
using SafeERC20 for IERC20;

error NotMorpho();

uint256 private constant FEE = 0;

IMorpho public immutable morpho;

constructor(IMorpho _morpho, IPendleRouterV3 _pendleRouter) BasePendleWrapper(_pendleRouter) {
morpho = _morpho;
}

/// @inheritdoc IERC7399
function maxFlashLoan(address asset) public view returns (uint256) {
if (IPPrincipalToken(asset).isExpired()) return 0;
return IPPrincipalToken(asset).SY().yieldToken().balanceOf(address(morpho));
}

/// @inheritdoc IERC7399
function flashFee(address asset, uint256 amount) external view returns (uint256) {
uint256 max = maxFlashLoan(asset);
return amount >= max ? type(uint256).max : FEE;
}

function _flashLoan(address asset, uint256 amount, bytes memory data) internal override {
address underlying = address(IPPrincipalToken(asset).SY().yieldToken());
bytes memory metadata = abi.encode(underlying, asset, data);
morpho.flashLoan(underlying, amount, metadata);
}

function onMorphoFlashLoan(uint256 amount, bytes calldata params) external override {
if (msg.sender != address(morpho)) revert NotMorpho();

(IERC20 underlying, IERC20 asset, bytes memory data) = abi.decode(params, (IERC20, IERC20, bytes));

_handleFlashLoan(underlying, asset, amount, FEE, data);

_approveRepayment(address(underlying), amount, FEE);
}
}
2 changes: 2 additions & 0 deletions src/pendle/UniswapV3PendleWrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ contract UniswapV3PendleWrapper is BasePendleWrapper, IUniswapV3FlashCallback, A

/// @inheritdoc IERC7399
function maxFlashLoan(address asset) public view returns (uint256) {
if (IPPrincipalToken(asset).isExpired()) return 0;

IERC20 underlying = IPPrincipalToken(asset).SY().yieldToken();
(, uint256 poolBalance, uint24 poolFee) = _maxFlashLoan(address(underlying));
if (poolBalance == 0) return 0;
Expand Down
102 changes: 102 additions & 0 deletions test/MorphoPendleWrapper.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity >=0.8.19 <0.9.0;

import { Test } from "forge-std/Test.sol";
import { console2 } from "forge-std/console2.sol";

import { IERC20Metadata as IERC20 } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

import { Arrays } from "src/utils/Arrays.sol";

import { IMorpho } from "../src/morpho/interfaces/IMorpho.sol";
import { MockBorrower } from "./MockBorrower.sol";
import { MorphoPendleWrapper, IPendleRouterV3 } from "../src/pendle/MorphoPendleWrapper.sol";

/// @dev If this is your first time with Forge, read this tutorial in the Foundry Book:
/// https://book.getfoundry.sh/forge/writing-tests
contract MorphoPendleWrapperTest is Test {
using Arrays for uint256;
using Arrays for address;
using SafeERC20 for IERC20;

MorphoPendleWrapper internal wrapper;
MockBorrower internal borrower;
address internal token;
IMorpho internal morpho;
IPendleRouterV3 internal pendleRouter;

uint256 internal dust = 1e10;

/// @dev A function invoked before each test case is run.
function setUp() public virtual {
// Revert if there is no API key.
string memory alchemyApiKey = vm.envOr("API_KEY_ALCHEMY", string(""));
if (bytes(alchemyApiKey).length == 0) {
revert("API_KEY_ALCHEMY variable missing");
}

vm.createSelectFork({ urlOrAlias: "mainnet", blockNumber: 19_788_676 });
morpho = IMorpho(0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb);
pendleRouter = IPendleRouterV3(0x00000000005BBB0EF59571E58418F9a4357b68A0);
token = 0xc69Ad9baB1dEE23F4605a82b3354F8E40d1E5966; // PT-weETH-27JUN2024

wrapper = new MorphoPendleWrapper(morpho, pendleRouter);
borrower = new MockBorrower(wrapper);
}

/// @dev Basic test. Run it with `forge test -vvv` to see the console log.
function test_flashFee() external {
console2.log("test_flashFee");
assertEq(wrapper.flashFee(token, 1e18), 0, "Fee not zero");
}

function test_maxFlashLoan() external {
console2.log("test_maxFlashLoan");
assertEqDecimal(wrapper.maxFlashLoan(token), 6451.923761191633930312e18, 18, "Max flash loan not right");
}

function test_maxFlashLoan_unsupportedAsset() external {
console2.log("test_maxFlashLoan");
vm.expectRevert();
assertEq(wrapper.maxFlashLoan(address(1)), 0, "Max flash loan not right");
}

function test_flashFee_insufficientLiquidity() external {
console2.log("test_flashFee");
assertEq(wrapper.flashFee(token, 20_000e18), type(uint256).max, "Fee not zero");
}

function test_flashLoan() external {
console2.log("test_flashLoan");
uint256 loan = 10e18;
uint256 fee = wrapper.flashFee(token, loan);
IERC20(token).safeTransfer(address(borrower), fee);
bytes memory result = borrower.flashBorrow(token, loan);

// Test the return values passed through the wrapper
(bytes32 callbackReturn) = abi.decode(result, (bytes32));
assertEq(uint256(callbackReturn), uint256(borrower.ERC3156PP_CALLBACK_SUCCESS()), "Callback failed");

// Test the borrower state during the callback
assertEq(borrower.flashInitiator(), address(borrower));
assertEq(address(borrower.flashAsset()), address(token));
assertEq(borrower.flashAmount(), loan);
assertEq(borrower.flashBalance(), loan + fee); // The amount we transferred to pay for fees, plus the amount we
// borrowed
assertEq(borrower.flashFee(), fee);
}

function test_onMorphoFlashLoan_permissions() public {
vm.expectRevert(MorphoPendleWrapper.NotMorpho.selector);
wrapper.onMorphoFlashLoan({ amount: 0, params: "" });
}

function test_measureFlashLoanGas() public {
console2.log("test_measureFlashLoanGas");
uint256 loan = 10e18;
uint256 fee = wrapper.flashFee(token, loan);
IERC20(token).safeTransfer(address(borrower), fee);
borrower.flashBorrowMeasureGas(token, loan, "MorphoPendle");
}
}

0 comments on commit d61ab3e

Please sign in to comment.