Skip to content

[#243] ZapPlotLinkV2: multi-token Zap with mainnet deploy#59

Merged
realproject7 merged 2 commits intomainfrom
task/433-zap-v2-contract
Mar 22, 2026
Merged

[#243] ZapPlotLinkV2: multi-token Zap with mainnet deploy#59
realproject7 merged 2 commits intomainfrom
task/433-zap-v2-contract

Conversation

@realproject7
Copy link
Copy Markdown
Owner

@realproject7 realproject7 commented Mar 22, 2026

Summary

  • Re-forked from MintPad ZapUniV4MCV2 (0xa2e7BcA51A84Ed635909a8E845d5f66602742A75) with PlotLink adaptations
  • Multi-token input: ETH (address(0)), USDC, HUNT, and PLOT (direct)
  • Two-hop path: fromToken → PLOT (Uniswap V4) → storyline token (MCV2_Bond)
  • Owner-updatable PLOT token via setPlotToken()
  • Owner-updatable pool parameters via setPoolKey(fee, tickSpacing)
  • Uses Universal Router pattern (matching MintPad reference)
  • Non-view estimate functions for frontend (estimateMint, estimateMintReverse)
  • Proper canonical token sorting for V4 pool keys via _sortPoolKey()

Deployed Address (Base Mainnet)

Contract Address
ZapPlotLinkV2 0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC3033
PLOT token 0xF8A2C39111FCEB9C950aAf28A9E34EBaD99b85C1
Owner 0x017596303EE2F3C1250Aa67d2d33DBae1D1c4dBf

Pool Status (Operator Gate)

Uniswap V4 pools for fromToken/PLOT pairs do not yet exist on Base mainnet. The contract is deployed and verified, but swap-based mints (ETH, USDC, HUNT inputs) will revert with PoolNotInitialized until pools are created and seeded. Direct PLOT input works immediately.

Test plan

  • Verify Sourcify verification completed
  • Create required V4 pools (ETH/PLOT, USDC/PLOT, HUNT/PLOT) — operator gate
  • Test mint() with PLOT input (works now)
  • Test mint() with ETH input (after pool creation)
  • Test mintReverse() with USDC input (after pool creation)
  • Test estimateMint() and estimateMintReverse() via eth_call

Fixes realproject7/agent-os#243

🤖 Generated with Claude Code

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>
Copy link
Copy Markdown
Collaborator

@project7-interns project7-interns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 both USDC (0x8335…) and HUNT (0x37f0…) sort before PLOT (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/currency1 by address sort for every pair and compute zeroForOne from the actual input/output direction against that sorted key before quoting or routing.
  • [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.

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.

Copy link
Copy Markdown
Collaborator

@project7-interns project7-interns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:}. Add nonReentrant for 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 — add if (fromTokenUsed < maxFromTokenAmount) guard.
  • Infinite approvals to Bond, BondPeriphery, Permit2 — if any is compromised, all held tokens drainable.

NIT

  • uint128 casts in estimate functions could silently truncate
  • rescueETH sends 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>
Copy link
Copy Markdown
Collaborator

@project7-interns project7-interns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 is 0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC3033. 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.

Copy link
Copy Markdown
Collaborator

@project7-interns project7-interns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

T2b Re-Review — APPROVED

All 3 previous BLOCKs resolved or acceptably downgraded:

  1. Currency ordering RESOLVED_sortPoolKey() correctly sorts addresses and derives zeroForOne. Called consistently across all swap/quote paths.
  2. ⚠️ Broadcast artifacts → WARN — still in diff, but no secrets exposed. T3 committed to follow-up cleanup. Acceptable.
  3. ⚠️ Intermediate slippage → WARN — T3's justification valid: outer maxFromTokenAmount (mint) and minStorylineAmount (mintReverse) bound the full trade pipeline. Matches MintPad reference.

Additional notes

  • setPoolKey(fee, tickSpacing) added with onlyOwner — good.
  • PR body still shows old deployed address (0x504365...), should update to 0xEF6a8640c836b16Eb8cCD8016Ead4C8517aC3033.
  • New deploy verified in broadcast artifacts.

No blockers remaining. Clean to merge.

Copy link
Copy Markdown
Collaborator

@project7-interns project7-interns left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@realproject7 realproject7 merged commit 4d761ad into main Mar 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants