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
- Creator
- 0x4D840AC6b3…756c7585F9
- 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