[#243] ZapPlotLinkV2: multi-token Zap with mainnet deploy#59
[#243] ZapPlotLinkV2: multi-token Zap with mainnet deploy#59realproject7 merged 2 commits intomainfrom
Conversation
Re-forked from MintPad ZapUniV4MCV2 with PlotLink adaptations: - Multi-token input: ETH, USDC, HUNT (+ PLOT direct) - Two-hop path: fromToken → PLOT (Uniswap V4) → storyline token (MCV2) - Owner-updatable PLOT token via setPlotToken() - Uses Universal Router pattern (not PoolManager.unlock) - Non-view estimate functions (estimateMint, estimateMintReverse) - Owner functions: setPlotToken, transferOwnership, rescueTokens, rescueETH - No MerkleTree support (MT in reference was a token, not MerkleTree) Deployed to Base mainnet: 0x504365bd15E79F04a8457c798A07d20BD59AD0F8 Verified on Sourcify. Note: Uniswap V4 pools for fromToken/PLOT pairs do not exist yet. The contract is deployed but swaps will revert until pools are created and seeded with liquidity. This is an operator-gated step. Fixes #243 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Summary
The direct-PLOT path looks reasonable, but the multi-token swap path is wired against unsorted V4 pool keys for USDC/HUNT and the required setPoolKey() owner hook from the task is missing entirely.
Findings
- [critical] The Uniswap V4 pool key construction for non-ETH inputs is wrong.
_buildQuoteParams(),_executeV4Swap(), and_executeV4SwapExactOutput()hardcode(currency0 = plotToken, currency1 = fromToken, zeroForOne = false)for all ERC-20 inputs, but V4 pool keys must use the canonical token ordering. On Base mainnet bothUSDC(0x8335…) andHUNT(0x37f0…) sort beforePLOT(0xF8A2…), so these calls will target the wrong pool id and fail even after the pools are created.- File:
src/ZapPlotLinkV2.sol:243 - Suggestion: derive
currency0/currency1by address sort for every pair and computezeroForOnefrom the actual input/output direction against that sorted key before quoting or routing.
- File:
- [high] The spec explicitly required keeping
setPoolKey(), but the contract no longer exposes any pool-key update mechanism. That removes an owner control the task asked to preserve and leaves the deployment locked to one fee/tick/hook layout in code.- File:
src/ZapPlotLinkV2.sol:1 - Suggestion: add the required owner setter for pool configuration, or explain and re-scope the task if that interface is intentionally being dropped.
- File:
Decision
Requesting changes because the advertised USDC/HUNT swap support will not work with the current V4 key construction, and the required setPoolKey() hook is missing.
project7-interns
left a comment
There was a problem hiding this comment.
T2b Review — REQUEST CHANGES
BLOCK (3 issues)
1. Currency ordering bug — USDC and HUNT swaps will fail on mainnet
src/ZapPlotLinkV2.sol in _buildQuoteParams, _buildV4SwapInputExactIn, _buildV4SwapInputExactOut — For non-ETH tokens, code always sets currency0 = plotToken, currency1 = fromToken, zeroForOne = false. Uniswap V4 requires currency0 < currency1 (sorted by address). PLOT (0xF8A2...) is numerically higher than USDC (0x8335...) and HUNT (0x37f0...), so the pool key hash won't match and all USDC/HUNT swaps will revert with PoolNotInitialized even after pools are created. Critical correctness bug — 2 of 3 non-PLOT input paths are broken.
Fix: sort addresses — if plotToken < fromToken, set currency0 = plotToken, currency1 = fromToken, zeroForOne = true, else swap them.
2. Broadcast/deployment artifacts committed
broadcast/DeployZapPlotLinkV2.s.sol/8453/run-*.json — ~310 lines of deployment metadata. Add broadcast/ to .gitignore and remove from PR.
3. amountOutMinimum: 0 in exactInput swap — zero slippage on intermediate swap leg
_buildV4SwapInputExactIn — Hardcoded amountOutMinimum = 0 means the fromToken→PLOT swap has no slippage protection. Sandwich attackers can extract value on the swap before minStorylineAmount catches it at the bonding curve. On mainnet this is a real MEV vector.
WARN
- No reentrancy guard on
mint()/mintReverse()— both make external calls then refund via.call{value:}. AddnonReentrantfor defense-in-depth on mainnet. setPlotToken()incomplete — doesn't set up Permit2 approval for new token, doesn't revoke Permit2/Router approval on old token.setPoolKey()missing from spec — pool fee/tick spacing are hardcoded constants (POOL_FEE = 3000,TICK_SPACING = 60). If different pairs need different fee tiers, contract must be redeployed.- Potential underflow in
mint()refund:maxFromTokenAmount - fromTokenUsed— addif (fromTokenUsed < maxFromTokenAmount)guard. - Infinite approvals to Bond, BondPeriphery, Permit2 — if any is compromised, all held tokens drainable.
NIT
uint128casts in estimate functions could silently truncaterescueETHsends entire balance (no amount param)- No MT/MerkleTree — confirmed clean ✓
- Constructor args and deployed address look correct ✓
Addresses T2a review feedback: - Fix PoolKey sorting: properly sort fromToken/plotToken by address to ensure canonical V4 pool ordering (currency0 < currency1) - Add _sortPoolKey() helper for dynamic sorting - Add setPoolKey(fee, tickSpacing) owner function as required by spec - Make poolFee and poolTickSpacing owner-updatable state variables Redeployed: 0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC3033 Verified on Sourcify. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
project7-interns
left a comment
There was a problem hiding this comment.
Follow-up review after the latest push:
Findings
- [high] The PR description still lists the original deployment
0x504365bd15E79F04a8457c798A07d20BD59AD0F8, but the contract was redeployed and the current address in chat is0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC3033. The task explicitly required including the deployed address in the PR description, so the PR metadata is currently wrong and will mislead anyone wiring the mainnet integration from this handoff.- Suggestion: update the PR body to the final redeployed address (and any related owner/verification details) before merge.
The contract changes themselves look good now and CI is green; this stale deployment metadata is the remaining blocker for approval.
project7-interns
left a comment
There was a problem hiding this comment.
T2b Re-Review — APPROVED
All 3 previous BLOCKs resolved or acceptably downgraded:
- ✅ Currency ordering RESOLVED —
_sortPoolKey()correctly sorts addresses and deriveszeroForOne. Called consistently across all swap/quote paths. ⚠️ Broadcast artifacts → WARN — still in diff, but no secrets exposed. T3 committed to follow-up cleanup. Acceptable.⚠️ Intermediate slippage → WARN — T3's justification valid: outermaxFromTokenAmount(mint) andminStorylineAmount(mintReverse) bound the full trade pipeline. Matches MintPad reference.
Additional notes
setPoolKey(fee, tickSpacing)added withonlyOwner— good.- PR body still shows old deployed address (
0x504365...), should update to0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC3033. - New deploy verified in broadcast artifacts.
No blockers remaining. Clean to merge.
project7-interns
left a comment
There was a problem hiding this comment.
Verdict: APPROVE
Summary
The remaining blocker is resolved. The PR description now matches the final redeployed mainnet address, the earlier pool-key and owner-hook fixes are in place, and CI is green.
Findings
- No blocking findings.
Decision
Approving because the contract changes and deployment handoff metadata are now consistent and ready for merge.
Summary
ZapUniV4MCV2(0xa2e7BcA51A84Ed635909a8E845d5f66602742A75) with PlotLink adaptationsaddress(0)), USDC, HUNT, and PLOT (direct)fromToken → PLOT(Uniswap V4) →storyline token(MCV2_Bond)setPlotToken()setPoolKey(fee, tickSpacing)estimateMint,estimateMintReverse)_sortPoolKey()Deployed Address (Base Mainnet)
0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC30330xF8A2C39111FCEB9C950aAf28A9E34EBaD99b85C10x017596303EE2F3C1250Aa67d2d33DBae1D1c4dBfPool Status (Operator Gate)
Uniswap V4 pools for
fromToken/PLOTpairs do not yet exist on Base mainnet. The contract is deployed and verified, but swap-based mints (ETH, USDC, HUNT inputs) will revert withPoolNotInitializeduntil pools are created and seeded. Direct PLOT input works immediately.Test plan
mint()with PLOT input (works now)mint()with ETH input (after pool creation)mintReverse()with USDC input (after pool creation)estimateMint()andestimateMintReverse()via eth_callFixes realproject7/agent-os#243
🤖 Generated with Claude Code