harb/onchain/src/LiquidityManager.sol

479 lines
23 KiB
Solidity
Raw Normal View History

// SPDX-License-Identifier: GPL-3.0-or-later
2024-03-28 19:55:01 +01:00
pragma solidity ^0.8.19;
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
import "@uniswap-v3-core/libraries/FixedPoint128.sol";
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
import "@aperture/uni-v3-lib/TickMath.sol";
2024-07-04 10:24:06 +02:00
import {LiquidityAmounts} from "@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";
2024-04-28 07:00:53 +02:00
import "@openzeppelin/utils/math/SignedMath.sol";
2024-08-12 19:07:07 +02:00
import {Math} from "@openzeppelin/utils/math/Math.sol";
2024-03-28 21:50:22 +01:00
import {ABDKMath64x64} from "@abdk/ABDKMath64x64.sol";
import "./interfaces/IWETH9.sol";
2024-07-18 07:35:39 +02:00
import {Harberg} from "./Harberg.sol";
2024-03-28 21:50:22 +01:00
/**
2024-07-18 07:35:39 +02:00
* @title LiquidityManager for Harberg Token on Uniswap V3
* @notice Manages liquidity provisioning on Uniswap V3 for the Harberg token by maintaining three distinct positions:
* - Floor Position: Ensures a minimum price support by having enough reserve assets to potentially buy back the circulating supply of Harberg.
2024-07-13 18:33:47 +02:00
* - Anchor Position: Provides liquidity around the current market price to facilitate trading and maintain market stability.
2024-07-18 07:35:39 +02:00
* - Discovery Position: Expands liquidity by minting new Harberg tokens as the price rises, capturing potential growth in the ecosystem.
* The contract dynamically adjusts these positions in response to market movements to maintain strategic liquidity levels and support the Harberg token's price.
2024-07-13 18:33:47 +02:00
* It also collects and transfers fees generated from trading activities to a designated fee destination.
* @dev Utilizes Uniswap V3's concentrated liquidity feature, enabling highly efficient use of capital.
2024-07-13 18:33:47 +02:00
*/
contract LiquidityManager {
2024-08-12 19:07:07 +02:00
using Math for uint256;
2024-07-18 07:35:39 +02:00
// State variables to track total ETH spent
2024-08-12 19:07:07 +02:00
uint256 public cumulativeVolumeWeightedPriceX96;
2024-07-18 07:35:39 +02:00
uint256 public cumulativeVolume;
2024-07-16 19:47:39 +02:00
// the minimum granularity of liquidity positions in the Uniswap V3 pool. this is a 1% pool.
2024-07-09 18:00:39 +02:00
int24 internal constant TICK_SPACING = 200;
2024-07-16 19:47:39 +02:00
// defines the width of the anchor position from the current price to discovery position.
2024-07-09 18:00:39 +02:00
int24 internal constant ANCHOR_SPACING = 5 * TICK_SPACING;
2024-07-16 19:47:39 +02:00
// DISCOVERY_SPACING determines the range above the current price where new tokens are minted and sold.
// 11000 ticks represent 3x the current price
2024-07-09 18:00:39 +02:00
int24 internal constant DISCOVERY_SPACING = 11000;
2024-07-16 19:47:39 +02:00
// how much more liquidity per tick discovery is holding over anchor
uint128 internal constant DISCOVERY_DEPTH = 200; // 500 // 500%
2024-07-16 19:47:39 +02:00
// only working with UNI V3 1% fee tier pools
2024-07-09 18:00:39 +02:00
uint24 internal constant FEE = uint24(10_000);
// ANCHOR_LIQ_SHARE is the mininum share of total ETH in control
2024-07-16 19:47:39 +02:00
// that will be left to put into anchor positon.
uint256 internal constant MIN_ANCHOR_LIQ_SHARE = 5; // 5 = 5%
uint256 internal constant MAX_ANCHOR_LIQ_SHARE = 25;
// virtual liabilities that are added to push the calculated floor price down artificially,
// creating a security margin for attacks on liquidity
uint256 internal constant MIN_CAPITAL_INEFFICIENCY = 70;
2024-07-16 19:47:39 +02:00
uint256 internal constant MAX_CAPITAL_INEFFICIENCY = 200;
2024-07-18 16:50:23 +02:00
// used to double-check price with uni oracle
uint32 internal constant PRICE_STABILITY_INTERVAL = 300; // 5 minutes in seconds
int24 internal constant MAX_TICK_DEVIATION = 50; // how much is that?
2024-07-16 19:47:39 +02:00
// the address of the Uniswap V3 factory
2024-07-13 14:56:13 +02:00
address private immutable factory;
IWETH9 private immutable weth;
2024-07-18 07:35:39 +02:00
Harberg private immutable harb;
2024-07-13 14:56:13 +02:00
IUniswapV3Pool private immutable pool;
bool private immutable token0isWeth;
2024-07-09 18:00:39 +02:00
PoolKey private poolKey;
uint256 private harbPulled; // temporary variable to store amount of harb pulled by Uni
2024-07-16 20:47:06 +02:00
// the 3 positions this contract is managing
enum Stage { FLOOR, ANCHOR, DISCOVERY }
struct TokenPosition {
// the liquidity of the position
uint128 liquidity;
int24 tickLower;
int24 tickUpper;
}
2024-07-16 19:47:39 +02:00
mapping(Stage => TokenPosition) public positions;
// the address where liquidity fees will be sent
2024-07-09 18:00:39 +02:00
address public feeDestination;
2024-07-16 19:47:39 +02:00
// the minimum share of ETH that will be put into the anchor
uint256 public anchorLiquidityShare;
// the higher the inefficiency, the more conservative the positioning of floor
uint256 public capitalInfefficiency;
2024-07-13 14:56:13 +02:00
error ZeroAddressInSetter();
error AddressAlreadySet();
2024-07-16 20:47:06 +02:00
event EthScarcity(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 capitalInfefficiency, uint256 anchorLiquidityShare, int24 vwapTick);
event EthAbundance(int24 currentTick, uint256 ethBalance, uint256 outstandingSupply, uint256 vwap, uint256 capitalInfefficiency, uint256 anchorLiquidityShare, int24 vwapTick);
2024-07-16 19:47:39 +02:00
/// @dev Function modifier to ensure that the caller is the feeDestination
modifier onlyFeeDestination() {
require(msg.sender == address(feeDestination), "only callable by feeDestination");
_;
}
2024-07-18 07:35:39 +02:00
/// @notice Creates a liquidity manager for managing Harberg token liquidity on Uniswap V3.
2024-07-13 18:33:47 +02:00
/// @param _factory The address of the Uniswap V3 factory.
/// @param _WETH9 The address of the WETH contract for handling ETH in trades.
2024-07-18 07:35:39 +02:00
/// @param _harb The address of the Harberg token contract.
/// @dev Computes the Uniswap pool address for the Harberg-WETH pair and sets up the initial configuration for the liquidity manager.
constructor(address _factory, address _WETH9, address _harb) {
factory = _factory;
weth = IWETH9(_WETH9);
poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
2024-07-18 07:35:39 +02:00
harb = Harberg(_harb);
token0isWeth = _WETH9 < _harb;
2024-07-16 19:47:39 +02:00
anchorLiquidityShare = MAX_ANCHOR_LIQ_SHARE;
capitalInfefficiency = MIN_CAPITAL_INEFFICIENCY; // starting at 95% fuzzer passes tests
}
2024-07-13 18:33:47 +02:00
/// @notice Callback function that Uniswap V3 calls for liquidity actions requiring minting or burning of tokens.
/// @param amount0Owed The amount of token0 owed for the liquidity provision.
/// @param amount1Owed The amount of token1 owed for the liquidity provision.
2024-07-18 07:35:39 +02:00
/// @dev This function mints Harberg tokens as needed and handles WETH deposits for ETH conversions during liquidity interactions.
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
CallbackValidation.verifyCallback(factory, poolKey);
2024-04-11 07:28:54 +02:00
// take care of harb
harbPulled = token0isWeth ? amount1Owed : amount0Owed;
harb.mint(harbPulled);
2024-04-11 07:28:54 +02:00
// pack ETH
2024-04-28 07:00:53 +02:00
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
if (weth.balanceOf(address(this)) < ethOwed) {
weth.deposit{value: address(this).balance}();
}
2024-04-23 06:58:34 +02:00
// do transfers
if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed);
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
}
2024-07-13 18:33:47 +02:00
/// @notice Sets the address to which trading fees are transferred.
/// @param feeDestination_ The address that will receive the collected trading fees.
/// @dev Can only be called once to set the fee destination, further attempts will revert.
2024-06-09 16:06:41 +02:00
function setFeeDestination(address feeDestination_) external {
2024-07-13 14:56:13 +02:00
if (address(0) == feeDestination_) revert ZeroAddressInSetter();
if (feeDestination != address(0)) revert AddressAlreadySet();
2024-06-09 16:06:41 +02:00
feeDestination = feeDestination_;
}
2024-07-16 19:47:39 +02:00
function setAnchorLiquidityShare(uint256 anchorLiquidityShare_) external onlyFeeDestination {
require(anchorLiquidityShare_ >= MIN_ANCHOR_LIQ_SHARE, "");
require(anchorLiquidityShare_ <= MAX_ANCHOR_LIQ_SHARE, "");
anchorLiquidityShare = anchorLiquidityShare_;
}
function setCapitalInfefficiency(uint256 capitalInfefficiency_) external onlyFeeDestination {
require(capitalInfefficiency_ >= MIN_CAPITAL_INEFFICIENCY, "");
require(capitalInfefficiency_ <= MAX_CAPITAL_INEFFICIENCY, "");
capitalInfefficiency = capitalInfefficiency_;
}
2024-07-17 14:08:53 +02:00
function setMinStakeSupplyFraction(uint256 mssf_) external onlyFeeDestination {
harb.setMinStakeSupplyFraction(mssf_);
}
2024-04-11 07:28:54 +02:00
receive() external payable {
}
2024-08-12 19:07:07 +02:00
2024-07-18 07:35:39 +02:00
/// @notice Calculates the Uniswap V3 tick corresponding to a given price ratio between Harberg and ETH.
2024-07-13 18:33:47 +02:00
/// @param t0isWeth Boolean flag indicating if token0 is WETH.
2024-07-18 07:35:39 +02:00
/// @param tokenAmount Amount of the Harberg token.
2024-07-13 18:33:47 +02:00
/// @param ethAmount Amount of Ethereum.
/// @return tick_ The calculated tick for the given price ratio.
2024-07-09 18:00:39 +02:00
function tickAtPrice(bool t0isWeth, uint256 tokenAmount, uint256 ethAmount) internal pure returns (int24 tick_) {
2024-03-28 21:50:22 +01:00
require(ethAmount > 0, "ETH amount cannot be zero");
2024-04-11 07:28:54 +02:00
if (tokenAmount == 0) {
// HARB/ETH
tick_ = TickMath.MAX_TICK;
2024-04-11 07:28:54 +02:00
} 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.
2024-08-12 19:07:07 +02:00
int128 priceRatioX64 = ABDKMath64x64.div(
2024-04-11 07:28:54 +02:00
int128(int256(tokenAmount)),
int128(int256(ethAmount))
);
// HARB/ETH
tick_ = tickAtPriceRatio(priceRatioX64);
2024-04-11 07:28:54 +02:00
}
// convert to tick in a pool
tick_ = t0isWeth ? tick_ : -tick_;
2024-08-12 19:07:07 +02:00
}
function tickAtPriceRatio(int128 priceRatioX64) internal pure returns (int24 tick_) {
2024-08-12 19:07:07 +02:00
// Convert the price ratio into a sqrt price in the format expected by Uniswap's TickMath.
uint160 sqrtPriceX96 = uint160(
int160(ABDKMath64x64.sqrt(priceRatioX64) << 32)
);
2024-03-28 21:50:22 +01:00
tick_ = TickMath.getTickAtSqrtRatio(sqrtPriceX96);
2024-07-09 18:00:39 +02:00
}
2024-08-12 19:07:07 +02:00
/// @notice Calculates the price ratio from a given Uniswap V3 tick as HARB/ETH.
2024-07-13 18:33:47 +02:00
/// @param tick The tick for which to calculate the price ratio.
2024-08-12 19:07:07 +02:00
/// @return priceRatioX96 The price ratio corresponding to the given tick.
function priceAtTick(int24 tick) private pure returns (uint256 priceRatioX96) {
//tick = (tick < 0) ? -tick : tick;
uint256 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(tick);
priceRatioX96 = sqrtRatioX96.mulDiv(sqrtRatioX96, (1 << 96));
2024-03-28 19:55:01 +01:00
}
2024-07-13 18:33:47 +02:00
/// @notice Internal function to mint liquidity positions in the Uniswap V3 pool.
/// @param stage The liquidity stage (floor, anchor, discovery) being adjusted.
/// @param tickLower The lower bound of the tick range for the position.
/// @param tickUpper The upper bound of the tick range for the position.
/// @param liquidity The amount of liquidity to mint at the specified range.
2024-04-27 07:04:33 +02:00
function _mint(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal {
2024-03-28 21:50:22 +01:00
// create position
pool.mint(
address(this),
tickLower,
tickUpper,
liquidity,
abi.encode(poolKey)
);
// put into storage
2024-04-27 07:04:33 +02:00
positions[stage] = TokenPosition({
2024-03-28 21:50:22 +01:00
liquidity: liquidity,
tickLower: tickLower,
2024-06-09 16:06:41 +02:00
tickUpper: tickUpper
2024-03-28 21:50:22 +01:00
});
}
2024-07-13 18:33:47 +02:00
/// @notice Internal function to set or adjust the floor, anchor, and discovery positions based on current market conditions and the manager's strategy.
/// @param currentTick The current market tick.
/// @dev Recalculates and realigns all liquidity positions according to the latest market data and strategic requirements.
2024-07-16 19:47:39 +02:00
function _set(int24 currentTick) internal {
2024-07-06 18:36:13 +02:00
// estimate the lower tick of the anchor
2024-07-06 18:36:13 +02:00
int24 vwapTick;
uint256 outstandingSupply = harb.outstandingSupply();
uint256 ethBalance = (address(this).balance + weth.balanceOf(address(this)));
uint256 floorEthBalance = ethBalance * (100 - anchorLiquidityShare) / 100;
if (outstandingSupply > 0) {
vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , floorEthBalance);
} else {
vwapTick = token0isWeth ? currentTick + ANCHOR_SPACING : currentTick - ANCHOR_SPACING;
}
// move vwapTick below currentTick, if needed
if (token0isWeth) {
vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick;
} else {
vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick;
}
// set Anchor position
{
int24 tickLower = token0isWeth ? currentTick - ANCHOR_SPACING : vwapTick;
int24 tickUpper = token0isWeth ? vwapTick : currentTick + ANCHOR_SPACING;
tickLower = tickLower / TICK_SPACING * TICK_SPACING;
tickUpper = tickUpper / TICK_SPACING * TICK_SPACING;
uint160 sqrtRatioX96 = TickMath.getSqrtRatioAtTick(currentTick);
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
uint256 anchorEthBalance = ethBalance - floorEthBalance;
uint128 anchorLiquidity;
if (token0isWeth) {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioX96, sqrtRatioBX96, anchorEthBalance
);
} else {
anchorLiquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioX96, anchorEthBalance
);
}
_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;
int24 tickUpper = token0isWeth ? currentTick - ANCHOR_SPACING : currentTick + DISCOVERY_SPACING + ANCHOR_SPACING;
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(tickLower);
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(tickUpper);
uint256 discoveryAmount = harbPulled * uint24(DISCOVERY_SPACING) * uint24(DISCOVERY_DEPTH) / uint24(ANCHOR_SPACING) / 100;
uint128 liquidity;
if (token0isWeth) {
liquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioBX96, discoveryAmount
);
} else {
liquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioAX96, sqrtRatioBX96, discoveryAmount
);
}
_mint(Stage.DISCOVERY, tickLower, tickUpper, liquidity);
harb.burn(harb.balanceOf(address(this)));
}
// set Floor position
2024-07-06 18:36:13 +02:00
{
outstandingSupply = harb.outstandingSupply();
2024-08-12 19:07:07 +02:00
uint256 vwapX96 = 0;
2024-07-06 18:36:13 +02:00
uint256 requiredEthForBuyback = 0;
if (cumulativeVolume > 0) {
vwapX96 = cumulativeVolumeWeightedPriceX96 * capitalInfefficiency / 100 / cumulativeVolume; // in harb/eth
2024-08-12 19:07:07 +02:00
requiredEthForBuyback = outstandingSupply.mulDiv(vwapX96, (1 << 96));
2024-07-06 18:36:13 +02:00
}
// make a new calculation of the vwapTick, having updated outstandingSupply
2024-07-16 19:47:39 +02:00
if (floorEthBalance < requiredEthForBuyback) {
2024-07-06 18:36:13 +02:00
// not enough ETH, find a lower price
2024-07-16 19:47:39 +02:00
requiredEthForBuyback = floorEthBalance;
vwapTick = tickAtPrice(token0isWeth, outstandingSupply * capitalInfefficiency / 100 , requiredEthForBuyback);
emit EthScarcity(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick);
2024-08-12 19:07:07 +02:00
} else if (vwapX96 == 0) {
2024-07-16 19:47:39 +02:00
requiredEthForBuyback = floorEthBalance;
2024-07-06 18:36:13 +02:00
vwapTick = currentTick;
} else {
// ETH/HARB tick
vwapTick = tickAtPriceRatio(int128(int256(vwapX96 >> 32)));
// convert to pool tick
vwapTick = token0isWeth ? -vwapTick : vwapTick;
2024-08-12 19:07:07 +02:00
emit EthAbundance(currentTick, ethBalance, outstandingSupply, vwapX96, capitalInfefficiency, anchorLiquidityShare, vwapTick);
2024-07-16 19:47:39 +02:00
}
2024-07-06 18:36:13 +02:00
// move floor below anchor, if needed
if (token0isWeth) {
vwapTick = (vwapTick < currentTick + ANCHOR_SPACING) ? currentTick + ANCHOR_SPACING : vwapTick;
2024-07-06 18:36:13 +02:00
} else {
vwapTick = (vwapTick > currentTick - ANCHOR_SPACING) ? currentTick - ANCHOR_SPACING : vwapTick;
2024-07-06 18:36:13 +02:00
}
// normalize tick position for pool
2024-07-06 18:36:13 +02:00
vwapTick = vwapTick / TICK_SPACING * TICK_SPACING;
// calculate liquidity
uint160 sqrtRatioAX96 = TickMath.getSqrtRatioAtTick(vwapTick);
2024-07-09 18:00:39 +02:00
int24 floorTick = token0isWeth ? vwapTick + TICK_SPACING: vwapTick - TICK_SPACING;
2024-07-06 18:36:13 +02:00
uint160 sqrtRatioBX96 = TickMath.getSqrtRatioAtTick(floorTick);
// adding a 2% balance margin, because liquidity calculations are inherently unprecise
floorEthBalance = floorEthBalance * 98 / 100;
2024-07-16 19:47:39 +02:00
uint128 liquidity;
if (token0isWeth) {
liquidity = LiquidityAmounts.getLiquidityForAmount0(
sqrtRatioAX96, sqrtRatioBX96, floorEthBalance
2024-07-16 19:47:39 +02:00
);
} else {
liquidity = LiquidityAmounts.getLiquidityForAmount1(
sqrtRatioAX96, sqrtRatioBX96, floorEthBalance
2024-07-16 19:47:39 +02:00
);
}
2024-07-06 18:36:13 +02:00
_mint(Stage.FLOOR, token0isWeth ? vwapTick : floorTick, token0isWeth ? floorTick : vwapTick, liquidity);
}
2024-04-03 21:43:12 +02:00
}
2024-08-12 19:07:07 +02:00
function _recordVolumeAndPrice(uint256 currentPriceX96, uint256 fee) internal {
2024-07-18 16:50:23 +02:00
// assuming FEE is 1%
uint256 volume = fee * 100;
2024-08-12 19:07:07 +02:00
uint256 volumeWeightedPriceX96 = currentPriceX96 * volume;
2024-07-18 16:50:23 +02:00
// Check for potential overflow. 10**70 is close to 2^256
2024-08-12 19:07:07 +02:00
if (cumulativeVolumeWeightedPriceX96 > 10**70) {
2024-07-18 16:50:23 +02:00
uint256 zipFactor = 10**35;
uint256 desiredPrecision = 10**5;
while (zipFactor * desiredPrecision > cumulativeVolume) {
zipFactor /= desiredPrecision;
}
// Handle overflow: zip historic trade data
2024-08-12 19:07:07 +02:00
cumulativeVolumeWeightedPriceX96 = cumulativeVolumeWeightedPriceX96 / zipFactor;
2024-07-18 16:50:23 +02:00
// cumulativeVolume should be well higer than zipFactor
cumulativeVolume = cumulativeVolume / zipFactor;
}
2024-08-12 19:07:07 +02:00
cumulativeVolumeWeightedPriceX96 += volumeWeightedPriceX96;
2024-07-18 16:50:23 +02:00
cumulativeVolume += volume;
}
2024-07-06 18:36:13 +02:00
function _scrape() internal {
2024-06-09 16:06:41 +02:00
uint256 fee0 = 0;
uint256 fee1 = 0;
2024-07-06 18:36:13 +02:00
uint256 currentPrice;
2024-06-09 16:06:41 +02:00
for (uint256 i=uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
TokenPosition storage position = positions[Stage(i)];
if (position.liquidity > 0) {
(uint256 amount0, uint256 amount1) = pool.burn(position.tickLower, position.tickUpper, position.liquidity);
// Collect the maximum possible amounts which include fees
(uint256 collected0, uint256 collected1) = pool.collect(
address(this),
position.tickLower,
position.tickUpper,
type(uint128).max, // Collect the max uint128 value, effectively trying to collect all
type(uint128).max
);
// Calculate the fees
fee0 += collected0 - amount0;
fee1 += collected1 - amount1;
if (i == uint256(Stage.ANCHOR)) {
2024-07-18 16:50:23 +02:00
// the historic archor position is only an approximation for the price
2024-07-09 18:00:39 +02:00
int24 tick = token0isWeth ? -1 * (position.tickLower + ANCHOR_SPACING): position.tickUpper - ANCHOR_SPACING;
2024-08-12 19:07:07 +02:00
currentPrice = priceAtTick(tick);
2024-06-09 16:06:41 +02:00
}
}
}
2024-07-06 18:36:13 +02:00
2024-06-09 16:06:41 +02:00
// Transfer fees to the fee destination
2024-07-06 18:36:13 +02:00
// and record transaction totals
2024-06-09 16:06:41 +02:00
if (fee0 > 0) {
2024-07-06 18:36:13 +02:00
if (token0isWeth) {
IERC20(address(weth)).transfer(feeDestination, fee0);
2024-07-18 16:50:23 +02:00
_recordVolumeAndPrice(currentPrice, fee0);
2024-07-06 18:36:13 +02:00
} else {
IERC20(address(harb)).transfer(feeDestination, fee0);
}
2024-06-09 16:06:41 +02:00
}
if (fee1 > 0) {
2024-07-06 18:36:13 +02:00
if (token0isWeth) {
IERC20(address(harb)).transfer(feeDestination, fee1);
} else {
IERC20(address(weth)).transfer(feeDestination, fee1);
2024-07-18 16:50:23 +02:00
_recordVolumeAndPrice(currentPrice, fee1);
2024-07-06 18:36:13 +02:00
}
2024-06-09 16:06:41 +02:00
}
}
2024-06-07 12:33:20 +02:00
function _isPriceStable(int24 currentTick) internal view returns (bool) {
2024-06-07 11:22:22 +02:00
uint32[] memory secondsAgo = new uint32[](2);
2024-07-18 16:50:23 +02:00
secondsAgo[0] = PRICE_STABILITY_INTERVAL; // 5 minutes ago
2024-06-07 11:22:22 +02:00
secondsAgo[1] = 0; // current block timestamp
2024-07-09 18:00:39 +02:00
int56 tickCumulativeDiff;
int24 averageTick;
try pool.observe(secondsAgo) returns (int56[] memory tickCumulatives, uint160[] memory) {
tickCumulativeDiff = tickCumulatives[1] - tickCumulatives[0];
2024-07-18 16:50:23 +02:00
averageTick = int24(tickCumulativeDiff / int56(int32(PRICE_STABILITY_INTERVAL)));
2024-07-09 18:00:39 +02:00
} catch {
2024-07-18 16:50:23 +02:00
// try with a higher timeframe
secondsAgo[0] = PRICE_STABILITY_INTERVAL * 200;
(int56[] memory tickCumulatives, ) = pool.observe(secondsAgo);
tickCumulativeDiff = tickCumulatives[1] - tickCumulatives[0];
averageTick = int24(tickCumulativeDiff / int56(int32(PRICE_STABILITY_INTERVAL)));
2024-07-09 18:00:39 +02:00
}
2024-06-07 11:22:22 +02:00
return (currentTick >= averageTick - MAX_TICK_DEVIATION && currentTick <= averageTick + MAX_TICK_DEVIATION);
}
2024-07-18 07:35:39 +02:00
/// @notice Adjusts liquidity positions in response to an increase or decrease in the Harberg token's price.
2024-07-16 20:47:06 +02:00
/// @dev This function should be called when significant price movement is detected. It recalibrates the liquidity ranges to align with the new market conditions.
function recenter() external {
2024-04-03 21:43:12 +02:00
// Fetch the current tick from the Uniswap V3 pool
2024-07-16 19:47:39 +02:00
(, int24 currentTick, , , , , ) = pool.slot0();
2024-06-07 11:22:22 +02:00
// check slippage with oracle
2024-06-07 12:33:20 +02:00
require(_isPriceStable(currentTick), "price deviated from oracle");
2024-04-03 21:43:12 +02:00
2024-07-16 20:47:06 +02:00
bool isUp = false;
// check how price moved
2024-04-03 21:43:12 +02:00
if (positions[Stage.ANCHOR].liquidity > 0) {
2024-07-16 20:47:06 +02:00
// get the anchor position
2024-04-03 21:43:12 +02:00
int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper;
2024-06-09 16:06:41 +02:00
2024-04-03 21:43:12 +02:00
// center tick can be calculated positive and negative numbers the same
2024-07-09 18:00:39 +02:00
int24 centerTick = token0isWeth ? anchorTickLower + ANCHOR_SPACING : anchorTickUpper - ANCHOR_SPACING;
2024-04-29 06:27:28 +02:00
uint256 minAmplitude = uint256(uint24((anchorTickUpper - anchorTickLower) * 3 / 20));
2024-04-03 21:43:12 +02:00
// Determine the correct comparison direction based on token0isWeth
2024-07-16 20:47:06 +02:00
isUp = token0isWeth ? currentTick < centerTick : currentTick > centerTick;
2024-04-29 06:27:28 +02:00
bool isEnough = SignedMath.abs(currentTick - centerTick) > minAmplitude;
2024-04-03 21:43:12 +02:00
// Check Conditions
2024-07-16 20:47:06 +02:00
require(isEnough, "amplitude not reached.");
2024-04-03 21:43:12 +02:00
}
2024-07-16 20:47:06 +02:00
// take out all old positions
2024-06-09 16:06:41 +02:00
_scrape();
2024-07-16 20:47:06 +02:00
if (isUp) {
harb.setPreviousTotalSupply(harb.totalSupply());
}
// set new positions
2024-07-16 19:47:39 +02:00
_set(currentTick);
}
}