From df93bffc9f2ec73142107936347ab29100bbe129 Mon Sep 17 00:00:00 2001 From: JulesCrown Date: Thu, 23 Nov 2023 22:14:47 +0100 Subject: [PATCH] license and LiqMan WIP --- src/BloodX.sol | 2 +- src/LiquidityManager.sol | 238 +++++++++++++++++++++++++++++++++++++++ src/StakeX.sol | 2 +- test/BloodX.t.sol | 2 +- 4 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 src/LiquidityManager.sol diff --git a/src/BloodX.sol b/src/BloodX.sol index 6551f57..99b187e 100644 --- a/src/BloodX.sol +++ b/src/BloodX.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.13; import "./ERC1363.sol"; diff --git a/src/LiquidityManager.sol b/src/LiquidityManager.sol new file mode 100644 index 0000000..ad53d33 --- /dev/null +++ b/src/LiquidityManager.sol @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +pragma solidity ^0.8.20; + +import {BloodX} from "src/BloodX.sol"; +import "@uniswap/v3-core/contracts/libraries/TickMath.sol"; +import "@uniswap/v3-periphery/contracts/libraries/LiquidityAmounts.sol"; +import "@uniswap/v3-core/contracts/interfaces/IUniswapV3Pool.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; + +/** + * @title LiquidityManager - A contract that supports the $bloodX ecosystem. It + * protects the communities liquidity while allowing a manager role to + * take strategic liqudity positions. + */ +contract LiquidityManager { + + // default fee of 1% + uint24 constant FEE = uint24(10_000); + + /// @notice WETH9, which is an underlying pool token. + address immutable WETH; + + struct AddLiquidityParams { + address token0; + address token1; + int24 tickLower; + int24 tickUpper; + uint256 amount0Desired; + uint256 amount1Desired; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct DecreaseLiquidityParams { + address token0; + address token1; + int24 tickLower; + int24 tickUpper; + uint128 liquidity; + uint256 amount0Min; + uint256 amount1Min; + uint256 deadline; + } + + struct TokenPosition { + // the liquidity of the position + uint128 liquidity; + // the fee growth of the aggregate position as of the last action on the individual position + uint256 feeGrowthInside0LastX128; + uint256 feeGrowthInside1LastX128; + // how many uncollected tokens are owed to the position, as of the last computation + uint128 tokensOwed0; + uint128 tokensOwed1; + } + + /// @dev The token ID position data + mapping(bytes32 => TokenPosition) private _positions; + + constructor(address _weth) { + WETH = _weth; + } + + function getToken(address token0, address token1) internal view returns (address) { + bool token0isWeth = (token0 == WETH); + require(token0isWeth || token1 == WETH, "token error"); + return (token0isWeth) ? token1 : token0; + } + + function posKey(address _token, uint24 _tickLower, uint24, tickUpper) internal pure returns (uint256) { + return uint160(_token) << 48 + tickLower << 24 + tickUpper; + } + + function positions(address _token, int24 _tickLower, int24 _tickUpper) external view + returns ( + uint128 liquidity, + uint256 feeGrowthInside0LastX128, + uint256 feeGrowthInside1LastX128, + uint128 tokensOwed0, + uint128 tokensOwed1 + ) + { + TokenPosition memory position = _positions[posKey(_token, tickLower, tickUpper)]; + return ( + position.liquidity, + position.feeGrowthInside0LastX128, + position.feeGrowthInside1LastX128, + position.tokensOwed0, + position.tokensOwed1 + ); + } + + /// @inheritdoc IUniswapV3MintCallback + function uniswapV3MintCallback( + uint256 amount0Owed, + uint256 amount1Owed, + bytes calldata data + ) external override { + PoolAddress.PoolKey memory poolKey = abi.decode(data, (PoolAddress.PoolKey)); + CallbackValidation.verifyCallback(factory, poolKey); + + if (amount0Owed > 0) ERC20(poolKey.token0).transfer(msg.sender, amount0Owed); + if (amount1Owed > 0) ERC20(poolKey.token1).transfer(msg.sender, amount1Owed); + } + + /// @notice Add liquidity to an initialized pool + function addLiquidity(AddLiquidityParams memory params) + external + checkDeadline(params.deadline) + returns ( + uint128 liquidity, + uint256 amount0, + uint256 amount1 + ) + { + PoolAddress.PoolKey memory poolKey = + PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: FEE}); + pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); + + // compute the liquidity amount + { + (uint160 sqrtPriceX96, , , , , , ) = pool.slot0(); + uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(params.tickLower); + uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(params.tickUpper); + + liquidity = LiquidityAmounts.getLiquidityForAmounts( + sqrtPriceX96, + sqrtRatioAX96, + sqrtRatioBX96, + params.amount0Desired, + params.amount1Desired + ); + } + + (amount0, amount1) = pool.mint( + address(this), + params.tickLower, + params.tickUpper, + liquidity, + abi.encode(PoolAddress.PoolKey(poolKey)) + ); + + require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'); + + bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); + + // this is now updated to the current transaction + (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); + + + TokenPosition memory position = _positions[posKey(getToken(params.token0, params.token1), tickLower, tickUpper)]; + if (liquidity == 0) { + // create entry + position = TokenPosition({ + liquidity: liquidity, + feeGrowthInside0LastX128: feeGrowthInside0LastX128, + feeGrowthInside1LastX128: feeGrowthInside1LastX128, + tokensOwed0: 0, + tokensOwed1: 0 + }); + } else { + // update entry + position.tokensOwed0 += uint128( + FullMath.mulDiv( + feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, + position.liquidity, + FixedPoint128.Q128 + ) + ); + position.tokensOwed1 += uint128( + FullMath.mulDiv( + feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, + position.liquidity, + FixedPoint128.Q128 + ) + ); + + position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + position.liquidity += liquidity; + } + + emit IncreaseLiquidity(tokenId, liquidity, amount0, amount1); + } + + /// @inheritdoc INonfungiblePositionManager + function decreaseLiquidity(DecreaseLiquidityParams calldata params) + external + payable + override + checkDeadline(params.deadline) + returns (uint256 amount0, uint256 amount1) + { + require(params.liquidity > 0); + + TokenPosition memory position = _positions[posKey(getToken(params.token0, params.token1), tickLower, tickUpper)]; + uint128 positionLiquidity = position.liquidity; + require(positionLiquidity >= params.liquidity); + + PoolAddress.PoolKey memory poolKey = + PoolAddress.PoolKey({token0: params.token0, token1: params.token1, fee: FEE}); + pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey)); + + (amount0, amount1) = pool.burn(params.tickLower, params.tickUpper, params.liquidity); + + require(amount0 >= params.amount0Min && amount1 >= params.amount1Min, 'Price slippage check'); + + bytes32 positionKey = PositionKey.compute(address(this), params.tickLower, params.tickUpper); + // this is now updated to the current transaction + (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, , ) = pool.positions(positionKey); + + position.tokensOwed0 += + uint128(amount0) + + uint128( + FullMath.mulDiv( + feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128, + positionLiquidity, + FixedPoint128.Q128 + ) + ); + position.tokensOwed1 += + uint128(amount1) + + uint128( + FullMath.mulDiv( + feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128, + positionLiquidity, + FixedPoint128.Q128 + ) + ); + + position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128; + position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128; + // subtraction is safe because we checked positionLiquidity is gte params.liquidity + position.liquidity = positionLiquidity - params.liquidity; + + emit DecreaseLiquidity(params.tokenId, params.liquidity, amount0, amount1); + } +} \ No newline at end of file diff --git a/src/StakeX.sol b/src/StakeX.sol index fcceac2..bc1cd88 100644 --- a/src/StakeX.sol +++ b/src/StakeX.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.13; // didn't use solmate here because totalSupply needs override diff --git a/test/BloodX.t.sol b/test/BloodX.t.sol index b596bad..aa356a0 100644 --- a/test/BloodX.t.sol +++ b/test/BloodX.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED +// SPDX-License-Identifier: GPL-3.0-or-later pragma solidity ^0.8.13; import "forge-std/Test.sol";