-
Notifications
You must be signed in to change notification settings - Fork 45
POC Borrow and lend callbacks #552
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
f224c90
c23627a
92d19aa
aa6611e
8abe36e
0b9f0c7
5faf553
e787891
40008a9
ee5c8fd
6b5a399
2f95591
5690f7e
bdd585c
3accf71
85ac46a
a24a9d9
55c4758
67c5563
9dc82f1
5be0183
681e3f6
8c1fafd
fc99601
12840e8
fd9d353
b9bf808
de3e296
b707d4a
c9d5cc3
864df15
c297aac
adb8a02
bdf8858
c350113
486aebc
d3d6aa1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| // Copyright (c) 2025 Morpho Association | ||
| pragma solidity 0.8.34; | ||
|
|
||
| import {Market} from "../interfaces/IMidnight.sol"; | ||
| import {Midnight} from "../Midnight.sol"; | ||
| import {ISellCallback} from "../interfaces/ICallbacks.sol"; | ||
| import {CALLBACK_SUCCESS} from "../libraries/ConstantsLib.sol"; | ||
|
|
||
| struct CollateralData { | ||
| uint256 collateralIndex; | ||
| uint256 amount; | ||
| } | ||
|
|
||
| contract BorrowerCallback is ISellCallback { | ||
| error NotMidnight(); | ||
|
|
||
| address public immutable MIDNIGHT; | ||
|
|
||
| constructor(address _midnight) { | ||
| MIDNIGHT = _midnight; | ||
| } | ||
|
|
||
| /// @dev Callback to supply collateral on behalf of borrower. | ||
| /// @dev The callback contract should be authorized to supply collateral on behalf of the borrower. | ||
| function onSell(bytes32, Market memory market, address seller, uint256, uint256, bytes memory data) | ||
| external | ||
| returns (bytes32) | ||
| { | ||
| require(msg.sender == MIDNIGHT, NotMidnight()); | ||
| CollateralData[] memory collateralData = abi.decode(data, (CollateralData[])); | ||
| for (uint256 i = 0; i < collateralData.length; i++) { | ||
| Midnight(MIDNIGHT) | ||
| .supplyCollateral(market, collateralData[i].collateralIndex, collateralData[i].amount, seller); | ||
| } | ||
| return CALLBACK_SUCCESS; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| // Copyright (c) 2025 Morpho Association | ||
| pragma solidity >=0.5.0; | ||
|
|
||
| interface IERC20 { | ||
| function decimals() external view returns (uint8); | ||
| function name() external view returns (string memory); | ||
| function symbol() external view returns (string memory); | ||
| function totalSupply() external view returns (uint256); | ||
| function balanceOf(address account) external view returns (uint256); | ||
| function transfer(address to, uint256 shares) external returns (bool success); | ||
| function transferFrom(address from, address to, uint256 shares) external returns (bool success); | ||
| function approve(address spender, uint256 shares) external returns (bool success); | ||
| function allowance(address owner, address spender) external view returns (uint256); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| // Copyright (c) 2025 Morpho Association | ||
| pragma solidity >=0.5.0; | ||
|
|
||
| import {IERC20} from "./IERC20.sol"; | ||
|
|
||
| interface IERC4626 is IERC20 { | ||
| function asset() external view returns (address); | ||
| function totalAssets() external view returns (uint256); | ||
| function convertToAssets(uint256 shares) external view returns (uint256 assets); | ||
| function convertToShares(uint256 assets) external view returns (uint256 shares); | ||
| function deposit(uint256 assets, address onBehalf) external returns (uint256 shares); | ||
| function mint(uint256 shares, address onBehalf) external returns (uint256 assets); | ||
| function withdraw(uint256 assets, address receiver, address onBehalf) external returns (uint256 shares); | ||
| function redeem(uint256 shares, address receiver, address onBehalf) external returns (uint256 assets); | ||
| function previewDeposit(uint256 assets) external view returns (uint256 shares); | ||
| function previewMint(uint256 shares) external view returns (uint256 assets); | ||
| function previewWithdraw(uint256 assets) external view returns (uint256 shares); | ||
| function previewRedeem(uint256 shares) external view returns (uint256 assets); | ||
| function maxDeposit(address onBehalf) external view returns (uint256 assets); | ||
| function maxMint(address onBehalf) external view returns (uint256 shares); | ||
| function maxWithdraw(address onBehalf) external view returns (uint256 assets); | ||
| function maxRedeem(address onBehalf) external view returns (uint256 shares); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,33 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| // Copyright (c) 2025 Morpho Association | ||
| pragma solidity 0.8.34; | ||
|
|
||
| import {Market} from "../interfaces/IMidnight.sol"; | ||
| import {Midnight} from "../Midnight.sol"; | ||
| import {IBuyCallback} from "../interfaces/ICallbacks.sol"; | ||
| import {IERC20} from "./IERC20.sol"; | ||
| import {CALLBACK_SUCCESS} from "../libraries/ConstantsLib.sol"; | ||
|
|
||
| contract ObligationLenderCallback is IBuyCallback { | ||
| error NotMidnight(); | ||
|
|
||
| address public immutable MIDNIGHT; | ||
|
|
||
| constructor(address _midnight) { | ||
| MIDNIGHT = _midnight; | ||
| } | ||
|
|
||
| /// @dev Callback to withdraw funds from another Midnight obligation. | ||
| /// @dev The callback contract should be authorized to withdraw funds on behalf of the lender. | ||
| function onBuy(bytes32, Market memory market, address buyer, uint256 buyerAssets, uint256, bytes memory data) | ||
| external | ||
| returns (bytes32) | ||
| { | ||
| require(msg.sender == MIDNIGHT, NotMidnight()); | ||
| bytes32 otherMarketId = abi.decode(data, (bytes32)); | ||
| Market memory otherMarket = abi.decode(address(uint160(uint256(otherMarketId))).code, (Market)); | ||
| Midnight(MIDNIGHT).withdraw(otherMarket, buyerAssets, buyer, address(this)); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Useful? React with 👍 / 👎. |
||
| IERC20(market.loanToken).approve(MIDNIGHT, buyerAssets); | ||
| return CALLBACK_SUCCESS; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| // Copyright (c) 2025 Morpho Association | ||
| pragma solidity 0.8.34; | ||
|
|
||
| import {Market} from "../interfaces/IMidnight.sol"; | ||
| import {IERC4626} from "./IERC4626.sol"; | ||
| import {IERC20} from "./IERC20.sol"; | ||
| import {IBuyCallback} from "../interfaces/ICallbacks.sol"; | ||
| import {CALLBACK_SUCCESS} from "../libraries/ConstantsLib.sol"; | ||
|
|
||
| contract VaultLenderCallback is IBuyCallback { | ||
| error NotMidnight(); | ||
|
|
||
| address public immutable MIDNIGHT; | ||
|
|
||
| constructor(address _midnight) { | ||
| MIDNIGHT = _midnight; | ||
| } | ||
|
|
||
| /// @dev Callback to withdraw funds from an ERC4626 vault. | ||
| /// @dev The callback contract should be authorized to withdraw funds on behalf of the lender. | ||
| function onBuy(bytes32, Market memory market, address buyer, uint256 buyerAssets, uint256, bytes memory data) | ||
| external | ||
| returns (bytes32) | ||
| { | ||
| require(msg.sender == MIDNIGHT, NotMidnight()); | ||
| address vault = abi.decode(data, (address)); | ||
| IERC4626(vault).withdraw(buyerAssets, address(this), buyer); | ||
| IERC20(market.loanToken).approve(MIDNIGHT, buyerAssets); | ||
| return CALLBACK_SUCCESS; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,186 @@ | ||
| // SPDX-License-Identifier: GPL-2.0-or-later | ||
| // Copyright (c) 2025 Morpho Association | ||
| pragma solidity ^0.8.0; | ||
|
|
||
| import {Market, Offer, CollateralParams} from "../src/interfaces/IMidnight.sol"; | ||
| import {WAD, LLTV_2} from "../src/libraries/ConstantsLib.sol"; | ||
| import {UtilsLib} from "../src/libraries/UtilsLib.sol"; | ||
| import {TickLib, MAX_TICK} from "../src/libraries/TickLib.sol"; | ||
| import {BorrowerCallback, CollateralData} from "../src/periphery/BorrowerCallback.sol"; | ||
|
|
||
| import {BaseTest} from "./BaseTest.sol"; | ||
| import {ERC20} from "./erc20s/ERC20.sol"; | ||
|
|
||
| contract BorrowerCallbackTest is BaseTest { | ||
| using UtilsLib for uint256; | ||
|
|
||
| BorrowerCallback internal borrowerCallback; | ||
| Market internal obligation; | ||
| bytes32 internal id; | ||
| Offer internal borrowerOffer; | ||
|
|
||
| function setUp() public override { | ||
| super.setUp(); | ||
|
|
||
| borrowerCallback = new BorrowerCallback(address(midnight)); | ||
|
|
||
| obligation.loanToken = address(loanToken); | ||
| obligation.maturity = block.timestamp + 100; | ||
| obligation.collateralParams | ||
| .push( | ||
| CollateralParams({ | ||
| token: address(collateralToken1), | ||
| lltv: LLTV_2, | ||
| maxLif: maxLif(LLTV_2, 0.25e18), | ||
| oracle: address(oracle1) | ||
| }) | ||
| ); | ||
| obligation.collateralParams | ||
| .push( | ||
| CollateralParams({ | ||
| token: address(collateralToken2), | ||
| lltv: LLTV_2, | ||
| maxLif: maxLif(LLTV_2, 0.25e18), | ||
| oracle: address(oracle2) | ||
| }) | ||
| ); | ||
| obligation.collateralParams = sortCollateralParams(obligation.collateralParams); | ||
| obligation.rcfThreshold = 0; | ||
|
|
||
| id = toId(obligation); | ||
|
|
||
| borrowerOffer.buy = false; | ||
| borrowerOffer.maker = borrower; | ||
| borrowerOffer.receiverIfMakerIsSeller = borrower; | ||
| borrowerOffer.maxUnits = type(uint256).max; | ||
| borrowerOffer.market = obligation; | ||
| borrowerOffer.ratifier = address(ecrecoverRatifier); | ||
| borrowerOffer.expiry = block.timestamp + 200; | ||
| borrowerOffer.tick = MAX_TICK; | ||
| } | ||
|
|
||
| function testConstructor() public view { | ||
| assertEq(borrowerCallback.MIDNIGHT(), address(midnight)); | ||
| } | ||
|
|
||
| function testOnSellSingleCollateralMaker(uint256 units) public { | ||
| units = bound(units, 1, 1e33); | ||
| uint256 collateral = units.mulDivUp(WAD, obligation.collateralParams[0].lltv); | ||
|
|
||
| borrowerOffer.callback = address(borrowerCallback); | ||
| CollateralData[] memory collateralData = new CollateralData[](1); | ||
| collateralData[0] = CollateralData({collateralIndex: 0, amount: collateral}); | ||
| borrowerOffer.callbackData = abi.encode(collateralData); | ||
| borrowerOffer.maxUnits = units; | ||
|
|
||
| // Fund lender with loan tokens. | ||
| uint256 price = TickLib.tickToPrice(MAX_TICK); | ||
| deal(address(loanToken), lender, units.mulDivUp(price, WAD)); | ||
|
|
||
| // Fund callback with collateral tokens and approve midnight. | ||
| deal(obligation.collateralParams[0].token, address(borrowerCallback), collateral); | ||
| vm.prank(address(borrowerCallback)); | ||
| ERC20(obligation.collateralParams[0].token).approve(address(midnight), collateral); | ||
|
|
||
| // Authorize callback to supply collateral on behalf of borrower. | ||
| vm.prank(borrower); | ||
| midnight.setIsAuthorized(borrower, address(borrowerCallback), true); | ||
|
|
||
| assertEq(midnight.collateral(id, borrower, 0), 0); | ||
|
|
||
| take(units, lender, borrowerOffer); | ||
|
|
||
| assertEq(midnight.collateral(id, borrower, 0), collateral); | ||
| } | ||
|
|
||
| function testOnSellMultipleCollateralsMaker(uint256 units) public { | ||
| units = bound(units, 1, 1e33); | ||
| uint256 collateral0 = units.mulDivUp(WAD, obligation.collateralParams[0].lltv); | ||
| uint256 collateral1 = units.mulDivUp(WAD, obligation.collateralParams[1].lltv); | ||
|
|
||
| borrowerOffer.callback = address(borrowerCallback); | ||
| CollateralData[] memory collateralData = new CollateralData[](2); | ||
| collateralData[0] = CollateralData({collateralIndex: 0, amount: collateral0}); | ||
| collateralData[1] = CollateralData({collateralIndex: 1, amount: collateral1}); | ||
| borrowerOffer.callbackData = abi.encode(collateralData); | ||
| borrowerOffer.maxUnits = units; | ||
|
|
||
| // Fund lender with loan tokens. | ||
| uint256 price = TickLib.tickToPrice(MAX_TICK); | ||
| deal(address(loanToken), lender, units.mulDivUp(price, WAD)); | ||
|
|
||
| // Fund callback with collateral tokens and approve midnight. | ||
| deal(obligation.collateralParams[0].token, address(borrowerCallback), collateral0); | ||
| deal(obligation.collateralParams[1].token, address(borrowerCallback), collateral1); | ||
| vm.prank(address(borrowerCallback)); | ||
| ERC20(obligation.collateralParams[0].token).approve(address(midnight), collateral0); | ||
| vm.prank(address(borrowerCallback)); | ||
| ERC20(obligation.collateralParams[1].token).approve(address(midnight), collateral1); | ||
|
|
||
| // Authorize callback to supply collateral on behalf of borrower. | ||
| vm.prank(borrower); | ||
| midnight.setIsAuthorized(borrower, address(borrowerCallback), true); | ||
|
|
||
| assertEq(midnight.collateral(id, borrower, 0), 0); | ||
| assertEq(midnight.collateral(id, borrower, 1), 0); | ||
|
|
||
| take(units, lender, borrowerOffer); | ||
|
|
||
| assertEq(midnight.collateral(id, borrower, 0), collateral0); | ||
| assertEq(midnight.collateral(id, borrower, 1), collateral1); | ||
| } | ||
|
|
||
| function testOnSellTaker(uint256 units) public { | ||
| units = bound(units, 1, 1e33); | ||
| uint256 collateral = units.mulDivUp(WAD, obligation.collateralParams[0].lltv); | ||
|
|
||
| // Lender makes a buy offer. | ||
| Offer memory lenderOffer; | ||
| lenderOffer.buy = true; | ||
| lenderOffer.maker = lender; | ||
| lenderOffer.maxUnits = units; | ||
| lenderOffer.market = obligation; | ||
| lenderOffer.ratifier = address(ecrecoverRatifier); | ||
| lenderOffer.expiry = block.timestamp + 200; | ||
| lenderOffer.tick = MAX_TICK; | ||
|
|
||
| // Fund lender with loan tokens. | ||
| uint256 price = TickLib.tickToPrice(MAX_TICK); | ||
| deal(address(loanToken), lender, units.mulDivDown(price, WAD)); | ||
|
|
||
| // Fund callback with collateral tokens and approve midnight. | ||
| deal(obligation.collateralParams[0].token, address(borrowerCallback), collateral); | ||
| vm.prank(address(borrowerCallback)); | ||
| ERC20(obligation.collateralParams[0].token).approve(address(midnight), collateral); | ||
|
|
||
| // Authorize callback to supply collateral on behalf of borrower. | ||
| vm.prank(borrower); | ||
| midnight.setIsAuthorized(borrower, address(borrowerCallback), true); | ||
|
|
||
| CollateralData[] memory collateralData = new CollateralData[](1); | ||
| collateralData[0] = CollateralData({collateralIndex: 0, amount: collateral}); | ||
|
|
||
| assertEq(midnight.collateral(id, borrower, 0), 0); | ||
|
|
||
| // Borrower takes the lender's buy offer, passing BorrowerCallback as taker callback. | ||
| vm.prank(borrower); | ||
| midnight.take( | ||
| units, | ||
| borrower, | ||
| address(borrowerCallback), | ||
| abi.encode(collateralData), | ||
| borrower, | ||
| lenderOffer, | ||
| merkleRatifierData([lenderOffer]) | ||
| ); | ||
|
|
||
| assertEq(midnight.collateral(id, borrower, 0), collateral); | ||
| } | ||
|
|
||
| function testOnSellUnauthorized() public { | ||
| Market memory ob; | ||
| vm.prank(makeAddr("attacker")); | ||
| vm.expectRevert(BorrowerCallback.NotMidnight.selector); | ||
| borrowerCallback.onSell(bytes32(0), ob, address(0), 0, 0, ""); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BorrowerCallback.onSellcallsMidnight.supplyCollateralfrom the callback address, but this contract never grantsMIDNIGHTan allowance for collateral tokens and exposes no function to do so. On-chain, a regular ERC20 collateral therefore reverts on first use whensupplyCollateralreachessafeTransferFromwithallowance(callback, MIDNIGHT) == 0. The new tests pass only because they impersonate the callback address withvm.prank(...)to callapprove, which is not possible in production.Useful? React with 👍 / 👎.