From 16e65f0f15a4eaaf3364a0c0a3133e3ff198b9f5 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Thu, 15 Aug 2024 15:17:44 +0200 Subject: [PATCH] take harb minted for staking into account when setting floor --- onchain/src/LiquidityManager.sol | 170 +++++++++++++++------------- onchain/test/LiquidityManager.t.sol | 14 +-- 2 files changed, 96 insertions(+), 88 deletions(-) diff --git a/onchain/src/LiquidityManager.sol b/onchain/src/LiquidityManager.sol index 0df1b22..bd69b5c 100644 --- a/onchain/src/LiquidityManager.sol +++ b/onchain/src/LiquidityManager.sol @@ -36,15 +36,12 @@ contract LiquidityManager { // defines the width of the anchor position from the current price to discovery position. int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING; // DISCOVERY_SPACING determines the range above the current price where new tokens are minted and sold. - // This spacing is much wider, allowing the contract to place liquidity far from the current market price, - // aiming to capture potential price increases and support token issuance within strategic market bounds. + // 11000 ticks represent 3x the current price int24 internal constant DISCOVERY_SPACING = 11000; // how much more liquidity per tick discovery is holding over anchor - uint128 internal constant DISCOVERY_DEPTH = 450; // 500 // 500% - int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that? + uint128 internal constant DISCOVERY_DEPTH = 200; // 500 // 500% // only working with UNI V3 1% fee tier pools uint24 internal constant FEE = uint24(10_000); - uint160 internal constant MIN_SQRT_RATIO = 4295128739; // ANCHOR_LIQ_SHARE is the mininum share of total ETH in control // that will be left to put into anchor positon. uint256 internal constant MIN_ANCHOR_LIQ_SHARE = 5; // 5 = 5% @@ -55,6 +52,7 @@ contract LiquidityManager { uint256 internal constant MAX_CAPITAL_INEFFICIENCY = 200; // used to double-check price with uni oracle uint32 internal constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes in seconds + int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that? // the address of the Uniswap V3 factory address private immutable factory; @@ -63,6 +61,7 @@ contract LiquidityManager { IUniswapV3Pool private immutable pool; bool private immutable token0isWeth; PoolKey private poolKey; + uint256 private harbPulled; // temporary variable to store amount of harb pulled by Uni // the 3 positions this contract is managing enum Stage { FLOOR, ANCHOR, DISCOVERY } @@ -117,7 +116,8 @@ contract LiquidityManager { function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); // take care of harb - harb.mint(token0isWeth ? amount1Owed : amount0Owed); + harbPulled = token0isWeth ? amount1Owed : amount0Owed; + harb.mint(harbPulled); // pack ETH uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed; if (weth.balanceOf(address(this)) < ethOwed) { @@ -227,70 +227,25 @@ contract LiquidityManager { /// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements. function _set(int24 currentTick) internal { - // set Floor position + // estimate the lower tick of the anchor int24 vwapTick; - { - uint256 outstandingSupply = harb.outstandingSupply(); - uint256 vwapX96 = 0; - uint256 requiredEthForBuyback = 0; - if (cumulativeVolume > 0) { - 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 - uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100; - if (floorEthBalance < requiredEthForBuyback) { - // not enough ETH, find a lower price - requiredEthForBuyback = floorEthBalance; - vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback); - emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick); - } else if (vwapX96 == 0) { - requiredEthForBuyback = floorEthBalance; - vwapTick = currentTick; - } else { - // recalculate vwap with capital inefficiency - vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth - // ETH/HARB tick - vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32))); - // convert to pool tick - vwapTick = token0isWeth ? -vwapTick : vwapTick; - emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick); - } - // never make floor smaller than anchor - if (requiredEthForBuyback < ethBalance * 3 / 4) { - // use 3/4 instead of 1/2 to also account for liquidity of harb in anchor - requiredEthForBuyback = ethBalance * 3 / 4; - } - // move floor below anchor, if needed - if (token0isWeth) { - vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; - } else { - vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick; - } + uint256 outstandingSupply = harb.outstandingSupply(); + uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); + uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100; + if (outstandingSupply > 0) { + vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , floorEthBalance); + } else { + vwapTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING; + } - // normalize tick position for pool - vwapTick = vwapTick / TICK_SPACING * TICK_SPACING; - // calculate liquidity - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick); - int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING; - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick); - uint128 liquidity; - if (token0isWeth) { - liquidity = LiquidityAmounts.getLiquidityForAmount0( - sqrtRatioAX96, sqrtRatioBX96, requiredEthForBuyback - ); - } else { - liquidity = LiquidityAmounts.getLiquidityForAmount1( - sqrtRatioAX96, sqrtRatioBX96, requiredEthForBuyback - ); - } - _mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); + // move vwapTick below currentTick, if needed + if (token0isWeth) { + vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; + } else { + vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick; } // set Anchor position - uint128 anchorLiquidity; - uint24 anchorWidth; { int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick; int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING; @@ -300,21 +255,22 @@ contract LiquidityManager { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - // adding a 2% balance margin, because liquidity calculations are inherently unprecise - uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))) * 98 / 100; + + uint256 anchorEthBalance = ethBalance - floorEthBalance; + uint128 anchorLiquidity; if (token0isWeth) { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( - sqrtRatioX96, sqrtRatioBX96, ethBalance + sqrtRatioX96, sqrtRatioBX96, anchorEthBalance ); } else { anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( - sqrtRatioAX96, sqrtRatioX96, ethBalance + sqrtRatioAX96, sqrtRatioX96, anchorEthBalance ); } - anchorWidth = uint24(tickUpper - tickLower); _mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity); } + harb.burn(harb.balanceOf(address(this))); currentTick = currentTick / TICK_SPACING * TICK_SPACING; // set Discovery position @@ -323,25 +279,77 @@ contract LiquidityManager { int24 tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + DISCOVERY_SPACING + ANCHOR_SPACING; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - // discovery with x times as much liquidity per tick as anchor - uint128 liquidity = anchorLiquidity * uint128(uint24(DISCOVERY_SPACING)) * DISCOVERY_DEPTH / 100 / anchorWidth; - uint256 harbInDiscovery; + + uint256 discoveryAmount = harbPulled * uint24(DISCOVERY_SPACING) * uint24(DISCOVERY_DEPTH) / uint24(ANCHOR_SPACING) / 100; + uint128 liquidity; if (token0isWeth) { - harbInDiscovery = LiquidityAmounts.getAmount0ForLiquidity( - sqrtRatioAX96, - sqrtRatioBX96, - liquidity + liquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioBX96, discoveryAmount ); } else { - harbInDiscovery = LiquidityAmounts.getAmount1ForLiquidity( - sqrtRatioAX96, - sqrtRatioBX96, - liquidity + liquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioAX96, sqrtRatioBX96, discoveryAmount ); } _mint(Stage.DISCOVERY, tickLower, tickUpper, liquidity); harb.burn(harb.balanceOf(address(this))); } + + // set Floor position + { + outstandingSupply = harb.outstandingSupply(); + uint256 vwapX96 = 0; + uint256 requiredEthForBuyback = 0; + if (cumulativeVolume > 0) { + vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth + requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96)); + } + // make a new calculation of the vwapTick, having updated outstandingSupply + if (floorEthBalance < requiredEthForBuyback) { + // not enough ETH, find a lower price + requiredEthForBuyback = floorEthBalance; + vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback); + emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick); + } else if (vwapX96 == 0) { + requiredEthForBuyback = floorEthBalance; + vwapTick = currentTick; + } else { + // ETH/HARB tick + vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32))); + // convert to pool tick + vwapTick = token0isWeth ? -vwapTick : vwapTick; + emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick); + } + // move floor below anchor, if needed + if (token0isWeth) { + vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick; + } else { + vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick; + } + + // normalize tick position for pool + vwapTick = vwapTick / TICK_SPACING * TICK_SPACING; + // calculate liquidity + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick); + int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING; + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick); + + // adding a 2% balance margin, because liquidity calculations are inherently unprecise + floorEthBalance = floorEthBalance * 98 / 100; + + uint128 liquidity; + if (token0isWeth) { + liquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioAX96, sqrtRatioBX96, floorEthBalance + ); + } else { + liquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioBX96, floorEthBalance + ); + } + _mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); + } + } function _recordVolumeAndPrice(uint256 currentPriceX96, uint256 fee) internal { diff --git a/onchain/test/LiquidityManager.t.sol b/onchain/test/LiquidityManager.t.sol index e649ecc..650691b 100644 --- a/onchain/test/LiquidityManager.t.sol +++ b/onchain/test/LiquidityManager.t.sol @@ -398,7 +398,7 @@ contract LiquidityManagerTest is Test { assertGt(cumulativeVolumeWeightedPriceX96, type(uint256).max / 2, "Initial cumulativeVolumeWeightedPrice is not near max uint256"); - buy(50 ether); + buy(25 ether); shift(); @@ -498,28 +498,28 @@ contract LiquidityManagerTest is Test { // function testScenarioB() public { // setUpCustomToken0(false); - // vm.deal(account, 201 ether); + // vm.deal(account, 501 ether); // vm.prank(account); - // weth.deposit{value: 201 ether}(); + // weth.deposit{value: 501 ether}(); // uint256 traderBalanceBefore = weth.balanceOf(account); // // Setup initial liquidity // slide(false); - // buy(50 ether); + // buy(25 ether); // shift(); - // buy(50 ether); + // buy(45 ether); // shift(); - // buy(50 ether); + // buy(80 ether); // shift(); - // buy(50 ether); + // buy(120 ether); // shift();