Skip to content

Commit 1129ef7

Browse files
authored
Merge pull request #1 from init4tech/prestwich/morpho-example
feat: morpho example supplying/repaying from rollup
2 parents 996ec1f + ddd3c5a commit 1129ef7

File tree

3 files changed

+625
-6
lines changed

3 files changed

+625
-6
lines changed

README.md

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,34 @@ are effectively MEV, you can do some really interesting things with them. Like
1414
- Impress your friends
1515
- And more!
1616

17-
[MevWallet]: https://github.com/blunt-instruments/MevWallet
18-
[Signet Orders]: https://signet.sh/docs/learn-about-signet/cross-chain-transfers/
17+
## App Examples
18+
19+
- [`Morpho.sol`](./src/examples/Morpho.sol) - Contracts that allow you to drive
20+
Morpho lending market positions using Signet Orders. This allows you to
21+
supply, borrow, and lend Morpho positions on Ethereum from Signet instantly
22+
and atomically.
1923

20-
## Main Examples
24+
## Orders Examples
2125

2226
- [`SignetStd.sol`](./src/SignetStd.sol) - A simple contract that
2327
auto-configures Signet system parameters, based on the chain id.
2428
- [`Flash.sol`](./src/examples/Flash.sol) - Allows your contract to flash borrow
25-
any asset (provided some searcher will provide it). Flash loans work by having an input and output of the same asset. The Output is then used as the Input to its own Order. This is pretty neat 🎀
29+
any asset (provided some searcher will provide it). Flash loans work by
30+
having an input and output of the same asset. The Output is then used as the
31+
Input to its own Order. This is pretty neat 🎀
2632
- [`GetOut.sol`](./src/examples/GetOut.sol) - A shortcut contract for
2733
exiting Signet (by offering searchers a 50 bps fee).
2834
- [`PayMe.sol`](./src/examples/PayMe.sol) - Payment gating for smart contracts,
29-
using a Signet Order with no inputs. These ensures that contract execution is invalid unless SOMEONE has filled the Order. Unlike traditional payment gates that check `msg.value`, this does NOT require the calling contract to manage cash flow. Instead _any third party_ can fill the order. The calling contract can be blind to the payment. This greatly simplifies contract logic required
35+
using a Signet Order with no inputs. These ensures that contract execution is
36+
invalid unless SOMEONE has filled the Order. Unlike traditional payment gates
37+
that check `msg.value`, this does NOT require the calling contract to manage
38+
cash flow. Instead _any third party_ can fill the order. The calling contract
39+
can be blind to the payment. This greatly simplifies contract logic required
3040
to implement payment gates.
3141
- [`PayYou.sol`](./src/examples/PayYou.sol) - The opposite of payment gating,
32-
this allows a contract to generate MEV by offering a Signet Order with no outputs. This payment becomes a bounty for calling the contract, and functions as an incentivized scheduling system.
42+
this allows a contract to generate MEV by offering a Signet Order with no
43+
outputs. This payment becomes a bounty for calling the contract, and
44+
functions as an incentivized scheduling system.
3345

3446
## Basic Repo Instructions
3547

@@ -50,3 +62,6 @@ $ forge test
5062
```shell
5163
$ forge fmt
5264
```
65+
66+
[MevWallet]: https://github.com/blunt-instruments/MevWallet
67+
[Signet Orders]: https://signet.sh/docs/learn-about-signet/cross-chain-transfers/

src/apps/Morpho.sol

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.13;
3+
4+
import {IMorpho, MarketParams} from "../interfaces/IMorpho.sol";
5+
import {IERC20} from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
6+
import {SignetStd} from "../SignetStd.sol";
7+
import {RollupOrders} from "zenith/src/orders/RollupOrders.sol";
8+
import {Passage} from "zenith/src/passage/Passage.sol";
9+
10+
// How this works:
11+
// - The Signet Orders system calls `transferFrom` and check for the presence
12+
// of a `Fill` event.
13+
// - The MorphoShortcut contract implements `transferFrom` to run
14+
// supply/repay logic on a specific Morpho Market.
15+
// - The rollup contract uses Orders to send tokens to the shortcut
16+
// and then call the shortcut to perform the action.
17+
// - This all occurs in the same block.
18+
19+
// The rollup order is structured as follows:
20+
// - Input: X token on rollup chain.
21+
// - Output: transfer X token on host chain to shortcut.
22+
// - Output: invoke host chain shortcut contract.
23+
//
24+
// The Signet STF logic ensures that the Input is not delivered unless the
25+
// outputs are succesful.
26+
27+
// Note:
28+
// We've provided a rollup contract for the example, but this could also be
29+
// initiated via a signed order from an EOA. No contract is needed. Both
30+
// contracts and EOAs can use the Morpho shortcut to interact with Morpho from
31+
// Signet.
32+
33+
// Note:
34+
// The example order has no spread. Real orders can have a spread, and can also
35+
// have their inputs in ANY asset. I.e. the user can use rollup ETH to pay for
36+
// USDC collateral or repay a WBTC loan on the host Morpho market. This is
37+
// pretty neat.
38+
39+
// The Rollup contract creates orders to interact with Morpho on host net via
40+
// the shortcut.
41+
contract UseMorpho is SignetStd {
42+
/// @dev Address of the shortcut on the host chain.
43+
address immutable repayShortcut;
44+
address immutable supplyShortcut;
45+
address immutable borrowShortcut;
46+
47+
address immutable hostLoanToken;
48+
address immutable hostCollateralToken;
49+
50+
IERC20 immutable ruLoanToken;
51+
IERC20 immutable ruCollateralToken;
52+
53+
constructor(address _repayShortcut, address _supplyShortcut, address _hostLoan, address _hostCollateral)
54+
SignetStd()
55+
{
56+
repayShortcut = _repayShortcut;
57+
supplyShortcut = _supplyShortcut;
58+
59+
// Autodetect rollup tokens based on token addresses on host network.
60+
hostLoanToken = _hostLoan;
61+
hostCollateralToken = _hostCollateral;
62+
if (_hostLoan == HOST_WETH) {
63+
ruLoanToken = WETH;
64+
} else if (_hostLoan == HOST_WBTC) {
65+
ruLoanToken = WBTC;
66+
} else if (_hostLoan == HOST_USDC || _hostLoan == HOST_USDT) {
67+
ruLoanToken = WUSD;
68+
} else {
69+
revert("Unsupported loan token");
70+
}
71+
if (_hostCollateral == HOST_WETH) {
72+
ruCollateralToken = WETH;
73+
} else if (_hostCollateral == HOST_WBTC) {
74+
ruCollateralToken = WBTC;
75+
} else if (_hostCollateral == HOST_USDC || _hostCollateral == HOST_USDT) {
76+
ruCollateralToken = WUSD;
77+
} else {
78+
revert("Unsupported collateral token");
79+
}
80+
81+
// Pre-emptively approve the Orders contract to spend our tokens.
82+
ruLoanToken.approve(address(ORDERS), type(uint256).max);
83+
ruCollateralToken.approve(address(ORDERS), type(uint256).max);
84+
}
85+
86+
// Supply some amount of the collateral token on behalf of the user.
87+
function supplyCollateral(address onBehalf, uint256 amount) public {
88+
if (amount > 0) {
89+
ruCollateralToken.transferFrom(msg.sender, address(this), amount);
90+
}
91+
92+
// the amount is whatever our current balance is
93+
amount = ruCollateralToken.balanceOf(address(this));
94+
95+
RollupOrders.Input[] memory inputs = new RollupOrders.Input[](1);
96+
inputs[0] = makeInput(address(ruCollateralToken), amount);
97+
98+
// The first output pays the collateral token to the shortcut.
99+
// The second output calls the shortcut to supply the collateral.
100+
RollupOrders.Output[] memory outputs = new RollupOrders.Output[](2);
101+
outputs[0] = makeHostOutput(address(hostCollateralToken), amount, supplyShortcut);
102+
outputs[1] = makeHostOutput(supplyShortcut, amount, onBehalf);
103+
104+
ORDERS.initiate(
105+
block.timestamp, // no deadline
106+
inputs,
107+
outputs
108+
);
109+
}
110+
111+
// Repay some amount of the loan token on behalf of the user.
112+
function repay(address onBehalf, uint256 amount) public {
113+
if (amount > 0) {
114+
ruLoanToken.transferFrom(msg.sender, address(this), amount);
115+
}
116+
117+
// Send all tokens.
118+
amount = ruLoanToken.balanceOf(address(this));
119+
120+
RollupOrders.Input[] memory inputs = new RollupOrders.Input[](1);
121+
inputs[0] = makeInput(address(ruLoanToken), amount);
122+
123+
// The first output pays the loan token to the shortcut.
124+
// The second output calls the shortcut to repay the loan.
125+
RollupOrders.Output[] memory outputs = new RollupOrders.Output[](2);
126+
outputs[0] = makeHostOutput(address(hostLoanToken), amount, repayShortcut);
127+
outputs[1] = makeHostOutput(repayShortcut, amount, onBehalf);
128+
129+
ORDERS.initiate(
130+
block.timestamp, // no deadline
131+
inputs,
132+
outputs
133+
);
134+
}
135+
136+
// Borrow some amount of the loan token and send it to the rollup.
137+
function borrow(address onBehalf, uint256 amount) public {
138+
RollupOrders.Input[] memory inputs = new RollupOrders.Input[](0);
139+
140+
// The first output calls the shortcut to borrow the loan.
141+
// The second output sends the loan token to the user on the rollup.
142+
RollupOrders.Output[] memory outputs = new RollupOrders.Output[](2);
143+
outputs[0] = makeHostOutput(borrowShortcut, amount, onBehalf);
144+
outputs[1] = makeRollupOutput(address(ruLoanToken), amount, msg.sender);
145+
146+
ORDERS.initiate(
147+
block.timestamp, // no deadline
148+
inputs,
149+
outputs
150+
);
151+
}
152+
153+
// Supply collateral and then borrow against it.
154+
function supplyCollateralBorrow(address onBehalf, uint256 supplyAmnt, uint256 borrowAmnt) external {
155+
// Note: this could be more gas efficient by combining into a single
156+
// order.
157+
supplyCollateral(onBehalf, supplyAmnt);
158+
borrow(onBehalf, borrowAmnt);
159+
}
160+
}
161+
162+
abstract contract HostMorphoUser {
163+
IMorpho immutable morpho;
164+
165+
// This is an unrolled MarketParams struct.
166+
IERC20 immutable loanToken;
167+
IERC20 immutable collateralToken;
168+
169+
address immutable oracle;
170+
address immutable irm;
171+
uint256 immutable lltv;
172+
173+
error InsufficentTokensReceived(uint256 received, uint256 required);
174+
175+
constructor(IMorpho _morpho, MarketParams memory _params) {
176+
morpho = _morpho;
177+
178+
loanToken = IERC20(_params.loanToken);
179+
collateralToken = IERC20(_params.collateralToken);
180+
oracle = _params.oracle;
181+
irm = _params.irm;
182+
lltv = _params.lltv;
183+
184+
loanToken.approve(address(_morpho), type(uint256).max);
185+
collateralToken.approve(address(_morpho), type(uint256).max);
186+
}
187+
188+
function checkReceived(uint256 received, uint256 required) internal pure {
189+
if (received < required) {
190+
revert InsufficentTokensReceived(received, required);
191+
}
192+
}
193+
194+
function loadParams() internal view returns (MarketParams memory params) {
195+
params.loanToken = address(loanToken);
196+
params.collateralToken = address(collateralToken);
197+
params.oracle = oracle;
198+
params.irm = irm;
199+
params.lltv = lltv;
200+
}
201+
}
202+
203+
// This contract should be deployed on the host chain. It is used as a shortcut
204+
// to supply collateral to Morpho and can be invoked by the rollup via an Order.
205+
contract HostMorphoRepay is HostMorphoUser {
206+
constructor(IMorpho _morpho, MarketParams memory _params) HostMorphoUser(_morpho, _params) {}
207+
208+
/// Uses the ERC20 transferFrom interface to invoke contract logic. This
209+
/// allows us to invoke logic from the Orders contract
210+
function transferFrom(address, address recipient, uint256 amount) external returns (bool) {
211+
uint256 loanTokenBalance = loanToken.balanceOf(address(this));
212+
checkReceived(loanTokenBalance, amount);
213+
morpho.repay(loadParams(), loanTokenBalance, 0, recipient, "");
214+
return true;
215+
}
216+
}
217+
218+
contract HostMorphoSupply is HostMorphoUser {
219+
constructor(IMorpho _morpho, MarketParams memory _params) HostMorphoUser(_morpho, _params) {}
220+
221+
/// Uses the ERC20 transferFrom interface to invoke contract logic. This
222+
/// allows us to invoke logic from the Orders contract
223+
function transferFrom(address, address recipient, uint256 amount) external returns (bool) {
224+
uint256 collateralTokenBalance = collateralToken.balanceOf(address(this));
225+
226+
checkReceived(collateralTokenBalance, amount);
227+
228+
morpho.supplyCollateral(loadParams(), collateralTokenBalance, recipient, "");
229+
230+
// Future extension:
231+
// borrow some amount of loanToken
232+
// and send it to the rollup
233+
234+
return true;
235+
}
236+
}
237+
238+
contract HosyMorphoBorrow is HostMorphoUser {
239+
constructor(IMorpho _morpho, MarketParams memory _params) HostMorphoUser(_morpho, _params) {}
240+
241+
// This function
242+
function calculateBorrow() internal pure returns (uint256 tokens) {
243+
return 0;
244+
}
245+
246+
function transferFrom(address filler, address onBehalf, uint256 amount) external returns (bool) {
247+
// borrow some amount of loanToken
248+
morpho.borrow(loadParams(), amount, 0, onBehalf, filler);
249+
return true;
250+
}
251+
}

0 commit comments

Comments
 (0)