Skip to content

Commit fa42572

Browse files
committed
V2 Adapter
1 parent a14db54 commit fa42572

15 files changed

+1162
-35
lines changed

.github/workflows/certora.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ jobs:
3838
- uses: actions/checkout@v4
3939
with:
4040
submodules: recursive
41+
token: ${{ secrets.MORPHO_V2_READ_TOKEN }}
4142

4243
- uses: actions/setup-java@v4
4344
with:

.github/workflows/foundry-sizes.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ jobs:
1717
- uses: actions/checkout@v4
1818
with:
1919
submodules: recursive
20+
token: ${{ secrets.MORPHO_V2_READ_TOKEN }}
2021

2122
- name: Install Foundry
2223
uses: foundry-rs/foundry-toolchain@v1

.github/workflows/foundry.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
- uses: actions/checkout@v4
2121
with:
2222
submodules: recursive
23+
token: ${{ secrets.MORPHO_V2_READ_TOKEN }}
2324

2425
- name: Install Foundry
2526
uses: foundry-rs/foundry-toolchain@v1

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@
1313
[submodule "lib/metamorpho-v1.1"]
1414
path = lib/metamorpho-v1.1
1515
url = git@github.com:morpho-org/metamorpho-v1.1.git
16+
[submodule "lib/morpho-v2"]
17+
path = lib/morpho-v2
18+
url = https://github.com/morpho-org/morpho-v2

foundry.lock

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"lib/forge-std": {
3+
"rev": "77041d2ce690e692d6e03cc812b57d1ddaa4d505"
4+
},
5+
"lib/metamorpho": {
6+
"rev": "00da9ad27da8051bce663eeac02f3b9c0c0aa8d8"
7+
},
8+
"lib/metamorpho-v1.1": {
9+
"rev": "2d160ba9bb945ca3bf12efb182427445dce59c27"
10+
},
11+
"lib/morpho-blue": {
12+
"rev": "d89ca53ff6cbbacf8717a8ce819ee58f49bcc592"
13+
},
14+
"lib/openzeppelin-contracts": {
15+
"rev": "b72e3da0ec1f47e4a7911a4c06dc92e78c646607"
16+
}
17+
}
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
// SPDX-License-Identifier: GPL-2.0-or-later
2+
// Copyright (c) 2025 Morpho Association
3+
pragma solidity ^0.8.0;
4+
5+
import {MorphoV2} from "lib/morpho-v2/src/MorphoV2.sol";
6+
import {Offer, Signature, Obligation, Collateral, Seizure, Proof} from "lib/morpho-v2/src/interfaces/IMorphoV2.sol";
7+
import {IERC20} from "../interfaces/IERC20.sol";
8+
import {SafeERC20Lib} from "../libraries/SafeERC20Lib.sol";
9+
import {MathLib} from "../libraries/MathLib.sol";
10+
import {MathLib as MorphoV2MathLib} from "lib/morpho-v2/src/libraries/MathLib.sol";
11+
import {IVaultV2} from "../interfaces/IVaultV2.sol";
12+
import {IMorphoMarketV2Adapter, ObligationPosition, Maturity, IAdapter} from "./interfaces/IMorphoMarketV2Adapter.sol";
13+
14+
/// @dev Approximates held assets by linearly accounting for interest separately for each obligation.
15+
/// @dev Losses are immdiately accounted minus a discount applied to the remaining interest to be earned, in proportion
16+
/// to the relative sizes of the loss and the adapter's position in the obligation hit by the loss.
17+
/// @dev The adapter must have the allocator role in its parent vault to be able to buy & sell obligations.
18+
contract MorphoMarketV2Adapter is IMorphoMarketV2Adapter {
19+
using MathLib for uint256;
20+
21+
/* IMMUTABLES */
22+
23+
address public immutable asset;
24+
address public immutable parentVault;
25+
address public immutable morphoV2;
26+
27+
/* MANAGEMENT */
28+
29+
address public manager;
30+
address public skimRecipient;
31+
uint256 public minTimeToMaturity;
32+
uint256 public minRate;
33+
34+
/* ACCOUNTING */
35+
36+
uint256 public lastRealAssetsEstimate;
37+
uint48 public lastUpdate;
38+
uint48 public firstMaturity;
39+
uint128 public currentGrowth;
40+
mapping(uint256 timestamp => Maturity) public _maturities;
41+
mapping(bytes32 obligationId => ObligationPosition) public _positions;
42+
/* CONSTRUCTOR */
43+
44+
constructor(address _parentVault, address _morphoV2) {
45+
asset = IVaultV2(_parentVault).asset();
46+
parentVault = _parentVault;
47+
morphoV2 = _morphoV2;
48+
lastUpdate = uint48(block.timestamp);
49+
manager = IVaultV2(parentVault).curator();
50+
SafeERC20Lib.safeApprove(asset, _morphoV2, type(uint256).max);
51+
SafeERC20Lib.safeApprove(asset, _parentVault, type(uint256).max);
52+
firstMaturity = type(uint48).max;
53+
}
54+
55+
/* GETTERS */
56+
57+
function positions(bytes32 obligationId) public view returns (ObligationPosition memory) {
58+
return _positions[obligationId];
59+
}
60+
61+
function maturities(uint256 date) public view returns (Maturity memory) {
62+
return _maturities[date];
63+
}
64+
65+
/* SKIM FUNCTIONS */
66+
67+
function setSkimRecipient(address newSkimRecipient) external {
68+
require(msg.sender == IVaultV2(parentVault).owner(), NotAuthorized());
69+
skimRecipient = newSkimRecipient;
70+
emit SetSkimRecipient(newSkimRecipient);
71+
}
72+
73+
/// @dev Skims the adapter's balance of `token` and sends it to `skimRecipient`.
74+
/// @dev This is useful to handle rewards that the adapter has earned.
75+
function skim(address token) external {
76+
require(msg.sender == skimRecipient, NotAuthorized());
77+
uint256 balance = IERC20(token).balanceOf(address(this));
78+
SafeERC20Lib.safeTransfer(token, skimRecipient, balance);
79+
emit Skim(token, balance);
80+
}
81+
82+
/* MANAGEMENT FUNCTIONS */
83+
84+
function setMinTimeToMaturity(uint256 _minTimeToMaturity) external {
85+
require(msg.sender == manager, NotAuthorized());
86+
require(_minTimeToMaturity <= type(uint48).max, IncorrectMinTimeToMaturity());
87+
minTimeToMaturity = _minTimeToMaturity;
88+
}
89+
90+
function setManager(address _manager) external {
91+
require(msg.sender == manager || msg.sender == IVaultV2(parentVault).curator(), NotAuthorized());
92+
manager = _manager;
93+
}
94+
95+
// Do not cleanup the linked list if we end up at 0 growth
96+
function withdraw(Obligation memory obligation, uint256 obligationUnits, uint256 shares) external {
97+
require(msg.sender == manager, NotAuthorized());
98+
(obligationUnits, shares) = MorphoV2(morphoV2).withdraw(obligation, obligationUnits, shares, address(this));
99+
removeUnits(obligation, obligationUnits);
100+
IVaultV2(parentVault)
101+
.deallocate(address(this), abi.encode(obligationUnits, vaultIds(obligation)), obligationUnits);
102+
}
103+
104+
/* RATIFICATION FUNCTIONS */
105+
106+
function setRatified(
107+
Offer memory offer,
108+
Signature memory signature,
109+
bytes32 root,
110+
bytes32[] memory proof,
111+
bool isRatified
112+
) external {}
113+
114+
/* ACCRUAL */
115+
116+
function accrueInterestView() public view returns (uint48, uint128, uint256) {
117+
uint256 lastChange = lastUpdate;
118+
uint48 nextMaturity = firstMaturity;
119+
uint128 newGrowth = currentGrowth;
120+
uint256 gainedAssets;
121+
122+
while (nextMaturity < block.timestamp) {
123+
gainedAssets += uint256(newGrowth) * (nextMaturity - lastChange);
124+
newGrowth -= _maturities[nextMaturity].growthLostAtMaturity;
125+
lastChange = nextMaturity;
126+
nextMaturity = _maturities[nextMaturity].nextMaturity;
127+
}
128+
129+
gainedAssets += uint256(newGrowth) * (block.timestamp - lastChange);
130+
131+
return (nextMaturity, newGrowth, lastRealAssetsEstimate + gainedAssets);
132+
}
133+
134+
function accrueInterest() public {
135+
if (lastUpdate != block.timestamp) {
136+
(uint48 nextMaturity, uint128 newGrowth, uint256 newTotalAssets) = accrueInterestView();
137+
lastRealAssetsEstimate = newTotalAssets;
138+
lastUpdate = uint48(block.timestamp);
139+
firstMaturity = nextMaturity;
140+
currentGrowth = newGrowth;
141+
}
142+
}
143+
144+
function realAssets() external view returns (uint256) {
145+
(,, uint256 newTotalAssets) = accrueInterestView();
146+
return newTotalAssets;
147+
}
148+
149+
/* LOSS REALIZATION */
150+
151+
function realizeLoss(Obligation memory obligation) external {
152+
bytes32 obligationId = _obligationId(obligation);
153+
uint256 remainingUnits = MorphoV2(morphoV2).sharesOf(address(this), obligationId)
154+
.mulDivDown(
155+
MorphoV2(morphoV2).totalUnits(obligationId) + 1, MorphoV2(morphoV2).totalShares(obligationId) + 1
156+
);
157+
158+
uint256 lostUnits = _positions[obligationId].units - remainingUnits;
159+
removeUnits(obligation, lostUnits);
160+
IVaultV2(parentVault).deallocate(address(this), abi.encode(lostUnits, vaultIds(obligation)), 0);
161+
}
162+
163+
/* ALLOCATION FUNCTIONS */
164+
165+
/// @dev Can only be called from a buy callback where the adapter is the maker.
166+
function allocate(bytes memory data, uint256, bytes4, address vaultAllocator)
167+
external
168+
view
169+
returns (bytes32[] memory, int256)
170+
{
171+
require(vaultAllocator == address(this), SelfAllocationOnly());
172+
(uint256 obligationUnits, bytes32[] memory _ids) = abi.decode(data, (uint256, bytes32[]));
173+
return (_ids, obligationUnits.toInt256());
174+
}
175+
176+
/// @dev Can only be called from vault.deallocate from a sell callback where the adapter is the maker.
177+
/// @dev Can be called from vault.forceDeallocate to trigger a sell take by the adapter.
178+
/// @dev In a forceDeallocate, the user may have to set a buyer price above 1 so that the seller price is at least 1
179+
/// despite the fees.
180+
function deallocate(bytes memory data, uint256 sellerAssets, bytes4 messageSig, address caller)
181+
external
182+
returns (bytes32[] memory, int256)
183+
{
184+
if (messageSig == IVaultV2.forceDeallocate.selector) {
185+
(Offer memory offer, Proof memory proof, Signature memory signature) =
186+
abi.decode(data, (Offer, Proof, Signature));
187+
require(offer.buy && offer.obligation.loanToken == asset, IncorrectOffer());
188+
require(offer.maker == caller, IncorrectOwner());
189+
190+
(,, uint256 obligationUnits,) = MorphoV2(morphoV2)
191+
.take(0, sellerAssets, 0, 0, address(this), offer, proof, signature, address(0), hex"");
192+
193+
require(sellerAssets >= obligationUnits, PriceBelowOne());
194+
require(MorphoV2(morphoV2).debtOf(address(this), _obligationId(offer.obligation)) == 0, NoBorrowing());
195+
196+
removeUnits(offer.obligation, obligationUnits);
197+
return (vaultIds(offer.obligation), -obligationUnits.toInt256());
198+
} else {
199+
require(caller == address(this), SelfAllocationOnly());
200+
(uint256 obligationUnits, bytes32[] memory _ids) = abi.decode(data, (uint256, bytes32[]));
201+
return (_ids, -obligationUnits.toInt256());
202+
}
203+
}
204+
205+
/* MORPHO V2 CALLBACKS */
206+
207+
function onRatify(Offer memory offer, address signer) external view returns (bool) {
208+
// Collaterals will be checked at the level of vault ids.
209+
require(msg.sender == address(morphoV2), NotMorphoV2());
210+
require(offer.obligation.loanToken == asset, LoanAssetMismatch());
211+
require(offer.maker == address(this), IncorrectOwner());
212+
require(offer.callback == address(this), IncorrectCallbackAddress());
213+
require(bytes32(offer.callbackData) != "forceDeallocate", IncorrectCallbackData());
214+
require(offer.obligation.maturity >= minTimeToMaturity + block.timestamp, IncorrectMaturity());
215+
require(offer.start <= block.timestamp, IncorrectStart());
216+
// uint48.max is the list end pointer
217+
require(offer.obligation.maturity < type(uint48).max, IncorrectMaturity());
218+
require(signer == manager, IncorrectSigner());
219+
return true;
220+
}
221+
222+
function onBuy(
223+
Obligation memory obligation,
224+
address buyer,
225+
uint256 buyerAssets,
226+
uint256,
227+
uint256 obligationUnits,
228+
uint256,
229+
bytes memory data
230+
) external {
231+
require(msg.sender == address(morphoV2), NotMorphoV2());
232+
require(buyer == address(this), NotSelf());
233+
bytes32 obligationId = _obligationId(obligation);
234+
uint48 prevMaturity = abi.decode(data, (uint48));
235+
require(prevMaturity < obligation.maturity, IncorrectHint());
236+
237+
accrueInterest();
238+
if (obligation.maturity > block.timestamp) {
239+
uint128 timeToMaturity = uint128(obligation.maturity - block.timestamp);
240+
uint128 gainedGrowth = ((obligationUnits - buyerAssets) / timeToMaturity).toUint128();
241+
lastRealAssetsEstimate += buyerAssets + (obligationUnits - buyerAssets) % timeToMaturity;
242+
_positions[obligationId].growth += gainedGrowth;
243+
_maturities[obligation.maturity].growthLostAtMaturity += gainedGrowth;
244+
currentGrowth += gainedGrowth;
245+
} else {
246+
lastRealAssetsEstimate += obligationUnits;
247+
}
248+
249+
_positions[obligationId].units += obligationUnits.toUint128();
250+
251+
uint48 nextMaturity;
252+
if (prevMaturity == 0) {
253+
nextMaturity = firstMaturity;
254+
} else {
255+
nextMaturity = _maturities[prevMaturity].nextMaturity;
256+
require(nextMaturity != 0, IncorrectHint());
257+
}
258+
259+
while (nextMaturity < obligation.maturity) {
260+
prevMaturity = nextMaturity;
261+
nextMaturity = _maturities[prevMaturity].nextMaturity;
262+
}
263+
264+
if (nextMaturity > obligation.maturity) {
265+
_maturities[obligation.maturity].nextMaturity = nextMaturity;
266+
if (prevMaturity == 0) {
267+
firstMaturity = obligation.maturity.toUint48();
268+
} else {
269+
_maturities[prevMaturity].nextMaturity = obligation.maturity.toUint48();
270+
}
271+
}
272+
273+
IVaultV2(parentVault).allocate(address(this), abi.encode(obligationUnits, vaultIds(obligation)), buyerAssets);
274+
}
275+
276+
function onSell(
277+
Obligation memory obligation,
278+
address seller,
279+
uint256,
280+
uint256 sellerAssets,
281+
uint256 obligationUnits,
282+
uint256,
283+
bytes memory
284+
) external {
285+
require(msg.sender == address(morphoV2), NotMorphoV2());
286+
require(seller == address(this), NotSelf());
287+
require(MorphoV2(morphoV2).debtOf(seller, _obligationId(obligation)) == 0, NoBorrowing());
288+
289+
uint256 vaultRealAssets = IERC20(asset).balanceOf(address(parentVault));
290+
uint256 adaptersLength = IVaultV2(parentVault).adaptersLength();
291+
for (uint256 i = 0; i < adaptersLength; i++) {
292+
vaultRealAssets += IAdapter(IVaultV2(parentVault).adapters(i)).realAssets();
293+
}
294+
uint256 vaultBuffer = vaultRealAssets.zeroFloorSub(IVaultV2(parentVault).totalAssets());
295+
296+
uint256 realAssetsEstimateBefore = lastRealAssetsEstimate;
297+
removeUnits(obligation, obligationUnits);
298+
require(vaultBuffer >= realAssetsEstimateBefore.zeroFloorSub(lastRealAssetsEstimate), BufferTooLow());
299+
300+
IVaultV2(parentVault).deallocate(address(this), abi.encode(obligationUnits, vaultIds(obligation)), sellerAssets);
301+
}
302+
303+
/// INTERNAL FUNCTIONS ///
304+
305+
/// @dev The assets estimate can go up after removing units to compensate for the rounded up lost growth.
306+
function removeUnits(Obligation memory obligation, uint256 removedUnits) internal {
307+
accrueInterest();
308+
bytes32 obligationId = _obligationId(obligation);
309+
if (obligation.maturity > block.timestamp) {
310+
uint256 timeToMaturity = obligation.maturity - block.timestamp;
311+
uint128 removedGrowth = uint256(_positions[obligationId].growth)
312+
.mulDivUp(removedUnits, _positions[obligationId].units).toUint128();
313+
_maturities[obligation.maturity].growthLostAtMaturity -= removedGrowth;
314+
_positions[obligationId].growth -= removedGrowth;
315+
_positions[obligationId].units -= removedUnits.toUint128();
316+
lastRealAssetsEstimate = lastRealAssetsEstimate + (removedGrowth * timeToMaturity) - removedUnits;
317+
} else {
318+
lastRealAssetsEstimate -= removedUnits;
319+
_positions[obligationId].units -= removedUnits.toUint128();
320+
}
321+
}
322+
323+
function _obligationId(Obligation memory obligation) internal pure returns (bytes32) {
324+
return keccak256(abi.encode(obligation));
325+
}
326+
327+
function vaultIds(Obligation memory) internal pure returns (bytes32[] memory) {
328+
// TODO return correct ids
329+
return new bytes32[](0);
330+
}
331+
332+
function onLiquidate(Seizure[] memory, address, address, bytes memory) external pure {
333+
revert();
334+
}
335+
}

0 commit comments

Comments
 (0)