PmavLaneZap

0xfb5d7327a9916b23435f5a93cedb96be4fa73c94

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
6,592 bytes
ABI entries
13
4 read · 4 write
License
none

Contract information

Address
0xfb5d7327a9916b23435f5a93cedb96be4fa73c94
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0xd8c3059749…8b71580875

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (4)

ETH_USDG_FEE()uint24
ETH_USDG_TS()int24
USDG()address
poolManager()address

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_pm",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_usdg",
        "type": "address"
      },
      {
        "internalType": "uint24",
        "name": "ethUsdgFee",
        "type": "uint24"
      },
      {
        "internalType": "int24",
        "name": "ethUsdgTs",
        "type": "int24"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "BadV3Callback",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotPoolManager",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TooLittleOut",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ETH_USDG_FEE",
    "outputs": [
      {
        "internalType": "uint24",
        "name": "",
        "type": "uint24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "ETH_USDG_TS",
    "outputs": [
      {
        "internalType": "int24",
        "name": "",
        "type": "int24"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "USDG",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "coin",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "stock",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "laneHook",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "v3Pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minCoinOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "buyWithEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "coinOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "poolManager",
    "outputs": [
      {
        "internalType": "contract IPoolManager",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "coin",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "stock",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "laneHook",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "v3Pool",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "coinIn",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "minEthOut",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "recipient",
        "type": "address"
      }
    ],
    "name": "sellForEth",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "ethOut",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "int256",
        "name": "amount0Delta",
        "type": "int256"
      },
      {
        "internalType": "int256",
        "name": "amount1Delta",
        "type": "int256"
      },
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "uniswapV3SwapCallback",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes",
        "name": "data",
        "type": "bytes"
      }
    ],
    "name": "unlockCallback",
    "outputs": [
      {
        "internalType": "bytes",
        "name": "",
        "type": "bytes"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

// PmavLaneZap — native-ETH in/out router for lane coins whose QUOTE token has
// no deep Uniswap v4 liquidity but IS deeply traded on Uniswap v3 (e.g. the
// tokenized GME stock: $200k v3 GME/USDG pool, no usable v4 base route).
//
// Our existing lane trades route ETH -> USDG -> stock -> coin all as v4 hops
// through the chain's UniversalRouter. That build cannot cross a v3 pool, and
// a stock like GME has its only base-asset liquidity on v3. This periphery
// contract bridges the gap: it does the two v4 legs through the canonical
// PoolManager (unlock/settle/take, same primitives our hook uses) and the one
// v3 leg by calling the v3 pool directly, all inside a SINGLE unlock so the
// user pays/receives native ETH in ONE transaction.
//
//   BUY  (ETH in, coin out):  ETH ->(v4 hookless)-> USDG ->(v3)-> STOCK ->(v4 lane hook)-> COIN
//   SELL (coin in, ETH out):  COIN ->(v4 lane hook)-> STOCK ->(v3)-> USDG ->(v4 hookless)-> ETH
//
// The lane hook enforces the curve, fee split, 2% cap, anti-snipe and any
// over-cap refund on the STOCK->COIN (and COIN->STOCK) hop exactly as it does
// for a normal lane trade; this contract only moves the quote side to/from
// ETH. tx.origin (the real user) is preserved for the hook's per-wallet cap.
//
// Owns nothing, custodies nothing between calls, holds no admin keys. Every
// route param is supplied per call and validated; minOut guards slippage.

import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";

interface IERC20Z {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
    function balanceOf(address a) external view returns (uint256);
    function approve(address s, uint256 a) external returns (bool);
}

interface IUniswapV3PoolZ {
    function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data)
        external
        returns (int256 amount0, int256 amount1);
    function token0() external view returns (address);
    function token1() external view returns (address);
}

contract PmavLaneZap is IUnlockCallback {
    IPoolManager public immutable poolManager;
    address public immutable USDG;
    // the deepest native-ETH/USDG v4 pool leg (hookless), fixed on this chain
    uint24 public immutable ETH_USDG_FEE;
    int24 public immutable ETH_USDG_TS;
    // lane pools use the dynamic-fee flag + a fixed spacing (same as every lane)
    uint24 internal constant DYNAMIC_FEE = 0x800000;
    int24 internal constant LANE_TS = 10;
    uint160 internal constant MIN_LIMIT = TickMath.MIN_SQRT_PRICE + 1;
    uint160 internal constant MAX_LIMIT = TickMath.MAX_SQRT_PRICE - 1;
    // Uniswap v3 uses DIFFERENT sqrt-price bounds than v4 — using v4's on a v3
    // pool reverts with SPL. These are v3's MIN/MAX_SQRT_RATIO (+/- 1).
    uint160 internal constant V3_MIN_LIMIT = 4295128739 + 1;
    uint160 internal constant V3_MAX_LIMIT = 1461446703485210103287273052203988822378723970342 - 1;
    address internal constant NATIVE = address(0);

    error NotPoolManager();
    error TooLittleOut();
    error BadV3Callback();

    // transient route context for the v3 callback (set within a single unlock)
    address private v3ExpectedPool;
    address private v3PayToken;

    constructor(address _pm, address _usdg, uint24 ethUsdgFee, int24 ethUsdgTs) {
        poolManager = IPoolManager(_pm);
        USDG = _usdg;
        ETH_USDG_FEE = ethUsdgFee;
        ETH_USDG_TS = ethUsdgTs;
    }

    // ------------------------------------------------------------------ BUY
    // ethIn = msg.value. coin/stock identify the lane; v3Pool is the STOCK/USDG
    // v3 pool; laneHook is the coin's lane hook. Coin is sent to `recipient`.
    function buyWithEth(address coin, address stock, address laneHook, address v3Pool, uint256 minCoinOut, address recipient)
        external
        payable
        returns (uint256 coinOut)
    {
        bytes memory res = poolManager.unlock(
            abi.encode(true, coin, stock, laneHook, v3Pool, msg.value, minCoinOut, recipient == address(0) ? msg.sender : recipient)
        );
        coinOut = abi.decode(res, (uint256));
    }

    // ------------------------------------------------------------------ SELL
    // pulls `coinIn` coin from the caller (needs prior coin approval to this
    // contract), routes to ETH, sends ETH to `recipient`.
    function sellForEth(address coin, address stock, address laneHook, address v3Pool, uint256 coinIn, uint256 minEthOut, address recipient)
        external
        returns (uint256 ethOut)
    {
        require(IERC20Z(coin).transferFrom(msg.sender, address(this), coinIn), "coin pull");
        bytes memory res = poolManager.unlock(
            abi.encode(false, coin, stock, laneHook, v3Pool, coinIn, minEthOut, recipient == address(0) ? msg.sender : recipient)
        );
        ethOut = abi.decode(res, (uint256));
    }

    // --------------------------------------------------------------- unlock
    function unlockCallback(bytes calldata data) external returns (bytes memory) {
        if (msg.sender != address(poolManager)) revert NotPoolManager();
        (
            bool isBuy,
            address coin,
            address stock,
            address laneHook,
            address v3Pool,
            uint256 amtIn,
            uint256 minOut,
            address recipient
        ) = abi.decode(data, (bool, address, address, address, address, uint256, uint256, address));

        if (isBuy) return abi.encode(_buy(coin, stock, laneHook, v3Pool, amtIn, minOut, recipient));
        return abi.encode(_sell(coin, stock, laneHook, v3Pool, amtIn, minOut, recipient));
    }

    function _v4Swap(PoolKey memory key, bool zeroForOne, int256 amountSpecified) internal returns (BalanceDelta d) {
        d = poolManager.swap(
            key,
            SwapParams({zeroForOne: zeroForOne, amountSpecified: amountSpecified, sqrtPriceLimitX96: zeroForOne ? MIN_LIMIT : MAX_LIMIT}),
            ""
        );
    }

    // v4 hookless ETH/USDG pool key: ETH (0) is always < USDG
    function _ethUsdgKey() internal view returns (PoolKey memory) {
        return PoolKey({
            currency0: Currency.wrap(NATIVE),
            currency1: Currency.wrap(USDG),
            fee: ETH_USDG_FEE,
            tickSpacing: ETH_USDG_TS,
            hooks: IHooks(address(0))
        });
    }

    // lane coin/stock pool key, sorted, dynamic fee, lane hook
    function _laneKey(address coin, address stock, address laneHook) internal pure returns (PoolKey memory k, bool coinIs0) {
        coinIs0 = uint160(coin) < uint160(stock);
        k = PoolKey({
            currency0: Currency.wrap(coinIs0 ? coin : stock),
            currency1: Currency.wrap(coinIs0 ? stock : coin),
            fee: DYNAMIC_FEE,
            tickSpacing: LANE_TS,
            hooks: IHooks(laneHook)
        });
    }

    // ETH -> USDG (v4 hookless, exact in): pay native, take USDG here.
    function _ethToUsdg(uint256 ethIn) internal returns (uint256 usdgOut) {
        BalanceDelta d = _v4Swap(_ethUsdgKey(), true, -int256(ethIn));
        poolManager.settle{value: ethIn}();
        usdgOut = uint256(int256(d.amount1()));
        poolManager.take(Currency.wrap(USDG), address(this), usdgOut);
    }

    // USDG -> ETH (v4 hookless, exact in): settle USDG, take native to `to`.
    function _usdgToEth(uint256 usdgIn, address to, uint256 minEthOut) internal returns (uint256 ethOut) {
        BalanceDelta d = _v4Swap(_ethUsdgKey(), false, -int256(usdgIn));
        poolManager.sync(Currency.wrap(USDG));
        IERC20Z(USDG).transfer(address(poolManager), usdgIn);
        poolManager.settle();
        ethOut = uint256(int256(d.amount0()));
        if (ethOut < minEthOut) revert TooLittleOut();
        poolManager.take(Currency.wrap(NATIVE), to, ethOut);
    }

    // STOCK -> COIN (v4 lane hook, exact in): settle stock, take coin to `to`.
    function _stockToCoin(address coin, address stock, address laneHook, uint256 stockIn, address to, uint256 minCoinOut)
        internal
        returns (uint256 coinOut)
    {
        (PoolKey memory lk, bool coinIs0) = _laneKey(coin, stock, laneHook);
        // buy = stock in, coin out; move toward coin. coinIs0 -> stock is
        // currency1 -> oneForZero; !coinIs0 -> stock is currency0 -> zeroForOne.
        BalanceDelta d = _v4Swap(lk, !coinIs0, -int256(stockIn));
        poolManager.sync(Currency.wrap(stock));
        IERC20Z(stock).transfer(address(poolManager), stockIn);
        poolManager.settle();
        coinOut = uint256(int256(coinIs0 ? d.amount0() : d.amount1()));
        if (coinOut < minCoinOut) revert TooLittleOut();
        poolManager.take(Currency.wrap(coin), to, coinOut);
    }

    // COIN -> STOCK (v4 lane hook, exact in): settle coin, take stock here.
    function _coinToStock(address coin, address stock, address laneHook, uint256 coinIn) internal returns (uint256 stockOut) {
        (PoolKey memory lk, bool coinIs0) = _laneKey(coin, stock, laneHook);
        BalanceDelta d = _v4Swap(lk, coinIs0, -int256(coinIn)); // coin in -> away from coin
        poolManager.sync(Currency.wrap(coin));
        IERC20Z(coin).transfer(address(poolManager), coinIn);
        poolManager.settle();
        stockOut = uint256(int256(coinIs0 ? d.amount1() : d.amount0()));
        poolManager.take(Currency.wrap(stock), address(this), stockOut);
    }

    function _buy(address coin, address stock, address laneHook, address v3Pool, uint256 ethIn, uint256 minCoinOut, address recipient)
        internal
        returns (uint256)
    {
        uint256 usdgOut = _ethToUsdg(ethIn);
        uint256 stockOut = _v3Swap(v3Pool, USDG, stock, usdgOut);
        return _stockToCoin(coin, stock, laneHook, stockOut, recipient, minCoinOut);
    }

    function _sell(address coin, address stock, address laneHook, address v3Pool, uint256 coinIn, uint256 minEthOut, address recipient)
        internal
        returns (uint256)
    {
        uint256 stockOut = _coinToStock(coin, stock, laneHook, coinIn);
        uint256 usdgOut = _v3Swap(v3Pool, stock, USDG, stockOut);
        return _usdgToEth(usdgOut, recipient, minEthOut);
    }

    // one exact-in v3 swap tokenIn->tokenOut through `pool`, returns amountOut
    function _v3Swap(address pool, address tokenIn, address tokenOut, uint256 amountIn) internal returns (uint256 amountOut) {
        bool zeroForOne = uint160(tokenIn) < uint160(tokenOut);
        v3ExpectedPool = pool;
        v3PayToken = tokenIn;
        (int256 a0, int256 a1) = IUniswapV3PoolZ(pool).swap(
            address(this),
            zeroForOne,
            int256(amountIn),
            zeroForOne ? V3_MIN_LIMIT : V3_MAX_LIMIT,
            abi.encode(tokenIn, amountIn)
        );
        v3ExpectedPool = address(0);
        v3PayToken = address(0);
        // amountOut is the negative (received) side
        amountOut = zeroForOne ? uint256(-a1) : uint256(-a0);
    }

    function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {
        if (msg.sender != v3ExpectedPool) revert BadV3Callback();
        (address payToken, ) = abi.decode(data, (address, uint256));
        uint256 owed = amount0Delta > 0 ? uint256(amount0Delta) : uint256(amount1Delta);
        IERC20Z(payToken).transfer(msg.sender, owed);
    }

    receive() external payable {}
}
Chain explorer5797msChain node87ms