MigrationManagerV3

0xdf6e6e5cfce09b64fd24fe9f433f452649f87428

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
9,002 bytes
ABI entries
30
14 read · 3 write
License
none

Contract information

Address
0xdf6e6e5cfce09b64fd24fe9f433f452649f87428
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0xab28ff2a03…9b25259e4a

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (14)

BPS_DENOMINATOR()uint256
FEE_TIER()uint24
MAX_TICK_USABLE()int24
MIN_TICK_USABLE()int24
TICK_SPACING()int24
TOL_BPS()uint256
WETH()address
deployer()address
launchpadFactory()address
lockVault()address
nonfungiblePositionManager()address
positionIdOf(address)uint256
swapRouter()address
v3Factory()address

Events (3)

GraduatedLockVaultSetSwept

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_launchpadFactory",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_v3Factory",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_nonfungiblePositionManager",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_swapRouter",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_WETH",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "LockVaultAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "LockVaultNotSet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoLiquiditySeeded",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotBondingCurve",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotDeployer",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "PriceCorrectionFailed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "Reentrancy",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "tokenId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint160",
        "name": "sqrtPriceX96",
        "type": "uint160"
      },
      {
        "indexed": false,
        "internalType": "uint128",
        "name": "liquidity",
        "type": "uint128"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      }
    ],
    "name": "Graduated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "vault",
        "type": "address"
      }
    ],
    "name": "LockVaultSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Swept",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "BPS_DENOMINATOR",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "FEE_TIER",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_TICK_USABLE",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MIN_TICK_USABLE",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TICK_SPACING",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "TOL_BPS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "deployer",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "launchpadFactory",
    "outputs": [
      {
        "internalType": "contract ILaunchpadFactory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "lockVault",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "migrate",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "nonfungiblePositionManager",
    "outputs": [
      {
        "internalType": "contract INonfungiblePositionManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "positionIdOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_lockVault",
        "type": "address"
      }
    ],
    "name": "setLockVault",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "swapRouter",
    "outputs": [
      {
        "internalType": "contract ISwapRouter",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "tokenAddr",
        "type": "address"
      }
    ],
    "name": "sweep",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "v3Factory",
    "outputs": [
      {
        "internalType": "contract IUniswapV3Factory",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.28;

import { ReentrancyGuard } from "solady/utils/ReentrancyGuard.sol";
import { SafeTransferLib } from "solady/utils/SafeTransferLib.sol";
import { FixedPointMathLib } from "solady/utils/FixedPointMathLib.sol";

import { IMigrationManager } from "../interfaces/IMigrationManager.sol";
import { ILaunchpadFactory } from "../interfaces/ILaunchpadFactory.sol";
import { IERC20 } from "../interfaces/IERC20.sol";
import { IWETH } from "../interfaces/IWETH.sol";

import { IUniswapV3Factory } from "../dex/v3/core/interfaces/IUniswapV3Factory.sol";
import { IUniswapV3Pool } from "../dex/v3/core/interfaces/IUniswapV3Pool.sol";
import { INonfungiblePositionManager } from "../dex/v3/periphery/interfaces/INonfungiblePositionManager.sol";
import { ISwapRouter } from "../dex/v3/periphery/interfaces/ISwapRouter.sol";
import { FullMath } from "../dex/v3/core/libraries/FullMath.sol";
import { TickMath } from "../dex/v3/core/libraries/TickMath.sol";

import { MigrationLockVault } from "./MigrationLockVault.sol";

/// @title  MigrationManagerV3
/// @notice Atomically migrates a graduated LaunchToken from its bonding curve
///         into a Uniswap V3-style pool. Called by `BondingCurve.graduate()`
///         once the curve threshold is hit; pulls the DEX reserve from
///         LaunchpadFactory, combines with any leftover bonding-curve tokens
///         and the collected ETH, creates the V3 pool, forces it to the
///         derived starting price (adopting and repricing any pre-existing
///         pool), and mints a single full-range LP NFT straight into the
///         immutable {MigrationLockVault}.
///
/// @dev    Decisions encoded here come from
///         `docs/internal/V3_MIGRATION_HARDENING.md`:
///           - Range:       full range (`MIN_TICK_USABLE`–`MAX_TICK_USABLE` at 0.30%
///                          spacing of 60)
///           - Custody:     mint to MigrationLockVault (rug-proof, immutable)
///           - Fee tier:    0.30% locked for graduations
///           - Pre-init:    a graduated token's pool lives at a deterministic
///                          address and `initialize` is permissionless, so a
///                          griefer can pre-initialize it. Instead of reverting
///                          (a permanent DoS), `migrate` ADOPTS whatever pool
///                          exists and REPRICES it to the fair price — or refuses
///                          to seed (revert, retry later) rather than seed at a
///                          wrong price. It never depends on a third party's
///                          pool state and never gives up permanently.
///           - Donation:    the fair price's token side is the canonical
///                          `DEX_RESERVE_SUPPLY` ALONE (never raw balances, never
///                          `leftoverTokens`), so tokens donated to EITHER this
///                          manager OR the bonding curve cannot skew the launch
///                          price. Donated residue is priced out of the mint and
///                          swept to the treasury via {sweep}. See ADR-0009.
contract MigrationManagerV3 is IMigrationManager, ReentrancyGuard {
    using SafeTransferLib for address;

    // ---------------- Constants ----------------

    /// @notice V3 fee tier used for all graduated-token pools (0.30%).
    uint24 public constant FEE_TIER = 3000;

    /// @notice Tick spacing for {FEE_TIER}; canonical V3 value.
    int24 public constant TICK_SPACING = 60;

    /// @notice Lowest usable tick at {TICK_SPACING}, for full-range positions.
    /// @dev    `floor(MIN_TICK_V3 / 60) * 60` where `MIN_TICK_V3 = -887272`.
    int24 public constant MIN_TICK_USABLE = -887_220;

    /// @notice Highest usable tick at {TICK_SPACING}, for full-range positions.
    int24 public constant MAX_TICK_USABLE = 887_220;

    uint256 public constant BPS_DENOMINATOR = 10_000;

    /// @notice Tolerance band (in basis points) for (a) deciding the pool is
    ///         already at the fair price (skip reprice) and (b) the post-reprice
    ///         post-condition. Kept TIGHT (1 bp) on purpose: the price-limited
    ///         swap and `initialize` both land EXACTLY on the fair target
    ///         (`sqrtPriceX96 == sqrtPriceLimitX96`), so every legitimate success
    ///         path has diff 0 and passes — no flaky reverts. A loose band would
    ///         instead let a griefer pre-initialize the empty pool ~1% off and
    ///         have us seed the locked LP directly at that wrong price (a ~1%
    ///         arbitrage skim). The only failure mode a tight band rejects is a
    ///         whale who locked so much liquidity we couldn't reach target —
    ///         safe, temporary and self-healing (retry once the skew is arbed).
    uint256 public constant TOL_BPS = 1;

    // ---------------- Immutables ----------------

    ILaunchpadFactory public immutable launchpadFactory;
    IUniswapV3Factory public immutable v3Factory;
    INonfungiblePositionManager public immutable nonfungiblePositionManager;
    ISwapRouter public immutable swapRouter;
    address public immutable WETH;
    address public immutable deployer;

    // ---------------- Storage ----------------

    /// @notice Address of the {MigrationLockVault}; set once via {setLockVault}
    ///         post-construction (resolves the deploy-time circular dependency
    ///         between this manager and the vault).
    address public lockVault;

    /// @notice The V3 NPM tokenId of the locked LP position seeded for a
    ///         graduated `token`; 0 while the token has not graduated.
    /// @dev    On-chain `token → tokenId` link so integrators (claim UI,
    ///         bots) don't need the `Graduated` event log. NPM ids start at
    ///         1, so 0 is unambiguous for "not graduated".
    mapping(address => uint256) public positionIdOf;

    // ---------------- Errors ----------------

    error ZeroAddress();
    error UnknownToken();
    error NotBondingCurve();
    error NotDeployer();
    error LockVaultAlreadySet();
    error LockVaultNotSet();
    error PriceCorrectionFailed();
    error NoLiquiditySeeded();

    // ---------------- Events ----------------

    event LockVaultSet(address indexed vault);
    event Swept(address indexed token, address indexed to, uint256 amount);
    event Graduated(
        address indexed token,
        address indexed pool,
        uint256 indexed tokenId,
        uint160 sqrtPriceX96,
        uint128 liquidity,
        uint256 tokenAmount,
        uint256 ethAmount
    );

    // ---------------- Construction ----------------

    constructor(
        address _launchpadFactory,
        address _v3Factory,
        address _nonfungiblePositionManager,
        address _swapRouter,
        address _WETH
    ) {
        if (
            _launchpadFactory == address(0) || _v3Factory == address(0) || _nonfungiblePositionManager == address(0)
                || _swapRouter == address(0) || _WETH == address(0)
        ) revert ZeroAddress();
        launchpadFactory = ILaunchpadFactory(_launchpadFactory);
        v3Factory = IUniswapV3Factory(_v3Factory);
        nonfungiblePositionManager = INonfungiblePositionManager(_nonfungiblePositionManager);
        swapRouter = ISwapRouter(_swapRouter);
        WETH = _WETH;
        deployer = msg.sender;
    }

    // ---------------- Lock vault wiring (deploy-time only) ----------------

    /// @notice One-shot setter for the {lockVault} address. Resolves the
    ///         circular dependency at deploy time. After this call, the
    ///         vault address is effectively immutable — the function can
    ///         never be invoked again.
    /// @dev    Callable only by the {deployer} EOA from construction; the
    ///         deployer's role ends here.
    function setLockVault(
        address _lockVault
    ) external {
        if (msg.sender != deployer) revert NotDeployer();
        if (_lockVault == address(0)) revert ZeroAddress();
        if (lockVault != address(0)) revert LockVaultAlreadySet();
        lockVault = _lockVault;
        emit LockVaultSet(_lockVault);
    }

    // ---------------- Migration ----------------

    /// @inheritdoc IMigrationManager
    /// @dev    The fair price is derived from the protocol-known legit amounts
    ///         (`DEX_RESERVE_SUPPLY() + leftoverTokens` on the token side,
    ///         `msg.value` on the ETH side) — NOT this contract's raw balances —
    ///         so tokens donated to the manager cannot skew the launch price.
    ///
    ///         Slither detector exclusions for this function + helpers (all FPs):
    ///         - reentrancy-* / reentrancy-benign / reentrancy-events:
    ///           `nonReentrant` guards re-entry; every external call is to an
    ///           immutable, audited contract (WETH9, ThorV3Factory, ThorV3Pool,
    ///           ThorPositionManager, ThorSwapRouter). None can call back into
    ///           this contract through any code path.
    ///         - incorrect-equality (sqrtNow == 0 / liquidity == 0): comparing
    ///           the pool's uninitialized sqrtPrice and the NPM's uint128
    ///           liquidity to the literal 0 are the canonical "pool uninitialized"
    ///           and "did the mint produce liquidity" checks.
    ///         - unused-return on npm.mint / decreaseLiquidity / collect /
    ///           swapRouter.exactInputSingle / pool.slot0: only a subset of the
    ///           returned tuple is needed in each case.
    // slither-disable-start reentrancy-balance
    // slither-disable-start reentrancy-no-eth
    // slither-disable-start reentrancy-benign
    // slither-disable-start reentrancy-events
    // slither-disable-start incorrect-equality
    // slither-disable-start unused-return
    function migrate(
        address token,
        // Intentionally unnamed: the curve's leftover-token count is UNTRUSTED
        // for pricing (see the legit-inputs block below). The tokens themselves
        // are already forwarded to this manager by the curve before this call.
        uint256
    ) external payable override nonReentrant {
        if (lockVault == address(0)) revert LockVaultNotSet();

        // ----- Authorization -----
        if (!launchpadFactory.isThorToken(token)) revert UnknownToken();
        if (msg.sender != launchpadFactory.bondingCurveOf(token)) revert NotBondingCurve();

        // ----- Pull DEX reserve from LaunchpadFactory -----
        launchpadFactory.releaseDexReserve(token);

        // ----- Donation-proof legit inputs (used ONLY to derive the fair price) -----
        // The token side of the fair price is the CANONICAL DEX reserve alone —
        // never the curve's real token balance. An honest curve forwards exactly
        // 0 leftover tokens (it sells its whole BONDING_CURVE_SUPPLY, so its real
        // balance at the floor is provably 0), so `DEX_RESERVE_SUPPLY` is the true
        // token amount. `leftoverTokens` is deliberately EXCLUDED: `LaunchToken`
        // is a bare ERC-20, so anyone can `transfer` tokens straight into the
        // curve pre-graduation; those donated tokens are inert to the curve's
        // virtual-reserve math but would otherwise ride into `leftoverTokens` and
        // depress the launch price. They are still forwarded here by the curve
        // and, priced out, land on the over-supplied token side of the mint —
        // unused by the full-range position and swept to the treasury via {sweep}
        // rather than repricing the pool. msg.value is the non-donated ETH amount
        // (force-sent ETH can only raise the price — a gift to holders, never a
        // grief), so it is used as-is.
        uint256 legitTokens = launchpadFactory.DEX_RESERVE_SUPPLY();
        uint256 legitEth = msg.value;

        // ----- Wrap ETH so the V3 pool sees an ERC20 on both sides -----
        IWETH(WETH).deposit{ value: legitEth }();

        // ----- Sort token0 < token1 (V3 invariant), pairing the legit amounts -----
        (address token0, address token1, uint256 legit0, uint256 legit1) =
            token < WETH ? (token, WETH, legitTokens, legitEth) : (WETH, token, legitEth, legitTokens);

        // ----- Get-or-create the V3 pool -----
        address pool = v3Factory.getPool(token0, token1, FEE_TIER);
        if (pool == address(0)) {
            pool = v3Factory.createPool(token0, token1, FEE_TIER);
        }

        // ----- Fair price from the donation-proof legit amounts -----
        // sqrtPriceX96 = sqrt((legit1 << 192) / legit0) via 512-bit FullMath.
        uint160 sqrtTarget = _computeSqrtPriceX96(legit0, legit1);

        // ----- Initialize-or-reprice: never depend on a third party's pool state -----
        (uint160 sqrtNow,,,,,,) = IUniswapV3Pool(pool).slot0();
        if (sqrtNow == 0) {
            // Happy path: pool unopened — set it to the fair price.
            IUniswapV3Pool(pool).initialize(sqrtTarget);
        } else if (!_withinTolerance(sqrtNow, sqrtTarget)) {
            // Pre-initialized at a wrong price (griefer) — force it to target.
            _repriceToTarget(pool, token0, token1, sqrtTarget);
        }
        // else: already within tolerance of the fair price — seed directly.

        // ----- Re-read balances AFTER any reprice; mint full-range to the lock vault -----
        // The price is pinned by initialize/reprice (+ post-condition) and the tx
        // is nonReentrant, so market-slippage mins are meaningless here: amounts
        // are seeded at the fair price and any leftover dust is swept via {sweep}.
        uint256 amount0 = IERC20(token0).balanceOf(address(this));
        uint256 amount1 = IERC20(token1).balanceOf(address(this));

        token0.safeApprove(address(nonfungiblePositionManager), amount0);
        token1.safeApprove(address(nonfungiblePositionManager), amount1);

        (uint256 tokenId, uint128 liquidity,,) = nonfungiblePositionManager.mint(
            INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: FEE_TIER,
                tickLower: MIN_TICK_USABLE,
                tickUpper: MAX_TICK_USABLE,
                amount0Desired: amount0,
                amount1Desired: amount1,
                amount0Min: 0,
                amount1Min: 0,
                recipient: lockVault,
                deadline: block.timestamp
            })
        );

        token0.safeApprove(address(nonfungiblePositionManager), 0);
        token1.safeApprove(address(nonfungiblePositionManager), 0);

        if (liquidity == 0) revert NoLiquiditySeeded();

        // ----- Bind the current fee wallet inside the vault -----
        // Snapshot the factory's fee wallet AT GRADUATION (creator by default,
        // possibly customized at creation or handed off pre-graduation). Any
        // unaccepted factory proposal lapses here — the vault seeds from the
        // holder, never the pending slot.
        address feeWallet = launchpadFactory.feeWalletOf(token);
        MigrationLockVault(lockVault).recordFeeWallet(tokenId, feeWallet);
        positionIdOf[token] = tokenId;

        emit Graduated(token, pool, tokenId, sqrtTarget, liquidity, legitTokens, legitEth);
    }

    // slither-disable-end unused-return
    // slither-disable-end incorrect-equality
    // slither-disable-end reentrancy-events
    // slither-disable-end reentrancy-benign
    // slither-disable-end reentrancy-no-eth
    // slither-disable-end reentrancy-balance

    // ---------------- Dust sweep ----------------

    /// @notice Permissionlessly forward any residual balance of `tokenAddr` held
    ///         by this manager to the protocol treasury (`feeRecipient`).
    /// @dev    Cannot touch in-flight migration funds: {migrate} is atomic and
    ///         `nonReentrant`, so this can only ever run in a separate tx, and
    ///         the destination is fixed to the immutable treasury. Used to
    ///         collect post-migration mint dust and any donated residue.
    function sweep(
        address tokenAddr
    ) external nonReentrant {
        address to = launchpadFactory.feeRecipient();
        uint256 bal = IERC20(tokenAddr).balanceOf(address(this));
        if (bal > 0) {
            tokenAddr.safeTransfer(to, bal);
        }
        emit Swept(tokenAddr, to, bal);
    }

    // ---------------- Internal: reprice a pre-initialized pool ----------------

    /// @dev Force `pool`'s spot price to `sqrtTarget` by trading against it,
    ///      bootstrapping movable liquidity first if the pool is empty. Reverts
    ///      with {PriceCorrectionFailed} if the price cannot be brought within
    ///      {TOL_BPS} of target (no funds lost — the whole migration reverts and
    ///      retries later, self-healing as the skew is arbitraged away).
    // slither-disable-start reentrancy-balance
    // slither-disable-start reentrancy-no-eth
    // slither-disable-start reentrancy-benign
    // slither-disable-start reentrancy-events
    // slither-disable-start incorrect-equality
    // slither-disable-start unused-return
    function _repriceToTarget(
        address pool,
        address token0,
        address token1,
        uint160 sqrtTarget
    ) internal {
        // Explicitly zero-initialized (Solidity defaults) for the no-bootstrap path.
        uint256 bootstrapTokenId = 0;
        uint128 bootstrapLiquidity = 0;
        bool bootstrapped = false;

        // An empty pool cannot be swapped (no liquidity to trade against). Mint a
        // small, near-single-sided position straddling the TARGET price so there
        // is movable liquidity exactly where the swap needs to land — this works
        // even when the griefed price sits at an extreme tick (where a full-range
        // bootstrap would round to zero liquidity on the scarce side and revert).
        if (IUniswapV3Pool(pool).liquidity() == 0) {
            (bootstrapTokenId, bootstrapLiquidity) = _bootstrapLiquidity(token0, token1, sqrtTarget);
            bootstrapped = true;
        }

        // Direction: if the pool price is above target, sell token0 (zeroForOne
        // lowers price); otherwise sell token1. The price limit halts the swap
        // exactly at target and leaves the unused input with this manager.
        (uint160 sqrtNow,,,,,,) = IUniswapV3Pool(pool).slot0();
        (address tokenIn, address tokenOut) = sqrtNow > sqrtTarget ? (token0, token1) : (token1, token0);
        uint256 amountIn = IERC20(tokenIn).balanceOf(address(this));

        tokenIn.safeApprove(address(swapRouter), amountIn);
        swapRouter.exactInputSingle(
            ISwapRouter.ExactInputSingleParams({
                tokenIn: tokenIn,
                tokenOut: tokenOut,
                fee: FEE_TIER,
                recipient: address(this),
                deadline: block.timestamp,
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: sqrtTarget
            })
        );
        tokenIn.safeApprove(address(swapRouter), 0);

        // Recover the bootstrap position so its tokens flow into the locked mint
        // (or get swept). The now-empty NFT is left owned by this manager.
        if (bootstrapped) {
            nonfungiblePositionManager.decreaseLiquidity(
                INonfungiblePositionManager.DecreaseLiquidityParams({
                    tokenId: bootstrapTokenId,
                    liquidity: bootstrapLiquidity,
                    amount0Min: 0,
                    amount1Min: 0,
                    deadline: block.timestamp
                })
            );
            nonfungiblePositionManager.collect(
                INonfungiblePositionManager.CollectParams({
                    tokenId: bootstrapTokenId,
                    recipient: address(this),
                    amount0Max: type(uint128).max,
                    amount1Max: type(uint128).max
                })
            );
        }

        // Post-condition: the pool must now sit within tolerance of the fair price.
        (uint160 sqrtAfter,,,,,,) = IUniswapV3Pool(pool).slot0();
        if (!_withinTolerance(sqrtAfter, sqrtTarget)) revert PriceCorrectionFailed();
    }

    /// @dev Mint a narrow full-fee-tier position straddling `sqrtTarget`, owned by
    ///      this manager, to give an empty griefed pool movable liquidity. Because
    ///      the band hugs the target and the griefed price is (by construction,
    ///      having failed the {TOL_BPS} check) outside it, the mint is effectively
    ///      single-sided and therefore cheap to fund at any extreme. ~1/1000 of
    ///      the manager's balances is committed; it is fully recovered afterwards.
    function _bootstrapLiquidity(
        address token0,
        address token1,
        uint160 sqrtTarget
    ) internal returns (uint256 tokenId, uint128 liquidity) {
        // Band = the single tick-spacing interval that contains sqrtTarget.
        // The divide-then-multiply is intentional tick-grid flooring (snap the
        // target tick down to a multiple of TICK_SPACING), not a precision bug.
        int24 targetTick = TickMath.getTickAtSqrtRatio(sqrtTarget);
        // slither-disable-next-line divide-before-multiply
        int24 tickLower = (targetTick / TICK_SPACING) * TICK_SPACING;
        // Solidity truncates toward zero; floor for negative ticks with remainder.
        if (targetTick < 0 && targetTick % TICK_SPACING != 0) tickLower -= TICK_SPACING;
        int24 tickUpper = tickLower + TICK_SPACING;

        // Commit ~0.1% of each side; the binding side funds the (single-sided)
        // position, the other is left untouched by the mint. Always > 0 for any
        // realistic graduation (DEX reserve ~2e26 wei, ETH ~1e18 wei).
        uint256 desired0 = IERC20(token0).balanceOf(address(this)) / 1000;
        uint256 desired1 = IERC20(token1).balanceOf(address(this)) / 1000;

        token0.safeApprove(address(nonfungiblePositionManager), desired0);
        token1.safeApprove(address(nonfungiblePositionManager), desired1);
        (tokenId, liquidity,,) = nonfungiblePositionManager.mint(
            INonfungiblePositionManager.MintParams({
                token0: token0,
                token1: token1,
                fee: FEE_TIER,
                tickLower: tickLower,
                tickUpper: tickUpper,
                amount0Desired: desired0,
                amount1Desired: desired1,
                amount0Min: 0,
                amount1Min: 0,
                recipient: address(this),
                deadline: block.timestamp
            })
        );
        token0.safeApprove(address(nonfungiblePositionManager), 0);
        token1.safeApprove(address(nonfungiblePositionManager), 0);

        if (liquidity == 0) revert PriceCorrectionFailed();
    }

    // slither-disable-end unused-return
    // slither-disable-end incorrect-equality
    // slither-disable-end reentrancy-events
    // slither-disable-end reentrancy-benign
    // slither-disable-end reentrancy-no-eth
    // slither-disable-end reentrancy-balance

    // ---------------- Internal: math helpers ----------------

    /// @dev `sqrtPriceX96 = sqrt((amount1 << 192) / amount0)`, computed without
    ///      overflow via FullMath's 512-bit intermediate.
    function _computeSqrtPriceX96(
        uint256 amount0,
        uint256 amount1
    ) internal pure returns (uint160) {
        // ratioX192 = (amount1 * 2^192) / amount0, full-precision.
        uint256 ratioX192 = FullMath.mulDiv(amount1, 1 << 192, amount0);
        uint256 sqrt = FixedPointMathLib.sqrt(ratioX192);
        return uint160(sqrt);
    }

    /// @dev True iff `a` is within {TOL_BPS} basis points of `target`.
    ///      |a - target| * BPS_DENOMINATOR <= target * TOL_BPS. Both inputs are
    ///      uint160, so the multiplications cannot overflow uint256.
    function _withinTolerance(
        uint160 a,
        uint160 target
    ) internal pure returns (bool) {
        uint256 diff = a > target ? uint256(a) - target : uint256(target) - a;
        return diff * BPS_DENOMINATOR <= uint256(target) * TOL_BPS;
    }
}
Chain explorer3242msChain node77ms