SimpleLens

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) {
        PoolKey memory poolKey = manager.poolKey();
        int24 resolvedCenterTick;
        uint256 targetWeight0;
        uint256 targetWeight1;

        // Scope: Calculate weights
        {
            (uint160 sqrtPriceX96, int24 currentTick, , ) = poolManager.getSlot0(poolKey.toId());

            // Resolve center tick
            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;

            // Calculate target weights (proportional from strategy if both are 0)
            targetWeight0 = params.weight0;
            targetWeight1 = params.weight1;
            if (params.weight0 == 0 && params.weight1 == 0) {
                (targetWeight0, targetWeight1) = RebalanceLogic.calculateWeightsFromStrategy(
                    ILiquidityStrategy(params.strategyAddress),
                    resolvedCenterTick,
                    params.ticksLeft,
                    params.ticksRight,
                    poolKey.tickSpacing,
                    params.useCarpet,
                    sqrtPriceX96,
                    currentTick
                );
            }
        }

        // Scope: Simulate swap
        {
            uint256 t0;
            uint256 t1;
            (t0, t1, , ) = manager.getTotalAmounts();

            (adjustedTotal0, adjustedTotal1) = SimpleLensRatioUtils.simulateSwapForRebalance(
                manager, t0, t1, targetWeight0, targetWeight1
            );

            // 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;
            }
        }

        // 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 = targetWeight0;
        ctx.weight1 = targetWeight1;
        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 {
        // Create stats for ALL positions (base + limit)
        preview.expectedPositions = new SimpleLensRatioUtils.PositionStats[](preview.ranges.length);
        preview.expectedTotal0 = 0;
        preview.expectedTotal1 = 0;

        PoolKey memory poolKey = manager.poolKey();
        (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;
        }
    }

    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
        );
    }

    /**
     * @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);
    }

    // InitialDepositWithSwapParams, PreviewContext, and InitialDepositParams structs moved to libraries

    /**
     * @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 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() external view returns (
    address strategy,
    int24 centerTick,
    uint24 ticksLeft,
    uint24 ticksRight,
    uint24 limitWidth,
    uint120 weight0,
    uint120 weight1,
    bool useCarpet
  );

}
"
    },
    "src/strategies/ILiquidityStrategy.sol": {
      "content": "// SPDX-License-Identifier: BUSL-1.1
pragma solidity 0.8.26;

/**
 * @title ILiquidityStrategy
 * @notice Interface for liquidity distribution strategies matching Python shapes
 * @dev Each strategy implements a different distribution pattern (uniform, triangle, gaussian, etc.)
 */
interface ILiquidityStrategy {
    /**
     * @notice Generate tick ranges with optional carpet positions
     * @param centerTick The center tick for the strategy
     * @param ticksLeft Number of ticks to the left of center for the distribution
     * @param ticksRight Number of ticks to the right of center for the distribution
     * @param tickSpacing The tick spacing of the pool
     * @param useCarpet Whether to add carpet positions at extreme ticks for TWAP support
     * @return lowerTicks Array of lower ticks for each position
     * @return upperTicks Array of upper ticks for each position
     */
    function generateRanges(
        int24 centerTick,
        uint24 ticksLeft,
        uint24 ticksRight,
        int24 tickSpacing,
        bool useCarpet
    ) external view returns (
        int24[] memory lowerTicks,
        int24[] memory upperTicks
    );

    /**
     * @notice Get the strategy type identifier
     * @return strategyType String identifier for the strategy (e.g., "uniform", "triangle", "gaussian")
     */
    function getStrategyType() external view returns (string memory strategyType);
    
    /**
     * @notice Get a description of the strategy
     * @return description Human-readable description of the distribution pattern
     */
    function getDescription() external view returns (string memory description);

    /**
     * @notice Check if this strategy supports weighted distribution
     * @return supported True if the strategy implements calculateDensitiesWithWeights
     */
    function supportsWeights() external pure returns (bool supported);

    /**
     * @notice Calculate density weights with token weights and carpet options
     * @dev Comprehensive function that supports both token weights and carpet liquidity
     * @param lowerTicks Array of lower ticks for each position
     * @param upperTicks Array of upper ticks for each position
     * @param currentTick Current tick of the pool
     * @param centerTick Center tick for the distribution
     * @param ticksLeft Number of ticks to the left of center for the shape
     * @param ticksRight Number of ticks to the right of center for the shape
     * @param weight0 Weight preference for token0 (scaled to 1e18, e.g., 0.8e18 for 80%)
     * @param weight1 Weight preference for token1 (scaled to 1e18, e.g., 0.2e18 for 20%)
     * @param useCarpet Whether to add carpet liquidity at extremes (0.01% each)
     * @param tickSpacing The tick spacing of the pool
     * @param weightsAreProportional True if weights were auto-calculated from available tokens (should not filter ranges)
     * @return weights Array of weights for each position (scaled to 1e18, sum = 1e18)
     */
    function calculateDensities(
        int24[] memory lowerTicks,
        int24[] memory upperTicks,
        int24 currentTick,
        int24 centerTick,
        uint24 ticksLeft,
        uint24 ticksRight,
        uint256 weight0,
        uint256 weight1,
        bool useCarpet,
        int24 tickSpacing,
        bool weightsAreProportional
    ) external view returns (uint256[] memory weights);
}"
    },
    "lib/v4-periphery/lib/v4-core/src/interfaces/IPoolManager.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {Currency} from "../types/Currency.sol";
import {PoolKey} from "../types/PoolKey.sol";
import {IHooks} from "./IHooks.sol";
import {IERC6909Claims} from "./external/IERC6909Claims.sol";
import {IProtocolFees} from "./IProtocolFees.sol";
import {BalanceDelta} from "../types/BalanceDelta.sol";
import {PoolId} from "../types/PoolId.sol";
import {IExtsload} from "./IExtsload.sol";
import {IExttload} from "./IExttload.sol";

/// @notice Interface for the PoolManager
interface IPoolManager is IProtocolFees, IERC6909Claims, IExtsload, IExttload {
    /// @notice Thrown when a currency is not netted out after the contract is unlocked
    error CurrencyNotSettled();

    /// @notice Thrown when trying to interact with a non-initialized pool
    error PoolNotInitialized();

    /// @notice Thrown when unlock is called, but the contract is already unlocked
    error AlreadyUnlocked();

    /// @notice Thrown when a function is called that requires the contract to be unlocked, but it is not
    error ManagerLocked();

    /// @notice Pools are limited to type(int16).max tickSpacing in #initialize, to prevent overflow
    error TickSpacingTooLarge(int24 tickSpacing);

    /// @notice Pools must have a positive non-zero tickSpacing passed to #initialize
    error TickSpacingTooSmall(int24 tickSpacing);

    /// @notice PoolKey must have currencies where address(currency0) < address(currency1)
    error CurrenciesOutOfOrderOrEqual(address currency0, address currency1);

    /// @notice Thrown when a call to updateDynamicLPFee is made by an address that is not the hook,
    /// or on a pool that does not have a dynamic swap fee.
    error UnauthorizedDynamicLPFeeUpdate();

    /// @notice Thrown when trying to swap amount of 0
    error SwapAmountCannotBeZero();

    ///@notice Thrown when native currency is passed to a non native settlement
    error NonzeroNativeValue();

    /// @notice Thrown when `clear` is called with an amount that is not exactly equal to the open currency delta.
    error MustClearExactPositiveDelta();

    /// @notice Emitted when a new pool is initialized
    /// @param id The abi encoded hash of the pool key struct for the new pool
    /// @param currency0 The first currency of the pool by address sort order
    /// @param currency1 The second currency of the pool by address sort order
    /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
    /// @param tickSpacing The minimum number of ticks between initialized ticks
    /// @param hooks The hooks contract address for the pool, or address(0) if none
    /// @param sqrtPriceX96 The price of the pool on initialization
    /// @param tick The initial tick of the pool corresponding to the initialized price
    event Initialize(
        PoolId indexed id,
        Currency indexed currency0,
        Currency indexed currency1,
        uint24 fee,
        int24 tickSpacing,
        IHooks hooks,
        uint160 sqrtPriceX96,
        int24 tick
    );

    /// @notice Emitted when a liquidity position is modified
    /// @param id The abi encoded hash of the pool key struct for the pool that was modified
    /// @param sender The address that modified the pool
    /// @param tickLower The lower tick of the position
    /// @param tickUpper The upper tick of the position
    /// @param liquidityDelta The amount of liquidity that was added or removed
    /// @param salt The extra data to make positions unique
    event ModifyLiquidity(
        PoolId indexed id, address indexed sender, int24 tickLower, int24 tickUpper, int256 liquidityDelta, bytes32 salt
    );

    /// @notice Emitted for swaps between currency0 and currency1
    /// @param id The abi encoded hash of the pool key struct for the pool that was modified
    /// @param sender The address that initiated the swap call, and that received the callback
    /// @param amount0 The delta of the currency0 balance of the pool
    /// @param amount1 The delta of the currency1 balance of the pool
    /// @param sqrtPriceX96 The sqrt(price) of the pool after the swap, as a Q64.96
    /// @param liquidity The liquidity of the pool after the swap
    /// @param tick The log base 1.0001 of the price of the pool after the swap
    /// @param fee The swap fee in hundredths of a bip
    event Swap(
        PoolId indexed id,
        address indexed sender,
        int128 amount0,
        int128 amount1,
        uint160 sqrtPriceX96,
        uint128 liquidity,
        int24 tick,
        uint24 fee
    );

    /// @notice Emitted for donations
    /// @param id The abi encoded hash of the pool key struct for the pool that was donated to
    /// @param sender The address that initiated the don

Tags:
ERC20, ERC165, Multisig, Mintable, Burnable, Swap, Liquidity, Upgradeable, Multi-Signature, Factory|addr:0x4d4aca6f8a0ad933c1a9fbd7dda475cd42c19356|verified:true|block:23685803|tx:0xc5f07797aab86138b68db910e333342391ed5def2c5ffe3368313c89e55b74bf|first_check:1761820496

Submitted on: 2025-10-30 11:34:59

Comments

Log in to comment.

No comments yet.