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
}
}}
Submitted on: 2025-10-27 17:20:07
Comments
Log in to comment.
No comments yet.