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