The discovery position was incorrectly calculating ETH amount instead of KRAIKEN amount when determining how much to subtract from outstanding supply. This caused the floor position to be placed at extreme ticks (141k+) instead of bordering the anchor position. When token0isWeth=true: - Before: discoveryAmount = getAmount0 (ETH amount) - After: discoveryAmount = getAmount1 (KRAIKEN amount) This ensures the outstanding supply calculation properly excludes all KRAIKEN tokens locked in liquidity positions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
278 lines
No EOL
11 KiB
Solidity
278 lines
No EOL
11 KiB
Solidity
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
pragma solidity ^0.8.19;
|
|
|
|
import "@uniswap-v3-periphery/libraries/PositionKey.sol";
|
|
import "@uniswap-v3-core/interfaces/IUniswapV3Pool.sol";
|
|
import "@aperture/uni-v3-lib/PoolAddress.sol";
|
|
import "@aperture/uni-v3-lib/CallbackValidation.sol";
|
|
import "@openzeppelin/token/ERC20/IERC20.sol";
|
|
import "./interfaces/IWETH9.sol";
|
|
import {Kraiken} from "./Kraiken.sol";
|
|
import {Optimizer} from "./Optimizer.sol";
|
|
import "./abstracts/ThreePositionStrategy.sol";
|
|
import "./abstracts/PriceOracle.sol";
|
|
|
|
/**
|
|
* @title LiquidityManager
|
|
* @notice Manages liquidity provisioning on Uniswap V3 using the three-position anti-arbitrage strategy
|
|
* @dev Inherits from modular contracts for better separation of concerns and testability
|
|
*
|
|
* Key features:
|
|
* - Three-position anti-arbitrage strategy (ANCHOR, DISCOVERY, FLOOR)
|
|
* - Dynamic parameter adjustment via Optimizer contract
|
|
* - Asymmetric slippage profile prevents profitable arbitrage
|
|
* - Exclusive minting rights for KRAIKEN token
|
|
*
|
|
* Price Validation:
|
|
* - 5-minute TWAP with 50-tick tolerance
|
|
* - Prevents oracle manipulation attacks
|
|
*/
|
|
contract LiquidityManager is ThreePositionStrategy, PriceOracle {
|
|
/// @notice Uniswap V3 fee tier (1%) - 10,000 basis points
|
|
uint24 internal constant FEE = uint24(10_000);
|
|
|
|
/// @notice Immutable contract references
|
|
address private immutable factory;
|
|
IWETH9 private immutable weth;
|
|
Kraiken private immutable harb;
|
|
Optimizer private immutable optimizer;
|
|
IUniswapV3Pool private immutable pool;
|
|
bool private immutable token0isWeth;
|
|
PoolKey private poolKey;
|
|
|
|
/// @notice Access control and fee management
|
|
address private recenterAccess;
|
|
address public feeDestination;
|
|
|
|
/// @notice Custom errors
|
|
error ZeroAddressInSetter();
|
|
error AddressAlreadySet();
|
|
|
|
/// @notice Access control modifier
|
|
modifier onlyFeeDestination() {
|
|
require(msg.sender == address(feeDestination), "only callable by feeDestination");
|
|
_;
|
|
}
|
|
|
|
/// @notice Constructor initializes all contract references and pool configuration
|
|
/// @param _factory The address of the Uniswap V3 factory
|
|
/// @param _WETH9 The address of the WETH contract
|
|
/// @param _harb The address of the Kraiken token contract
|
|
/// @param _optimizer The address of the optimizer contract
|
|
constructor(address _factory, address _WETH9, address _harb, address _optimizer) {
|
|
factory = _factory;
|
|
weth = IWETH9(_WETH9);
|
|
poolKey = PoolAddress.getPoolKey(_WETH9, _harb, FEE);
|
|
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
|
|
harb = Kraiken(_harb);
|
|
token0isWeth = _WETH9 < _harb;
|
|
optimizer = Optimizer(_optimizer);
|
|
}
|
|
|
|
/// @notice Callback function for Uniswap V3 mint operations
|
|
/// @param amount0Owed Amount of token0 owed for the liquidity provision
|
|
/// @param amount1Owed Amount of token1 owed for the liquidity provision
|
|
function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
|
|
CallbackValidation.verifyCallback(factory, poolKey);
|
|
|
|
// Handle HARB minting
|
|
uint256 harbPulled = token0isWeth ? amount1Owed : amount0Owed;
|
|
harb.mint(harbPulled);
|
|
|
|
// Handle WETH conversion
|
|
uint256 ethOwed = token0isWeth ? amount0Owed : amount1Owed;
|
|
if (weth.balanceOf(address(this)) < ethOwed) {
|
|
weth.deposit{value: address(this).balance}();
|
|
}
|
|
|
|
// Transfer tokens to pool
|
|
if (amount0Owed > 0) IERC20(poolKey.token0).transfer(msg.sender, amount0Owed);
|
|
if (amount1Owed > 0) IERC20(poolKey.token1).transfer(msg.sender, amount1Owed);
|
|
}
|
|
|
|
/// @notice Sets the fee destination address (can only be called once)
|
|
/// @param feeDestination_ The address that will receive trading fees
|
|
function setFeeDestination(address feeDestination_) external {
|
|
if (address(0) == feeDestination_) revert ZeroAddressInSetter();
|
|
if (feeDestination != address(0)) revert AddressAlreadySet();
|
|
feeDestination = feeDestination_;
|
|
}
|
|
|
|
/// @notice Sets recenter access for testing/emergency purposes
|
|
/// @param addr Address to grant recenter access
|
|
function setRecenterAccess(address addr) external onlyFeeDestination {
|
|
recenterAccess = addr;
|
|
}
|
|
|
|
/// @notice Revokes recenter access
|
|
function revokeRecenterAccess() external onlyFeeDestination {
|
|
recenterAccess = address(0);
|
|
}
|
|
|
|
/// @notice Adjusts liquidity positions in response to price movements
|
|
/// @return isUp True if price moved up (relative to token ordering)
|
|
function recenter() external returns (bool isUp) {
|
|
(, int24 currentTick,,,,,) = pool.slot0();
|
|
|
|
// Validate access and price stability
|
|
if (recenterAccess != address(0)) {
|
|
require(msg.sender == recenterAccess, "access denied");
|
|
} else {
|
|
require(_isPriceStable(currentTick), "price deviated from oracle");
|
|
}
|
|
|
|
// Check if price movement is sufficient for recentering
|
|
isUp = false;
|
|
if (positions[Stage.ANCHOR].liquidity > 0) {
|
|
int24 anchorTickLower = positions[Stage.ANCHOR].tickLower;
|
|
int24 anchorTickUpper = positions[Stage.ANCHOR].tickUpper;
|
|
int24 centerTick = anchorTickLower + (anchorTickUpper - anchorTickLower) / 2;
|
|
|
|
bool isEnough;
|
|
(isUp, isEnough) = _validatePriceMovement(currentTick, centerTick, TICK_SPACING, token0isWeth);
|
|
require(isEnough, "amplitude not reached.");
|
|
}
|
|
|
|
// Remove all existing positions and collect fees
|
|
_scrapePositions();
|
|
|
|
// Update total supply tracking if price moved up
|
|
if (isUp) {
|
|
harb.setPreviousTotalSupply(harb.totalSupply());
|
|
}
|
|
|
|
// Get optimizer parameters and set new positions
|
|
try optimizer.getLiquidityParams() returns (
|
|
uint256 capitalInefficiency,
|
|
uint256 anchorShare,
|
|
uint24 anchorWidth,
|
|
uint256 discoveryDepth
|
|
) {
|
|
// Clamp parameters to valid ranges
|
|
PositionParams memory params = PositionParams({
|
|
capitalInefficiency: (capitalInefficiency > 10 ** 18) ? 10 ** 18 : capitalInefficiency,
|
|
anchorShare: (anchorShare > 10 ** 18) ? 10 ** 18 : anchorShare,
|
|
anchorWidth: (anchorWidth > 100) ? 100 : anchorWidth,
|
|
discoveryDepth: (discoveryDepth > 10 ** 18) ? 10 ** 18 : discoveryDepth
|
|
});
|
|
|
|
_setPositions(currentTick, params);
|
|
} catch {
|
|
// Fallback to default parameters if optimizer fails
|
|
PositionParams memory defaultParams = PositionParams({
|
|
capitalInefficiency: 5 * 10 ** 17, // 50%
|
|
anchorShare: 5 * 10 ** 17, // 50%
|
|
anchorWidth: 50, // 50%
|
|
discoveryDepth: 5 * 10 ** 17 // 50%
|
|
});
|
|
|
|
_setPositions(currentTick, defaultParams);
|
|
}
|
|
}
|
|
|
|
/// @notice Removes all positions and collects fees
|
|
function _scrapePositions() internal {
|
|
uint256 fee0 = 0;
|
|
uint256 fee1 = 0;
|
|
uint256 currentPrice;
|
|
|
|
for (uint256 i = uint256(Stage.FLOOR); i <= uint256(Stage.DISCOVERY); i++) {
|
|
TokenPosition storage position = positions[Stage(i)];
|
|
if (position.liquidity > 0) {
|
|
// Burn liquidity and collect tokens + fees
|
|
(uint256 amount0, uint256 amount1) = pool.burn(
|
|
position.tickLower,
|
|
position.tickUpper,
|
|
position.liquidity
|
|
);
|
|
|
|
(uint256 collected0, uint256 collected1) = pool.collect(
|
|
address(this),
|
|
position.tickLower,
|
|
position.tickUpper,
|
|
type(uint128).max,
|
|
type(uint128).max
|
|
);
|
|
|
|
// Calculate fees
|
|
fee0 += collected0 - amount0;
|
|
fee1 += collected1 - amount1;
|
|
|
|
// Record price from anchor position for VWAP
|
|
if (i == uint256(Stage.ANCHOR)) {
|
|
int24 tick = position.tickLower + ((position.tickUpper - position.tickLower) / 2);
|
|
currentPrice = _priceAtTick(token0isWeth ? -1 * tick : tick);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Transfer fees and record volume for VWAP
|
|
if (fee0 > 0) {
|
|
if (token0isWeth) {
|
|
IERC20(address(weth)).transfer(feeDestination, fee0);
|
|
_recordVolumeAndPrice(currentPrice, fee0);
|
|
} else {
|
|
IERC20(address(harb)).transfer(feeDestination, fee0);
|
|
}
|
|
}
|
|
|
|
if (fee1 > 0) {
|
|
if (token0isWeth) {
|
|
IERC20(address(harb)).transfer(feeDestination, fee1);
|
|
} else {
|
|
IERC20(address(weth)).transfer(feeDestination, fee1);
|
|
_recordVolumeAndPrice(currentPrice, fee1);
|
|
}
|
|
}
|
|
|
|
// Burn any remaining HARB tokens
|
|
harb.burn(harb.balanceOf(address(this)));
|
|
}
|
|
|
|
/// @notice Allow contract to receive ETH
|
|
receive() external payable {}
|
|
|
|
// ========================================
|
|
// ABSTRACT FUNCTION IMPLEMENTATIONS
|
|
// ========================================
|
|
|
|
/// @notice Implementation of abstract function from PriceOracle
|
|
function _getPool() internal view override returns (IUniswapV3Pool) {
|
|
return pool;
|
|
}
|
|
|
|
/// @notice Implementation of abstract function from ThreePositionStrategy
|
|
function _getHarbToken() internal view override returns (address) {
|
|
return address(harb);
|
|
}
|
|
|
|
/// @notice Implementation of abstract function from ThreePositionStrategy
|
|
function _getWethToken() internal view override returns (address) {
|
|
return address(weth);
|
|
}
|
|
|
|
/// @notice Implementation of abstract function from ThreePositionStrategy
|
|
function _isToken0Weth() internal view override returns (bool) {
|
|
return token0isWeth;
|
|
}
|
|
|
|
/// @notice Implementation of abstract function from ThreePositionStrategy
|
|
function _mintPosition(Stage stage, int24 tickLower, int24 tickUpper, uint128 liquidity) internal override {
|
|
pool.mint(address(this), tickLower, tickUpper, liquidity, abi.encode(poolKey));
|
|
positions[stage] = TokenPosition({
|
|
liquidity: liquidity,
|
|
tickLower: tickLower,
|
|
tickUpper: tickUpper
|
|
});
|
|
}
|
|
|
|
/// @notice Implementation of abstract function from ThreePositionStrategy
|
|
function _getEthBalance() internal view override returns (uint256) {
|
|
return address(this).balance + weth.balanceOf(address(this));
|
|
}
|
|
|
|
/// @notice Implementation of abstract function from ThreePositionStrategy
|
|
function _getOutstandingSupply() internal view override returns (uint256) {
|
|
return harb.outstandingSupply();
|
|
}
|
|
} |