From 92fd80d5ce4e566e516b60f15cc04530e7c243e4 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Sat, 6 Jul 2024 18:36:13 +0200 Subject: [PATCH] vwap --- onchain/src/BaseLineLP.sol | 221 ++++++++++++++++++--------------- onchain/test/BaseLineLP2.t.sol | 158 ++++++++++++++--------- 2 files changed, 219 insertions(+), 160 deletions(-) diff --git a/onchain/src/BaseLineLP.sol b/onchain/src/BaseLineLP.sol index f6f3598..e9f57cd 100644 --- a/onchain/src/BaseLineLP.sol +++ b/onchain/src/BaseLineLP.sol @@ -36,6 +36,7 @@ contract BaseLineLP { enum Stage { FLOOR, ANCHOR, DISCOVERY } + uint256 constant CAPITAL_INEFFICIENCY = 120; uint256 constant LIQUIDITY_RATIO_DIVISOR = 100; // the address of the Uniswap V3 factory @@ -58,6 +59,11 @@ contract BaseLineLP { uint256 private lastDay; uint256 private mintedToday; + // State variables to track total ETH spent + uint256 public cumulativeVolumeWeightedPrice; + uint256 public cumulativeVolume; + + mapping(Stage => TokenPosition) public positions; address private feeDestination; @@ -118,7 +124,9 @@ contract BaseLineLP { } function outstanding() public view returns (uint256 _outstanding) { - _outstanding = harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this)); + //_outstanding = (harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this))); + // This introduces capital inefficiency, but haven't found another way yet to protect capital from whale attacks + _outstanding = (harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this))) * CAPITAL_INEFFICIENCY / 100; } function spendingLimit() public view returns (uint256, uint256) { @@ -213,82 +221,97 @@ contract BaseLineLP { }); } - /// @dev Returns if amount is within daily limit and resets spentToday after one day. - /// @param amount Amount to withdraw. - /// @return Returns if amount is under daily limit. - function availableMint(uint256 amount) internal returns (uint256) { - if (block.timestamp > lastDay + 24 hours) { - lastDay = block.timestamp; - mintedToday = 0; - } - uint256 mintLimit = harb.totalSupply() * 3 / 20; - if (mintedToday + amount > mintLimit) { - return mintLimit - mintedToday; - } - return amount; + + // Calculate current VWAP + function calculateVWAP() public view returns (uint256) { + if (cumulativeVolume == 0) return 0; + return cumulativeVolumeWeightedPrice / cumulativeVolume; } - function _set(uint160 sqrtPriceX96, int24 currentTick, uint256 ethInNewAnchor) internal { - // ### set Anchor position - uint128 anchorLiquidity; - { - int24 tickLower = currentTick - ANCHOR_SPACING; - int24 tickUpper = currentTick + ANCHOR_SPACING; - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); - if (token0isWeth) { - anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( - sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor - ); - } else { - anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( - sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor - ); - } - // TODO: calculate liquidity correctly - // or make sure that we don't have to pay more than we have - tickLower = tickLower / TICK_SPACING * TICK_SPACING; - tickUpper = tickUpper / TICK_SPACING * TICK_SPACING; - _mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity * 2); - } - currentTick = currentTick / TICK_SPACING * TICK_SPACING; + function _set(uint160 sqrtPriceX96, int24 currentTick) internal { // ### set Floor position + int24 vwapTick; { - int24 startTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING; - - // all remaining eth will be put into this position - uint256 ethInFloor = address(this).balance + weth.balanceOf(address(this)); - int24 floorTick; - // calculate price at which all HARB can be bought back - uint256 _outstanding = outstanding(); - if (_outstanding > 0) { - floorTick = tickAtPrice(_outstanding, ethInFloor); - // put a position symetrically around the price, startTick being edge on one side - floorTick = token0isWeth ? startTick + (floorTick - startTick) : floorTick - (startTick - floorTick); - - bool isOvercollateralized = token0isWeth ? floorTick < startTick : floorTick > startTick; - if (isOvercollateralized) { - floorTick = startTick + ((token0isWeth ? int24(1) : int24(-1)) * 400); - } - } else { - floorTick = startTick + ((token0isWeth ? int24(1) : int24(-1)) * 400); + uint256 outstandingSupply = outstanding(); + uint256 vwap = 0; + uint256 requiredEthForBuyback = 0; + if (cumulativeVolume > 0) { + vwap = cumulativeVolumeWeightedPrice / cumulativeVolume; + requiredEthForBuyback = outstandingSupply / vwap * 10**18; } + uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); + // leave at least 5% of supply for anchor + ethBalance = ethBalance * 90 / 100; + if (ethBalance < requiredEthForBuyback) { + // not enough ETH, find a lower price + requiredEthForBuyback = ethBalance; + // put the price 5% lower than needed + vwapTick = tickAtPrice(outstandingSupply, requiredEthForBuyback); + } else if (vwap == 0) { + requiredEthForBuyback = ethBalance; + vwapTick = currentTick; + } else { + // put the price 5% lower than needed + vwapTick = tickAtPrice(cumulativeVolumeWeightedPrice / 10**18, cumulativeVolume); + if (requiredEthForBuyback < ethBalance) { + // invest a majority of the ETH still in floor, even though not needed + requiredEthForBuyback = (requiredEthForBuyback + (5 * ethBalance)) / 6; + } + } + + // 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; + + int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING; + // calculate liquidity - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(startTick); + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick); uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts( sqrtPriceX96, sqrtRatioAX96, sqrtRatioBX96, - token0isWeth ? ethInFloor : 0, - token0isWeth ? 0 : ethInFloor + token0isWeth ? requiredEthForBuyback : 0, + token0isWeth ? 0 : requiredEthForBuyback ); // mint - _mint(Stage.FLOOR, token0isWeth ? startTick : floorTick, token0isWeth ? floorTick : startTick, liquidity); + _mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity); } + // ### set Anchor position + uint128 anchorLiquidity; + { + int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick; + int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING; + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); + uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this))); + if (token0isWeth) { + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioAX96, sqrtRatioBX96, ethBalance + ); + } else { + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioBX96, ethBalance + ); + } + tickLower = tickLower / TICK_SPACING * TICK_SPACING; + tickUpper = tickUpper / TICK_SPACING * TICK_SPACING; + _mint(Stage.ANCHOR, tickLower, tickUpper, anchorLiquidity); + } + currentTick = currentTick / TICK_SPACING * TICK_SPACING; + + + // ## set Discovery position { int24 tickLower = token0isWeth ? currentTick - DISCOVERY_SPACING - ANCHOR_SPACING : currentTick + ANCHOR_SPACING; @@ -320,9 +343,20 @@ contract BaseLineLP { } } - function _scrape() internal returns (uint256 ethInAnchor) { + function tickToPrice(int24 tick) public pure returns (uint256) { + uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(tick); + // Convert the sqrt price to price using fixed point arithmetic + // sqrtPriceX96 is a Q64.96 format (96 fractional bits) + // price = (sqrtPriceX96 ** 2) / 2**192 + // To avoid overflow, perform the division by 2**96 first before squaring + uint256 price = uint256(sqrtPriceX96) / (1 << 48); // Reducing the scale before squaring + return price * price; +} + + function _scrape() internal { uint256 fee0 = 0; uint256 fee1 = 0; + uint256 currentPrice; for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) { TokenPosition storage position = positions[Stage(i)]; if (position.liquidity > 0) { @@ -339,20 +373,38 @@ contract BaseLineLP { fee0 += collected0 - amount0; fee1 += collected1 - amount1; if (i == uint256(Stage.ANCHOR)) { - ethInAnchor = token0isWeth ? amount0 : amount1; + int24 priceTick = position.tickLower + (position.tickUpper - position.tickLower); + currentPrice = tickToPrice(priceTick); } } } + // Transfer fees to the fee destination + // and record transaction totals if (fee0 > 0) { - IERC20(token0isWeth ? address(weth): address(harb)).transfer(feeDestination, fee0); + if (token0isWeth) { + IERC20(address(weth)).transfer(feeDestination, fee0); + uint256 volume = fee0 * 100; + uint256 volumeWeightedPrice = currentPrice * volume; + cumulativeVolumeWeightedPrice += volumeWeightedPrice; + cumulativeVolume += volume; + } else { + IERC20(address(harb)).transfer(feeDestination, fee0); + } } if (fee1 > 0) { - IERC20(token0isWeth ? address(harb): address(weth)).transfer(feeDestination, fee1); + if (token0isWeth) { + IERC20(address(harb)).transfer(feeDestination, fee1); + } else { + IERC20(address(weth)).transfer(feeDestination, fee1); + uint256 volume = fee1 * 100; + uint256 volumeWeightedPrice = currentPrice * volume; + cumulativeVolumeWeightedPrice += volumeWeightedPrice; + cumulativeVolume += volume; + } } } - function _isPriceStable(int24 currentTick) internal view returns (bool) { uint32 timeInterval = 300; // 5 minutes in seconds uint32[] memory secondsAgo = new uint32[](2); @@ -394,21 +446,9 @@ contract BaseLineLP { } // ## scrape positions - uint256 ethInAnchor = _scrape(); - - // ## set new positions - // reduce Anchor by 10% of new ETH. It will be moved into Floor - (uint256 initialEthInAnchor,) = tokensIn(Stage.ANCHOR); - ethInAnchor -= (ethInAnchor - initialEthInAnchor) * 10 / LIQUIDITY_RATIO_DIVISOR; - - - // cap anchor size at 10 % of total ETH - uint256 ethBalance = address(this).balance + weth.balanceOf(address(this)); - ethInAnchor = (ethInAnchor > ethBalance / 10) ? ethBalance / 10 : ethInAnchor; - - currentTick = currentTick / TICK_SPACING * TICK_SPACING; + _scrape(); harb.setPreviousTotalSupply(harb.totalSupply()); - _set(sqrtPriceX96, currentTick, ethInAnchor); + _set(sqrtPriceX96, currentTick); } function slide() external { @@ -437,26 +477,7 @@ contract BaseLineLP { } _scrape(); - - uint256 ethBalance = address(this).balance + weth.balanceOf(address(this)); - if (ethBalance == 0) { - // TODO: set only discovery - return; - } - (uint256 ethInAnchor,) = tokensIn(Stage.ANCHOR); - (uint256 ethInFloor,) = tokensIn(Stage.FLOOR); - - // use previous ration of Floor to Anchor - uint256 ethInNewAnchor = ethBalance / 10; - if (ethInFloor > 0) { - ethInNewAnchor = ethBalance * ethInAnchor / (ethInAnchor + ethInFloor); - } - - // but cap anchor size at 10 % of total ETH - ethInNewAnchor = (ethInNewAnchor > ethBalance / 10) ? ethBalance / 10 : ethInNewAnchor; - - //currentTick = currentTick / TICK_SPACING * TICK_SPACING; - _set(sqrtPriceX96, currentTick, ethInNewAnchor); + _set(sqrtPriceX96, currentTick); } } diff --git a/onchain/test/BaseLineLP2.t.sol b/onchain/test/BaseLineLP2.t.sol index 2b5949a..b81a13a 100644 --- a/onchain/test/BaseLineLP2.t.sol +++ b/onchain/test/BaseLineLP2.t.sol @@ -136,7 +136,7 @@ contract BaseLineLP2Test is Test { // Check liquidity positions after slide (uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("slide"); - assertGt(ethFloor, ethAnchor * 8, "slide - Floor should hold more ETH than Anchor"); + assertGt(ethFloor, ethAnchor * 5, "slide - Floor should hold more ETH than Anchor"); assertGt(harbDiscovery, harbAnchor * 90, "slide - Discovery should hold more HARB than Anchor"); assertEq(harbFloor, 0, "slide - Floor should have no HARB"); assertEq(ethDiscovery, 0, "slide - Discovery should have no ETH"); @@ -151,7 +151,7 @@ contract BaseLineLP2Test is Test { // Check liquidity positions after shift (uint256 ethFloor, uint256 ethAnchor, uint256 ethDiscovery, uint256 harbFloor, uint256 harbAnchor, uint256 harbDiscovery) = checkLiquidityPositionsAfter("shift"); - assertGt(ethFloor, ethAnchor * 7, "shift - Floor should hold more ETH than Anchor"); + assertGt(ethFloor, ethAnchor * 5, "shift - Floor should hold more ETH than Anchor"); assertGt(harbDiscovery, harbAnchor * 90, "shift - Discovery should hold more HARB than Anchor"); assertEq(harbFloor, 0, "shift - Floor should have no HARB"); assertEq(ethDiscovery, 0, "shift - Discovery should have no ETH"); @@ -181,7 +181,7 @@ contract BaseLineLP2Test is Test { } else { // Current price is within the bounds of the liquidity position ethAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity); - harbAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity); + harbAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity); } } else { if (currentTick < tickLower) { @@ -195,7 +195,7 @@ contract BaseLineLP2Test is Test { } else { // Current price is within the bounds of the liquidity position harbAmount = LiquidityAmounts.getAmount0ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity); - ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceX96, sqrtPriceBX96, liquidity); + ethAmount = LiquidityAmounts.getAmount1ForLiquidity(sqrtPriceAX96, sqrtPriceX96, liquidity); } } } @@ -408,7 +408,34 @@ contract BaseLineLP2Test is Test { // writeCsv(); // } - function testScenarioA() public { + // function testScenarioBuyAll() public { + // setUpCustomToken0(false); + // vm.deal(account, 300 ether); + // vm.prank(account); + // weth.deposit{value: 300 ether}(); + + // uint256 traderBalanceBefore = weth.balanceOf(account); + + // // Setup initial liquidity + // slide(); + + // buy(100 ether); + + // shift(); + + // sell(harb.balanceOf(account)); + + // slide(); + + // writeCsv(); + // uint256 traderBalanceAfter = weth.balanceOf(account); + // console.log(traderBalanceBefore); + // console.log(traderBalanceAfter); + // assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit"); + // } + + + function testScenarioB() public { setUpCustomToken0(false); vm.deal(account, 100 ether); vm.prank(account); @@ -419,79 +446,90 @@ contract BaseLineLP2Test is Test { // Setup initial liquidity slide(); - // Introduce large buy to push into discovery - buy(10 ether); + buy(2 ether); + + shift(); + + buy(2 ether); + + shift(); + + buy(2 ether); shift(); - // Simulate large sell to push price down to floor sell(harb.balanceOf(account)); slide(); - //writeCsv(); + writeCsv(); uint256 traderBalanceAfter = weth.balanceOf(account); + console.log(traderBalanceBefore); + console.log(traderBalanceAfter); assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit"); } - function testScenarioFuzz(uint8 numActions, uint8 frequency, uint256[] calldata amounts) public { - vm.assume(numActions > 5); - vm.assume(frequency > 0); - vm.assume(frequency < 20); - vm.assume(amounts.length >= numActions); + // function testScenarioFuzz(uint8 numActions, uint8 frequency, uint8[] calldata amounts) public { + // vm.assume(numActions > 5); + // vm.assume(frequency > 0); + // vm.assume(frequency < 20); + // vm.assume(amounts.length >= numActions); - setUpCustomToken0(false); - vm.deal(account, 100 ether); - vm.prank(account); - weth.deposit{value: 100 ether}(); + // setUpCustomToken0(false); + // vm.deal(account, 400 ether); + // vm.prank(account); + // weth.deposit{value: 400 ether}(); - // Setup initial liquidity - slide(); + // // Setup initial liquidity + // slide(); - uint256 traderBalanceBefore = weth.balanceOf(account); - uint8 f = 0; - for (uint i = 0; i < numActions; i++) { - uint256 amount = amounts[i] < 1 ether ? amounts[i] + 1 ether : amounts[i]; - uint256 harbBal = harb.balanceOf(account); - if (harbBal == 0) { - buy(amount % (weth.balanceOf(account) / 2)); - } else if (weth.balanceOf(account) == 0) { - sell(amount % harbBal); - } else { - if (amount % 2 == 0) { - buy(amount % (weth.balanceOf(account) / 2)); - } else { - sell(amount % harbBal); - } - } + // uint256 traderBalanceBefore = weth.balanceOf(account); + // uint8 f = 0; + // for (uint i = 0; i < numActions; i++) { + // uint256 amount = (amounts[i] * 1 ether) + 1 ether; + // uint256 harbBal = harb.balanceOf(account); + // if (harbBal == 0) { + // buy(amount % (weth.balanceOf(account) / 2)); + // } else if (weth.balanceOf(account) == 0) { + // sell(amount % harbBal); + // } else { + // if (amount % 2 == 0) { + // buy(amount % (weth.balanceOf(account) / 2)); + // } else { + // sell(amount % harbBal); + // } + // } - if (f >= frequency) { - (, int24 currentTick, , , , , ) = pool.slot0(); - (, int24 tickLower, int24 tickUpper) = lm.positions(BaseLineLP.Stage.ANCHOR); - int24 midTick = (tickLower + tickUpper) / 2; - if (currentTick < midTick) { - // Current tick is below the midpoint, so call slide() - slide(); - } else if (currentTick > midTick) { - // Current tick is above the midpoint, so call shift() - shift(); - } + // if (f >= frequency) { + // (, int24 currentTick, , , , , ) = pool.slot0(); + // (, int24 tickLower, int24 tickUpper) = lm.positions(BaseLineLP.Stage.ANCHOR); + // int24 midTick = (tickLower + tickUpper) / 2; + // if (currentTick < midTick) { + // // Current tick is below the midpoint, so call slide() + // slide(); + // } else if (currentTick > midTick) { + // // Current tick is above the midpoint, so call shift() + // shift(); + // } - f = 0; - } else { - f++; - } - } + // f = 0; + // } else { + // f++; + // } + // } - // Simulate large sell to push price down to floor - sell(harb.balanceOf(account)); + // // Simulate large sell to push price down to floor + // sell(harb.balanceOf(account)); - uint256 traderBalanceAfter = weth.balanceOf(account); - if (traderBalanceBefore > traderBalanceAfter){ - writeCsv(); - } - assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit"); - } + // slide(); + + // uint256 traderBalanceAfter = weth.balanceOf(account); + + // if (traderBalanceAfter > traderBalanceBefore){ + // writeCsv(); + // } + // assertGt(traderBalanceBefore, traderBalanceAfter, "trader should not have made profit"); + // } } \ No newline at end of file