diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 1e719a4..aaa51af 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -10,6 +10,7 @@ import "@aperture/uni-v3-lib/PoolAddress.sol"; import "@aperture/uni-v3-lib/CallbackValidation.sol"; import "@openzeppelin/token/ERC20/IERC20.sol"; import "@openzeppelin/utils/math/SignedMath.sol"; +import {Math} from "@openzeppelin/utils/math/Math.sol"; import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol"; import "./interfaces/IWETH9.sol"; import {Harberg} from "./Harberg.sol"; @@ -26,8 +27,9 @@ import {Harberg} from "./Harberg.sol"; * @dev Utilizes Uniswap V3's concentrated liquidity feature, enabling highly efficient use of capital. */ contract LiquidityManager { + using Math for uint256; // State variables to track total ETH spent - uint256 public cumulativeVolumeWeightedPrice; + uint256 public cumulativeVolumeWeightedPriceX96; uint256 public cumulativeVolume; // the minimum granularity of liquidity positions in the Uniswap V3 pool. this is a 1% pool. int24 internal constant TICK_SPACING = 200; @@ -154,6 +156,8 @@ contract LiquidityManager { receive() external payable { } + + /// @notice Calculates the Uniswap V3 tick corresponding to a given price ratio between Harberg and ETH. /// @param t0isWeth Boolean flag indicating if token0 is WETH. /// @param tokenAmount Amount of the Harberg token. @@ -161,32 +165,35 @@ contract LiquidityManager { /// @return tick_ The calculated tick for the given price ratio. function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) internal pure returns (int24 tick_) { require(ethAmount > 0, "ETH amount cannot be zero"); - uint160 sqrtPriceX96; if (tokenAmount == 0) { - sqrtPriceX96 = MIN_SQRT_RATIO; + tick_ = t0isWeth ? TickMath.MIN_TICK : TickMath.MAX_TICK; } else { // Use a fixed-point library or more precise arithmetic for the division here. // For example, using ABDKMath64x64 for a more precise division and square root calculation. - int128 priceRatio = ABDKMath64x64.div( + int128 priceRatioX64 = ABDKMath64x64.div( int128(int256(tokenAmount)), int128(int256(ethAmount)) ); - // Convert the price ratio into a sqrt price in the format expected by Uniswap's TickMath. - sqrtPriceX96 = uint160( - int160(ABDKMath64x64.sqrt(priceRatio) << 32) - ); + tick_ = tickAtPriceRatio(t0isWeth, priceRatioX64); } + } + + function tickAtPriceRatio(bool t0isWeth, int128 priceRatioX64) internal pure returns (int24 tick_) { + // Convert the price ratio into a sqrt price in the format expected by Uniswap's TickMath. + uint160 sqrtPriceX96 = uint160( + int160(ABDKMath64x64.sqrt(priceRatioX64) << 32) + ); tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96); tick_ = t0isWeth ? tick_ : -tick_; } - /// @notice Calculates the price ratio from a given Uniswap V3 tick. + /// @notice Calculates the price ratio from a given Uniswap V3 tick as HARB/ETH. /// @param tick The tick for which to calculate the price ratio. - /// @return priceRatio The price ratio corresponding to the given tick. - function tickToPrice(int24 tick) public pure returns (uint256 priceRatio) { - uint160 sqrtRatio = TickMath.getSqrtRatioAtTick(tick); - uint256 adjustedSqrtRatio = uint256(sqrtRatio) / (1 << 48); - priceRatio = adjustedSqrtRatio * adjustedSqrtRatio; + /// @return priceRatioX96 The price ratio corresponding to the given tick. + function priceAtTick(int24 tick) private pure returns (uint256 priceRatioX96) { + //tick = (tick < 0) ? -tick : tick; + uint256 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick); + priceRatioX96 = sqrtRatioX96.mulDiv(sqrtRatioX96, (1 << 96)); } /// @notice Internal function to mint liquidity positions in the Uniswap V3 pool. @@ -221,11 +228,11 @@ contract LiquidityManager { int24 vwapTick; { uint256 outstandingSupply = harb.outstandingSupply(); - uint256 vwap = 0; + uint256 vwapX96 = 0; uint256 requiredEthForBuyback = 0; if (cumulativeVolume > 0) { - vwap = cumulativeVolumeWeightedPrice / cumulativeVolume; - requiredEthForBuyback = outstandingSupply * 10**18 / vwap; + vwapX96 = cumulativeVolumeWeightedPriceX96 / cumulativeVolume; + requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96)); } uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); // leave at least ANCHOR_LIQ_SHARE% of supply for anchor @@ -234,16 +241,17 @@ contract LiquidityManager { // not enough ETH, find a lower price requiredEthForBuyback = floorEthBalance; vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback); - emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwap, capitalInfefficiency, anchorLiquidityShare, vwapTick); - } else if (vwap == 0) { + emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick); + } else if (vwapX96 == 0) { requiredEthForBuyback = floorEthBalance; vwapTick = currentTick; } else { // recalculate vwap with capital inefficiency - vwap = cumulativeVolumeWeightedPrice * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth - vwapTick = tickAtPrice(token0isWeth, token0isWeth ? vwap : 10**18, token0isWeth ? 10**18 : vwap); + vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth + vwapTick = tickAtPriceRatio(token0isWeth, int128(int256 (vwapX96 >> 32))); + vwapTick = token0isWeth ? vwapTick : -vwapTick; - emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwap, capitalInfefficiency, anchorLiquidityShare, vwapTick); + emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick); } // never make floor smaller than anchor if (requiredEthForBuyback < ethBalance * 3 / 4) { @@ -332,23 +340,23 @@ contract LiquidityManager { } } - function _recordVolumeAndPrice(uint256 currentPrice, uint256 fee) internal { + function _recordVolumeAndPrice(uint256 currentPriceX96, uint256 fee) internal { // assuming FEE is 1% uint256 volume = fee * 100; - uint256 volumeWeightedPrice = currentPrice * volume; + uint256 volumeWeightedPriceX96 = currentPriceX96 * volume; // Check for potential overflow. 10**70 is close to 2^256 - if (cumulativeVolumeWeightedPrice > 10**70) { + if (cumulativeVolumeWeightedPriceX96 > 10**70) { uint256 zipFactor = 10**35; uint256 desiredPrecision = 10**5; while (zipFactor * desiredPrecision > cumulativeVolume) { zipFactor /= desiredPrecision; } // Handle overflow: zip historic trade data - cumulativeVolumeWeightedPrice = cumulativeVolumeWeightedPrice / zipFactor; + cumulativeVolumeWeightedPriceX96 = cumulativeVolumeWeightedPriceX96 / zipFactor; // cumulativeVolume should be well higer than zipFactor cumulativeVolume = cumulativeVolume / zipFactor; } - cumulativeVolumeWeightedPrice += volumeWeightedPrice; + cumulativeVolumeWeightedPriceX96 += volumeWeightedPriceX96; cumulativeVolume += volume; } @@ -374,7 +382,7 @@ contract LiquidityManager { if (i == uint256(Stage.ANCHOR)) { // the historic archor position is only an approximation for the price int24 tick = token0isWeth ? -1 * (position.tickLower + ANCHOR_SPACING): position.tickUpper - ANCHOR_SPACING; - currentPrice = tickToPrice(tick); + currentPrice = priceAtTick(tick); } } } diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index dd769a0..56a9094 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -390,23 +390,23 @@ contract LiquidityManagerTest is Test { ); - uint256 cumulativeVolumeWeightedPrice = lm.cumulativeVolumeWeightedPrice(); + uint256 cumulativeVolumeWeightedPriceX96 = lm.cumulativeVolumeWeightedPriceX96(); uint256 beforeCumulativeVolume = lm.cumulativeVolume(); - assertGt(cumulativeVolumeWeightedPrice, type(uint256).max / 2, "Initial cumulativeVolumeWeightedPrice is not near max uint256"); + assertGt(cumulativeVolumeWeightedPriceX96, type(uint256).max / 2, "Initial cumulativeVolumeWeightedPrice is not near max uint256"); buy(50 ether); shift(); - cumulativeVolumeWeightedPrice = lm.cumulativeVolumeWeightedPrice(); + cumulativeVolumeWeightedPriceX96 = lm.cumulativeVolumeWeightedPriceX96(); uint256 cumulativeVolume = lm.cumulativeVolume(); // Assert that the values after wrap-around are valid and smaller than max uint256 assertGt(beforeCumulativeVolume, cumulativeVolume, "cumulativeVolume after wrap-around is smaller than before"); // Assert that the price is reasonable - uint256 calculatedPrice = cumulativeVolumeWeightedPrice / cumulativeVolume; + uint256 calculatedPrice = cumulativeVolumeWeightedPriceX96 / cumulativeVolume; assertTrue(calculatedPrice > 0 && calculatedPrice < 10**40, "Calculated price after wrap-around is not within a reasonable range"); } @@ -520,6 +520,18 @@ contract LiquidityManagerTest is Test { // shift(); + // sell(harberg.balanceOf(account) / 4); + + // slide(true); + + // sell(harberg.balanceOf(account) / 4); + + // slide(true); + + // sell(harberg.balanceOf(account) / 4); + + // slide(true); + // sell(harberg.balanceOf(account)); // slide(true); @@ -529,6 +541,7 @@ contract LiquidityManagerTest is Test { // console.log(traderBalanceBefore); // console.log(traderBalanceAfter); // assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit"); + // revert(); // } function testScenarioFuzz(uint8 numActions, uint8 frequency, uint8[] calldata amounts) public {