Skip to content

Commit 4499ac2

Browse files
authored
Merge pull request #227 from lista-dao/feat/v3-rebalance-center-rate-guard
feat(provider): assert expectedCenterRate on rebalance
2 parents 15380a5 + 20752c7 commit 4499ac2

10 files changed

Lines changed: 180 additions & 59 deletions

File tree

.github/workflows/upgrade-safety.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ jobs:
3838
- name: Install upgrade-guard
3939
run: |
4040
cargo install --git https://github.com/razww/upgrade-guard \
41-
--tag v0.1.1 upgrade-guard --locked
41+
--tag v0.1.2 upgrade-guard --locked
4242
4343
# Faster alternative: skip the Rust build, download the prebuilt binary.
4444
# - run: |
45-
# gh release download v0.1.1 --repo razww/upgrade-guard \
45+
# gh release download v0.1.2 --repo razww/upgrade-guard \
4646
# --pattern 'upgrade-guard-x86_64-linux*' --dir /tmp/ug
4747
# ( cd /tmp/ug && sha256sum -c upgrade-guard-x86_64-linux.sha256 )
4848
# install -m0755 /tmp/ug/upgrade-guard-x86_64-linux /usr/local/bin/upgrade-guard

src/provider/interfaces/IV3DexAdapter.sol

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ interface IV3DexAdapter {
6969
/// @notice Current pool spot price (slot0).
7070
function spotSqrtPriceX96() external view returns (uint160);
7171

72+
/// @notice Live LST↔native center rate the next rebalance will use (0 for pure-TWAP pairs).
73+
function centerRate() external view returns (uint256);
74+
7275
/// @notice Simulate adding `amount0Desired/amount1Desired` at the current spot price.
7376
function previewAddLiquidity(
7477
uint256 amount0Desired,
@@ -147,13 +150,22 @@ interface IV3DexAdapter {
147150
/// @notice Set the spot-vs-fair deviation gate (onlyRole MANAGER; bps of price, 0 disables).
148151
function setMaxSpotDeviationBps(uint256 maxSpotDeviationBps) external;
149152

150-
/// @notice Recenter the position to its range and convert inventory to the optimal ratio.
151-
/// onlyProvider — the provider gates the caller with the BOT role.
153+
/// @notice Max |live center rate − BOT-supplied expectedCenterRate| deviation (bps) tolerated on
154+
/// rebalance (0 disables the guard).
155+
function maxCenterRateDeviationBps() external view returns (uint256);
156+
157+
/// @notice Set the center-rate deviation gate (onlyRole MANAGER; bps, 0 disables).
158+
function setMaxCenterRateDeviationBps(uint256 maxCenterRateDeviationBps) external;
159+
160+
/// @notice Recenter the position and convert inventory to the optimal ratio. onlyProvider (the provider
161+
/// gates on BOT). `expectedCenterRate` = the live rate the BOT built against (0 = skip); reverts
162+
/// if it deviates from the on-chain rate by more than `maxCenterRateDeviationBps`.
152163
function rebalance(
153164
uint256 minAmount0,
154165
uint256 minAmount1,
155166
uint256 minLiquidity,
156167
uint160 targetSqrtPriceX96,
168+
uint256 expectedCenterRate,
157169
uint256 deadline,
158170
bytes calldata swapData
159171
) external;

src/provider/v3/SlisBNBV3Provider.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ contract SlisBNBV3Provider is V3Provider {
6060
uint256 minAmount1,
6161
uint256 minLiquidity,
6262
uint160 targetSqrtPriceX96,
63+
uint256 expectedCenterRate,
6364
uint256 deadline,
6465
bytes calldata swapData
6566
) external onlyRole(BOT) nonReentrant {
66-
_guardedRebalance(minAmount0, minAmount1, minLiquidity, targetSqrtPriceX96, deadline, swapData);
67+
_guardedRebalance(minAmount0, minAmount1, minLiquidity, targetSqrtPriceX96, expectedCenterRate, deadline, swapData);
6768
}
6869

6970
/* ─────────────────── slisBNBx: sync / view ──────────────────────── */

src/provider/v3/V3DexAdapter.sol

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,12 @@ abstract contract V3DexAdapter is
118118
/// that vector. 0 disables the gate. Default INITIAL_RANGE_BPS (1%).
119119
uint256 public maxSpotDeviationBps;
120120

121+
/// @dev Max |live center rate − BOT expectedCenterRate| deviation on rebalance (BPS; 0 = off). Guards
122+
/// the range anchor against a build↔exec rate anomaly the fair-NAV loss caps can't see.
123+
uint256 public maxCenterRateDeviationBps;
124+
121125
/// @dev Reserved storage for future base variables (keep subclass storage stable on upgrade).
122-
uint256[45] private __gap;
126+
uint256[44] private __gap;
123127

124128
/* ───────────────────────────── events ───────────────────────────── */
125129

@@ -133,6 +137,7 @@ abstract contract V3DexAdapter is
133137
event SwapPairWhitelistSet(address indexed swapPair, bool status);
134138
event MaxSwapLossBpChanged(uint256 maxSwapLossBp);
135139
event MaxSpotDeviationBpsChanged(uint256 maxSpotDeviationBps);
140+
event MaxCenterRateDeviationBpsChanged(uint256 maxCenterRateDeviationBps);
136141
event CompoundSkippedSpotDeviated(uint160 spotSqrtPriceX96, uint160 fairSqrtPriceX96);
137142
event CompoundSkippedNoLiquidity(uint256 idleToken0, uint256 idleToken1);
138143
event IdleCredited(uint256 amount0, uint256 amount1);
@@ -161,6 +166,7 @@ abstract contract V3DexAdapter is
161166
error InsufficientBalance();
162167
error InsufficientAmount();
163168
error SpotDeviationTooHigh();
169+
error CenterRateDeviationTooHigh();
164170
error ZeroAmount();
165171

166172
/* ─────────────────────────── constructor ────────────────────────── */
@@ -383,6 +389,14 @@ abstract contract V3DexAdapter is
383389
emit MaxSpotDeviationBpsChanged(_maxSpotDeviationBps);
384390
}
385391

392+
/// @notice Max relative deviation (BPS) allowed between the live center rate and the BOT-supplied
393+
/// expectedCenterRate on rebalance. 0 disables the guard (default).
394+
function setMaxCenterRateDeviationBps(uint256 _maxCenterRateDeviationBps) external onlyRole(MANAGER) {
395+
if (_maxCenterRateDeviationBps > BPS) revert InvalidThreshold();
396+
maxCenterRateDeviationBps = _maxCenterRateDeviationBps;
397+
emit MaxCenterRateDeviationBpsChanged(_maxCenterRateDeviationBps);
398+
}
399+
386400
/// @notice Whitelist (or remove) a swap venue the rebalance inventory conversion may call. Backend-built
387401
/// calldata can only target whitelisted venues.
388402
/// @dev Defense-in-depth: a swap venue must never be a token / pool / NPM the adapter holds or trusts,
@@ -410,6 +424,7 @@ abstract contract V3DexAdapter is
410424
uint256 minAmount1,
411425
uint256 minLiquidity,
412426
uint160 targetSqrtPriceX96,
427+
uint256 expectedCenterRate,
413428
uint256 deadline,
414429
bytes calldata swapData
415430
) external onlyProvider nonReentrant {
@@ -421,6 +436,14 @@ abstract contract V3DexAdapter is
421436
bool rateImplied = centerRate != 0;
422437
if (rateImplied) _requireCenterRateDeviation(centerRate);
423438

439+
// Assert the live rate still matches the BOT's expectedCenterRate; 0 keeps prior behavior.
440+
if (rateImplied && expectedCenterRate != 0 && maxCenterRateDeviationBps != 0) {
441+
uint256 delta = centerRate > expectedCenterRate
442+
? centerRate - expectedCenterRate
443+
: expectedCenterRate - centerRate;
444+
if ((delta * BPS) / expectedCenterRate > maxCenterRateDeviationBps) revert CenterRateDeviationTooHigh();
445+
}
446+
424447
(int24 newTickLower, int24 newTickUpper) = _initialTickRange(centerRate);
425448
int24 oldTickLower = tickLower;
426449
int24 oldTickUpper = tickUpper;
@@ -639,6 +662,11 @@ abstract contract V3DexAdapter is
639662
(sqrtPriceX96, ) = IV3PoolMinimal(POOL).slot0();
640663
}
641664

665+
/// @inheritdoc IV3DexAdapter
666+
function centerRate() external view returns (uint256) {
667+
return _lstNativeRate();
668+
}
669+
642670
/// @dev True when the pool spot price is within `maxSpotDeviationBps` (bps of price) of the fair price.
643671
/// Prices are compared in Q96 space (price = sqrtPriceX96^2 / 2^96) via FullMath, overflow-safe.
644672
/// `maxSpotDeviationBps == 0` disables the gate. `fair` is rate-anchored (flash-loan-immune for the

src/provider/v3/V3Provider.sol

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,13 +705,22 @@ abstract contract V3Provider is
705705
uint256 minAmount1,
706706
uint256 minLiquidity,
707707
uint160 targetSqrtPriceX96,
708+
uint256 expectedCenterRate,
708709
uint256 deadline,
709710
bytes calldata swapData
710711
) internal {
711712
uint256 navBefore = _positionValueUsd();
712713
if (totalSupply() != 0 && navBefore == 0) revert OracleZero();
713714

714-
IV3DexAdapter(ADAPTER).rebalance(minAmount0, minAmount1, minLiquidity, targetSqrtPriceX96, deadline, swapData);
715+
IV3DexAdapter(ADAPTER).rebalance(
716+
minAmount0,
717+
minAmount1,
718+
minLiquidity,
719+
targetSqrtPriceX96,
720+
expectedCenterRate,
721+
deadline,
722+
swapData
723+
);
715724

716725
uint256 navAfter = _positionValueUsd();
717726

src/provider/v3/WbETHV3Provider.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,10 @@ contract WbETHV3Provider is V3Provider {
3636
uint256 minAmount1,
3737
uint256 minLiquidity,
3838
uint160 targetSqrtPriceX96,
39+
uint256 expectedCenterRate,
3940
uint256 deadline,
4041
bytes calldata swapData
4142
) external onlyRole(BOT) nonReentrant {
42-
_guardedRebalance(minAmount0, minAmount1, minLiquidity, targetSqrtPriceX96, deadline, swapData);
43+
_guardedRebalance(minAmount0, minAmount1, minLiquidity, targetSqrtPriceX96, expectedCenterRate, deadline, swapData);
4344
}
4445
}

src/provider/v3/WstETHV3Provider.sol

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ contract WstETHV3Provider is V3Provider {
3535
uint256 minAmount1,
3636
uint256 minLiquidity,
3737
uint160 targetSqrtPriceX96,
38+
uint256 expectedCenterRate,
3839
uint256 deadline,
3940
bytes calldata swapData
4041
) external onlyRole(BOT) nonReentrant {
41-
_guardedRebalance(minAmount0, minAmount1, minLiquidity, targetSqrtPriceX96, deadline, swapData);
42+
_guardedRebalance(minAmount0, minAmount1, minLiquidity, targetSqrtPriceX96, expectedCenterRate, deadline, swapData);
4243
}
4344
}

0 commit comments

Comments
 (0)