harb/onchain/src/LiquidityManager.sol
johba 400ab325ed refactor: Complete project renaming from HARB/Harberger to KRAIKEN
- Updated all production code references from 'harb' to 'kraiken'
- Changed 'Harberger tax' references to 'self-assessed tax'
- Updated function names (_getHarbToken -> _getKraikenToken)
- Modified documentation and comments to reflect new branding
- Updated token symbol from HARB to KRAIKEN in tests
- Maintained backward compatibility with test variable names

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-19 11:05:08 +02:00

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 kraiken;
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 _kraiken The address of the Kraiken token contract
/// @param _optimizer The address of the optimizer contract
constructor(address _factory, address _WETH9, address _kraiken, address _optimizer) {
factory = _factory;
weth = IWETH9(_WETH9);
poolKey = PoolAddress.getPoolKey(_WETH9, _kraiken, FEE);
pool = IUniswapV3Pool(PoolAddress.computeAddress(factory, poolKey));
kraiken = Kraiken(_kraiken);
token0isWeth = _WETH9 < _kraiken;
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 KRAIKEN minting
uint256 kraikenPulled = token0isWeth ? amount1Owed : amount0Owed;
kraiken.mint(kraikenPulled);
// 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) {
kraiken.setPreviousTotalSupply(kraiken.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(kraiken)).transfer(feeDestination, fee0);
}
}
if (fee1 > 0) {
if (token0isWeth) {
IERC20(address(kraiken)).transfer(feeDestination, fee1);
} else {
IERC20(address(weth)).transfer(feeDestination, fee1);
_recordVolumeAndPrice(currentPrice, fee1);
}
}
// Burn any remaining KRAIKEN tokens
kraiken.burn(kraiken.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 _getKraikenToken() internal view override returns (address) {
return address(kraiken);
}
/// @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 kraiken.outstandingSupply();
}
}