From d9ee15f812f3610641adf3c477b3bd894d926397 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Thu, 11 Apr 2024 07:28:54 +0200 Subject: [PATCH] wip --- onchain/src/BaseLineLP.sol | 112 ++++++++++++++-------------------- onchain/src/Stake.sol | 20 ++++++ onchain/test/BaseLineLP.t.sol | 89 +++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 65 deletions(-) create mode 100644 onchain/test/BaseLineLP.t.sol diff --git a/onchain/src/BaseLineLP.sol b/onchain/src/BaseLineLP.sol index 12ae181..86d110f 100644 --- a/onchain/src/BaseLineLP.sol +++ b/onchain/src/BaseLineLP.sol @@ -80,13 +80,23 @@ contract BaseLineLP { token0isWeth = _WETH9 < _harb; } + event UniCallback(uint256 indexed amount0Owed, uint256 indexed amount1Owed); + function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external { CallbackValidation.verifyCallback(factory, poolKey); + // take care of harb + harb.mint(token0isWeth ? amount1Owed : amount0Owed); + // pack ETH + weth.deposit{value: token0isWeth ? amount0Owed : amount1Owed}(); // ## mint harb if needed if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed); if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed); } + receive() external payable { + + } + function createPosition(Stage positionIndex, int24 tickLower, int24 tickUpper, uint256 amount) internal { uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); @@ -108,50 +118,6 @@ contract BaseLineLP { } - // called once at the beginning - function deployLiquidity(int24 startTick, uint256 amount) external { - 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; - - // create floor - if (token0isWeth) { - tickLower = startTick; - tickUpper = startTick + 200; - createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10); - } else { - tickLower = startTick - 200; - tickUpper = startTick; - createPosition(Stage.FLOOR, tickLower, tickUpper, amount / 10); - } - - // create anchor - if (token0isWeth) { - tickLower += 201; - tickUpper += 601; - createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 20); - } else { - tickLower -= 601; - tickUpper -= 201; - createPosition(Stage.ANCHOR, tickLower, tickUpper, amount / 10); - } - - // create discovery - if (token0isWeth) { - tickLower += 601; - tickUpper += 11001; - createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this))); - } else { - tickLower -= 11001; - tickUpper -= 601; - createPosition(Stage.DISCOVERY, tickLower, tickUpper, harb.balanceOf(address(this))); - } - } - - function outstanding() public view returns (uint256 _outstanding) { _outstanding = harb.totalSupply() - harb.balanceOf(address(pool)) - harb.balanceOf(address(this)); } @@ -174,18 +140,25 @@ contract BaseLineLP { } } + uint160 internal constant MIN_SQRT_RATIO = 4295128739; + function tickAtPrice(uint256 tokenAmount, uint256 ethAmount) internal view returns (int24 tick_) { require(ethAmount > 0, "ETH amount cannot be zero"); - // 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(int256(tokenAmount)), - int128(int256(ethAmount)) - ); - // Convert the price ratio into a sqrt price in the format expected by Uniswap's TickMath. - uint160 sqrtPriceX96 = uint160( - int160(ABDKMath64x64.sqrt(priceRatio) << 32) - ); + uint160 sqrtPriceX96; + if (tokenAmount == 0) { + sqrtPriceX96 = MIN_SQRT_RATIO; + } 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(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) + ); + } // Proceed as before. tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96); tick_ = tick_ / TICK_SPACING * TICK_SPACING; @@ -235,20 +208,22 @@ contract BaseLineLP { return amount; } + event FLOOR_TICK(int24 indexed floorTick, int24 indexed startTick); + function _set(uint160 sqrtPriceX96, int24 currentTick, uint256 ethInNewAnchor) internal { // ### set Anchor position uint128 anchorLiquidity; { - int24 tickLower = currentTick - 300; - int24 tickUpper = currentTick + 300; + int24 tickLower = currentTick - 400; + int24 tickUpper = currentTick + 400; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); if (token0isWeth) { - anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor ); } else { - anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1( + anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0( sqrtRatioAX96, sqrtRatioBX96, ethInNewAnchor ); } @@ -257,14 +232,16 @@ contract BaseLineLP { // ### set Floor position { - int24 startTick = token0isWeth ? currentTick - 301 : currentTick + 301; + int24 startTick = token0isWeth ? currentTick - 400 : currentTick + 400; // all remaining eth will be put into this position uint256 ethInFloor = address(this).balance; // calculate price at which all HARB can be bought back int24 floorTick = tickAtPrice(outstanding(), ethInFloor); + emit FLOOR_TICK(floorTick, startTick); // put a position symetrically around the price, startTick being edge on one side floorTick = token0isWeth ? floorTick - (startTick - floorTick) : startTick + (floorTick - startTick); + emit FLOOR_TICK(floorTick, startTick); // calculate liquidity uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(floorTick); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(startTick); @@ -281,8 +258,8 @@ contract BaseLineLP { // ## set Discovery position { - int24 tickLower = token0isWeth ? currentTick + 301 : currentTick - 11301; - int24 tickUpper = token0isWeth ? currentTick + 11301 : currentTick - 301; + int24 tickLower = token0isWeth ? currentTick + 401 : currentTick - 11401; + int24 tickUpper = token0isWeth ? currentTick + 11401 : currentTick - 401; uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); // discovery with 1.5 times as much liquidity per tick as anchor @@ -316,9 +293,10 @@ contract BaseLineLP { if(amount < harbInDiscovery) { // calculate new ticks so that discovery liquidity is still // deeper than anchor, but less wide - int24 tickWidth = int24(int256(11000 * amount / harbInDiscovery)) + 301; - tickLower = token0isWeth ? currentTick + 301 : currentTick - tickWidth; - tickUpper = token0isWeth ? currentTick + tickWidth : currentTick - 301; + int24 tickWidth = int24(int256(11000 * amount / harbInDiscovery)) + 401; + tickWidth = tickWidth / TICK_SPACING * TICK_SPACING; + tickLower = token0isWeth ? currentTick + 401 : currentTick - tickWidth; + tickUpper = token0isWeth ? currentTick + tickWidth : currentTick - 401; sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower); sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper); liquidity = LiquidityAmounts.getLiquidityForAmounts( @@ -423,11 +401,15 @@ contract BaseLineLP { uint256 ethInFloor = ethIn(Stage.FLOOR); // use previous ration of Floor to Anchor - uint256 ethInNewAnchor = ethBalance * ethInAnchor / (ethInAnchor + ethInFloor); + 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); } diff --git a/onchain/src/Stake.sol b/onchain/src/Stake.sol index 2bf1da0..522d2d0 100644 --- a/onchain/src/Stake.sol +++ b/onchain/src/Stake.sol @@ -4,6 +4,7 @@ pragma solidity ^0.8.19; import {IERC20} from "@openzeppelin/token/ERC20/ERC20.sol"; import "@openzeppelin/token/ERC20/extensions/IERC20Metadata.sol"; +import "@openzeppelin/token/ERC20/extensions/ERC20Permit.sol"; import {SafeERC20} from "@openzeppelin/token/ERC20/utils/SafeERC20.sol"; import {Math} from "@openzeppelin/utils/math/Math.sol"; import "./interfaces/IStake.sol"; @@ -72,6 +73,25 @@ contract Stake is IStake { return shares.mulDiv(tokenContract.totalSupply(), totalSupply, rounding); } + function permitAndSnatch( + uint256 assets, + address receiver, + uint32 taxRate, + uint256[] calldata positionsToSnatch, + // address owner, + // address spender, + // uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external + returns (uint256 positionId) + { + ERC20Permit(address(tokenContract)).permit(receiver, address(this), assets, deadline, v, r, s); + return snatch(assets, receiver, taxRate, positionsToSnatch); + } + /** * TODO: deal with metatransactions: While these are generally available * via msg.sender and msg.data, they should not be accessed in such a direct diff --git a/onchain/test/BaseLineLP.t.sol b/onchain/test/BaseLineLP.t.sol new file mode 100644 index 0000000..6b53554 --- /dev/null +++ b/onchain/test/BaseLineLP.t.sol @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.19; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import {TwabController} from "pt-v5-twab-controller/TwabController.sol"; +import {PoolAddress, PoolKey} from "@aperture/uni-v3-lib/PoolAddress.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Factory.sol"; +import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol"; +import "../src/interfaces/IWETH9.sol"; +import "../src/Harb.sol"; +import {BaseLineLP} from "../src/BaseLineLP.sol"; +import {Stake, ExceededAvailableStake} from "../src/Stake.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; +address constant V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; +address constant TAX_POOL = address(2); +// default fee of 1% +uint24 constant FEE = uint24(10_000); + +contract BaseLineLPTest is Test { + uint256 mainnetFork; + IWETH9 weth; + Harb harb; + IUniswapV3Factory factory; + Stake stake; + BaseLineLP liquidityManager; + + function sqrt(uint256 y) internal pure returns (uint256 z) { + if (y > 3) { + z = y; + uint256 x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + // z is now the integer square root of y, or the closest integer to the square root of y. + } + + function initializePoolFor1Cent(bool isEthToken0, address pool) public { + uint256 price; + if (isEthToken0) { + // ETH as token0, so we are setting the price of 1 ETH in terms of token1 (USD cent) + price = 3700 * 10**20; // 1 ETH = 3700 USD, scaled by 10^18 for precision + } else { + // Token (valued at 1 USD cent) as token0, ETH as token1 + // We invert the logic to represent the price of 1 token in terms of ETH + price = uint256(10**16) / 3700; // Adjust for 18 decimal places + } + + uint160 sqrtPriceX96 = uint160(sqrt(price) * 2**96 / 10**18); // Adjust sqrt value to 96-bit precision + + // Initialize pool with the calculated sqrtPriceX96 + IUniswapV3Pool(pool).initialize(sqrtPriceX96); + } + + function setUp() public { + mainnetFork = vm.createFork(vm.envString("ETH_NODE_URI_MAINNET"), 19615864); + vm.selectFork(mainnetFork); + weth = IWETH9(WETH); + TwabController tc = new TwabController(60 * 60 * 24, uint32(block.timestamp)); + harb = new Harb("HARB", "HARB", V3_FACTORY, WETH, tc); + factory = IUniswapV3Factory(V3_FACTORY); + address pool = factory.createPool(address(weth), address(harb), FEE); + + initializePoolFor1Cent(address(weth) < address(harb), pool); + + stake = new Stake(address(harb)); + harb.setStakingPool(address(stake)); + liquidityManager = new BaseLineLP(V3_FACTORY, WETH, address(harb)); + harb.setLiquidityManager(address(liquidityManager)); + } + + function testLP(address account, uint256 amount) public { + vm.deal(account, 10 ether); + vm.prank(account); + (bool sent, ) = address(liquidityManager).call{value: 10 ether}(""); + require(sent, "Failed to send Ether"); + + vm.expectRevert(); + liquidityManager.shift(); + + int24 startTick = (address(weth) < address(harb)) ? int24(128219) : int24(-128219); //initialize at 1 cent + liquidityManager.slide(); + } +}