Skip to content

Commit 8060825

Browse files
committed
perf: read the position aggregates once per rebalance snapshot
_positionSnapshot returns the aggregate LTV and the net value from a single read of collateralAmountQuoted/debtAmount, replacing the two helpers that each re-read the pair. The post-call check hands the LTV back to the direction checks so they do not read it again, which also keeps valueAfter off the rebalance stack (it would otherwise re-trigger stack-too-deep). Halves the module iterations on the rebalancer path (8 to 4) and drops the runtime size by 169 bytes.
1 parent bb68ca9 commit 8060825

1 file changed

Lines changed: 44 additions & 43 deletions

File tree

‎src/manager/rebalancer/Retargetter.sol‎

Lines changed: 44 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -481,13 +481,13 @@ contract Retargetter is IRetargetter, IFlashLoanReceiver, OwnableRoles, Initiali
481481
}
482482
}
483483

484-
// Bridge value conservation: the snapshot is armed only while the operation's Request
485-
// is unrepaid and short of its deadline; see {_bridgeValueSnapshot}/{_checkBridgeValue}
486-
int256 valueBefore = _bridgeValueSnapshot(positionManager);
487-
488-
// Snapshot the goalposts: target, aggregate LTV and per-module LTVs
484+
// Snapshot the goalposts: target, aggregate LTV, net value and per-module LTVs. Both
485+
// aggregate quantities come from one read of the position, then the value gate is
486+
// disarmed unless the bridge is outstanding, by parking the snapshot at the sentinel
487+
// no live value can exceed; see {_bridgeOutstanding} and the post-call check
489488
(uint256 target,) = IPositionManager(positionManager).config();
490-
uint256 ltvBefore = _positionManagerLtv(positionManager);
489+
(uint256 ltvBefore, int256 valueBefore) = _positionSnapshot(positionManager);
490+
if (!_bridgeOutstanding()) valueBefore = type(int256).max;
491491
address[] memory modules = IPositionManager(positionManager).borrowModules();
492492
uint256 modulesLength = modules.length;
493493
uint256[] memory moduleLtvsBefore = new uint256[](modulesLength);
@@ -502,14 +502,14 @@ contract Retargetter is IRetargetter, IFlashLoanReceiver, OwnableRoles, Initiali
502502
if (resolved.collateral > 0) collateralAsset.safeApprove(positionManager, 0);
503503
if (resolved.debt > 0) debtAsset.safeApprove(positionManager, 0);
504504

505-
_checkBridgeValue(positionManager, valueBefore);
505+
// Value conservation on the way out, then the direction checks read the LTV it returns
506+
uint256 ltvAfter = _checkValueConservation(positionManager, valueBefore);
506507

507508
// Direction checks; owner bypass is evaluated on msg.sender only, never through the
508509
// window (inside a window msg.sender is the flash-loan module). The aggregate must
509510
// strictly improve while above target; a single module only must not worsen, so a
510511
// module the payload never touched cannot block the other legs
511512
if (msg.sender != owner()) {
512-
uint256 ltvAfter = _positionManagerLtv(positionManager);
513513
if (ltvAfter > target && ltvAfter >= ltvBefore) {
514514
revert LibRetargetterErrors.AboveTargetLtv(ltvAfter, ltvBefore, target);
515515
}
@@ -909,55 +909,56 @@ contract Retargetter is IRetargetter, IFlashLoanReceiver, OwnableRoles, Initiali
909909
}
910910
}
911911

912-
/// @dev Arms the rebalance value-conservation gate: returns the position manager's current
913-
/// net value while the operation's Request is unrepaid, and the max sentinel (gate
914-
/// disarmed) otherwise. Once the Request is repaid, or auto-expired past its deadline
915-
/// with holders redeeming its remaining balance, folding residual value back into the
916-
/// position must reopen, which is why the gate keys on the effective repaid state
917-
/// rather than the operation being active. Read through the state-mutating sync (the
918-
/// same one resolve gates on) so a past-deadline Request reads repaid here instead of
919-
/// wedging the gate on a stale flag; that makes this the only non-view step of the
920-
/// rebalance guardrails.
921-
function _bridgeValueSnapshot(address positionManager) internal returns (int256) {
912+
/// @dev Whether the bridge is still outstanding, which is what arms the rebalance
913+
/// value-conservation gate. Once the Request is repaid, or auto-expired past its
914+
/// deadline with holders redeeming its remaining balance, folding residual value back
915+
/// into the position must reopen, which is why this keys on the effective repaid
916+
/// state rather than the operation being active. Read through the state-mutating sync
917+
/// (the same one resolve gates on) so a past-deadline Request reads repaid here
918+
/// instead of wedging the gate on a stale flag; that makes this the only non-view
919+
/// step of the rebalance guardrails.
920+
function _bridgeOutstanding() internal returns (bool) {
922921
address request = LibStorage.operationStorage().request;
923-
if (request == address(0) || IRequest(request).syncRepaidStatus()) return type(int256).max;
924-
return _positionValue(positionManager);
922+
return request != address(0) && !IRequest(request).syncRepaidStatus();
925923
}
926924

927925
/// @dev Value-conservation gate, applied to every caller (unlike the direction checks,
928926
/// this guards Request lenders rather than the steering): with the gate armed (a
929-
/// non-sentinel snapshot), the position manager's net value must not have grown
930-
/// across the rebalance. Growth would be Request capital parked in the position with
931-
/// no shares backing it, exposed to capture by any LP exit before the Request is
932-
/// repaid; pairing each input leg with an equivalent output leg in the same call
933-
/// (SUPPLY with BORROW, REPAY with WITHDRAW) is what keeps the bridge whole. Equality
934-
/// passes, so an operator sizing the output leg must cover any quoting round-up on
935-
/// the input leg; a leg one atom short of neutral is rejected rather than tolerated.
936-
function _checkBridgeValue(address positionManager, int256 valueBefore) internal view {
937-
if (valueBefore == type(int256).max) return;
938-
int256 valueAfter = _positionValue(positionManager);
927+
/// non-sentinel snapshot), the position's net value must not have grown across the
928+
/// rebalance. Growth would be Request capital parked in the position with no shares
929+
/// backing it, exposed to capture by any LP exit before the Request is repaid;
930+
/// pairing each input leg with an equivalent output leg in the same call (SUPPLY with
931+
/// BORROW, REPAY with WITHDRAW) is what keeps the bridge whole. Equality passes, so
932+
/// an operator sizing the output leg must cover any quoting round-up on the input
933+
/// leg; a leg one atom short of neutral is rejected rather than tolerated. Returns
934+
/// the aggregate LTV read in the same pass, so the direction checks do not re-read
935+
/// the position.
936+
function _checkValueConservation(address positionManager, int256 valueBefore)
937+
internal
938+
view
939+
returns (uint256 ltvAfter)
940+
{
941+
int256 valueAfter;
942+
(ltvAfter, valueAfter) = _positionSnapshot(positionManager);
939943
if (valueAfter > valueBefore) {
940944
revert LibRetargetterErrors.PositionValueIncreased(valueBefore, valueAfter);
941945
}
942946
}
943947

944-
/// @dev Net value of the whole position: quoted collateral minus debt summed over every
945-
/// module, negative when the book is underwater. Deliberately not the position
946-
/// manager's totalAssets, which drops any module whose debt exceeds its collateral
947-
/// and so reads flat while value is poured into an underwater module; the gate above
948-
/// must see that value, hence the bad-debt-inclusive aggregates. Both operands are
948+
/// @dev Both aggregate snapshot quantities from one read of the position: the LTV under
949+
/// the convention below, and the net value as quoted collateral minus debt, negative
950+
/// when the book is underwater. The value is deliberately not the position manager's
951+
/// totalAssets, which drops any module whose debt exceeds its collateral and so reads
952+
/// flat while value is poured into an underwater module, and which jumps by a whole
953+
/// module's worth as one wei of debt crosses that inclusion cliff; the gate compares
954+
/// snapshots, so it needs a continuous, bad-debt-inclusive measure. Both operands are
949955
/// sums of token amounts far below the signed range.
950-
function _positionValue(address positionManager) internal view returns (int256) {
956+
function _positionSnapshot(address positionManager) internal view returns (uint256 ltv, int256 value) {
951957
uint256 collateralQuoted = IPositionManager(positionManager).collateralAmountQuoted();
952958
uint256 debt = IPositionManager(positionManager).debtAmount();
959+
ltv = _ltv(debt, collateralQuoted);
953960
// forge-lint: disable-next-line(unsafe-typecast)
954-
return int256(collateralQuoted) - int256(debt);
955-
}
956-
957-
/// @dev Aggregate position manager LTV under the snapshot convention.
958-
function _positionManagerLtv(address positionManager) internal view returns (uint256) {
959-
return
960-
_ltv(IPositionManager(positionManager).debtAmount(), IPositionManager(positionManager).collateralAmountQuoted());
961+
value = int256(collateralQuoted) - int256(debt);
961962
}
962963

963964
/// @dev Single borrow module LTV under the snapshot convention.

0 commit comments

Comments
 (0)