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
- Creator
- 0x04a081D955…ABfE6Fcd17
- 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