From 29c34770f104b14102079ef9975cc6e37c2b6168 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 23 Mar 2026 15:00:27 +0000 Subject: [PATCH] [#470] Fix USDC zap mint: restore _encodeAsStruct for multi-hop V4 swaps Root cause: V4 Router's CalldataDecoder.decodeSwapExactInParams/Out reads the first word of action params as an offset pointer (assembly: swapParams := add(params.offset, calldataload(params.offset))). abi.encode(field1, field2, ...) produces a flat tuple starting with the first field value, but the decoder expects a struct-style encoding with an outer 0x20 offset word. Previous fix attempts (PR #469) correctly identified the need for _encodeAsStruct but also added a noPriceChecks (uint256[]) field matching the v4-periphery lib's new minHopPriceX36 struct field. The deployed Universal Router (Jan 2025) predates that field (added Nov 2025), so the extra array corrupted the ABI layout. Fix: restore _encodeAsStruct (adds 0x20 prefix) without noPriceChecks. Verified: successful USDC mint tx on Base mainnet: 0x417378c881237fbbd9bbfd59225c643ebc26851d9151af58b2a1047d5cc70ac5 New ZapPlotLinkV2: 0xAe50C9444DA2Ac80B209dC8B416d1B4A7D3939B0 Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- script/E2EZapTest.s.sol | 2 +- src/ZapPlotLinkV2.sol | 16 ++++++++++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 5debc0a..976a138 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Each storyline token trades on a Mint Club V2 bonding curve with 1% creator roya | Contract | Address | |----------|---------| | StoryFactory | [`0x337c5b96f03fB335b433291695A4171fd5dED8B0`](https://basescan.org/address/0x337c5b96f03fB335b433291695A4171fd5dED8B0) | -| ZapPlotLinkV2 | [`0x952606df750C01e0a12458C3F814598B94AD5C5f`](https://basescan.org/address/0x952606df750C01e0a12458C3F814598B94AD5C5f) | +| ZapPlotLinkV2 | [`0xAe50C9444DA2Ac80B209dC8B416d1B4A7D3939B0`](https://basescan.org/address/0xAe50C9444DA2Ac80B209dC8B416d1B4A7D3939B0) | ## External Dependencies diff --git a/script/E2EZapTest.s.sol b/script/E2EZapTest.s.sol index d0e4044..79de351 100644 --- a/script/E2EZapTest.s.sol +++ b/script/E2EZapTest.s.sol @@ -18,7 +18,7 @@ contract E2EZapTest is Script { // ----------------------------------------------------------------------- // Base mainnet addresses // ----------------------------------------------------------------------- - ZapPlotLinkV2 constant ZAP = ZapPlotLinkV2(payable(0x7bC192848003ab1Ba286C66AFD0dd8a1729c6b02)); + ZapPlotLinkV2 constant ZAP = ZapPlotLinkV2(payable(0xAe50C9444DA2Ac80B209dC8B416d1B4A7D3939B0)); IMCV2_Bond constant BOND = IMCV2_Bond(0xc5a076cad94176c2996B32d8466Be1cE757FAa27); IERC20 constant PLOT = IERC20(0xF8A2C39111FCEB9C950aAf28A9E34EBaD99b85C1); IERC20 constant HUNT = IERC20(0x37f0c2915CeCC7e977183B8543Fc0864d03E064C); diff --git a/src/ZapPlotLinkV2.sol b/src/ZapPlotLinkV2.sol index 8d127c0..dd78089 100644 --- a/src/ZapPlotLinkV2.sol +++ b/src/ZapPlotLinkV2.sol @@ -483,7 +483,10 @@ contract ZapPlotLinkV2 { hookData: bytes("") }); - actionParams[0] = abi.encode(USDC, path, amountIn, uint128(0)); + // V4 Router's CalldataDecoder.decodeSwapExactInParams reads the first word as + // an offset pointer to the struct data. _encodeAsStruct prepends 0x20 to match + // the ABI encoding of a single struct parameter (abi.encode(ExactInputParams)). + actionParams[0] = _encodeAsStruct(abi.encode(USDC, path, amountIn, uint128(0))); actionParams[1] = abi.encode(USDC, amountIn); actionParams[2] = abi.encode(plotToken, address(this), ActionConstants.OPEN_DELTA); @@ -520,7 +523,7 @@ contract ZapPlotLinkV2 { hookData: bytes("") }); - actionParams[0] = abi.encode(plotToken, path, plotAmountOut, maxUsdcIn); + actionParams[0] = _encodeAsStruct(abi.encode(plotToken, path, plotAmountOut, maxUsdcIn)); actionParams[1] = abi.encode(USDC, maxUsdcIn); actionParams[2] = abi.encode(plotToken, address(this), ActionConstants.OPEN_DELTA); @@ -531,6 +534,15 @@ contract ZapPlotLinkV2 { UNIVERSAL_ROUTER.execute(commands, inputs, block.timestamp); } + /// @dev Wraps ABI-encoded fields with an outer offset word (0x20) to match + /// struct-style ABI encoding expected by the V4 Router's CalldataDecoder. + /// decodeSwapExactInParams/decodeSwapExactOutParams read the first word + /// of params as an offset pointer to the struct data. abi.encode(field1, ...) + /// produces a flat tuple without this offset; this function adds it. + function _encodeAsStruct(bytes memory data) private pure returns (bytes memory) { + return abi.encodePacked(uint256(0x20), data); + } + function _refundPlot() private { uint256 balance = IERC20(plotToken).balanceOf(address(this)); if (balance > 0) {