Skip to content

Commit aa059ae

Browse files
committed
feat: fix rebalance calculations
1 parent f76f1f2 commit aa059ae

13 files changed

Lines changed: 547 additions & 224 deletions

contracts/interfaces/IFPMM.sol

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,15 +341,25 @@ interface IFPMM {
341341

342342
/**
343343
* @notice Gets current oracle and reserve prices
344-
* @return oraclePrice Oracle price in 18 decimals
345-
* @return reservePrice Pool reserve price in 18 decimals
346-
* @return _decimals0 Scaling factor for token0
347-
* @return _decimals1 Scaling factor for token1
344+
* @return oraclePriceNumerator The numerator of the oracle price.
345+
* @return oraclePriceDenominator The denominator of the oracle price.
346+
* @return reservePriceNumerator The numerator of the pool reserve price.
347+
* @return reservePriceDenominator The denominator of the pool reserve price.
348+
* @return priceDifference The price difference between the oracle and pool reserve prices in basis points.
349+
* @return reservePriceAboveOraclePrice Whether the pool reserve price is above the oracle price.
350+
* @dev The prices are returned in 18 decimals.
348351
*/
349352
function getPrices()
350353
external
351354
view
352-
returns (uint256 oraclePrice, uint256 reservePrice, uint256 _decimals0, uint256 _decimals1);
355+
returns (
356+
uint256 oraclePriceNumerator,
357+
uint256 oraclePriceDenominator,
358+
uint256 reservePriceNumerator,
359+
uint256 reservePriceDenominator,
360+
uint256 priceDifference,
361+
bool reservePriceAboveOraclePrice
362+
);
353363

354364
/**
355365
* @notice Calculates output amount for a given input

contracts/swap/FPMM.sol

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -225,20 +225,41 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
225225
function getPrices()
226226
public
227227
view
228-
returns (uint256 oraclePrice, uint256 reservePrice, uint256 _decimals0, uint256 _decimals1)
228+
returns (
229+
uint256 oraclePriceNumerator,
230+
uint256 oraclePriceDenominator,
231+
uint256 reservePriceNumerator,
232+
uint256 reservePriceDenominator,
233+
uint256 priceDifference,
234+
bool reservePriceAboveOraclePrice
235+
)
229236
{
230237
FPMMStorage storage $ = _getFPMMStorage();
231238

232239
require($.referenceRateFeedID != address(0), "FPMM: REFERENCE_RATE_NOT_SET");
233240
require($.reserve0 > 0 && $.reserve1 > 0, "FPMM: RESERVES_EMPTY");
234241

235-
_decimals0 = $.decimals0;
236-
_decimals1 = $.decimals1;
237-
238-
(uint256 rateNumerator, uint256 rateDenominator) = _getRateFeed();
239-
240-
oraclePrice = (rateNumerator * 1e18) / (rateDenominator);
241-
reservePrice = ($.reserve1 * _decimals0 * 1e18) / ($.reserve0 * _decimals1);
242+
(oraclePriceNumerator, oraclePriceDenominator) = _getRateFeed();
243+
244+
reservePriceNumerator = $.reserve1 * (1e18 / $.decimals1);
245+
reservePriceDenominator = $.reserve0 * (1e18 / $.decimals0);
246+
247+
uint256 oracleCrossProduct = oraclePriceNumerator * reservePriceDenominator;
248+
uint256 reserveCrossProduct = reservePriceNumerator * oraclePriceDenominator;
249+
reservePriceAboveOraclePrice = reserveCrossProduct > oracleCrossProduct;
250+
251+
uint256 absolutePriceDiff = reservePriceAboveOraclePrice
252+
? reserveCrossProduct - oracleCrossProduct
253+
: oracleCrossProduct - reserveCrossProduct;
254+
priceDifference = (absolutePriceDiff * BASIS_POINTS_DENOMINATOR) / oracleCrossProduct;
255+
return (
256+
oraclePriceNumerator,
257+
oraclePriceDenominator,
258+
reservePriceNumerator,
259+
reservePriceDenominator,
260+
priceDifference,
261+
reservePriceAboveOraclePrice
262+
);
242263
}
243264

244265
/// @inheritdoc IFPMM
@@ -366,7 +387,7 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
366387
swapData.amount1Out = amount1Out;
367388

368389
(swapData.rateNumerator, swapData.rateDenominator) = _getRateFeed();
369-
swapData.initialReserveValue = _totalValueInToken1(
390+
swapData.initialReserveValue = _totalValueInToken1Scaled(
370391
$.reserve0,
371392
$.reserve1,
372393
swapData.rateNumerator,
@@ -417,14 +438,21 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
417438
swapData.amount0Out = amount0Out;
418439
swapData.amount1Out = amount1Out;
419440

420-
(swapData.rateNumerator, swapData.rateDenominator) = _getRateFeed();
421-
swapData.initialReserveValue = _totalValueInToken1(
441+
(
442+
swapData.rateNumerator,
443+
swapData.rateDenominator,
444+
,
445+
,
446+
swapData.initialPriceDifference,
447+
swapData.reservePriceAboveOraclePrice
448+
) = getPrices();
449+
450+
swapData.initialReserveValue = _totalValueInToken1Scaled(
422451
$.reserve0,
423452
$.reserve1,
424453
swapData.rateNumerator,
425454
swapData.rateDenominator
426455
);
427-
(swapData.initialPriceDifference, swapData.reservePriceAboveOraclePrice) = _calculatePriceDifference();
428456

429457
uint256 threshold = swapData.reservePriceAboveOraclePrice ? $.rebalanceThresholdAbove : $.rebalanceThresholdBelow;
430458
require(swapData.initialPriceDifference >= threshold, "FPMM: PRICE_DIFFERENCE_TOO_SMALL");
@@ -446,6 +474,9 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
446474
"FPMM: REBALANCE_DIRECTION_INVALID"
447475
);
448476

477+
swapData.amount0In = amount0In;
478+
swapData.amount1In = amount1In;
479+
449480
_update();
450481

451482
uint256 newPriceDifference = _rebalanceCheck(swapData);
@@ -565,44 +596,31 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
565596
}
566597

567598
/**
568-
* @notice Calculates total value of a given amount of tokens in terms of token1
599+
* @notice Calculates total value of a given amount of tokens in terms of token1 scaled to 18 decimals
569600
* @param amount0 Amount of token0
570601
* @param amount1 Amount of token1
571602
* @param rateNumerator Oracle rate numerator
572603
* @param rateDenominator Oracle rate denominator
573604
* @return Total value in token1
574605
*/
575-
function _totalValueInToken1(
606+
function _totalValueInToken1Scaled(
576607
uint256 amount0,
577608
uint256 amount1,
578609
uint256 rateNumerator,
579610
uint256 rateDenominator
580611
) private view returns (uint256) {
581612
FPMMStorage storage $ = _getFPMMStorage();
582613

583-
uint256 token0ValueInToken1 = convertWithRate(amount0, $.decimals0, $.decimals1, rateNumerator, rateDenominator);
614+
uint256 token0ValueInToken1 = convertWithRate(amount0, $.decimals0, 1e18, rateNumerator, rateDenominator);
615+
amount1 = amount1 * (1e18 / $.decimals1);
584616
return token0ValueInToken1 + amount1;
585617
}
586618

587-
/**
588-
* @notice Calculates price difference between oracle and reserves in basis points
589-
* @return priceDifference Price difference in basis points
590-
*/
591-
function _calculatePriceDifference()
592-
private
593-
view
594-
returns (uint256 priceDifference, bool reservePriceAboveOraclePrice)
595-
{
596-
(uint256 oraclePrice, uint256 reservePrice, , ) = getPrices();
597-
598-
reservePriceAboveOraclePrice = reservePrice > oraclePrice;
599-
uint256 absolutePriceDiff = reservePriceAboveOraclePrice ? reservePrice - oraclePrice : oraclePrice - reservePrice;
600-
priceDifference = (absolutePriceDiff * BASIS_POINTS_DENOMINATOR) / oraclePrice;
601-
}
602-
603619
function _getRateFeed() private view returns (uint256 rateNumerator, uint256 rateDenominator) {
604620
FPMMStorage storage $ = _getFPMMStorage();
605621
(rateNumerator, rateDenominator) = $.sortedOracles.medianRate($.referenceRateFeedID);
622+
rateNumerator = rateNumerator / 1e6;
623+
rateDenominator = rateDenominator / 1e6;
606624
if ($.revertRateFeed) {
607625
(rateNumerator, rateDenominator) = (rateDenominator, rateNumerator);
608626
}
@@ -619,7 +637,7 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
619637
FPMMStorage storage $ = _getFPMMStorage();
620638

621639
bool reservePriceAboveOraclePrice;
622-
(newPriceDifference, reservePriceAboveOraclePrice) = _calculatePriceDifference();
640+
(, , , , newPriceDifference, reservePriceAboveOraclePrice) = getPrices();
623641

624642
// Ensure price difference is smaller than before
625643
require(newPriceDifference < swapData.initialPriceDifference, "FPMM: PRICE_DIFFERENCE_NOT_IMPROVED");
@@ -629,32 +647,29 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
629647
"FPMM: PRICE_DIFFERENCE_MOVED_IN_WRONG_DIRECTION"
630648
);
631649

632-
// Check for excessive value loss
633-
uint256 newReserveValue = _totalValueInToken1(
634-
$.reserve0,
635-
$.reserve1,
636-
swapData.rateNumerator,
637-
swapData.rateDenominator
638-
);
650+
if (swapData.amount0In > 0) {
651+
uint256 expectedAmount0In = convertWithRate(
652+
swapData.amount1Out,
653+
$.decimals1,
654+
1e18,
655+
swapData.rateDenominator,
656+
swapData.rateNumerator
657+
);
658+
uint256 minAmount0In = expectedAmount0In - (expectedAmount0In * $.rebalanceIncentive) / BASIS_POINTS_DENOMINATOR;
639659

640-
uint256 amountOutInToken1;
641-
if (swapData.amount0Out > 0) {
642-
amountOutInToken1 = convertWithRate(
660+
require(swapData.amount0In >= minAmount0In, "FPMM: INSUFFICIENT_AMOUNT_0_IN");
661+
} else {
662+
uint256 expectedAmount1In = convertWithRate(
643663
swapData.amount0Out,
644664
$.decimals0,
645-
$.decimals1,
665+
1e18,
646666
swapData.rateNumerator,
647667
swapData.rateDenominator
648668
);
649-
} else {
650-
amountOutInToken1 = swapData.amount1Out;
669+
expectedAmount1In = expectedAmount1In / (1e18 / $.decimals1);
670+
uint256 minAmount1In = expectedAmount1In - (expectedAmount1In * $.rebalanceIncentive) / BASIS_POINTS_DENOMINATOR;
671+
require(swapData.amount1In >= minAmount1In, "FPMM: INSUFFICIENT_AMOUNT_1_IN");
651672
}
652-
653-
uint256 maxRebalanceIncentiveInToken1 = (amountOutInToken1 * $.rebalanceIncentive) / BASIS_POINTS_DENOMINATOR;
654-
require(
655-
newReserveValue >= swapData.initialReserveValue - maxRebalanceIncentiveInToken1,
656-
"FPMM: EXCESSIVE_VALUE_LOSS"
657-
);
658673
}
659674

660675
/**
@@ -664,13 +679,14 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
664679
function _swapCheck(SwapData memory swapData) private view {
665680
FPMMStorage storage $ = _getFPMMStorage();
666681

667-
uint256 newReserveValue = _totalValueInToken1(
682+
uint256 newReserveValue = _totalValueInToken1Scaled(
668683
$.reserve0,
669684
$.reserve1,
670685
swapData.rateNumerator,
671686
swapData.rateDenominator
672687
);
673688

689+
// TODO: think about rounding here
674690
uint256 expectedAmount0In = convertWithRate(
675691
swapData.amount1Out,
676692
$.decimals1,
@@ -679,6 +695,7 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
679695
swapData.rateNumerator
680696
);
681697

698+
// TODO: think about rounding here
682699
uint256 expectedAmount1In = convertWithRate(
683700
swapData.amount0Out,
684701
$.decimals0,
@@ -702,6 +719,8 @@ contract FPMM is IFPMM, ReentrancyGuardUpgradeable, ERC20Upgradeable, OwnableUpg
702719
swapData.rateDenominator
703720
);
704721
uint256 totalFeeInToken1 = fee0InToken1 + fee1;
722+
// convert to 18 decimals
723+
totalFeeInToken1 = totalFeeInToken1 * (1e18 / $.decimals1);
705724

706725
// Check the reserve value is not decreased
707726
uint256 expectedReserveValue = swapData.initialReserveValue + totalFeeInToken1;

contracts/swap/LiquidityStrategy.sol

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -129,35 +129,40 @@ abstract contract LiquidityStrategy is ILiquidityStrategy, OwnableUpgradeable, R
129129

130130
IFPMM fpmm = IFPMM(pool);
131131
// slither-disable-next-line unused-return
132-
(uint256 oraclePrice, uint256 poolPrice, , ) = fpmm.getPrices();
133-
require(oraclePrice > 0 && poolPrice > 0, "LS: INVALID_PRICES");
132+
(
133+
uint256 oraclePriceNumerator,
134+
uint256 oraclePriceDenominator,
135+
uint256 poolPriceNumerator,
136+
,
137+
uint256 priceDifference,
138+
bool reservePriceAboveOraclePrice
139+
) = fpmm.getPrices();
140+
141+
require(oraclePriceNumerator > 0 && poolPriceNumerator > 0, "LS: INVALID_PRICES");
134142

135143
uint256 upperThresholdBps = fpmm.rebalanceThresholdAbove();
136144
require(upperThresholdBps > 0 && upperThresholdBps <= BPS_SCALE, "LS: INVALID_UPPER_THRESHOLD");
137145

138146
uint256 lowerThresholdBps = fpmm.rebalanceThresholdBelow();
139147
require(lowerThresholdBps > 0 && lowerThresholdBps <= BPS_SCALE, "LS: INVALID_LOWER_THRESHOLD");
140148

141-
uint256 upperBound = (oraclePrice * (BPS_SCALE + upperThresholdBps)) / BPS_SCALE;
142-
uint256 lowerBound = (oraclePrice * (BPS_SCALE - lowerThresholdBps)) / BPS_SCALE;
143-
144149
// slither-disable-next-line uninitialized-local
145150
PriceDirection priceDirection;
146151

147-
if (poolPrice >= upperBound) {
152+
if (upperThresholdBps <= priceDifference && reservePriceAboveOraclePrice) {
148153
priceDirection = PriceDirection.ABOVE_ORACLE;
149-
} else if (poolPrice <= lowerBound) {
154+
} else if (lowerThresholdBps <= priceDifference && !reservePriceAboveOraclePrice) {
150155
priceDirection = PriceDirection.BELOW_ORACLE;
151156
} else {
152157
revert("LS: PRICE_IN_RANGE");
153158
}
154159

155-
_executeRebalance(pool, oraclePrice, priceDirection);
160+
_executeRebalance(pool, oraclePriceNumerator, oraclePriceDenominator, priceDirection);
156161
fpmmPoolConfigs[pool].lastRebalance = block.timestamp;
157162

158163
// slither-disable-next-line unused-return
159-
(, uint256 poolPriceAfterRebalance, , ) = fpmm.getPrices();
160-
emit RebalanceExecuted(pool, poolPrice, poolPriceAfterRebalance);
164+
(, , , , uint256 priceDifferenceAfter, ) = fpmm.getPrices();
165+
emit RebalanceExecuted(pool, priceDifference, priceDifferenceAfter);
161166
}
162167

163168
/* ==================== View Functions ==================== */
@@ -177,8 +182,14 @@ abstract contract LiquidityStrategy is ILiquidityStrategy, OwnableUpgradeable, R
177182
/**
178183
* @notice Contains the strategy-specific logic that executes the rebalancing.
179184
* @param pool The address of the pool to rebalance.
180-
* @param oraclePrice The off‑chain target price.
185+
* @param oraclePriceNumerator The numerator of the target price.
186+
* @param oraclePriceDenominator The denominator of the target price.
181187
* @param priceDirection The direction of the price movement.
182188
*/
183-
function _executeRebalance(address pool, uint256 oraclePrice, PriceDirection priceDirection) internal virtual;
189+
function _executeRebalance(
190+
address pool,
191+
uint256 oraclePriceNumerator,
192+
uint256 oraclePriceDenominator,
193+
PriceDirection priceDirection
194+
) internal virtual;
184195
}

0 commit comments

Comments
 (0)