|
| 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