ZAMMZapETHJPYC

Description:

Decentralized Finance (DeFi) protocol contract providing Swap, Liquidity, Factory functionality.

Blockchain: Ethereum

Source Code: View Code On The Blockchain

Solidity Source Code:

{{
  "language": "Solidity",
  "sources": {
    "src/ZAMMZapETHJPYC.sol": {
      "content": "// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

address constant ZROUTER = 0x00000000008892d085e0611eb8C8BDc9FD856fD3;
address constant ZQUOTER = 0x907DAE8d75369A21fFf57402Fe29Ef4e95523465;
address constant JPYC    = 0xE7C3D8C9a439feDe00D2600032D5dB0Be71C3c29;

uint256 constant BPS     = 10_000;

struct PoolKey {
    uint256 id0;
    uint256 id1;
    address token0;
    address token1;
    uint256 feeOrHook;
}

enum AMM {
    UNI_V2,
    SUSHI,
    ZAMM,
    UNI_V3,
    UNI_V4,
    CURVE
}

struct Quote {
    AMM source;
    uint256 feeBps;
    uint256 amountIn;
    uint256 amountOut;
}

interface IZQuoter {
    function buildBestSwapViaETHMulticall(
        address to,
        address refundTo,
        bool exactOut,        
        address tokenIn,      
        address tokenOut,     
        uint256 swapAmount,   
        uint256 slippageBps,  
        uint256 deadline
    )
        external
        view
        returns (
            Quote memory a,
            Quote memory b,
            bytes[] memory calls,
            bytes memory multicallBlobUnused,
            uint256 msgValueForSwap
        );
}

interface IZRouter {
    function multicall(bytes[] calldata data) external payable returns (bytes[] memory);

    function addLiquidity(
        PoolKey calldata poolKey,
        uint256 amount0Desired,
        uint256 amount1Desired,
        uint256 amount0Min,
        uint256 amount1Min,
        address to,
        uint256 deadline
    ) external payable returns (uint256 amount0, uint256 amount1, uint256 liquidity);

    function sweep(address token, uint256 id, uint256 amount, address to) external payable;
}

/*────────────────────────────────────────────────────────────
    ZAMMZapETHJPYC
────────────────────────────────────────────────────────────*/

/// @notice One-step zap for ETH → JPYC → LP on an existing ETH/JPYC ZAMM pool.
/// @dev Flow (zapAndAddLiquidity):
///  1. User sends ETH to this contract.
///  2. We split that ETH into:
///        ethSwap = portion to sell for JPYC
///        ethLP   = remainder kept in ETH for LP
///  3. We query zQuoter for the best route to sell `ethSwap` ETH → JPYC,
///     with outputs landing in zRouter and no premature refund.
///  4. We build a zRouter.multicall:
///       - all swap calls from zQuoter,
///       - zRouter.addLiquidity(poolKey, ethLP, jpycForLP, ...),
///       - zRouter.sweep(JPYC, ... to user),
///       - zRouter.sweep(ETH,  ... to user).
///     We send *all* the ETH (msg.value) into zRouter.multicall.
///     Internally, zRouter will spend ethSwap for swaps, keep ethLP,
///     add LP with ethLP + JPYC, then sweep leftovers.
///  5. We decode the `liquidity` minted from the addLiquidity leg and return it.
///
/// @dev Assumptions:
///  - pool is already initialized (not first liquidity).
///  - The pool key is ETH/JPYC with ETH sorted as token0:
///        poolKey.token0 == address(0)
///        poolKey.id0    == 0
///        poolKey.token1 == JPYC
///  - zRouter already has JPYC allowance granted to the underlying ZAMM via ensureAllowance.
///  - We are always operating in exactIn mode (exactOut=false).
contract ZAMMZapETHJPYC {
    error InvalidPoolKey();
    error BadParams();
    error ZeroQuote();
    error DeadlineExpired();

    constructor() payable {}

    /*────────────────────────────────────────────────────────────
        INTERNAL HELPERS
    ────────────────────────────────────────────────────────────*/

    /// @dev sanity-check: pool must be (ETH,id0=0) / JPYC / existing fee.
    /// We don't force-check feeOrHook here (e.g. "30 bps") because
    /// feeOrHook can also carry hook flags; we just assert the asset ordering.
    function _checkPoolKey(PoolKey calldata poolKey) internal pure {
        if (
            poolKey.token0 != address(0) || // ETH must be token0
            poolKey.id0 != 0 ||
            poolKey.token1 != JPYC
        ) {
            revert InvalidPoolKey();
        }
    }

    /// @dev split total ETH into swap portion vs LP portion using swapBps (1-9999).
    /// swapBps is share (in basis points) of total ETH to convert to JPYC.
    /// The remainder stays ETH as the LP leg.
    function _splitETH(uint256 ethTotal, uint256 swapBps)
        internal
        pure
        returns (uint256 ethSwap, uint256 ethLP)
    {
        // e.g. swapBps = 5000 → 50/50 split
        // must be within (0, 10000)
        if (swapBps == 0 || swapBps >= BPS) revert BadParams();
        ethSwap = (ethTotal * swapBps) / BPS;
        ethLP = ethTotal - ethSwap;
    }

    /// @dev haircut predicted JPYC by slippageBps so we don't over-ask ZAMM for JPYC.
    /// This value is safe to feed as amount1Desired.
    function _calcJpycForLP(uint256 predictedOut, uint256 slippageBps)
        internal
        pure
        returns (uint256 jpycForLP)
    {
        if (slippageBps > BPS) revert BadParams();
        jpycForLP = (predictedOut * (BPS - slippageBps)) / BPS;
        if (jpycForLP == 0) revert ZeroQuote();
    }

    /// @dev pull the "expected" out amount from quoter Quotes.
    /// On exactIn, if route is 2-hop, `b` is final; if 1-hop, `b.amountOut` may be 0 and `a` holds it.
    function _predictedOut(Quote memory a, Quote memory b) internal pure returns (uint256 outAmt) {
        outAmt = (b.amountOut != 0) ? b.amountOut : a.amountOut;
    }

    /*────────────────────────────────────────────────────────────
        VIEW: PREVIEW
        This is a helper for frontends/keepers. It does not move funds.
    ────────────────────────────────────────────────────────────*/

    /// @notice Pure preview of how we'd build the zap multicall, without sending anything.
    /// @param poolKey     Target ETH/JPYC pool. Must pass _checkPoolKey.
    /// @param ethTotal    Hypothetical ETH input amount in wei.
    /// @param swapBps     % of ethTotal to convert to JPYC (basis points).
    /// @param slippageBps Per-leg swap slippage tolerance (e.g. 50 = 0.5%).
    /// @param deadline    Timestamp after which we consider this invalid.
    /// @param to          Recipient of LP tokens and dust refunds.
    ///
    /// @return ethSwap         ETH that would be sold for JPYC.
    /// @return ethLP           ETH kept for LP.
    /// @return predictedJPYC   Optimistic JPYC output before haircut.
    /// @return jpycForLP       JPYC amount we'd *offer* to addLiquidity.
    /// @return finalCallsPrev  The calldata array we'd ultimately pass
    ///                         into zRouter.multicall([...]) on-chain.
    function previewZap(
        PoolKey calldata poolKey,
        uint256 ethTotal,
        uint256 swapBps,
        uint256 slippageBps,
        uint256 deadline,
        address to
    )
        public
        view
        returns (
            uint256 ethSwap,
            uint256 ethLP,
            uint256 predictedJPYC,
            uint256 jpycForLP,
            bytes[] memory finalCallsPrev
        )
    {
        _checkPoolKey(poolKey);
        if (deadline < block.timestamp) revert DeadlineExpired();
        if (ethTotal == 0) revert BadParams();

        // 1. Split ETH budget
        (ethSwap, ethLP) = _splitETH(ethTotal, swapBps);

        // 2. Ask zQuoter for ETH->JPYC multicall legs (exactIn mode).
        (
            Quote memory qa,
            Quote memory qb,
            bytes[] memory swapCalls,
            /* bytes memory unusedMulticall */,
            /* uint256 msgValueForSwap */
        ) = IZQuoter(ZQUOTER).buildBestSwapViaETHMulticall(
                ZROUTER,
                to,          // refundTo: used by exactOut path, irrelevant in exactIn but ok
                false,       // exactOut = false → we are selling exact ETH
                address(0),  // tokenIn = ETH
                JPYC,        // tokenOut = JPYC
                ethSwap,     // how much ETH to sell
                slippageBps,
                deadline
            );

        predictedJPYC = _predictedOut(qa, qb);
        if (predictedJPYC == 0) revert ZeroQuote();

        // 3. Conservative JPYC to actually try and LP with
        jpycForLP = _calcJpycForLP(predictedJPYC, slippageBps);

        // 4. Encode the follow-up router calls after swaps:

        // 4a. addLiquidity(poolKey, ethLP, jpycForLP, 0,0, to, deadline)
        // token0 == ETH, token1 == JPYC
        bytes memory addLiqCall = abi.encodeWithSelector(
            IZRouter.addLiquidity.selector,
            poolKey,
            ethLP,          // amount0Desired: ETH side
            jpycForLP,      // amount1Desired: JPYC side
            0,              // amount0Min (can tighten if you want LP slippage bounds)
            0,              // amount1Min
            to,             // LP receiver
            deadline
        );

        // 4b. sweep JPYC leftovers back to user
        bytes memory sweepJPYC = abi.encodeWithSelector(
            IZRouter.sweep.selector,
            JPYC,           // token
            0,              // id
            0,              // amount=0 means "sweep full balance"
            to
        );

        // 4c. sweep ETH/WETH leftovers back to user
        bytes memory sweepETH = abi.encodeWithSelector(
            IZRouter.sweep.selector,
            address(0),     // token = ETH sentinel
            0,
            0,
            to
        );

        // 5. Concatenate [swapCalls..., addLiqCall, sweepJPYC, sweepETH]
        uint256 n = swapCalls.length;
        finalCallsPrev = new bytes[](n + 3);
        for (uint256 i; i < n; ++i) {
            finalCallsPrev[i] = swapCalls[i];
        }
        finalCallsPrev[n] = addLiqCall;
        finalCallsPrev[n + 1] = sweepJPYC;
        finalCallsPrev[n + 2] = sweepETH;
    }

    /*────────────────────────────────────────────────────────────
        MAIN ACTION: EXECUTE THE ZAP
    ────────────────────────────────────────────────────────────*/

    /// @notice Perform the full zap using msg.value ETH.
    ///
    /// @param poolKey     Must be the canonical ETH/JPYC poolKey
    ///                    (token0 == address(0), id0 == 0, token1 == JPYC).
    /// @param swapBps     Split ratio in basis points.
    ///                    e.g. 5000 = sell 50% of the ETH for JPYC, keep 50% ETH.
    /// @param slippageBps Per-leg swap slippage tolerance to pass into zQuoter.
    ///                    e.g. 50 == 0.5%.
    /// @param deadline    Timestamp after which we revert.
    /// @param to          Recipient of LP tokens and final leftover refunds.
    ///
    /// @return liquidityMinted LP tokens minted to `to`.
    function zapAndAddLiquidity(
        PoolKey calldata poolKey,
        uint256 swapBps,
        uint256 slippageBps,
        uint256 deadline,
        address to
    ) public payable returns (uint256 liquidityMinted) {
        _checkPoolKey(poolKey);
        if (deadline < block.timestamp) revert DeadlineExpired();
        if (msg.value == 0) revert BadParams();

        // 1. Split incoming ETH between "sell for JPYC" and "keep as ETH leg".
        (uint256 ethSwap, uint256 ethLP) = _splitETH(msg.value, swapBps);

        // 2. Fetch optimal ETH->JPYC route from zQuoter. We operate in exactIn mode.
        (
            Quote memory qa,
            Quote memory qb,
            bytes[] memory swapCalls,
            /* bytes memory unusedMulticall */,
            /* uint256 msgValueForSwap */
        ) = IZQuoter(ZQUOTER).buildBestSwapViaETHMulticall(
                ZROUTER,
                to,          // refundTo (only really used for exactOut; harmless here)
                false,       // exactOut = false => we sell exactly ethSwap ETH
                address(0),  // tokenIn = ETH
                JPYC,        // tokenOut = JPYC
                ethSwap,     // amount of ETH to sell
                slippageBps,
                deadline
            );

        // 3. Predict JPYC and haircut to a safe "amount1Desired".
        uint256 predictedJPYC = _predictedOut(qa, qb);
        if (predictedJPYC == 0) revert ZeroQuote();

        uint256 jpycForLP = _calcJpycForLP(predictedJPYC, slippageBps);

        // 4. Build addLiquidity call for the ETH/JPYC pool.
        // Since the pool is already initialized, ZAMM will enforce ratio against reserves.
        // It will only actually consume what fits the pool price, and refund extra to zRouter.
        bytes memory addLiqCall = abi.encodeWithSelector(
            IZRouter.addLiquidity.selector,
            poolKey,
            ethLP,          // amount0Desired (ETH leg)
            jpycForLP,      // amount1Desired (JPYC leg, conservative)
            0,              // amount0Min
            0,              // amount1Min
            to,             // LP tokens -> user
            deadline
        );

        // 5. Sweep leftover JPYC back to user (0 means sweep full balance)
        bytes memory sweepJPYC = abi.encodeWithSelector(
            IZRouter.sweep.selector,
            JPYC,
            0,
            0,
            to
        );

        // 6. Sweep leftover ETH/WETH back to user
        bytes memory sweepETH = abi.encodeWithSelector(
            IZRouter.sweep.selector,
            address(0),
            0,
            0,
            to
        );

        // 7. Assemble finalCalls = [ all swapCalls..., addLiquidity, sweepJPYC, sweepETH ]
        uint256 n = swapCalls.length;
        bytes[] memory finalCalls = new bytes[](n + 3);
        for (uint256 i; i < n; ++i) {
            finalCalls[i] = swapCalls[i];
        }
        finalCalls[n] = addLiqCall;
        finalCalls[n + 1] = sweepJPYC;
        finalCalls[n + 2] = sweepETH;

        // 8. Execute on zRouter in a single atomic multicall, forwarding all ETH.
        //
        //    Why value = msg.value:
        //    - zQuoter's first leg(s) will internally consume `ethSwap` from that pool of ETH.
        //      Because we told quoter "to = ZROUTER", those legs won't auto-refund leftover ETH,
        //      so the remainder is effectively "held" by zRouter for LP.
        //    - addLiquidity then forwards ethLP into ZAMM.
        //    - ZAMM may refund unused ETH back to zRouter.
        //    - Finally, we sweep any dust (unused ETH or JPYC) back to `to`.
        //
        bytes[] memory routerResults = IZRouter(ZROUTER).multicall{value: msg.value}(finalCalls);

        // 9. Decode liquidity from the addLiquidity result.
        //
        // routerResults[i] corresponds 1:1 with finalCalls[i].
        // The addLiquidity call is at index `n`.
        //
        // IZRouter.addLiquidity returns (uint256 amount0, uint256 amount1, uint256 liquidity).
        {
            bytes memory liqRet = routerResults[n];
            (/* uint256 used0 */, /* uint256 used1 */, uint256 liq) =
                abi.decode(liqRet, (uint256, uint256, uint256));
            liquidityMinted = liq;
        }

        // done: LP tokens already sent to `to`, and sweeps done inside multicall.
    }
}
"
    }
  },
  "settings": {
    "remappings": [
      "@solady/=lib/solady/",
      "@soledge/=lib/soledge/",
      "@forge/=lib/forge-std/src/",
      "forge-std/=lib/forge-std/src/",
      "solady/=lib/solady/src/"
    ],
    "optimizer": {
      "enabled": true,
      "runs": 200
    },
    "metadata": {
      "useLiteralContent": false,
      "bytecodeHash": "ipfs",
      "appendCBOR": true
    },
    "outputSelection": {
      "*": {
        "*": [
          "evm.bytecode",
          "evm.deployedBytecode",
          "devdoc",
          "userdoc",
          "metadata",
          "abi"
        ]
      }
    },
    "evmVersion": "prague",
    "viaIR": true
  }
}}

Tags:
DeFi, Swap, Liquidity, Factory|addr:0x644c22269b0572f22a3fccb9cde24b604f56ec03|verified:true|block:23669625|tx:0xcb1f894188c8ef9a06d3445ed3a4496b4e91742522f3d096474a3e4d970b4730|first_check:1761582007

Submitted on: 2025-10-27 17:20:07

Comments

Log in to comment.

No comments yet.