2023-11-23 22:14:47 +01:00
|
|
|
// 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";
|
2023-11-25 20:59:15 +01:00
|
|
|
import "@uniswap/v3-periphery/contracts/libraries/PoolAddress.sol";
|
|
|
|
|
import "@uniswap/v3-periphery/contracts/libraries/CallbackValidation.sol";
|
|
|
|
|
import '@uniswap/v3-periphery/contracts/base/PeripheryImmutableState.sol';
|
2023-11-23 22:14:47 +01:00
|
|
|
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.
|
|
|
|
|
*/
|
2023-11-25 20:59:15 +01:00
|
|
|
contract LiquidityManager is PeripheryImmutableState {
|
2023-11-23 22:14:47 +01:00
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-25 19:47:37 +01:00
|
|
|
struct FeesOwed {
|
|
|
|
|
// how many uncollected tokens are owed
|
|
|
|
|
// updated by each position, as of the last computation
|
|
|
|
|
uint128 tokensOwed;
|
|
|
|
|
uint128 ethOwed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mapping(address => FeesOwed) private _feesOwed;
|
|
|
|
|
|
2023-11-23 22:14:47 +01:00
|
|
|
/// @dev The token ID position data
|
|
|
|
|
mapping(bytes32 => TokenPosition) private _positions;
|
|
|
|
|
|
2023-11-25 20:59:15 +01:00
|
|
|
constructor(
|
|
|
|
|
address _factory,
|
|
|
|
|
address _WETH9
|
|
|
|
|
) {
|
2023-11-23 22:14:47 +01:00
|
|
|
WETH = _weth;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-25 20:59:15 +01:00
|
|
|
modifier checkDeadline(uint256 deadline) {
|
|
|
|
|
require(block.timestamp <= deadline, 'Transaction too old');
|
|
|
|
|
_;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-23 22:14:47 +01:00
|
|
|
function getToken(address token0, address token1) internal view returns (address) {
|
|
|
|
|
bool token0isWeth = (token0 == WETH);
|
|
|
|
|
require(token0isWeth || token1 == WETH, "token error");
|
|
|
|
|
return (token0isWeth) ? token1 : token0;
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-25 19:47:37 +01:00
|
|
|
function getFeesOwed(address token) public view returns (uint128 tokensOwed, uint128 ethOwed) {
|
|
|
|
|
(tokensOwed, ethOwed) = _feesOwed[token];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateFeesOwed(bool token0isEth, address token, uint128 tokensOwed0, uint128 tokensOwed1) internal {
|
|
|
|
|
FeesOwed storage feesOwed = _feesOwed[token];
|
|
|
|
|
feesOwed = (feesOwed.tokensOwed += token0isEth ? tokensOwed1 : tokensOwed0,
|
|
|
|
|
feesOwed.ethOwed += token0isEth ? tokensOwed0 : tokensOwed1);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-25 20:59:15 +01:00
|
|
|
function posKey(address token, uint24 tickLower, uint24 tickUpper) internal pure returns (uint256) {
|
|
|
|
|
return uint160(token) << 48 + tickLower << 24 + tickUpper;
|
2023-11-23 22:14:47 +01:00
|
|
|
}
|
|
|
|
|
|
2023-11-25 20:59:15 +01:00
|
|
|
function positions(address token, int24 tickLower, int24 tickUpper) external view
|
2023-11-23 22:14:47 +01:00
|
|
|
returns (
|
|
|
|
|
uint128 liquidity,
|
|
|
|
|
uint256 feeGrowthInside0LastX128,
|
2023-11-25 19:47:37 +01:00
|
|
|
uint256 feeGrowthInside1LastX128
|
2023-11-23 22:14:47 +01:00
|
|
|
)
|
|
|
|
|
{
|
2023-11-25 20:59:15 +01:00
|
|
|
TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)];
|
2023-11-23 22:14:47 +01:00
|
|
|
return (
|
|
|
|
|
position.liquidity,
|
|
|
|
|
position.feeGrowthInside0LastX128,
|
2023-11-25 19:47:37 +01:00
|
|
|
position.feeGrowthInside1LastX128
|
2023-11-23 22:14:47 +01:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
2023-11-25 19:47:37 +01:00
|
|
|
(bool token0isWeth, address token) = getToken(params.token0, params.token1);
|
|
|
|
|
TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)];
|
2023-11-23 22:14:47 +01:00
|
|
|
if (liquidity == 0) {
|
|
|
|
|
// create entry
|
|
|
|
|
position = TokenPosition({
|
|
|
|
|
liquidity: liquidity,
|
|
|
|
|
feeGrowthInside0LastX128: feeGrowthInside0LastX128,
|
2023-11-25 19:47:37 +01:00
|
|
|
feeGrowthInside1LastX128: feeGrowthInside1LastX128
|
2023-11-23 22:14:47 +01:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// update entry
|
2023-11-25 19:47:37 +01:00
|
|
|
updateTokensOwed(token0isWeth, token, uint128(
|
2023-11-23 22:14:47 +01:00
|
|
|
FullMath.mulDiv(
|
|
|
|
|
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
|
|
|
|
|
position.liquidity,
|
|
|
|
|
FixedPoint128.Q128
|
|
|
|
|
)
|
2023-11-25 19:47:37 +01:00
|
|
|
), uint128(
|
2023-11-23 22:14:47 +01:00
|
|
|
FullMath.mulDiv(
|
|
|
|
|
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
|
|
|
|
position.liquidity,
|
|
|
|
|
FixedPoint128.Q128
|
|
|
|
|
)
|
2023-11-25 19:47:37 +01:00
|
|
|
));
|
2023-11-23 22:14:47 +01:00
|
|
|
|
|
|
|
|
position.feeGrowthInside0LastX128 = feeGrowthInside0LastX128;
|
|
|
|
|
position.feeGrowthInside1LastX128 = feeGrowthInside1LastX128;
|
|
|
|
|
position.liquidity += liquidity;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit IncreaseLiquidity(tokenId, liquidity, amount0, amount1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function decreaseLiquidity(DecreaseLiquidityParams calldata params)
|
|
|
|
|
external
|
|
|
|
|
payable
|
|
|
|
|
override
|
|
|
|
|
checkDeadline(params.deadline)
|
|
|
|
|
returns (uint256 amount0, uint256 amount1)
|
|
|
|
|
{
|
|
|
|
|
require(params.liquidity > 0);
|
|
|
|
|
|
2023-11-25 19:47:37 +01:00
|
|
|
(bool token0isWeth, address token) = getToken(params.token0, params.token1);
|
|
|
|
|
TokenPosition memory position = _positions[posKey(token, tickLower, tickUpper)];
|
2023-11-23 22:14:47 +01:00
|
|
|
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);
|
|
|
|
|
|
2023-11-25 19:47:37 +01:00
|
|
|
updateTokensOwed(token0isWeth, token, uint128(amount0) +
|
2023-11-23 22:14:47 +01:00
|
|
|
uint128(
|
|
|
|
|
FullMath.mulDiv(
|
|
|
|
|
feeGrowthInside0LastX128 - position.feeGrowthInside0LastX128,
|
|
|
|
|
positionLiquidity,
|
|
|
|
|
FixedPoint128.Q128
|
|
|
|
|
)
|
2023-11-25 19:47:37 +01:00
|
|
|
), uint128(amount1) +
|
2023-11-23 22:14:47 +01:00
|
|
|
uint128(
|
|
|
|
|
FullMath.mulDiv(
|
|
|
|
|
feeGrowthInside1LastX128 - position.feeGrowthInside1LastX128,
|
|
|
|
|
positionLiquidity,
|
|
|
|
|
FixedPoint128.Q128
|
|
|
|
|
)
|
2023-11-25 19:47:37 +01:00
|
|
|
));
|
2023-11-23 22:14:47 +01:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|