From 5ce676f0452a4121e999214b222e111f8cc60b99 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Thu, 28 Mar 2024 19:55:01 +0100 Subject: [PATCH] wip --- onchain/src/BaseLineLP.sol | 228 +++++++++++++++++++++++++++++-------- onchain/src/Stake.sol | 2 +- onchain/test/Harb.t.sol | 2 +- subgraph/harb/package.json | 12 +- 4 files changed, 188 insertions(+), 56 deletions(-) diff --git a/onchain/src/BaseLineLP.sol b/onchain/src/BaseLineLP.sol index fa89c7d..61c7f36 100644 --- a/onchain/src/BaseLineLP.sol +++ b/onchain/src/BaseLineLP.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.20; +pragma solidity ^0.8.19; import "@uniswap-v3-periphery/libraries/PositionKey.sol"; import "@uniswap-v3-core/libraries/FixedPoint128.sol"; @@ -9,6 +9,7 @@ import "@aperture/uni-v3-lib/LiquidityAmounts.sol"; import "@aperture/uni-v3-lib/PoolAddress.sol"; import "@aperture/uni-v3-lib/CallbackValidation.sol"; import "@openzeppelin/token/ERC20/IERC20.sol"; +import {ABDKMath64x64} from "abdk/ABDKMath64x64.sol"; import "./interfaces/IWETH9.sol"; import {Harb} from "./Harb.sol"; @@ -18,8 +19,16 @@ import {Harb} from "./Harb.sol"; * take strategic liqudity positions. */ contract BaseLineLP { + int24 constant TICK_SPACING = 200; // default fee of 1% uint24 constant FEE = uint24(10_000); + // uint256 constant FLOOR = 0; + // uint256 constant ANCHOR = 1; + // uint256 constant DISCOVERY = 2; + + enum Stage { FLOOR, ANCHOR, DISCOVERY } + + uint256 constant LIQUIDITY_RATIO_DIVISOR = 100; // the address of the Uniswap V3 factory address immutable factory; @@ -40,9 +49,7 @@ contract BaseLineLP { uint256 feeGrowthInside1LastX128; } - TokenPosition public floor; - TokenPosition public anchor; - TokenPosition public discovery; + mapping(uint256 => TokenPosition) positions; modifier checkDeadline(uint256 deadline) { require(block.timestamp <= deadline, "Transaction too old"); @@ -68,30 +75,25 @@ contract BaseLineLP { token0isWeth = _WETH9 < _harb; } - function posKey(int24 tickLower, int24 tickUpper) internal pure returns (bytes6 _posKey) { - bytes memory _posKeyBytes = abi.encodePacked(tickLower, tickUpper); - assembly { - _posKey := mload(add(_posKeyBytes, 6)) - } - } - function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); + // ## mint harb if needed if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } - function createPosition(int24 tickLower, int24 tickUpper, uint256 amount) internal { + function createPosition(uint256 positionIndex, int24 tickLower, int24 tickUpper, uint256 amount) internal { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); uint128 liquidity = LiquidityAmounts.getLiquidityForAmount1( - sqrtRatioAX96, sqrtRatioBX96, amount / 20 + sqrtRatioAX96, sqrtRatioBX96, amount ); - pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey)); + (uint256 amount0, uint256 amount1) = pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey)); + // TODO: check slippage // read position and start tracking in storage bytes32 positionKey = PositionKey.compute(address(this), tickLower, tickUpper); (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); - discovery = TokenPosition({ + positions[positionIndex] = TokenPosition({ liquidity: liquidity, tickLower: tickLower, tickUpper: tickUpper, @@ -103,9 +105,9 @@ contract BaseLineLP { // called once at the beginning function deployLiquidity(int24 startTick, uint256 amount) external { - require(floor.liquidity == 0, "already set up"); - require(anchor.liquidity == 0, "already set up"); - require(discovery.liquidity == 0, "already set up"); + require(positions[Stage.FLOOR].liquidity == 0, "already set up"); + require(positions[Stage.ANCHOR].liquidity == 0, "already set up"); + require(positions[Stage.DISCOVERY].liquidity == 0, "already set up"); harb.mint(amount); int24 tickLower; int24 tickUpper; @@ -145,51 +147,177 @@ contract BaseLineLP { } - function getInverseAmountsForLiquidity( - uint160 sqrtRatioX96, - uint160 sqrtRatioAX96, - uint160 sqrtRatioBX96, - uint128 liquidity - ) internal pure returns (uint256 amount0, uint256 amount1) { - if (sqrtRatioAX96 > sqrtRatioBX96) (sqrtRatioAX96, sqrtRatioBX96) = (sqrtRatioBX96, sqrtRatioAX96); + function outstanding() public view returns (uint256 _outstanding) { + harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this)); + } - if (sqrtRatioX96 <= sqrtRatioAX96) { - amount1 = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); - } else if (sqrtRatioX96 < sqrtRatioBX96) { - amount1 = LiquidityAmounts.getAmount1ForLiquidity(sqrtRatioX96, sqrtRatioBX96, liquidity); - amount0 = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioX96, liquidity); + function ethInAnchor() public view returns (uint256 _ethInAnchor) { + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(positions[Stage.ANCHOR].tickUpper); + if (token0isWeth) { + _ethInAnchor = LiquidityAmounts.getAmount0ForLiquidity( + sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity + ); } else { - amount0 = LiquidityAmounts.getAmount0ForLiquidity(sqrtRatioAX96, sqrtRatioBX96, liquidity); + _ethInAnchor = LiquidityAmounts.getAmount1ForLiquidity( + sqrtRatioAX96, sqrtRatioBX96, positions[Stage.ANCHOR].liquidity + ); } } - function calculateCapacity() public view returns (uint256 capacity) { - capacity = 0; - (, int24 currentTick, , , , , ) = pool.slot0(); - uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick); - // handle floor + /// @dev Calculates cycle tick from cycle price. + function tickAtPrice(uint256 price) internal view returns (int24 tick_) { + tick_ = TickMath.getTickAtSqrtRatio( + uint160(int160(ABDKMath64x64.sqrt(int128(int256(price << 64))) << 32)) + ); + tick_ = tick_ / TICK_SPACING * TICK_SPACING; + tick_ = token0isWeth ? tick_ : -tick_; + } + + // call this function when price has moved up 15% + function shift() external { + // Fetch the current tick from the Uniswap V3 pool + (uint160 sqrtPriceX96, int24 currentTick, , , , , ) = pool.slot0(); + // TODO: check slippage with oracle + // ## check price moved up { - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floor.tickLower); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floor.tickUpper); - (uint256 amount0, uint256 amount1) = getInverseAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, floor.liquidity); - capacity += token0isWeth ? amount1 : amount0; + // Check if current tick is within the specified range + int24 anchorTickLower = anchor.tickLower; + int24 anchorTickUpper = anchor.tickUpper; + // center tick can be calculated positive and negative numbers the same + int24 centerTick = anchorTickLower + ((anchorTickUpper - anchorTickLower) / 2); + int24 amplitudeTick = anchorTickLower + (anchorTickUpper - anchorTickLower) * 3 / 20; + if (token0isWeth) { + require(currentTick > centerTick, "call slide(), not shift()"); + // ### check price moved more than 15% of range + require(currentTick > amplitudeTick, "amplitude not reached, come back later"); + } else { + // ### check price moved up enough and add hysteresis + require(currentTick < centerTick, "call slide(), not shift()"); + // ### check price moved more than 15% of range + require(currentTick < amplitudeTick, "amplitude not reached, come back later"); + } + } + // ## scrape positions + uint256 ethAmountAnchor; + for (uint256 i=Stage.FLOOR; i <= Stage.DISCOVERY; i++) { + position = positions[i]; + (uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity); + if (i == ANCHOR) { + ethAmountAnchor = token0isWeth ? amount0 : amount1; + } + } + // TODO: handle fees + + // ## set new positions + // move 10% of new ETH into Floor + ethAmountAnchor -= (ethAmountAnchor - ethInAnchor()) * 10 / LIQUIDITY_RATIO_DIVISOR; + + // ### set Floor position + uint256 anchorLiquidity; + { + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick - 300); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 300); + if (token0isWeth) { + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( + sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor + ); + } else { + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( + sqrtRatioAX96, sqrtRatioBX96, ethAmountAnchor + ); + } + (uint256 amount0, uint256 amount1) = pool.mint(address(this), currentTick - 300, currentTick + 300, liquidity, abi.encode(poolKey)); + bytes32 positionKey = PositionKey.compute(address(this), currentTick - 300, currentTick + 300); + (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); + positions[Stage.ANCHOR] = TokenPosition({ + liquidity: anchorLiquidity, + tickLower: currentTick - 300, + tickUpper: currentTick + 300, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128 + }); } - // handle anchor + // ### set Floor position { - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(anchor.tickLower); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(anchor.tickUpper); - (uint256 amount0, uint256 amount1) = getInverseAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, anchor.liquidity); - capacity += token0isWeth ? amount1 : amount0; + uint256 ethAmountFloor = address(this).balance; + int24 floorTick = tickAtPrice(outstanding() / ethAmountFloor); + floorTick -= (currentTick - floorTick); + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick - 300); + uint256 liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + sqrtRatioAX96, + sqrtRatioBX96, + token0isWeth ? ethAmountFloor : 0, + token0isWeth ? 0: ethAmountFloor + ); + pool.mint( + address(this), + floorTick, + currentTick - 300, + liquidity, + abi.encode(poolKey) + ); + bytes32 positionKey = PositionKey.compute(address(this), floorTick, currentTick - 300); + (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); + positions[Stage.FLOOR] = TokenPosition({ + liquidity: liquidity, + tickLower: floorTick, + tickUpper: currentTick - 300, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128 + }); } - // handle discovery + // ##set discovery position { - uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(discovery.tickLower); - uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(discovery.tickUpper); - (uint256 amount0, uint256 amount1) = getInverseAmountsForLiquidity(sqrtRatioX96, sqrtRatioAX96, sqrtRatioBX96, discovery.liquidity); - capacity += token0isWeth ? amount1 : amount0; + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(currentTick + 300); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(currentTick + 11300); + // discovery with 1.5 times as much liquidity per tick as anchor + uint256 liquidity = anchorLiquidity * 55 / 2; + uint256 harbInDiscovery; + if (token0isWeth) { + harbInDiscovery = TickMath.getAmount1ForLiquidity( + sqrtRatioAX96, + sqrtRatioBX96, + liquidity + ); + } else { + harbInDiscovery = TickMath.getAmount0ForLiquidity( + sqrtRatioAX96, + sqrtRatioBX96, + liquidity + ); + } + pool.mint( + address(this), + currentTick + 300, + currentTick + 11300, + liquidity, + abi.encode(poolKey) + ); + bytes32 positionKey = PositionKey.compute(address(this), currentTick + 300, currentTick + 11300); + (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128,,) = pool.positions(positionKey); + positions[Stage.DISCOVERY] = TokenPosition({ + liquidity: liquidity, + tickLower: floorTick, + tickUpper: currentTick - 300, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128 + }); } + + } + + function slide() external { + // check price moved down + // check price moved down enough + // scrape positions + // get outstanding upply and capacity + // set new positions + // burn harb } } diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index 8555891..838257a 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.13; +pragma solidity ^0.8.19; import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; diff --git a/onchain/test/Harb.t.sol b/onchain/test/Harb.t.sol index 172db47..61cd168 100644 --- a/onchain/test/Harb.t.sol +++ b/onchain/test/Harb.t.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -pragma solidity ^0.8.13; +pragma solidity ^0.8.19; import "forge-std/Test.sol"; import "forge-std/console.sol"; diff --git a/subgraph/harb/package.json b/subgraph/harb/package.json index f9b9edd..79eec3b 100644 --- a/subgraph/harb/package.json +++ b/subgraph/harb/package.json @@ -11,9 +11,13 @@ "test": "graph test" }, "dependencies": { - "@graphprotocol/graph-cli": "0.68.5", - "@graphprotocol/graph-ts": "0.32.0", - "assemblyscript": "0.19.23" + "@graphprotocol/graph-cli": "0.69.0", + "@graphprotocol/graph-ts": "0.34.0", + "assemblyscript": "0.27.25" }, - "devDependencies": { "matchstick-as": "0.5.0" } + "devDependencies": { + "@types/node": "^20.11.30", + "matchstick-as": "0.5.0", + "typescript": "^5.4.3" + } }