Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions src/MorphoAllocator.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ contract MorphoAllocator is OwnableRoles, Initializable {

/// @notice Emitted when `run` (unlock + deallocate + allocate + depositManager) succeeds.
/// @param intentId The intent ID.
/// @param unlocked The amount of collateral credited to the intent by `unlock`.
/// @param unlocked The amount of collateral credited to the intent by `unlock`, deposited in
/// full via `Facility.depositManager`.
/// @param allocateAdapter The destination adapter as passed to `run`; address(0) when no allocation
/// was requested. Allocation is also skipped when `gatheredTotal` is 0.
/// @param gatheredTotal The total gathered across all deallocation sources. Allocated into
Expand Down Expand Up @@ -215,23 +216,25 @@ contract MorphoAllocator is OwnableRoles, Initializable {
/// market (validating post-withdrawal utilisation) → `MorphoVaultV2.allocate` of the gathered
/// total (skipped when `allocateAdapter == address(0)`) → `Facility.depositManager`. Any revert
/// (slippage, utilisation, unexpected state) reverts the whole call so it can be retried.
/// `depositAmount` is the caller's choice and need not equal the measured `unlocked` amount.
/// The measured `unlocked` amount is deposited in full, so the intent is left with no
/// collateral dust regardless of unlock slippage or fund price movement.
/// @param intentId The intent ID.
/// @param deallocations Sources to gather liquidity from (markets and/or idle); see {Deallocation}.
/// @param allocateAdapter Destination Morpho V1 Market adapter, or address(0) to skip allocation.
/// @param allocateMarket Destination market identifier (ignored when `allocateAdapter == address(0)`).
/// @param depositAmount Collateral amount to deposit via `Facility.depositManager`. Independent
/// of the unlocked amount, so the caller may deposit less than was unlocked.
/// @param minAmountReceived Minimum amount that must be unlocked. Occupies the slot that previously
/// fixed the deposit amount, keeping the signature unchanged for callers.
/// @param borrowAmount Amount to borrow via `Facility.depositManager`.
/// @param useTarget True to use the intent's target asset as the PositionManager,
/// false to use the deposit asset.
/// @param minSharesUnlocked Minimum amount that must be unlocked (slippage guard on unlock).
/// @param minSharesUnlocked Minimum amount that must be unlocked (slippage guard on unlock); the
/// stricter of it and `minAmountReceived` applies.
function run(
uint256 intentId,
Deallocation[] calldata deallocations,
address allocateAdapter,
MarketParams calldata allocateMarket,
uint256 depositAmount,
uint256 minAmountReceived,
uint256 borrowAmount,
bool useTarget,
uint256 minSharesUnlocked
Expand All @@ -257,11 +260,12 @@ contract MorphoAllocator is OwnableRoles, Initializable {
revert UnlockBalanceDecreased(intentId, balanceBefore, balanceAfter);
}
uint256 unlocked = balanceAfter - balanceBefore;
if (unlocked < minSharesUnlocked) revert SlippageExceeded(minSharesUnlocked, unlocked);
uint256 minUnlocked = minAmountReceived > minSharesUnlocked ? minAmountReceived : minSharesUnlocked;
if (unlocked < minUnlocked) revert SlippageExceeded(minUnlocked, unlocked);

uint256 gatheredTotal = _rebalance($.morphoVault, deallocations, allocateAdapter, allocateMarket);

_facility.depositManager(intentId, depositAmount, borrowAmount, useTarget);
_facility.depositManager(intentId, unlocked, borrowAmount, useTarget);

emit Allocated(intentId, unlocked, allocateAdapter, gatheredTotal, borrowAmount);
}
Expand Down
23 changes: 19 additions & 4 deletions test/MorphoAllocator.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -448,19 +448,32 @@ contract MorphoAllocatorTest is Test {
assertTrue(dUseTarget);
}

function test_run_depositAmountIndependentOfUnlocked() public {
function test_run_depositsUnlockedAmount() public {
_configureIntentWithTargetPm();
facility.setUnlockMint(collateralToken, 1_000e6); // full unlock = 1_000e6

// Deposit only 600e6 of the 1_000e6 unlocked; the event still reports the full unlocked amount.
// minAmountReceived (600e6) is below the 1_000e6 unlocked; the full unlocked amount is
// deposited, leaving no collateral dust on the intent.
vm.expectEmit(true, false, false, true, address(allocator));
emit Allocated(INTENT_ID, 1_000e6, allocAdapter, 500e6, 700e6);

vm.prank(executor);
allocator.run(INTENT_ID, _deals(500e6, WAD), allocAdapter, targetMarket, 600e6, 700e6, true, 0);

(, uint256 dDeposit,,) = facility.lastDepositManager();
assertEq(dDeposit, 600e6, "depositManager uses the provided amount, not unlocked");
assertEq(dDeposit, 1_000e6, "depositManager uses the measured unlocked amount");
}

function test_run_revertsWhenUnlockedBelowMinAmountReceived() public {
_configureIntentWithTargetPm();
facility.setUnlockMint(collateralToken, 1_000e6);

// minAmountReceived (1_100e6) is stricter than minSharesUnlocked (900e6) and above the
// 1_000e6 actually unlocked, so the unlock slippage guard trips.
vm.expectRevert(abi.encodeWithSelector(MorphoAllocator.SlippageExceeded.selector, 1_100e6, 1_000e6));
vm.prank(executor);
allocator.run(INTENT_ID, _deals(500e6, WAD), allocAdapter, targetMarket, 1_100e6, 700e6, true, 900e6);
assertEq(facility.depositManagerCount(), 0, "depositManager not called");
}

function test_run_happyPath_useDeposit_noRebalance() public {
Expand Down Expand Up @@ -582,7 +595,7 @@ contract MorphoAllocatorTest is Test {

vm.expectRevert(abi.encodeWithSelector(MorphoAllocator.SlippageExceeded.selector, 900e6, 800e6));
vm.prank(executor);
allocator.run(INTENT_ID, _deals(500e6, WAD), allocAdapter, targetMarket, 950e6, 700e6, true, 900e6);
allocator.run(INTENT_ID, _deals(500e6, WAD), allocAdapter, targetMarket, 0, 700e6, true, 900e6);

assertEq(vault.deallocateCount(), 0, "rebalance not reached");
assertEq(vault.allocateCount(), 0, "allocate not called");
Expand Down Expand Up @@ -680,6 +693,8 @@ contract MorphoAllocatorTest is Test {
vm.prank(executor);
allocator.run(INTENT_ID, _noDeals(), address(0), targetMarket, actual, 0, true, minOut);
assertEq(facility.depositManagerCount(), 1, "deposit on success");
(, uint256 dDeposit,,) = facility.lastDepositManager();
assertEq(dDeposit, actual, "deposit equals the unlocked amount");
}
}
}
Loading