Description:
Multi-signature wallet contract requiring multiple confirmations for transaction execution.
Blockchain: Ethereum
Source Code: View Code On The Blockchain
Solidity Source Code:
{{
"language": "Solidity",
"sources": {
"src/periphery/SimpleLens.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
import { MultiPositionManager } from "../MultiPositionManager.sol";
import { IMultiPositionManager } from "../interfaces/IMultiPositionManager.sol";
import { ILiquidityStrategy } from "../strategies/ILiquidityStrategy.sol";
import { IPoolManager } from "v4-core/interfaces/IPoolManager.sol";
import { StateLibrary } from "v4-core/libraries/StateLibrary.sol";
import { IHooks } from "v4-core/interfaces/IHooks.sol";
import { PoolId, PoolIdLibrary } from "v4-core/types/PoolId.sol";
import { PoolKey } from "v4-core/types/PoolKey.sol";
import { Currency } from "v4-core/types/Currency.sol";
import { TickMath } from "v4-core/libraries/TickMath.sol";
import { FullMath } from "v4-core/libraries/FullMath.sol";
import { DepositRatioLib } from "../libraries/DepositRatioLib.sol";
import { LiquidityAmounts } from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { PoolManagerUtils } from "../libraries/PoolManagerUtils.sol";
import { RebalanceLogic } from "../libraries/RebalanceLogic.sol";
import { PositionLogic } from "../libraries/PositionLogic.sol";
import { WithdrawLogic } from "../libraries/WithdrawLogic.sol";
import { SimpleLensInMin } from "../libraries/SimpleLens/SimpleLensInMin.sol";
import { SimpleLensRatioUtils } from "../libraries/SimpleLens/SimpleLensRatioUtils.sol";
/**
* @title SimpleLens
* @notice Simplified read-only contract for previewing MultiPositionManager withdrawals
*/
contract SimpleLens {
using StateLibrary for IPoolManager;
using PoolIdLibrary for PoolKey;
// Immutable storage
IPoolManager public immutable poolManager;
// Custom errors
error NoStrategySpecified();
error MaxSlippageExceeded();
error RatioMustBeLessThanOrEqualToOne();
error GenerateRangesFailed();
error CalculateDensitiesFailed();
uint256 constant PRECISION = 1e18;
constructor(IPoolManager _poolManager) {
poolManager = _poolManager;
}
// Use structs from SimpleLensInMin library to avoid duplication
// DensityCalcParams, InMinCalcData, and InMinRebalanceParams are now in SimpleLensInMin
struct DensityCalcContext {
int24[] lowerTicks;
int24[] upperTicks;
int24 currentTick;
int24 resolvedCenterTick;
int24 tickSpacing;
}
struct PriceData {
uint160 sqrtPriceX96;
uint256 price;
int24 tick;
}
// PreviewData and WithdrawPreviewResult structs moved to SimpleLensInMin library
struct Path3Params {
uint256 amount0Desired;
uint256 amount1Desired;
uint256 sharesWithdrawn;
uint256[2][] outMin;
bool previewRebalance;
IMultiPositionManager.RebalanceParams rebalanceParams;
}
// RebalancePreview struct moved to SimpleLensInMin library
struct InMinParams {
MultiPositionManager manager;
IMultiPositionManager.Range[] baseRanges;
address strategyAddress;
int24 centerTick;
uint24 ticksLeft;
uint24 ticksRight;
int24 limitWidth;
uint256 weight0;
uint256 weight1;
bool useCarpet;
uint256 maxSlippage;
}
struct PreviewLiquidityParams {
IMultiPositionManager.Range[] baseRanges;
uint256 total0;
uint256 total1;
address strategyAddress;
int24 centerTick;
uint24 ticksLeft;
uint24 ticksRight;
uint256 weight0;
uint256 weight1;
bool useCarpet;
}
/**
* @notice Get position statistics for a MultiPositionManager
*/
function getPositionStats(MultiPositionManager manager)
external
view
returns (SimpleLensRatioUtils.PositionStats[] memory stats)
{
return SimpleLensRatioUtils.getPositionStats(manager);
}
/**
* @notice Preview a single token withdrawal - exactly mirrors withdrawSingleToken logic
* @param manager The MultiPositionManager contract
* @param amount0Desired Amount of token0 to withdraw
* @param amount1Desired Amount of token1 to withdraw
* @param maxSlippage Maximum slippage for outMin calculation
* @param previewRebalance If true, returns expected positions after rebalance
* @param rebalanceParams Parameters for rebalance preview
* @return sharesWithdrawn Amount of shares withdrawn
* @return positionSharesBurned Amount of position shares burned
* @return outMin Minimum output amounts for withdrawal
* @return rebalancePreview Full rebalance preview (empty if previewRebalance false)
* @return isFullBurn True if all positions will be burned (positionSharesBurned == totalSupply)
* @return outMinForRebalance Empty array if full burn, otherwise [0,0] array sized for remaining positions
*/
function previewWithdrawCustom(
MultiPositionManager manager,
uint256 amount0Desired,
uint256 amount1Desired,
uint256 maxSlippage,
bool previewRebalance,
IMultiPositionManager.RebalanceParams memory rebalanceParams
) external view returns (
uint256 sharesWithdrawn,
uint256 positionSharesBurned,
uint256[2][] memory outMin,
SimpleLensInMin.RebalancePreview memory rebalancePreview,
bool isFullBurn,
uint256[2][] memory outMinForRebalance
) {
// Call library function for all preview logic
SimpleLensInMin.WithdrawPreviewResult memory result =
SimpleLensInMin.previewWithdrawCustomInternal(
manager,
amount0Desired,
amount1Desired,
maxSlippage,
previewRebalance,
rebalanceParams
);
return (
result.sharesWithdrawn,
result.positionSharesBurned,
result.outMin,
result.rebalancePreview,
result.isFullBurn,
result.outMinForRebalance
);
}
function _calculatePositionStats(
IMultiPositionManager.Position memory position,
uint128 liquidity,
uint160 sqrtPriceX96
) internal pure returns (SimpleLensRatioUtils.PositionStats memory stat) {
stat.tickLower = position.lowerTick;
stat.tickUpper = position.upperTick;
stat.sqrtPriceLower = TickMath.getSqrtPriceAtTick(position.lowerTick);
stat.sqrtPriceUpper = TickMath.getSqrtPriceAtTick(position.upperTick);
stat.liquidity = liquidity;
(stat.token0Quantity, stat.token1Quantity) = LiquidityAmounts.getAmountsForLiquidity(
sqrtPriceX96,
stat.sqrtPriceLower,
stat.sqrtPriceUpper,
liquidity
);
// Calculate value in token1
stat.valueInToken1 = stat.token1Quantity + FullMath.mulDiv(
stat.token0Quantity,
uint256(sqrtPriceX96) * uint256(sqrtPriceX96),
1 << 192
);
}
// Consolidated pool state helpers - reduces redundant code
function _getPoolState(PoolKey memory poolKey) internal view returns (uint160 sqrtPriceX96, int24 tick) {
(sqrtPriceX96, tick, , ) = poolManager.getSlot0(poolKey.toId());
}
/**
* @notice Preview the result of rebalanceWithStrategy with limitWidth and carpet positions
* @param manager The MultiPositionManager contract
* @param strategyAddress Address of strategy to use (generates ranges)
* @param centerTick Center tick for distribution
* @param ticksLeft Number of ticks to the left of center
* @param ticksRight Number of ticks to the right of center
* @param limitWidth Width of limit positions (0 for no limit positions)
* @param weight0 Weight for token0 (0 for proportional, otherwise explicit weight)
* @param weight1 Weight for token1 (0 for proportional, otherwise explicit weight)
* @param useCarpet Whether to include carpet positions
* @param maxSlippage Maximum slippage in basis points (10000 = 100%)
* @return preview Detailed preview of the rebalance operation
* @return outMin Minimum amounts for withdrawing from old positions
* @return inMin Minimum amounts for depositing to new positions
*/
function previewRebalanceWithStrategyAndCarpet(
MultiPositionManager manager,
address strategyAddress,
int24 centerTick,
uint24 ticksLeft,
uint24 ticksRight,
int24 limitWidth,
uint256 weight0,
uint256 weight1,
bool useCarpet,
uint256 maxSlippage
) public view returns (
SimpleLensInMin.RebalancePreview memory preview,
uint256[2][] memory outMin,
uint256[2][] memory inMin
) {
SimpleLensInMin.RebalancePreviewParams memory params = SimpleLensInMin.RebalancePreviewParams({
strategyAddress: strategyAddress,
centerTick: centerTick,
ticksLeft: ticksLeft,
ticksRight: ticksRight,
limitWidth: limitWidth,
weight0: weight0,
weight1: weight1,
useCarpet: useCarpet,
swap: false, // No swap for regular rebalance
maxSlippage: maxSlippage
});
// Generate preview in helper to reduce stack depth
preview = _generateCompletePreview(manager, params);
// Get outMin and inMin for slippage protection
(outMin, inMin) = _getOutAndInMinForPreview(manager, params);
}
/**
* @notice Preview the result of rebalanceSwap (with swap) with limitWidth and carpet positions
* @param manager The MultiPositionManager contract
* @param strategyAddress Address of strategy to use (generates ranges)
* @param centerTick Center tick for distribution
* @param ticksLeft Number of ticks to the left of center
* @param ticksRight Number of ticks to the right of center
* @param limitWidth Width of limit positions (0 for no limit positions)
* @param weight0 Weight for token0 (0 for proportional, otherwise explicit weight)
* @param weight1 Weight for token1 (0 for proportional, otherwise explicit weight)
* @param useCarpet Whether to include carpet positions
* @param maxSlippage Maximum slippage in basis points (10000 = 100%)
* @return preview Detailed preview of the rebalance operation including swap
* @return outMin Minimum amounts for withdrawing from old positions
* @return inMin Minimum amounts for depositing to new positions
* @return swapParams Swap parameters (direction, amount, target weights)
*/
function previewRebalanceSwapWithStrategyAndCarpet(
MultiPositionManager manager,
address strategyAddress,
int24 centerTick,
uint24 ticksLeft,
uint24 ticksRight,
int24 limitWidth,
uint256 weight0,
uint256 weight1,
bool useCarpet,
uint256 maxSlippage
) public view returns (
SimpleLensInMin.RebalancePreview memory preview,
uint256[2][] memory outMin,
uint256[2][] memory inMin,
SimpleLensRatioUtils.SwapParams memory swapParams
) {
SimpleLensInMin.RebalancePreviewParams memory params = SimpleLensInMin.RebalancePreviewParams({
strategyAddress: strategyAddress,
centerTick: centerTick,
ticksLeft: ticksLeft,
ticksRight: ticksRight,
limitWidth: limitWidth,
weight0: weight0,
weight1: weight1,
useCarpet: useCarpet,
swap: true, // Always swap for this function
maxSlippage: maxSlippage
});
// Generate preview with swap in helper to reduce stack depth
preview = _generateCompleteSwapPreview(manager, params);
// Get outMin and inMin for slippage protection
(outMin, inMin) = _getOutAndInMinForPreview(manager, params);
// Construct swap parameters from preview
swapParams = SimpleLensRatioUtils.SwapParams({
swapToken0: preview.swapToken0,
swapAmount: preview.swapAmount,
weight0: params.weight0,
weight1: params.weight1
});
}
function _generateCompleteSwapPreview(
MultiPositionManager manager,
SimpleLensInMin.RebalancePreviewParams memory params
) private view returns (SimpleLensInMin.RebalancePreview memory preview) {
preview.strategy = params.strategyAddress;
preview.ticksLeft = params.ticksLeft;
preview.ticksRight = params.ticksRight;
// Generate base liquidities with swap simulation
(uint256 adj0, uint256 adj1) = _generateSwapBaseLiquidities(manager, params, preview);
// Add limit positions if needed
if (params.limitWidth > 0) {
_addSwapLimitPositions(manager, params, adj0, adj1, preview);
}
// Calculate expected totals
_calculateExpectedTotals(manager, preview.ranges, preview);
}
function _generateSwapBaseLiquidities(
MultiPositionManager manager,
SimpleLensInMin.RebalancePreviewParams memory params,
SimpleLensInMin.RebalancePreview memory preview
) private view returns (uint256 adjustedTotal0, uint256 adjustedTotal1) {
// Get current amounts
uint256 t0;
uint256 t1;
(t0, t1, , ) = manager.getTotalAmounts();
// Simulate swap and calculate optimal swap amount
{
(adjustedTotal0, adjustedTotal1) = SimpleLensRatioUtils.simulateSwapForRebalance(
manager, t0, t1, params.weight0, params.weight1
);
// Calculate swap details
if (adjustedTotal0 < t0) {
// Swapping token0 for token1
preview.swapToken0 = true;
preview.swapAmount = t0 - adjustedTotal0;
preview.expectedAmountOut = adjustedTotal1 - t1;
} else if (adjustedTotal1 < t1) {
// Swapping token1 for token0
preview.swapToken0 = false;
preview.swapAmount = t1 - adjustedTotal1;
preview.expectedAmountOut = adjustedTotal0 - t0;
} else {
// No swap needed
preview.swapToken0 = false;
preview.swapAmount = 0;
preview.expectedAmountOut = 0;
}
}
PoolKey memory poolKey = manager.poolKey();
(uint160 sqrtPriceX96, int24 currentTick, , ) = poolManager.getSlot0(poolKey.toId());
// Resolve center tick
int24 resolvedCenterTick = params.centerTick;
if (params.centerTick == type(int24).max) {
int24 compressed = currentTick / poolKey.tickSpacing;
if (currentTick < 0 && currentTick % poolKey.tickSpacing != 0) compressed--;
resolvedCenterTick = compressed * poolKey.tickSpacing;
}
preview.centerTick = resolvedCenterTick;
// Build context and generate ranges/liquidities
RebalanceLogic.StrategyContext memory ctx;
ctx.resolvedStrategy = params.strategyAddress;
ctx.center = resolvedCenterTick;
ctx.tLeft = params.ticksLeft;
ctx.tRight = params.ticksRight;
ctx.strategy = ILiquidityStrategy(params.strategyAddress);
ctx.weight0 = params.weight0;
ctx.weight1 = params.weight1;
ctx.useCarpet = params.useCarpet;
ctx.limitWidth = 0;
ctx.weightsAreProportional = (params.weight0 == 0 && params.weight1 == 0);
// Use same function as actual rebalance
(preview.ranges, preview.liquidities) = RebalanceLogic.generateRangesAndLiquiditiesWithPoolKey(
poolKey, poolManager, ctx, adjustedTotal0, adjustedTotal1
);
}
function _getOutAndInMinForPreview(
MultiPositionManager manager,
SimpleLensInMin.RebalancePreviewParams memory params
) private view returns (uint256[2][] memory outMin, uint256[2][] memory inMin) {
return SimpleLensInMin.getOutMinAndInMinForRebalance(
manager,
params.strategyAddress,
params.centerTick,
params.ticksLeft,
params.ticksRight,
params.limitWidth,
params.weight0,
params.weight1,
params.useCarpet,
params.swap,
params.maxSlippage
);
}
function _generateCompletePreview(
MultiPositionManager manager,
SimpleLensInMin.RebalancePreviewParams memory params
) private view returns (SimpleLensInMin.RebalancePreview memory preview) {
preview.strategy = params.strategyAddress;
preview.ticksLeft = params.ticksLeft;
preview.ticksRight = params.ticksRight;
// Generate base ranges and liquidities
uint256 total0;
uint256 total1;
(total0, total1) = _generateBasePreviewRanges(manager, params, preview);
// Add limit positions if limitWidth > 0
if (params.limitWidth > 0) {
_addPreviewLimitPositions(manager, params, total0, total1, preview);
}
// Calculate expected totals
_calculateExpectedTotals(manager, preview.ranges, preview);
}
function _generateBasePreviewRanges(
MultiPositionManager manager,
SimpleLensInMin.RebalancePreviewParams memory params,
SimpleLensInMin.RebalancePreview memory preview
) private view returns (uint256 total0, uint256 total1) {
(total0, total1, , ) = manager.getTotalAmounts();
PoolKey memory poolKey = manager.poolKey();
(uint160 sqrtPriceX96, int24 currentTick, , ) = poolManager.getSlot0(poolKey.toId());
// Resolve center tick
int24 resolvedCenterTick = params.centerTick;
if (params.centerTick == type(int24).max) {
int24 compressed = currentTick / poolKey.tickSpacing;
if (currentTick < 0 && currentTick % poolKey.tickSpacing != 0) compressed--;
resolvedCenterTick = compressed * poolKey.tickSpacing;
}
preview.centerTick = resolvedCenterTick;
// Build context and generate ranges/liquidities
RebalanceLogic.StrategyContext memory ctx;
ctx.resolvedStrategy = params.strategyAddress;
ctx.center = resolvedCenterTick;
ctx.tLeft = params.ticksLeft;
ctx.tRight = params.ticksRight;
ctx.strategy = ILiquidityStrategy(params.strategyAddress);
ctx.weight0 = params.weight0;
ctx.weight1 = params.weight1;
ctx.useCarpet = params.useCarpet;
ctx.limitWidth = 0;
ctx.weightsAreProportional = (params.weight0 == 0 && params.weight1 == 0);
(preview.ranges, preview.liquidities) = RebalanceLogic.generateRangesAndLiquiditiesWithPoolKey(
poolKey,
poolManager,
ctx,
total0,
total1
);
}
function _addPreviewLimitPositions(
MultiPositionManager manager,
SimpleLensInMin.RebalancePreviewParams memory params,
uint256 total0,
uint256 total1,
SimpleLensInMin.RebalancePreview memory preview
) private view {
bool weightsAreProportional = (params.weight0 == 0 && params.weight1 == 0);
PoolKey memory poolKey = manager.poolKey();
// Get limit ranges and sqrtPrice
IMultiPositionManager.Range memory lowerLimit;
IMultiPositionManager.Range memory upperLimit;
uint160 sqrtPriceX96;
{
int24 currentTick;
(sqrtPriceX96, currentTick, , ) = poolManager.getSlot0(poolKey.toId());
(lowerLimit, upperLimit) = PositionLogic.calculateLimitRanges(
params.limitWidth, preview.ranges, poolKey.tickSpacing, currentTick
);
}
// Expand arrays
uint256 baseLength = preview.ranges.length;
IMultiPositionManager.Range[] memory allRanges = new IMultiPositionManager.Range[](baseLength + 2);
uint128[] memory allLiquidities = new uint128[](baseLength + 2);
// Copy base data
for (uint256 i = 0; i < baseLength; i++) {
allRanges[i] = preview.ranges[i];
allLiquidities[i] = preview.liquidities[i];
}
allRanges[baseLength] = lowerLimit;
allRanges[baseLength + 1] = upperLimit;
// Calculate limit liquidities for explicit weights
if (!weightsAreProportional) {
(uint256 consumed0, uint256 consumed1) = _calculateConsumedTokens(
preview.ranges, preview.liquidities, sqrtPriceX96
);
uint256 remainder0 = total0 > consumed0 ? total0 - consumed0 : 0;
uint256 remainder1 = total1 > consumed1 ? total1 - consumed1 : 0;
if (lowerLimit.lowerTick != lowerLimit.upperTick && remainder1 > 0) {
allLiquidities[baseLength] = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(lowerLimit.lowerTick),
TickMath.getSqrtPriceAtTick(lowerLimit.upperTick),
0, remainder1
);
}
if (upperLimit.lowerTick != upperLimit.upperTick && remainder0 > 0) {
allLiquidities[baseLength + 1] = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(upperLimit.lowerTick),
TickMath.getSqrtPriceAtTick(upperLimit.upperTick),
remainder0, 0
);
}
}
preview.ranges = allRanges;
preview.liquidities = allLiquidities;
}
function _addSwapLimitPositions(
MultiPositionManager manager,
SimpleLensInMin.RebalancePreviewParams memory params,
uint256 adjustedTotal0,
uint256 adjustedTotal1,
SimpleLensInMin.RebalancePreview memory preview
) private view {
bool weightsAreProportional = (params.weight0 == 0 && params.weight1 == 0);
PoolKey memory poolKey = manager.poolKey();
// Get limit ranges and sqrtPrice
IMultiPositionManager.Range memory lowerLimit;
IMultiPositionManager.Range memory upperLimit;
uint160 sqrtPriceX96;
{
int24 currentTick;
(sqrtPriceX96, currentTick, , ) = poolManager.getSlot0(poolKey.toId());
(lowerLimit, upperLimit) = PositionLogic.calculateLimitRanges(
params.limitWidth, preview.ranges, poolKey.tickSpacing, currentTick
);
}
// Expand arrays
uint256 baseLength = preview.ranges.length;
IMultiPositionManager.Range[] memory allRanges = new IMultiPositionManager.Range[](baseLength + 2);
uint128[] memory allLiquidities = new uint128[](baseLength + 2);
// Copy base data
for (uint256 i = 0; i < baseLength; i++) {
allRanges[i] = preview.ranges[i];
allLiquidities[i] = preview.liquidities[i];
}
allRanges[baseLength] = lowerLimit;
allRanges[baseLength + 1] = upperLimit;
// Calculate limit liquidities for explicit weights
if (!weightsAreProportional) {
(uint256 consumed0, uint256 consumed1) = _calculateConsumedTokens(
preview.ranges, preview.liquidities, sqrtPriceX96
);
uint256 remainder0 = adjustedTotal0 > consumed0 ? adjustedTotal0 - consumed0 : 0;
uint256 remainder1 = adjustedTotal1 > consumed1 ? adjustedTotal1 - consumed1 : 0;
if (lowerLimit.lowerTick != lowerLimit.upperTick && remainder1 > 0) {
allLiquidities[baseLength] = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(lowerLimit.lowerTick),
TickMath.getSqrtPriceAtTick(lowerLimit.upperTick),
0, remainder1
);
}
if (upperLimit.lowerTick != upperLimit.upperTick && remainder0 > 0) {
allLiquidities[baseLength + 1] = LiquidityAmounts.getLiquidityForAmounts(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(upperLimit.lowerTick),
TickMath.getSqrtPriceAtTick(upperLimit.upperTick),
remainder0, 0
);
}
}
preview.ranges = allRanges;
preview.liquidities = allLiquidities;
}
function _calculateExpectedTotals(
MultiPositionManager manager,
IMultiPositionManager.Range[] memory allRanges,
SimpleLensInMin.RebalancePreview memory preview
) private view {
_calculateExpectedTotalsWithPoolKey(manager.poolKey(), allRanges, preview);
}
function _generateRangesFromStrategyWithPoolKey(
PoolKey memory poolKey,
address strategyAddress,
int24 centerTick,
uint24 ticksLeft,
uint24 ticksRight,
bool useCarpet
) private view returns (IMultiPositionManager.Range[] memory) {
return SimpleLensRatioUtils.generateRangesFromStrategyWithPoolKey(
poolManager,
poolKey,
strategyAddress,
centerTick,
ticksLeft,
ticksRight,
useCarpet
);
}
// PoolKey-based version for use without MultiPositionManager
function _calculateExpectedTotalsWithPoolKey(
PoolKey memory poolKey,
IMultiPositionManager.Range[] memory allRanges,
SimpleLensInMin.RebalancePreview memory preview
) private view {
// Create stats for ALL positions (base + limit)
preview.expectedPositions = new SimpleLensRatioUtils.PositionStats[](preview.ranges.length);
preview.expectedTotal0 = 0;
preview.expectedTotal1 = 0;
(uint160 sqrtPriceX96, ) = _getPoolState(poolKey);
// Calculate stats for all positions including limit positions
for (uint256 i = 0; i < preview.ranges.length; i++) {
IMultiPositionManager.Position memory pos = IMultiPositionManager.Position({
poolKey: poolKey,
lowerTick: preview.ranges[i].lowerTick,
upperTick: preview.ranges[i].upperTick
});
preview.expectedPositions[i] = _calculatePositionStats(
pos,
preview.liquidities[i],
sqrtPriceX96
);
preview.expectedTotal0 += preview.expectedPositions[i].token0Quantity;
preview.expectedTotal1 += preview.expectedPositions[i].token1Quantity;
}
}
/**
* @notice Calculate minimum output amounts for withdrawal with slippage protection
* @param pos MultiPositionManager address
* @param shares Number of shares to burn
* @param maxSlippage Maximum slippage in basis points (10000 = 100%)
* @return outMin Array of minimum amounts for each base and limit position
*/
function getOutMinForShares(
address pos,
uint256 shares,
uint256 maxSlippage
) external view returns (uint256[2][] memory outMin) {
// if (maxSlippage > 10000) revert MaxSlippageExceeded();
MultiPositionManager manager = MultiPositionManager(payable(pos));
return SimpleLensInMin.getOutMinForShares(manager, shares, maxSlippage);
}
// Helper struct to avoid stack too deep errors
struct InitialDepositParams {
address strategyAddress;
int24 centerTick;
uint24 ticksLeft;
uint24 ticksRight;
int24 limitWidth;
uint256 weight0;
uint256 weight1;
bool useCarpet;
bool isToken0;
uint256 amount;
uint256 maxSlippageBps;
}
// InitialDepositWithSwapParams and PreviewContext structs moved to SimpleLensInMin library
/**
* @dev Calculate consumed tokens by base positions
*/
function _calculateConsumedTokens(
IMultiPositionManager.Range[] memory baseRanges,
uint128[] memory baseLiquidities,
uint160 sqrtPriceX96
) private pure returns (uint256 consumedToken0, uint256 consumedToken1) {
for (uint256 i = 0; i < baseRanges.length; i++) {
(uint256 amt0, uint256 amt1) = LiquidityAmounts.getAmountsForLiquidity(
sqrtPriceX96,
TickMath.getSqrtPriceAtTick(baseRanges[i].lowerTick),
TickMath.getSqrtPriceAtTick(baseRanges[i].upperTick),
baseLiquidities[i]
);
consumedToken0 += amt0;
consumedToken1 += amt1;
}
}
/**
* @notice Calculate deposit amounts for initial position and preview the rebalance
* @param poolKey The PoolKey for the Uniswap V4 pool
* @param params Parameters for the initial deposit calculation
* @return otherAmount The amount of the other token needed
* @return inMin The minimum amounts for each position (for slippage protection)
* @return preview Detailed preview of the rebalance operation
*/
function getAmountsForInitialDepositAndPreviewRebalance(
PoolKey memory poolKey,
InitialDepositParams calldata params
) external view returns (uint256 otherAmount, uint256[2][] memory inMin, SimpleLensInMin.RebalancePreview memory preview) {
(uint160 sqrtPriceX96, int24 currentTick, , ) = poolManager.getSlot0(poolKey.toId());
// Resolve center tick
int24 resolvedCenterTick;
if (params.centerTick == type(int24).max) {
int24 compressed = currentTick / poolKey.tickSpacing;
if (currentTick < 0 && currentTick % poolKey.tickSpacing != 0) compressed--;
resolvedCenterTick = compressed * poolKey.tickSpacing;
} else {
resolvedCenterTick = params.centerTick;
}
// Calculate otherAmount
otherAmount = SimpleLensInMin.calculateOtherAmountInline(
poolManager,
poolKey,
SimpleLensInMin.CalculateOtherAmountParams({
strategyAddress: params.strategyAddress,
resolvedCenterTick: resolvedCenterTick,
ticksLeft: params.ticksLeft,
ticksRight: params.ticksRight,
limitWidth: params.limitWidth,
weight0: params.weight0,
weight1: params.weight1,
useCarpet: params.useCarpet,
sqrtPriceX96: sqrtPriceX96,
isToken0: params.isToken0,
amount: params.amount
})
);
// Generate all ranges and liquidities with limit positions
IMultiPositionManager.Range[] memory allRanges;
uint128[] memory allLiquidities;
(allRanges, allLiquidities, inMin) =
_generateAllRangesAndInMin(poolKey, params, resolvedCenterTick, currentTick, sqrtPriceX96, otherAmount);
// Build preview
preview.strategy = params.strategyAddress;
preview.centerTick = resolvedCenterTick;
preview.ticksLeft = params.ticksLeft;
preview.ticksRight = params.ticksRight;
preview.ranges = allRanges;
preview.liquidities = allLiquidities;
_calculateExpectedTotalsWithPoolKey(poolKey, allRanges, preview);
}
/**
* @notice Preview initial deposit and rebalance with custom amounts (both token0 and token1)
* @dev Similar to getAmountsForInitialDepositAndPreviewRebalance but accepts custom otherAmount
* instead of calculating it. Use this when you want to deposit a custom ratio that differs
* from the balanced ratio SimpleLens would recommend.
* @param poolKey The pool key
* @param params Initial deposit parameters (params.amount is one token, otherAmount is the other)
* @param otherAmount The amount of the other token (if params.isToken0=true, this is token1 amount)
* @return inMin Minimum input amounts for each base position
* @return preview Detailed preview of the rebalance operation with actual distribution
*/
function previewCustomInitialDepositAndRebalance(
PoolKey memory poolKey,
InitialDepositParams calldata params,
uint256 otherAmount
) external view returns (uint256[2][] memory inMin, SimpleLensInMin.RebalancePreview memory preview) {
(uint160 sqrtPriceX96, int24 currentTick, , ) = poolManager.getSlot0(poolKey.toId());
// Resolve center tick
int24 resolvedCenterTick;
if (params.centerTick == type(int24).max) {
int24 compressed = currentTick / poolKey.tickSpacing;
if (currentTick < 0 && currentTick % poolKey.tickSpacing != 0) compressed--;
resolvedCenterTick = compressed * poolKey.tickSpacing;
} else {
resolvedCenterTick = params.centerTick;
}
// Use provided otherAmount directly (no calculation)
// Generate all ranges and liquidities with limit positions
IMultiPositionManager.Range[] memory allRanges;
uint128[] memory allLiquidities;
(allRanges, allLiquidities, inMin) =
_generateAllRangesAndInMin(poolKey, params, resolvedCenterTick, currentTick, sqrtPriceX96, otherAmount);
// Build preview
preview.strategy = params.strategyAddress;
preview.centerTick = resolvedCenterTick;
preview.ticksLeft = params.ticksLeft;
preview.ticksRight = params.ticksRight;
preview.ranges = allRanges;
preview.liquidities = allLiquidities;
_calculateExpectedTotalsWithPoolKey(poolKey, allRanges, preview);
}
/**
* @dev Generate all ranges (base + limit) and calculate inMin
*/
function _generateAllRangesAndInMin(
PoolKey memory poolKey,
InitialDepositParams calldata params,
int24 resolvedCenterTick,
int24 currentTick,
uint160 sqrtPriceX96,
uint256 otherAmount
) private view returns (
IMultiPositionManager.Range[] memory allRanges,
uint128[] memory allLiquidities,
uint256[2][] memory inMin
) {
IMultiPositionManager.Range[] memory baseRanges;
uint128[] memory baseLiquidities;
uint256 amount0 = params.isToken0 ? params.amount : otherAmount;
uint256 amount1 = params.isToken0 ? otherAmount : params.amount;
// Generate base ranges in scoped block
{
RebalanceLogic.StrategyContext memory ctx = RebalanceLogic.StrategyContext({
resolvedStrategy: params.strategyAddress,
center: resolvedCenterTick,
tLeft: params.ticksLeft,
tRight: params.ticksRight,
strategy: ILiquidityStrategy(params.strategyAddress),
weight0: params.weight0,
weight1: params.weight1,
useCarpet: params.useCarpet,
limitWidth: params.limitWidth,
weightsAreProportional: (params.weight0 == 0 && params.weight1 == 0)
});
(baseRanges, baseLiquidities) = RebalanceLogic.generateRangesAndLiquiditiesWithPoolKey(
poolKey,
poolManager,
ctx,
amount0,
amount1
);
}
// Add limit positions and calculate inMin
{
SimpleLensInMin.LimitPositionsParams memory limitParams = SimpleLensInMin.LimitPositionsParams({
limitWidth: params.limitWidth,
currentTick: currentTick,
tickSpacing: poolKey.tickSpacing,
maxSlippageBps: params.maxSlippageBps,
sqrtPriceX96: sqrtPriceX96,
totalAmount0: amount0,
totalAmount1: amount1
});
(allRanges, allLiquidities, inMin) = SimpleLensInMin.addLimitPositionsAndCalculateInMin(
baseRanges,
baseLiquidities,
limitParams
);
}
}
/**
* @dev Calculate other amount inline (lightweight version)
*/
/**
* @notice Calculate swap needed for initial deposit with any token ratio, then preview positions
* @dev Supports any ratio: 100/0, 90/10, 50/50, etc. Calculates optimal swap to achieve strategy weights
* @param poolKey The PoolKey for the Uniswap V4 pool
* @param params Parameters including both token amounts (any ratio)
* @return finalAmount0 Amount of token0 after optimal swap
* @return finalAmount1 Amount of token1 after optimal swap
* @return swapParams Swap details (direction, amount, target weights)
* @return inMin Minimum amounts for each position (slippage protection)
* @return preview Detailed preview of the rebalance operation
*/
function getAmountsForInitialDepositWithSwapAndPreview(
PoolKey memory poolKey,
SimpleLensInMin.InitialDepositWithSwapParams calldata params
) external view returns (
uint256 finalAmount0,
uint256 finalAmount1,
SimpleLensRatioUtils.SwapParams memory swapParams,
uint256[2][] memory inMin,
SimpleLensInMin.RebalancePreview memory preview
) {
// Delegate to library
return SimpleLensInMin.calculateSwapAndPreview(poolManager, poolKey, params);
}
/**
* @notice Calculate which token and how much to deposit/withdraw to achieve desired ratio
* @param manager The MultiPositionManager contract
* @param desiredRatio The desired ratio of token0 value to total value (1e18 = 100% token0, 5e17 = 50% token0)
* @param isDeposit True to fix ratio via deposit, false to fix via withdrawal
* @return isToken0 True if need to deposit/withdraw token0, false for token1
* @return amount The amount of token to deposit/withdraw
*/
function ratioFix(
MultiPositionManager manager,
uint256 desiredRatio,
bool isDeposit
) external view returns (bool isToken0, uint256 amount) {
return SimpleLensRatioUtils.ratioFix(manager, desiredRatio, isDeposit);
}
/**
* @notice Calculate the corresponding token amount needed to maintain the current ratio, preview expected positions, and calculate inMin for slippage protection
* @param manager The MultiPositionManager contract
* @param isToken0 True if the provided amount is token0, false if token1
* @param amount The amount of the token you want to deposit
* @param maxSlippage Maximum slippage in basis points (10000 = 100%)
* @return otherAmount The amount of the other token needed to maintain the current ratio
* @return inMin Array of minimum amounts for each base and limit position
* @return expectedPositions Array of PositionStats showing expected state after deposit
*/
function getAmountsForExactRatioDeposit(
MultiPositionManager manager,
bool isToken0,
uint256 amount,
uint256 maxSlippage
) external view returns (uint256 otherAmount, uint256[2][] memory inMin, SimpleLensRatioUtils.PositionStats[] memory expectedPositions) {
if (maxSlippage > 10000) revert MaxSlippageExceeded();
// Calculate other amount needed
otherAmount = SimpleLensRatioUtils.getAmountsForDeposit(manager, isToken0, amount);
// Get current positions
(IMultiPositionManager.Range[] memory ranges, IMultiPositionManager.PositionData[] memory positionData) =
manager.getPositions();
expectedPositions = new SimpleLensRatioUtils.PositionStats[](ranges.length);
uint256 basePositionsLength = manager.basePositionsLength();
if (basePositionsLength == 0) {
return (otherAmount, inMin, expectedPositions);
}
// Prepare amounts for deposit
uint256 deposit0 = isToken0 ? amount : otherAmount;
uint256 deposit1 = isToken0 ? otherAmount : amount;
// Calculate expected positions
expectedPositions = SimpleLensRatioUtils.calculateExpectedPositionsAfterDeposit(
manager,
ranges,
positionData,
deposit0,
deposit1
);
// Calculate inMin with slippage protection
uint256 limitPositionsLength = manager.limitPositionsLength();
inMin = new uint256[2][](basePositionsLength + limitPositionsLength);
inMin = SimpleLensRatioUtils.calculateDirectDepositInMin(
manager,
deposit0,
deposit1,
maxSlippage,
basePositionsLength,
inMin
);
}
/**
* @notice Calculate amounts and preview positions for compound with optional deposit and swap
* @dev Works for all scenarios: fee compound only, deposit+compound, with/without optimal swap
* Factors in: fees (from zeroBurn) + idle vault balance + deposit amounts
* @param pos MultiPositionManager address
* @param deposit0 Amount of token0 being deposited (0 for fee-only compound)
* @param deposit1 Amount of token1 being deposited (0 for fee-only compound)
* @param maxSlippageBps Maximum slippage in basis points (10000 = 100%)
* @param needsSwap Whether to calculate and apply optimal swap (false = no swap, true = with swap)
* @return finalAmount0 Final amount of token0 after optional swap (total available if no swap)
* @return finalAmount1 Final amount of token1 after optional swap (total available if no swap)
* @return swapParams Swap parameters (direction, amount, target weights)
* @return inMin Array of minimum amounts for each base and limit position
* @return expectedPositions Array of PositionStats showing expected state after compound
*/
function getAmountsForDepositAndCompound(
address pos,
uint256 deposit0,
uint256 deposit1,
uint256 maxSlippageBps,
bool needsSwap
) external view returns (
uint256 finalAmount0,
uint256 finalAmount1,
SimpleLensRatioUtils.SwapParams memory swapParams,
uint256[2][] memory inMin,
SimpleLensRatioUtils.PositionStats[] memory expectedPositions
) {
if (maxSlippageBps > 10000) revert MaxSlippageExceeded();
MultiPositionManager manager = MultiPositionManager(payable(pos));
return SimpleLensRatioUtils.getAmountsForDepositAndCompound(
manager,
deposit0,
deposit1,
maxSlippageBps,
needsSwap
);
}
}"
},
"src/MultiPositionManager.sol": {
"content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
import { IERC20 } from "@openzeppelin/contracts/interfaces/IERC20.sol";
import { ERC20, ERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol";
import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import { Initializable } from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import { IPoolManager } from "v4-core/interfaces/IPoolManager.sol";
import { StateLibrary } from "v4-core/libraries/StateLibrary.sol";
import { Currency } from "v4-core/types/Currency.sol";
import { PoolKey } from "v4-core/types/PoolKey.sol";
import { PoolIdLibrary } from "v4-core/types/PoolId.sol";
import { SafeCallback } from "v4-periphery/src/base/SafeCallback.sol";
import { IMultiPositionManager } from "./interfaces/IMultiPositionManager.sol";
import { IMultiPositionFactory } from "./interfaces/IMultiPositionFactory.sol";
import { PoolManagerUtils } from "./libraries/PoolManagerUtils.sol";
import { Multicall } from "./base/Multicall.sol";
import { SharedStructs } from "./base/SharedStructs.sol";
import { RebalanceLogic } from "./libraries/RebalanceLogic.sol";
import { WithdrawLogic } from "./libraries/WithdrawLogic.sol";
import { DepositLogic } from "./libraries/DepositLogic.sol";
import { PositionLogic } from "./libraries/PositionLogic.sol";
contract MultiPositionManager is
IMultiPositionManager,
Initializable,
ERC20Permit,
ReentrancyGuard,
Ownable,
SafeCallback,
Multicall
{
using SafeERC20 for IERC20;
using StateLibrary for IPoolManager;
using PoolIdLibrary for PoolKey;
uint256 public constant PRECISION = 1e36;
int24 public constant CENTER_AT_CURRENT_TICK = type(int24).max;
event RebalancerGranted(address indexed account);
event RebalancerRevoked(address indexed account);
SharedStructs.ManagerStorage internal s;
error UnauthorizedCaller();
error InvalidAction();
event Withdraw(
address indexed sender,
address indexed to,
uint256 shares,
uint256 amount0,
uint256 amount1
);
event Burn(
address indexed sender,
uint256 shares,
uint256 totalSupply,
uint256 amount0,
uint256 amount1
);
event WithdrawCustom(
address indexed sender,
address indexed to,
uint256 shares,
uint256 amount0Out,
uint256 amount1Out
);
event FeeChanged(uint16 newFee);
/**
* @notice Constructor for MultiPositionManager
* @dev Sets all immutable values and initializes the contract
* @param _poolManager The Uniswap V4 pool manager
* @param _poolKey The pool key defining the pool
* @param _owner The owner address
* @param _factory The factory address
* @param _name Token name
* @param _symbol Token symbol
* @param _fee The protocol fee denominator
*/
constructor(
IPoolManager _poolManager,
PoolKey memory _poolKey,
address _owner,
address _factory,
string memory _name,
string memory _symbol,
uint16 _fee
) ERC20Permit(_name) ERC20(_name, _symbol) Ownable(_owner) SafeCallback(_poolManager) {
s.poolKey = _poolKey;
s.poolId = _poolKey.toId();
s.currency0 = _poolKey.currency0;
s.currency1 = _poolKey.currency1;
s.factory = _factory;
s.fee = _fee;
}
function poolKey() external view returns (PoolKey memory) {
return s.poolKey;
}
function factory() external view returns (address) {
return s.factory;
}
function fee() external view returns (uint16) {
return s.fee;
}
function basePositionsLength() external view returns (uint256) {
return s.basePositionsLength;
}
function limitPositions(uint256 index) external view returns (Range memory) {
return s.limitPositions[index];
}
function limitPositionsLength() external view returns (uint256) {
return s.limitPositionsLength;
}
function lastStrategyParams() external view returns (
address strategy,
int24 centerTick,
uint24 ticksLeft,
uint24 ticksRight,
uint24 limitWidth,
uint120 weight0,
uint120 weight1,
bool useCarpet
) {
SharedStructs.StrategyParams memory params = s.lastStrategyParams;
return (
params.strategy,
params.centerTick,
params.ticksLeft,
params.ticksRight,
params.limitWidth,
params.weight0,
params.weight1,
params.useCarpet
);
}
function isRebalancer(address account) public view returns (bool) {
return s.rebalancers[account];
}
modifier onlyOwnerOrFactory() {
require(msg.sender == owner() || msg.sender == s.factory);
_;
}
modifier onlyOwnerOrRebalancerOrFactory() {
require(msg.sender == owner() || s.rebalancers[msg.sender] || msg.sender == s.factory);
_;
}
receive() external payable {}
/**
* @notice Deposit tokens to vault (idle balance). Use compound() to add to positions.
* @param deposit0Desired Maximum amount of token0 to deposit
* @param deposit1Desired Maximum amount of token1 to deposit
* @param to Address to which liquidity tokens are minted
* @param from Address from which asset tokens are transferred
* @return shares Number of shares minted
* @return deposit0 Actual amount of token0 deposited
* @return deposit1 Actual amount of token1 deposited
*/
function deposit(
uint256 deposit0Desired,
uint256 deposit1Desired,
address to,
address from
) external payable onlyOwnerOrFactory returns (
uint256 shares,
uint256 deposit0,
uint256 deposit1
) {
(shares, deposit0, deposit1) = DepositLogic.processDeposit(
s,
poolManager,
deposit0Desired,
deposit1Desired,
to,
from,
totalSupply(),
msg.value
);
_mint(to, shares);
_transferIn(from, s.currency0, deposit0);
_transferIn(from, s.currency1, deposit1);
}
/**
* @notice Compound idle vault balance + fees into existing positions
* @dev Collects fees via zeroBurn, then adds all idle balance to positions
* @param inMin Minimum amounts for each position (slippage protection)
*/
function compound(uint256[2][] calldata inMin) external onlyOwnerOrFactory {
poolManager.unlock(abi.encode(IMultiPositionManager.Action.COMPOUND, abi.encode(inMin)));
}
/**
* @notice Compound with swap: collect fees, swap to target ratio, then add to positions
* @param swapParams Swap parameters for DEX aggregator execution
* @param inMin Minimum amounts per position for slippage protection
*/
function compoundSwap(
RebalanceLogic.SwapParams calldata swapParams,
uint256[2][] calldata inMin
) external payable onlyOwnerOrFactory {
if (s.basePositionsLength > 0) {
poolManager.unlock(abi.encode(IMultiPositionManager.Action.ZERO_BURN));
}
RebalanceLogic.executeCompoundSwap(s, swapParams);
poolManager.unlock(
abi.encode(IMultiPositionManager.Action.COMPOUND, abi.encode(inMin))
);
}
/**
*
* @param shares Number of liquidity tokens to redeem as pool assets
* @param to Address to which redeemed pool assets are sent (ignored if withdrawToWallet is false)
* @param outMin min amount returned for shares of liq
* @param withdrawToWallet If true, transfers tokens to 'to' and burns shares. If false, keeps tokens in contract and preserves shares.
* @return amount0 Amount of token0 redeemed by the submitted liquidity tokens
* @return amount1 Amount of token1 redeemed by the submitted liquidity tokens
*/
function withdraw(
uint256 shares,
address to,
uint256[2][] memory outMin,
bool withdrawToWallet
) nonReentrant external returns (uint256 amount0, uint256 amount1) {
(amount0, amount1) = WithdrawLogic.processWithdraw(
s,
poolManager,
shares,
to,
outMin,
totalSupply(),
msg.sender,
withdrawToWallet
);
if (withdrawToWallet) {
_burn(msg.sender, shares);
}
}
/**
* @notice Withdraw custom amounts of both tokens
* @param amount0Desired Amount of token0 to withdraw
* @param amount1Desired Amount of token1 to withdraw
* @param to Address to receive the tokens
* @param outMin Minimum amounts per position for slippage protection
* @return amount0Out Amount of token0 withdrawn
* @return amount1Out Amount of token1 withdrawn
* @return sharesBurned Number of shares burned
*/
function withdrawCustom(
uint256 amount0Desired,
uint256 amount1Desired,
address to,
uint256[2][] memory outMin
) external nonReentrant returns (uint256 amount0Out, uint256 amount1Out, uint256 sharesBurned) {
WithdrawLogic.CustomWithdrawParams memory params = WithdrawLogic.CustomWithdrawParams({
amount0Desired: amount0Desired,
amount1Desired: amount1Desired,
to: to,
outMin: outMin,
totalSupply: totalSupply(),
senderBalance: balanceOf(msg.sender),
sender: msg.sender
});
(amount0Out, amount1Out, sharesBurned) = WithdrawLogic.processWithdrawCustom(s, poolManager, params);
_burn(msg.sender, sharesBurned);
}
/**
* @notice Unified rebalance function with optional weighted token distribution
* @param params Rebalance parameters including optional weights
* @param outMin Minimum output amounts for withdrawals
* @param inMin Minimum input amounts for new positions (slippage protection)
* @dev If weights are not specified or are both 0, defaults to 50/50 distribution
*/
function rebalance(
IMultiPositionManager.RebalanceParams calldata params,
uint256[2][] memory outMin,
uint256[2][] memory inMin
) public onlyOwnerOrRebalancerOrFactory {
(
IMultiPositionManager.Range[] memory baseRanges,
uint128[] memory liquidities,
int24 limitWidth
) = RebalanceLogic.rebalance(s, poolManager, params, outMin, inMin);
bytes memory encodedParams = abi.encode(baseRanges, liquidities, limitWidth, inMin, outMin, params);
poolManager.unlock(
abi.encode(IMultiPositionManager.Action.REBALANCE, encodedParams)
);
}
/**
* @notice Rebalances positions with an external DEX swap to achieve target weights
* @param params Swap and rebalance parameters including aggregator address and swap data
* @param outMin Minimum output amounts for burning current positions
* @param inMin Minimum input amounts for new positions (slippage protection)
* @dev Burns all positions first, then swaps to target ratio, then rebalances with new amounts
*/
function rebalanceSwap(
IMultiPositionManager.RebalanceSwapParams calldata params,
uint256[2][] memory outMin,
uint256[2][] memory inMin
) public payable onlyOwnerOrRebalancerOrFactory {
if (totalSupply() > 0 && (s.basePositionsLength > 0 || s.limitPositionsLength > 0)) {
poolManager.unlock(
abi.encode(IMultiPositionManager.Action.BURN_ALL, abi.encode(outMin))
);
}
(
IMultiPositionManager.Range[] memory baseRanges,
uint128[] memory liquidities,
int24 limitWidth
) = RebalanceLogic.executeSwapAndCalculateRanges(s, poolManager, params);
bytes memory encodedParams = abi.encode(baseRanges, liquidities, limitWidth, inMin, outMin, params.rebalanceParams);
poolManager.unlock(
abi.encode(IMultiPositionManager.Action.REBALANCE, encodedParams)
);
}
/**
* @notice Claims fees
* @dev If called by owner, performs zeroBurn and claims both owner and protocol fees
* @dev If called by factory owner or CLAIM_MANAGER, only claims existing protocol fees
*/
function claimFee() external {
if (msg.sender == owner()) {
poolManager.unlock(
abi.encode(IMultiPositionManager.Action.CLAIM_FEE, abi.encode(msg.sender))
);
} else if (IMultiPositionFactory(s.factory).hasRoleOrOwner(
IMultiPositionFactory(s.factory).CLAIM_MANAGER(),
msg.sender
)) {
poolManager.unlock(
abi.encode(IMultiPositionManager.Action.CLAIM_FEE, abi.encode(address(0)))
);
} else {
revert UnauthorizedCaller();
}
}
function setFee(uint16 newFee) external {
IMultiPositionFactory factoryContract = IMultiPositionFactory(s.factory);
require(factoryContract.hasRole(factoryContract.FEE_MANAGER(), msg.sender));
s.fee = newFee;
emit FeeChanged(newFee);
}
/**
* @notice Grant rebalancer role to an address
* @param account The address to grant the role to
*/
function grantRebalancerRole(address account) external onlyOwner {
require(account != address(0));
if (!s.rebalancers[account]) {
s.rebalancers[account] = true;
emit RebalancerGranted(account);
}
}
/**
* @notice Revoke rebalancer role from an address
* @param account The address to revoke the role from
*/
function revokeRebalancerRole(address account) external onlyOwner {
if (s.rebalancers[account]) {
s.rebalancers[account] = false;
emit RebalancerRevoked(account);
}
}
function getBasePositions() public view returns (
Range[] memory,
PositionData[] memory
) {
return PositionLogic.getBasePositions(s, poolManager);
}
function getPositions() public view returns (
Range[] memory,
PositionData[] memory
) {
return PositionLogic.getPositions(s, poolManager);
}
function getTotalAmounts() external view returns (
uint256 total0,
uint256 total1,
uint256 totalFee0,
uint256 totalFee1
) {
return WithdrawLogic.getTotalAmounts(s, poolManager);
}
function currentTick() public view returns (int24 tick) {
(, tick, , ) = poolManager.getSlot0(s.poolKey.toId());
}
function getRatios() external view returns (
uint256 pool0Ratio,
uint256 pool1Ratio,
uint256 total0Ratio,
uint256 total1Ratio,
uint256 inPositionRatio,
uint256 outOfPositionRatio,
uint256 baseRatio,
uint256 limitRatio,
uint256 base0Ratio,
uint256 base1Ratio,
uint256 limit0Ratio,
uint256 limit1Ratio
) {
PositionLogic.Ratios memory ratios = PositionLogic.getRatios(s, poolManager);
return (
ratios.pool0Ratio,
ratios.pool1Ratio,
ratios.total0Ratio,
ratios.total1Ratio,
ratios.inPositionRatio,
ratios.outOfPositionRatio,
ratios.baseRatio,
ratios.limitRatio,
ratios.base0Ratio,
ratios.base1Ratio,
ratios.limit0Ratio,
ratios.limit1Ratio
);
}
function _unlockCallback(bytes calldata data) internal override returns (bytes memory) {
(Action selector, bytes memory params) = abi.decode(
data,
(Action, bytes)
);
bytes memory result = _executeActionWithoutUnlock(selector, params);
_closePair();
return result;
}
function _executeActionWithoutUnlock(
Action selector,
bytes memory params
) internal returns (bytes memory result) {
if (selector == IMultiPositionManager.Action.WITHDRAW) {
WithdrawLogic.zeroBurnAllWithoutUnlock(s, poolManager);
(
uint256 shares,
uint256[2][] memory outMin
) = abi.decode(params, (uint256, uint256[2][]));
(uint256 amountOut0, uint256 amountOut1) = PositionLogic.burnLiquidities(poolManager, s, shares, totalSupply(), outMin);
return abi.encode(amountOut0, amountOut1);
} else if (selector == IMultiPositionManager.Action.REBALANCE) {
return RebalanceLogic.processRebalanceInCallback(s, poolManager, params, totalSupply());
} else if (selector == IMultiPositionManager.Action.ZERO_BURN) {
WithdrawLogic.zeroBurnAllWithoutUnlock(s, poolManager);
return "";
} else if (selector == IMultiPositionManager.Action.CLAIM_FEE) {
address caller = abi.decode(params, (address));
WithdrawLogic.processClaimFee(s, poolManager, caller, owner());
return "";
} else if (selector == IMultiPositionManager.Action.BURN_ALL) {
return WithdrawLogic.processBurnAllInCallback(s, poolManager, totalSupply(), params);
} else if (selector == IMultiPositionManager.Action.COMPOUND) {
uint256[2][] memory inMin = abi.decode(params, (uint256[2][]));
DepositLogic.processCompound(s, poolManager, inMin);
return "";
} else revert InvalidAction();
}
function _closePair() internal {
PoolManagerUtils.close(poolManager, s.currency1);
PoolManagerUtils.close(poolManager, s.currency0);
}
function _transferIn(address from, Currency currency, uint256 amount) internal {
if (currency.isAddressZero()) {
require(msg.value >= amount);
if (msg.value > amount)
payable(msg.sender).transfer(msg.value - amount);
} else if (amount != 0) {
IERC20(Currency.unwrap(currency)).safeTransferFrom(from, address(this), amount);
}
}
}
"
},
"src/interfaces/IMultiPositionManager.sol": {
"content": "/// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import { PoolKey } from "v4-core/types/PoolKey.sol";
import {IImmutableState} from "v4-periphery/src/interfaces/IImmutableState.sol";
import { IPoolManager } from "v4-core/interfaces/IPoolManager.sol";
import { RebalanceLogic } from "../libraries/RebalanceLogic.sol";
interface IMultiPositionManager is IERC20, IImmutableState {
enum Action {
WITHDRAW,
REBALANCE,
ZERO_BURN,
CLAIM_FEE,
BURN_ALL,
COMPOUND
}
struct Range {
int24 lowerTick;
int24 upperTick;
}
// @deprecated Use Range instead - Position included redundant poolKey
struct Position {
PoolKey poolKey;
int24 lowerTick;
int24 upperTick;
}
struct PositionData {
uint128 liquidity;
uint256 amount0;
uint256 amount1;
}
struct RebalanceParams {
address strategy;
int24 center;
uint24 tLeft;
uint24 tRight;
int24 limitWidth;
uint256 weight0;
uint256 weight1;
bool useCarpet;
}
struct RebalanceSwapParams {
RebalanceParams rebalanceParams;
RebalanceLogic.SwapParams swapParams;
}
event Rebalance(
Range[] ranges,
PositionData[] positionData,
RebalanceParams params
);
function getPositions() external view returns (
Range[] memory,
PositionData[] memory
);
function getBasePositions() external view returns (
Range[] memory,
PositionData[] memory
);
function poolKey() external view returns (PoolKey memory);
function basePositionsLength() external view returns (uint256);
function limitPositionsLength() external view returns (uint256);
function limitPositions(uint256 index) external view returns (Range memory);
function getTotalAmounts() external view returns (
uint256 total0,
uint256 total1,
uint256 totalFee0,
uint256 totalFee1
);
function currentTick() external view returns (int24);
function rebalance(
RebalanceParams calldata params,
uint256[2][] memory outMin,
uint256[2][] memory inMin
) external;
function rebalanceSwap(
RebalanceSwapParams calldata params,
uint256[2][] memory outMin,
uint256[2][] memory inMin
) external payable;
function claimFee() external;
function setFee(uint16 fee) external;
function factory() external view returns (address);
// function setTickOffset(uint24 offset) external;
function deposit(
uint256 deposit0Desired,
uint256 deposit1Desired,
address to,
address from
) external payable returns (uint256, uint256, uint256);
function compound(uint256[2][] calldata inMin) external;
function compoundSwap(
RebalanceLogic.SwapParams calldata swapParams,
uint256[2][] calldata inMin
) external payable;
function withdraw(
uint256 shares,
address to,
uint256[2][] memory outMin,
bool withdrawToWallet
) external returns (uint256 amount0, uint256 amount1);
function withdrawCustom(
uint256 amount0Desired,
uint256 amount1Desired,
address to,
uint256[2][] memory outMin
) external returns (uint256 amount0Out, uint256 amount1Out, uint256 sharesBurned);
// Role management functions
function grantRebalancerRole(address account) external;
function revokeRebalancerRole(address account) external;
function isRebalancer(address account) external view returns (bool);
// Ratio functions
function getRatios() external view returns (
uint256 pool0Ratio,
uint256 pool1Ratio,
uint256 total0Ratio,
uint256 total1Ratio,
uint256 inPositionRatio,
uint256 outOfPositionRatio,
uint256 baseRatio,
uint256 limitRatio,
uint256 base0Ratio,
uint256 base1Ratio,
uint256 limit0Ratio,
uint256 limit1Ratio
);
// Strategy parameters
function lastStrategyParams()
Submitted on: 2025-10-29 09:18:31
Comments
Log in to comment.
No comments yet.