Skip to content

Commit dc12967

Browse files
committed
Simplify Midas withdrawal helper to explicit amounts
1 parent 880879b commit dc12967

2 files changed

Lines changed: 31 additions & 99 deletions

File tree

‎src/request/scripts/SyncMidasWithdrawal.sol‎

Lines changed: 22 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,60 +2,38 @@
22
pragma solidity ^0.8.24;
33

44
import {IFacilityFunds} from "../../interfaces/facility/base/IFacilityFunds.sol";
5-
import {IFacilityIntents} from "../../interfaces/facility/base/IFacilityIntents.sol";
65
import {IFacilityPositionManager} from "../../interfaces/facility/base/IFacilityPositionManager.sol";
7-
import {IPositionManager} from "../../interfaces/manager/IPositionManager.sol";
86
import {WithdrawalStrategy} from "../../interfaces/manager/base/IPositionManagerAdmin.sol";
9-
import {Asset, IntentProperties} from "../../libs/facility/LibIntent.sol";
10-
import {FixedPointMathLib} from "lib/solady/src/utils/FixedPointMathLib.sol";
117

128
/// @title SyncMidasWithdrawal
13-
/// @notice Stateless script designed to be delegatecalled from MorphoFlashLoanRequest after a
14-
/// bonded Midas redeem has been unlocked for instant settlement.
15-
/// @dev Must NOT have any storage variables. The redeem order must already exist and its first
16-
/// commit must already have paid the bond. Repays enough live debt to restore the target LTV,
17-
/// withdraws the remaining redemption collateral, then executes second commit -> unlock;
18-
/// MorphoFlashLoanRequest supplies and repays the debt asset around this call.
9+
/// @notice Stateless script designed to be delegatecalled from MorphoFlashLoanRequest.
10+
/// Executes the remaining Midas redemption flow: withdrawManager → commit → unlock.
11+
/// @dev Must NOT have any storage variables. The redeem order must already exist, its bond
12+
/// leg must be complete, and instant redemption must be unlocked before execute().
1913
/// @author 3F Protocol
2014
contract SyncMidasWithdrawal {
21-
using FixedPointMathLib for uint256;
22-
23-
uint256 private constant _WAD = 1e18;
24-
25-
error InvalidPositionManager();
26-
error InvalidTargetLtv();
27-
error InvalidWithdrawAmount();
28-
error TargetLtvExceeded();
29-
30-
/// @notice Restores the target LTV, settles the remaining Midas redeem, and unlocks its proceeds.
15+
/// @notice Settles the remaining Midas redemption for a facility intent.
3116
/// @param facility The facility contract address.
3217
/// @param intentId The intent ID on the facility.
3318
/// @param withdrawAmount The remaining collateral required by the Midas redemption.
34-
/// @param targetLtv The maximum LTV after the withdrawal, in WAD precision.
35-
/// @param useTarget Whether the intent's target asset is the position manager.
36-
function run(address facility, uint256 intentId, uint256 withdrawAmount, uint256 targetLtv, bool useTarget) external {
37-
if (targetLtv >= _WAD) revert InvalidTargetLtv();
38-
39-
(IntentProperties memory properties,,,) = IFacilityIntents(facility).getIntent(intentId);
40-
Asset memory managerAsset = useTarget ? properties.targetAsset : properties.depositAsset;
41-
if (!managerAsset.isPositionManager) revert InvalidPositionManager();
42-
43-
IPositionManager manager = IPositionManager(managerAsset.asset);
44-
uint256 collateral = manager.collateralAmount();
45-
if (withdrawAmount == 0 || withdrawAmount > collateral) revert InvalidWithdrawAmount();
46-
47-
uint256 quotedCollateral = manager.collateralAmountQuoted();
48-
uint256 quotedWithdrawal = quotedCollateral.mulDivUp(withdrawAmount, collateral);
49-
uint256 targetDebt = (quotedCollateral - quotedWithdrawal).mulDiv(targetLtv, _WAD);
50-
uint256 liveDebt = manager.debtAmount();
51-
uint256 repayAmount = liveDebt > targetDebt ? liveDebt - targetDebt : 0;
52-
53-
IFacilityPositionManager(facility)
54-
.withdrawManager(intentId, withdrawAmount, repayAmount, useTarget, WithdrawalStrategy.PROPORTIONAL);
55-
// A partial withdrawal queue can remove more quoted value than the aggregate estimate.
56-
if (manager.debtAmount() > manager.collateralAmountQuoted().mulDiv(targetLtv, _WAD)) revert TargetLtvExceeded();
57-
19+
/// @param repayAmount The amount of debt to repay to the position manager.
20+
/// @param useTarget Whether to use the target asset for the position manager withdrawal.
21+
/// @param strategy The withdrawal strategy (SEQUENTIAL or PROPORTIONAL).
22+
function run(
23+
address facility,
24+
uint256 intentId,
25+
uint256 withdrawAmount,
26+
uint256 repayAmount,
27+
bool useTarget,
28+
WithdrawalStrategy strategy
29+
) external {
30+
// 1. Withdraw collateral from position manager (repay debt, get collateral back)
31+
IFacilityPositionManager(facility).withdrawManager(intentId, withdrawAmount, repayAmount, useTarget, strategy);
32+
33+
// 2. Commit the remaining redemption collateral
5834
IFacilityFunds(facility).commit(intentId);
35+
36+
// 3. Unlock the redemption proceeds
5937
IFacilityFunds(facility).unlock(intentId);
6038
}
6139
}

‎test/request/MorphoFlashLoanRequest.t.sol‎

Lines changed: 9 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {MockMorphoAllocator} from "test/mock/request/MockMorphoAllocator.sol";
1313
import {Offer} from "src/interfaces/request/IOfferReceiver.sol";
1414
import {LibCommonErrors} from "src/libs/common/LibCommonErrors.sol";
1515
import {MockERC20} from "test/mock/MockERC20.sol";
16-
import {MockBorrowPosition} from "test/mock/borrow/MockBorrowPosition.sol";
1716
import {Mode, State} from "src/libs/funds/Order.sol";
1817
import {IntentProperties, Asset} from "src/libs/facility/LibIntent.sol";
1918
import {WithdrawalStrategy} from "src/interfaces/manager/base/IPositionManagerAdmin.sol";
@@ -946,15 +945,14 @@ contract MorphoFlashLoanRequestTest is FacilityBaseTest {
946945
assertEq(collateralToken.balanceOf(address(facility)), 0);
947946
}
948947

949-
function test_execute_syncMidasWithdrawalCommitUnlock() public {
948+
function testFuzz_execute_syncMidasWithdrawalCommitUnlock(bool proportional) public {
950949
uint256 morphoBalBefore = debtToken.balanceOf(address(morpho));
951950
vm.prank(facilitator);
952951
facility.depositManager(intentId, 0, 5_000e18, false);
953952

954-
// A price drop makes the live LTV exceed the requested 50% target.
955-
oracle.setPrice(0.9e36);
956953
uint256 withdrawAmount = 1_000e18;
957-
uint256 repayAmount = 950e18; // 5,000 - (10,000 - 1,000) * 0.9 * 0.5
954+
uint256 repayAmount = 500e18;
955+
WithdrawalStrategy strategy = proportional ? WithdrawalStrategy.PROPORTIONAL : WithdrawalStrategy.SEQUENTIAL;
958956
vm.prank(facilitator);
959957
_setFund(intentId, address(mockFund));
960958
// MockFund represents the remaining redemption at the script's facility boundary.
@@ -969,67 +967,23 @@ contract MorphoFlashLoanRequestTest is FacilityBaseTest {
969967
repayAmount,
970968
_buildSetRequestParams(intentId, block.timestamp + 1 hours),
971969
address(syncMidasWithdrawal),
972-
abi.encodeCall(SyncMidasWithdrawal.run, (address(facility), intentId, withdrawAmount, 0.5e18, false))
970+
abi.encodeCall(
971+
SyncMidasWithdrawal.run, (address(facility), intentId, withdrawAmount, repayAmount, false, strategy)
972+
)
973973
);
974974

975975
assertEq(positionManager.collateralAmount(), 9_000e18);
976-
assertEq(positionManager.debtAmount(), 4_050e18);
977-
assertEq(positionManager.debtAmount() * 1e18 / positionManager.collateralAmountQuoted(), 0.5e18);
976+
assertEq(positionManager.debtAmount(), 4_500e18);
978977
assertEq(collateralToken.balanceOf(address(mockFund)), withdrawAmount);
979978
assertEq(collateralToken.balanceOf(address(facility)), 0);
980-
assertEq(debtToken.balanceOf(address(facility)), 5_050e18);
981-
assertEq(debtToken.balanceOf(address(morpho)), morphoBalBefore - 4_050e18);
979+
assertEq(debtToken.balanceOf(address(facility)), 5_500e18);
980+
assertEq(debtToken.balanceOf(address(morpho)), morphoBalBefore - 4_500e18);
982981
assertEq(debtToken.balanceOf(address(flashLoanRequest)), 0);
983982
(, address fund, address request,) = facility.getIntent(intentId);
984983
assertEq(fund, address(0));
985984
assertEq(request, address(0));
986985
}
987986

988-
function test_syncMidasWithdrawal_rejectsInvalidParameters() public {
989-
vm.expectRevert(SyncMidasWithdrawal.InvalidTargetLtv.selector);
990-
syncMidasWithdrawal.run(address(facility), intentId, 1, 1e18, false);
991-
vm.expectRevert(SyncMidasWithdrawal.InvalidPositionManager.selector);
992-
syncMidasWithdrawal.run(address(facility), intentId, 1, 0.5e18, true);
993-
vm.expectRevert(SyncMidasWithdrawal.InvalidWithdrawAmount.selector);
994-
syncMidasWithdrawal.run(address(facility), intentId, 0, 0.5e18, false);
995-
uint256 tooMuch = positionManager.collateralAmount() + 1;
996-
vm.expectRevert(SyncMidasWithdrawal.InvalidWithdrawAmount.selector);
997-
syncMidasWithdrawal.run(address(facility), intentId, tooMuch, 0.5e18, false);
998-
}
999-
1000-
function test_syncMidasWithdrawal_revertsWhenPartialQueueRemovesMoreQuotedCollateral() public {
1001-
vm.prank(facilitator);
1002-
facility.depositManager(intentId, 0, 4_500e18, false);
1003-
1004-
// This lower-priced module contributes to the aggregate quote but is excluded from withdrawals.
1005-
MockBorrowPosition excluded = new MockBorrowPosition(address(collateralToken), address(debtToken));
1006-
excluded.setTotalCollateral(10_000e18);
1007-
excluded.setTotalCollateralQuoted(5_000e18);
1008-
excluded.setSafeLtv(uint128(PM_LTV));
1009-
vm.mockCall(address(excluded), abi.encodeWithSignature("owner()"), abi.encode(address(positionManager)));
1010-
vm.prank(owner);
1011-
positionManager.addBorrowModule(address(excluded));
1012-
1013-
uint256 withdrawAmount = 1_000e18;
1014-
uint256 repayAmount = 225e18; // Aggregate quote estimates 750 removed; the queued position removes 1,000.
1015-
vm.prank(facilitator);
1016-
_setFund(intentId, address(mockFund));
1017-
mockFund.setNextCommitState(State.UNLOCKING);
1018-
mockFund.setUnlockAmount(withdrawAmount);
1019-
debtToken.setBalance(address(mockFund), withdrawAmount);
1020-
vm.prank(facilitator);
1021-
facility.create(intentId, withdrawAmount, withdrawAmount, Mode.REDEEM);
1022-
1023-
vm.prank(executor);
1024-
vm.expectRevert(SyncMidasWithdrawal.TargetLtvExceeded.selector);
1025-
flashLoanRequest.execute(
1026-
repayAmount,
1027-
_buildSetRequestParams(intentId, block.timestamp + 1 hours),
1028-
address(syncMidasWithdrawal),
1029-
abi.encodeCall(SyncMidasWithdrawal.run, (address(facility), intentId, withdrawAmount, 0.3e18, false))
1030-
);
1031-
}
1032-
1033987
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
1034988
/* I. pullFunds() TESTS */
1035989
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/

0 commit comments

Comments
 (0)