ZapV3
0xfef4bfb71d007ae8bfb63d415a6e56458c2412a0
Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
12,683 bytes
ABI entries
60
7 read · 13 write
License
none
Contract information
- Address
- 0xfef4bfb71d007ae8bfb63d415a6e56458c2412a0
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.24+commit.e11b9ed9
- Optimization
- Enabled
- Creator
- 0x4e59b44847…26c0B4956C
- Creation tx
- 0x5b1b67e0b7…1749423923
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (7)
MAX_POSITIONS() → uint256
WRAPPED_NATIVE() → address
owner() → address
paused() → bool
pendingOwner() → address
positionManagerWhitelist(address) → bool
routerWhitelist(address) → bool
Events (11)
LadderCreatedOwnershipTransferStartedOwnershipTransferredPausedPositionManagerSetRescuedRescuedETHRouterSetUnpausedWrappedNativeSetZapped
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "initialOwner",
"type": "address"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [],
"name": "AlreadySet",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "expected",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "actual",
"type": "uint256"
}
],
"name": "AmountMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
}
],
"name": "BadTokenOrder",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "blockTimestamp",
"type": "uint256"
}
],
"name": "DeadlineExpired",
"type": "error"
},
{
"inputs": [],
"name": "EmptyLadder",
"type": "error"
},
{
"inputs": [],
"name": "EnforcedPause",
"type": "error"
},
{
"inputs": [],
"name": "ExpectedPause",
"type": "error"
},
{
"inputs": [],
"name": "FailedCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "balance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "needed",
"type": "uint256"
}
],
"name": "InsufficientBalance",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "received",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minRequired",
"type": "uint256"
}
],
"name": "InsufficientOutput",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "pulled",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "swapAmount",
"type": "uint256"
}
],
"name": "InsufficientPulledForSwap",
"type": "error"
},
{
"inputs": [],
"name": "NotImplemented",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [],
"name": "ReentrancyGuardReentrantCall",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"inputs": [],
"name": "StableMustBeWrappedNative",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "swapAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "stableAmount",
"type": "uint256"
}
],
"name": "SwapAmountExceedsTotal",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes",
"name": "reason",
"type": "bytes"
}
],
"name": "SwapCallFailed",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "stable",
"type": "address"
},
{
"internalType": "address",
"name": "volatile",
"type": "address"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
}
],
"name": "TokensMismatch",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "count",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "max",
"type": "uint256"
}
],
"name": "TooManyPositions",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "pm",
"type": "address"
}
],
"name": "UnauthorizedPM",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "router",
"type": "address"
}
],
"name": "UnauthorizedRouter",
"type": "error"
},
{
"inputs": [],
"name": "WrappedNativeNotSet",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAddress",
"type": "error"
},
{
"inputs": [],
"name": "ZeroAmount",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "tokenId",
"type": "uint256"
}
],
"name": "ZeroLiquidityMinted",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
}
],
"name": "LadderCreated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferStarted",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Paused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "pm",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "PositionManagerSet",
"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": "Rescued",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "to",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "RescuedETH",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "router",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "RouterSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "Unpaused",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "wn",
"type": "address"
}
],
"name": "WrappedNativeSet",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "user",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "stable",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "volatile",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "stableSpent",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "volatileReceived",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "positions",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint8",
"name": "zapMode",
"type": "uint8"
}
],
"name": "Zapped",
"type": "event"
},
{
"inputs": [],
"name": "MAX_POSITIONS",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "WRAPPED_NATIVE",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "acceptOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "paused",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "pendingOwner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "positionManagerWhitelist",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "contract IERC20",
"name": "token",
"type": "address"
},
{
"internalType": "address",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescue",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address payable",
"name": "to",
"type": "address"
},
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "rescueETH",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "routerWhitelist",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "pm",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setPositionManager",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "router",
"type": "address"
},
{
"internalType": "bool",
"name": "allowed",
"type": "bool"
}
],
"name": "setRouter",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "wn",
"type": "address"
}
],
"name": "setWrappedNative",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "unpause",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "stable",
"type": "address"
},
{
"internalType": "address",
"name": "volatile",
"type": "address"
},
{
"internalType": "uint256",
"name": "stableAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "swapAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minVolatileOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "okxRouter",
"type": "address"
},
{
"internalType": "address",
"name": "okxApproveTarget",
"type": "address"
},
{
"internalType": "bytes",
"name": "okxCalldata",
"type": "bytes"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount0Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1Min",
"type": "uint256"
}
],
"internalType": "struct ZapV3.Position[]",
"name": "positions",
"type": "tuple[]"
}
],
"internalType": "struct ZapV3.ZapParams",
"name": "p",
"type": "tuple"
}
],
"name": "zapIn",
"outputs": [
{
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "stable",
"type": "address"
},
{
"internalType": "address",
"name": "volatile",
"type": "address"
},
{
"internalType": "uint256",
"name": "stableAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "swapAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minVolatileOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "okxRouter",
"type": "address"
},
{
"internalType": "address",
"name": "okxApproveTarget",
"type": "address"
},
{
"internalType": "bytes",
"name": "okxCalldata",
"type": "bytes"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount0Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1Min",
"type": "uint256"
}
],
"internalType": "struct ZapV3.Position[]",
"name": "positions",
"type": "tuple[]"
}
],
"internalType": "struct ZapV3.ZapParams",
"name": "p",
"type": "tuple"
},
{
"internalType": "uint256",
"name": "extraVolatileAmount",
"type": "uint256"
}
],
"name": "zapInDual",
"outputs": [
{
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"inputs": [
{
"components": [
{
"internalType": "address",
"name": "stable",
"type": "address"
},
{
"internalType": "address",
"name": "volatile",
"type": "address"
},
{
"internalType": "uint256",
"name": "stableAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "swapAmount",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "minVolatileOut",
"type": "uint256"
},
{
"internalType": "address",
"name": "okxRouter",
"type": "address"
},
{
"internalType": "address",
"name": "okxApproveTarget",
"type": "address"
},
{
"internalType": "bytes",
"name": "okxCalldata",
"type": "bytes"
},
{
"internalType": "address",
"name": "positionManager",
"type": "address"
},
{
"internalType": "address",
"name": "token0",
"type": "address"
},
{
"internalType": "address",
"name": "token1",
"type": "address"
},
{
"internalType": "uint24",
"name": "fee",
"type": "uint24"
},
{
"internalType": "uint256",
"name": "deadline",
"type": "uint256"
},
{
"components": [
{
"internalType": "int24",
"name": "tickLower",
"type": "int24"
},
{
"internalType": "int24",
"name": "tickUpper",
"type": "int24"
},
{
"internalType": "uint256",
"name": "amount0",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount0Min",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "amount1Min",
"type": "uint256"
}
],
"internalType": "struct ZapV3.Position[]",
"name": "positions",
"type": "tuple[]"
}
],
"internalType": "struct ZapV3.ZapParams",
"name": "p",
"type": "tuple"
}
],
"name": "zapInWithNative",
"outputs": [
{
"internalType": "uint256[]",
"name": "tokenIds",
"type": "uint256[]"
}
],
"stateMutability": "payable",
"type": "function"
},
{
"stateMutability": "payable",
"type": "receive"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.24;
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ZapBase} from "./ZapBase.sol";
import {INonfungiblePositionManager as IPM} from "./interfaces/INonfungiblePositionManager.sol";
/// @title ZapV3 (v2) — atomic swap + V3 ladder mint with balance-based distribution.
///
/// @notice One TX: pull stable from user (or wrap native), optionally swap a
/// portion to volatile, then mint N V3 position NFTs to the user.
///
/// @dev v2 changes vs v1 (see ZAP_PLAN.md):
/// - Balance-based budget: read post-swap balance delta and use it
/// as the authoritative basis. Backend stops bumping caps.
/// - Proportional distribution: per-position
/// `amount{0,1}Desired = floor(budget × raw_i / Σ raw)`; last
/// position absorbs the floor-div remainder. PM consumes only
/// what liquidity needs at current tick; leftover refunded.
/// `amount{0,1}Min` retained (backend sets 0).
/// - Three entry points: `zapIn` (ERC20), `zapInWithNative`
/// (wraps `msg.value`), `zapInDual` (functional, dormant until
/// backend ships). Native refunds unwrap WBNB → raw native.
///
/// V3 has no revert class to fix (uses `amountMin = 0`, no Permit2),
/// but suffers the SAME backend-bump dust pattern as V4. Bringing
/// V3 into the v2 design reduces dust from ~1-2% of user budget to
/// the floor-div remainder + aggregator overfill (~0.1-0.5%).
contract ZapV3 is ZapBase {
using SafeERC20 for IERC20;
/// @dev Per-position V3 mint recipe. `amount0`/`amount1` are RAW weights
/// from the backend distribution math — used here to scale a
/// per-position share of the balance-based budget. `amount0Min`/
/// `amount1Min` are retained (backend sets 0; defensive sanity, no
/// slippage enforcement at mint level).
struct Position {
int24 tickLower;
int24 tickUpper;
uint256 amount0;
uint256 amount1;
uint256 amount0Min;
uint256 amount1Min;
}
struct ZapParams {
// ── Swap leg ──
address stable;
address volatile;
uint256 stableAmount;
uint256 swapAmount;
uint256 minVolatileOut;
address okxRouter;
address okxApproveTarget;
bytes okxCalldata;
// ── Mint leg ──
address positionManager;
address token0;
address token1;
uint24 fee;
uint256 deadline;
Position[] positions;
}
error SwapAmountExceedsTotal(uint256 swapAmount, uint256 stableAmount);
error TokensMismatch(address stable, address volatile, address token0, address token1);
error BadTokenOrder(address token0, address token1);
error InsufficientPulledForSwap(uint256 pulled, uint256 swapAmount);
error ZeroLiquidityMinted(uint256 tokenId);
event LadderCreated(address indexed user, uint256[] tokenIds);
constructor(address initialOwner) ZapBase(initialOwner) {}
// ─── Entry points ─────────────────────────────────────────────
/// @notice Standard ERC20 zap. msg.value MUST be 0 (non-payable).
function zapIn(ZapParams calldata p)
external
nonReentrant
whenNotPaused
returns (uint256[] memory tokenIds)
{
return _zapInInternal(p, false, 0);
}
/// @notice Native zap. Wraps `msg.value` to WETH/WBNB; native refund on exit.
function zapInWithNative(ZapParams calldata p)
external
payable
nonReentrant
whenNotPaused
returns (uint256[] memory tokenIds)
{
if (msg.value == 0) revert ZeroAmount();
return _zapInInternal(p, true, 0);
}
/// @notice Dual zap. Pulls `extraVolatileAmount` of volatile from user
/// in addition to stable. Swap leg may be smaller or skipped.
/// @dev Native dual is detected by `msg.value > 0`. Audited as part
/// of v2; backend / frontend remain unwired in v1 of v2.
function zapInDual(ZapParams calldata p, uint256 extraVolatileAmount)
external
payable
nonReentrant
whenNotPaused
returns (uint256[] memory tokenIds)
{
if (extraVolatileAmount == 0) revert ZeroAmount();
return _zapInInternal(p, msg.value > 0, extraVolatileAmount);
}
// ─── Core flow ────────────────────────────────────────────────
function _zapInInternal(ZapParams calldata p, bool useNative, uint256 extraVolatile)
internal
returns (uint256[] memory tokenIds)
{
// Native-mode-specific guards fire FIRST so the user gets the most
// actionable error: a misconfigured native zap (WN unset, or stable
// ≠ WBNB) shouldn't surface as the generic TokensMismatch from
// downstream validation. `_acquireStable` retains the same checks as
// defense-in-depth.
if (useNative) {
if (WRAPPED_NATIVE == address(0)) revert WrappedNativeNotSet();
if (p.stable != WRAPPED_NATIVE) revert StableMustBeWrappedNative();
}
// ── Input validation (fail-fast, before any state change) ──
if (block.timestamp > p.deadline) revert DeadlineExpired(p.deadline, block.timestamp);
uint256 n = p.positions.length;
if (n == 0) revert EmptyLadder();
if (n > MAX_POSITIONS) revert TooManyPositions(n, MAX_POSITIONS);
if (p.stableAmount == 0) revert ZeroAmount();
// swapAmount may legitimately be 0 only in dual mode (extraVolatile > 0).
if (p.swapAmount == 0 && extraVolatile == 0) revert ZeroAmount();
if (p.swapAmount > p.stableAmount) {
revert SwapAmountExceedsTotal(p.swapAmount, p.stableAmount);
}
if (p.stable == address(0) || p.volatile == address(0)) revert ZeroAddress();
if (p.stable == p.volatile) revert TokensMismatch(p.stable, p.volatile, p.token0, p.token1);
if (!positionManagerWhitelist[p.positionManager]) revert UnauthorizedPM(p.positionManager);
if (p.token0 >= p.token1) revert BadTokenOrder(p.token0, p.token1);
// Single-line set match — saves a stack slot vs two named bools, which
// matters under via_ir=false. The compiler doesn't always reap dead
// bool locals between SSA blocks; we don't gain anything by naming.
if (
!(p.token0 == p.stable && p.token1 == p.volatile)
&& !(p.token0 == p.volatile && p.token1 == p.stable)
) {
revert TokensMismatch(p.stable, p.volatile, p.token0, p.token1);
}
IERC20 stable = IERC20(p.stable);
IERC20 volatile = IERC20(p.volatile);
// Snapshot pre-existing balances — done BEFORE any acquisition so
// wrap's WBNB credit shows up as user delta, not stuck dust.
uint256 stableBefore = stable.balanceOf(address(this));
uint256 volBefore = volatile.balanceOf(address(this));
// Reentry into public surface blocked by `nonReentrant`. `WrappedNative.deposit`
// (native) / `safeTransferFrom` of whitelisted stable (ERC20) — neither
// callbacks into msg.sender. `pulled` is RECOMPUTED post-call below, not
// reused. Slither's `reentrancy-balance` acknowledged (v1 audit I-6 pattern).
// slither-disable-next-line reentrancy-balance
_acquireStable(stable, p.stableAmount, useNative);
uint256 pulled = stable.balanceOf(address(this)) - stableBefore;
if (pulled < p.swapAmount) revert InsufficientPulledForSwap(pulled, p.swapAmount);
if (extraVolatile > 0) {
volatile.safeTransferFrom(msg.sender, address(this), extraVolatile);
}
uint256 volReceived = 0;
{
if (p.swapAmount > 0) {
(volReceived,) = _executeSwap(
SwapCall({
tokenIn: stable,
tokenOut: volatile,
maxAmountIn: p.swapAmount,
minAmountOut: p.minVolatileOut,
router: p.okxRouter,
approveTarget: p.okxApproveTarget
}),
p.okxCalldata
);
}
// Balance-based budget per canonical-order leg. Inlined branch
// (instead of declaring named budget0/budget1 locals) keeps the
// stack within the 16-slot limit under via_ir=false.
uint256 stableForMint = stable.balanceOf(address(this)) - stableBefore;
uint256 volForMint = volatile.balanceOf(address(this)) - volBefore;
tokenIds = (p.token0 == p.stable)
? _mintLadder(p, stableForMint, volForMint)
: _mintLadder(p, volForMint, stableForMint);
}
// Refund leftover only — V3 PM consumes only what liquidity needs at
// current tick, so per-position underspend accumulates here. Native
// mode unwraps WBNB → raw native via `_refundStable`.
{
uint256 stableLeft = stable.balanceOf(address(this)) - stableBefore;
_refundStable(stable, stableLeft, useNative);
}
{
uint256 volLeft = volatile.balanceOf(address(this)) - volBefore;
if (volLeft > 0) volatile.safeTransfer(msg.sender, volLeft);
}
uint8 zapMode = extraVolatile > 0 ? 2 : (useNative ? 1 : 0);
emit Zapped(msg.sender, p.stable, p.volatile, pulled, volReceived, n, zapMode);
emit LadderCreated(msg.sender, tokenIds);
}
/// @dev Approve PM up to the user's balance-based budget on each side,
/// distribute proportionally per RAW position weight (last position
/// absorbs the floor-div remainder), mint, then revoke approvals.
///
/// Distribution formula:
/// desired0_i = floor(budget0 × raw0_i / Σ raw0) (last: remainder)
/// desired1_i = floor(budget1 × raw1_i / Σ raw1) (last: remainder)
///
/// With `amount{0,1}Min = 0` (backend default), the PM consumes ≤
/// desired and never reverts on under-fill — leftover stays on the
/// contract for refund. Σ desired_i ≤ budget by construction, so
/// the per-token approval cap is always sufficient.
///
/// The mint loop is split into helpers (`_shareOf`, `_mintOne`) so
/// multiplication / `IPM.mint` parameter packing happen in their
/// own stack frames — keeps `_mintLadder` itself within the EVM
/// stack limit under via_ir=false.
function _mintLadder(ZapParams calldata p, uint256 budget0, uint256 budget1)
internal
returns (uint256[] memory tokenIds)
{
// Approve PM at the EXACT balance-based budget — capped at user
// contribution, so PM cannot consume any pre-existing stuck balance.
IERC20(p.token0).forceApprove(p.positionManager, budget0);
IERC20(p.token1).forceApprove(p.positionManager, budget1);
uint256 n = p.positions.length;
tokenIds = new uint256[](n);
// Σ raw weights — denominator for the proportional split.
uint256 sumRaw0 = 0;
uint256 sumRaw1 = 0;
for (uint256 i; i < n;) {
sumRaw0 += p.positions[i].amount0;
sumRaw1 += p.positions[i].amount1;
unchecked {
++i;
}
}
_runMintLoop(p, budget0, budget1, sumRaw0, sumRaw1, tokenIds);
IERC20(p.token0).forceApprove(p.positionManager, 0);
IERC20(p.token1).forceApprove(p.positionManager, 0);
}
/// @dev Per-position desire calculation. Extracted so the multiplication
/// temporary lives in this frame, not the caller's.
function _shareOf(uint256 budget, uint256 raw, uint256 sumRaw) internal pure returns (uint256) {
// Σ raw == 0 → 0 for non-last; last position absorbs remainder, so
// a one-sided ladder still settles correctly through the caller.
if (sumRaw == 0) return 0;
return (budget * raw) / sumRaw;
}
/// @dev Distribute (budget0, budget1) across all positions and mint each.
/// Caller (`_mintLadder`) has already approved PM and computed sums.
/// Mutates `tokenIds` in place (passed as memory pointer to avoid
/// a second return-allocation).
function _runMintLoop(
ZapParams calldata p,
uint256 budget0,
uint256 budget1,
uint256 sumRaw0,
uint256 sumRaw1,
uint256[] memory tokenIds
) internal {
uint256 n = p.positions.length;
uint256 distributed0 = 0;
uint256 distributed1 = 0;
for (uint256 i; i < n;) {
uint256 desired0;
uint256 desired1;
if (i == n - 1) {
// Last position absorbs the floor-div remainder so
// Σ desired == budget exactly. Underflow-safe: distributed
// is the running sum of prior floor-divs ≤ budget.
desired0 = budget0 - distributed0;
desired1 = budget1 - distributed1;
} else {
desired0 = _shareOf(budget0, p.positions[i].amount0, sumRaw0);
desired1 = _shareOf(budget1, p.positions[i].amount1, sumRaw1);
distributed0 += desired0;
distributed1 += desired1;
}
tokenIds[i] = _mintOne(p, i, desired0, desired1);
unchecked {
++i;
}
}
}
/// @dev Single-position mint. Extracted so the `IPM.MintParams` struct
/// packing happens in its own stack frame.
function _mintOne(ZapParams calldata p, uint256 i, uint256 desired0, uint256 desired1)
internal
returns (uint256 tid)
{
Position calldata pos = p.positions[i];
uint128 liquidity;
// slither-disable-next-line unused-return
(tid, liquidity,,) = IPM(p.positionManager).mint(
IPM.MintParams({
token0: p.token0,
token1: p.token1,
fee: p.fee,
tickLower: pos.tickLower,
tickUpper: pos.tickUpper,
amount0Desired: desired0,
amount1Desired: desired1,
amount0Min: pos.amount0Min,
amount1Min: pos.amount1Min,
recipient: msg.sender,
deadline: p.deadline
})
);
// A V3 mint that returns 0 liquidity is either a buggy PM or a
// pool/tick combo that produced an effectively-empty position —
// either way the user paid gas and tokens-out for an NFT with no
// LP value. Fail loud and roll the whole zap back. Slither's
// `incorrect-equality` is FP — sentinel 0 from whitelisted PM.
// slither-disable-next-line incorrect-equality
if (liquidity == 0) revert ZeroLiquidityMinted(tid);
}
}
Chain explorer2853msChain node82ms