Skip to content

Commit fba8d42

Browse files
authored
fix: Retargetter rebalance path and flash-loan callback hardening (#209)
* fix: harden Retargetter rebalance path and flash-loan callback - rebalance: totalAssets must not grow while the operation's Request is unrepaid and short of its deadline (value-conservation gate, all callers) - rebalance: a module above target only reverts when it worsened, so an untouched module no longer blocks the other legs - onFlashLoan: bound to the payload committed at the window open (transient digest) and to full delivery of the principal (pre-loan balance snapshot) * fix: measure bridge value on the whole book and scope window authority to the payload The value-conservation gate read the position manager's totalAssets, which drops any module whose debt exceeds its collateral, so it read flat while bridge capital was poured into an underwater module. It now measures quoted collateral minus debt summed over every module (PositionValueIncreased). Window step authority moved from the whole flash-loan call into the callback around the committed payload, so a module holds no authority in its own frame and cannot source the delivery check's balance growth from the position manager itself. * refactor: read the Request through syncRepaidStatus and store the payload digest as a uint The value gate derived the effective repaid state from the mirrored deadline plus the stale repaid flag; syncRepaidStatus returns exactly that disjunction and folds a past-deadline Request into the flag, the same idiom resolve uses. The payload digest now goes through the existing tStoreUint/tLoadUint, so LibTransientSlot is untouched by this PR. * 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. * docs: point the rebalance NatSpec at the renamed value-gate helpers
1 parent deb7130 commit fba8d42

9 files changed

Lines changed: 604 additions & 60 deletions

File tree

‎src/interfaces/manager/rebalancer/IRetargetter.sol‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,11 @@ interface IRetargetter {
251251
/// one balance snapshot taken before any leg executes and do not compose: every leg
252252
/// resolves against the same pre-call balances, never the state left by earlier legs,
253253
/// so REPAY sentinels across several modules can together commit more than the shared
254-
/// balance and revert.
254+
/// balance and revert. While the operation's Request is unrepaid, the position's net
255+
/// value (quoted collateral minus debt over every module, underwater ones included)
256+
/// must not grow across the call, for every caller including the owner: Request
257+
/// capital may enter the position only against equivalent value flowing back out in
258+
/// the same call.
255259
/// @param data The rebalancing data forwarded to the position manager
256260
function rebalance(RebalancingData calldata data) external;
257261

@@ -276,8 +280,10 @@ interface IRetargetter {
276280

277281
/// @notice Runs a whole retargetting operation atomically inside a flash loan.
278282
/// @dev The steps are supplied as a multicall payload executed inside the flash-loan
279-
/// callback. At window close no order may be stored and the Retargetter's balances
280-
/// of both bound assets must be within the configured residual tolerance.
283+
/// callback. The callback is bound to this exact payload (digest match) and requires
284+
/// the loan delivered in full before any step runs. At window close no order may be
285+
/// stored and the Retargetter's balances of both bound assets must be within the
286+
/// configured residual tolerance.
281287
/// @param positionManager The position manager to retarget (must match the bound pair)
282288
/// @param flashLoanModule The whitelisted flash-loan module to borrow through
283289
/// @param flashLoanAmount The flash-loan size, checked against the principal cap

‎src/libs/manager/rebalancer/LibRetargetterConstants.sol‎

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,13 @@ bytes32 constant OPERATION_STORAGE_SLOT = 0x79211724a6a2d25fad538c732b7f9fee62ff
116116
/* TRANSIENT STORAGE */
117117
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
118118

119-
/// @dev Transient slot for the flash-loan window: holds the module driving the open window,
120-
/// the only address besides the owner and rebalancer role holders that passes the step
121-
/// authorization (zero when no window is open). Distinct from MODULE_TSLOT, which
122-
/// authenticates the callback and is zeroed on callback entry to make it single-shot,
123-
/// while this slot keeps the window's authority until the window closes.
119+
/// @dev Transient slot for the executing payload: holds the module whose committed step
120+
/// calls are running, the only address besides the owner and rebalancer role holders
121+
/// that passes the step authorization (zero otherwise). Set and cleared around the
122+
/// payload inside the callback, so the module holds no authority in its own frame and
123+
/// cannot act on the Retargetter before the delivery check or after the steps end.
124+
/// Distinct from MODULE_TSLOT, which authenticates the callback and is zeroed on
125+
/// callback entry to make it single-shot.
124126
/// Computed as: bytes32(uint256(keccak256("retargetter.transient.window")) - 1)
125127
bytes32 constant WINDOW_TSLOT = 0xd6afb5b9c2dc000141f8a98b95795a2e10b482782ed7428b0e955204ff9c2c83;
126128

@@ -132,6 +134,20 @@ bytes32 constant MODULE_TSLOT = 0x3766ddc68b0c7bb3c206ee7037a0b3dd14384ba84828df
132134
/// Computed as: bytes32(uint256(keccak256("retargetter.transient.amount")) - 1)
133135
bytes32 constant AMOUNT_TSLOT = 0xc37562d71acab34fa73520c0ed6a5ead38685eb8f5e228c3c887ece9e742319b;
134136

137+
/// @dev Transient slot for the keccak256 digest of the flash-loan payload built by
138+
/// startSyncRetargetting (zeroed on callback entry alongside the module slot). The
139+
/// callback requires its incoming data to hash to this digest, so a module can only
140+
/// forward the payload verbatim, never substitute its own step calls.
141+
/// Computed as: bytes32(uint256(keccak256("retargetter.transient.payload")) - 1)
142+
bytes32 constant PAYLOAD_TSLOT = 0x82d191e7de0d5f5912d24aa67206a4049e4b39e437d0e09fea37efbbc2449b0f;
143+
144+
/// @dev Transient slot for the Retargetter's debt-asset balance snapshot taken just before
145+
/// the flash loan is requested. The callback requires the live balance to have grown by
146+
/// the nominal amount, so a module cannot collect the repayment approval for funds it
147+
/// never delivered.
148+
/// Computed as: bytes32(uint256(keccak256("retargetter.transient.balance")) - 1)
149+
bytes32 constant BALANCE_TSLOT = 0x0420a475633bdc4ca97e276f4192fd3de522d15a099866aba16352c41acd46eb;
150+
135151
/// @dev Transient slot for the reentrancy guard flag (nonzero while a guarded call runs).
136152
/// Computed as: bytes32(uint256(keccak256("retargetter.transient.reentrancy")) - 1)
137153
bytes32 constant REENTRANCY_TSLOT = 0x9085860804617f26160a86c0245a6d09a1576cd7c916d029770b32608c92fe70;

‎src/libs/manager/rebalancer/LibRetargetterErrors.sol‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,19 @@ library LibRetargetterErrors {
6868
/// @param target The position manager's target LTV (WAD)
6969
error AboveTargetLtv(uint256 ltvAfter, uint256 ltvBefore, uint256 target);
7070

71-
/// @notice Thrown when a rebalance leaves a single position above target without improving it.
71+
/// @notice Thrown when a rebalance leaves a single position above target while worsening it.
7272
/// @param position The offending borrow module
7373
error PositionAboveTarget(address position);
7474

75+
/// @notice Thrown when a rebalance grows the position manager's net value while the
76+
/// operation's Request is unrepaid and short of its deadline (the growth would be
77+
/// bridge capital left in the position manager with no shares backing it, exposed
78+
/// to capture by any LP exit before the Request is repaid).
79+
/// @param valueBefore The net value snapshot before the rebalance (quoted collateral minus
80+
/// debt, summed over every module including underwater ones, so it may be negative)
81+
/// @param valueAfter The net value after the rebalance, under the same convention
82+
error PositionValueIncreased(int256 valueBefore, int256 valueAfter);
83+
7584
/*´:°•.°+.*•´.*:˚.°*.˚•´.°:°•.°•.*•´.*:˚.°*.˚•´.°:°•.°+.*•´.*:*/
7685
/* ORDERS */
7786
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
@@ -99,9 +108,14 @@ library LibRetargetterErrors {
99108
/* FLASH LOANS */
100109
/*.•°:°.´+˚.*°.˚:*.´•*.+°.•°:´*.´•*.•°.•°:°.´:•˚°.*°.˚:*.´+°.•*/
101110

102-
/// @notice Thrown when onFlashLoan is not called by the expected module with the expected amount.
111+
/// @notice Thrown when onFlashLoan is not called by the expected module with the expected
112+
/// amount and the exact payload committed at the window open.
103113
error UnauthorizedFlashLoanCallback();
104114

115+
/// @notice Thrown when the flash-loan module has not delivered the full nominal amount
116+
/// before the callback payload runs.
117+
error PrincipalNotDelivered();
118+
105119
/// @notice Thrown when the adapter's provider callback is not called by the provider,
106120
/// or no flash loan is in flight.
107121
error UnauthorizedCaller();

0 commit comments

Comments
 (0)