-
Notifications
You must be signed in to change notification settings - Fork 23
feat: UniswapV4 mint/increase/decrease liquidity support #189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Conversation
WalkthroughAdds Uniswap V4 support: new git submodules and Foundry remapping, a UniswapV4Lib library, MainnetController relayer actions and per-pool tick limits, new interfaces, rate-limit helper signature changes, and extensive mainnet-fork tests. Changes
Sequence Diagram(s)sequenceDiagram
actor Relayer
participant MC as MainnetController
participant V4L as UniswapV4Lib
participant P2 as Permit2
participant PM as PositionManager
participant RL as RateLimits
Relayer->>MC: mintPositionUniswapV4(poolId, tickLower, tickUpper, liquidity, ...)
activate MC
MC->>V4L: mintPosition(proxy, rateLimits, poolId, tickLower, tickUpper, liquidity, ...)
activate V4L
V4L->>V4L: validate poolId & tickLimits
V4L->>P2: approve token allowances (Permit2)
V4L->>PM: call modifyLiquidities / router execute (calldata)
V4L->>V4L: compute pre/post balances, normalize deltas
V4L->>RL: update rate-limits (LIMIT_DEPOSIT / LIMIT_WITHDRAW)
V4L->>P2: reset approvals
deactivate V4L
MC-->>Relayer: return success
deactivate MC
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
Comment |
7a87195 to
0b598fe
Compare
abe98f0 to
48e1b57
Compare
|
@coderabbitai Review this PR and be very strict. Also provide a summary of how exactly this interacts with UniswapV4 and consider potential attack vectors. |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: Length of output: 83390 @lucas-manuel I'll conduct a thorough review of this PR. Let me examine the code in detail. 🏁 Script executed: Length of output: 15672 🏁 Script executed: Length of output: 3415 🏁 Script executed: Length of output: 7641 🏁 Script executed: Length of output: 3184 🏁 Script executed: Length of output: 4695 🏁 Script executed: Length of output: 2106 🏁 Script executed: Length of output: 4055 🏁 Script executed: Length of output: 5162 @lucas-manuel I've completed a thorough security review of this PR. Here are my findings: 🔴 CRITICAL SECURITY ISSUES1. Missing Ownership Validation in
|
|
This will address @coderabbitai's thorough security review.
|
|
Tip For best results, initiate chat on the files or code changes. @deluca-mike Thank you for the detailed response and thoughtful explanations! Your reasoning is sound on all points:
Thank you for considering the security review so thoroughly! Let me know if you need any assistance with the comment additions or have other questions. ✏️ Learnings added
|
48e1b57 to
0122e2c
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (15)
.gitmodules(1 hunks).lintstagedrc(1 hunks).prettierignore(1 hunks).prettierrc(1 hunks).solhint.json(1 hunks)foundry.toml(1 hunks)lib/uniswap-v4-core(1 hunks)lib/uniswap-v4-periphery(1 hunks)src/MainnetController.sol(8 hunks)src/RateLimitHelpers.sol(1 hunks)src/interfaces/Common.sol(1 hunks)src/interfaces/UniswapV4.sol(1 hunks)src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Curve.t.sol(0 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
💤 Files with no reviewable changes (1)
- test/mainnet-fork/Curve.t.sol
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.018Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.018Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-11T19:43:16.018Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.018Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
test/mainnet-fork/Uniswapv4.t.solsrc/MainnetController.solsrc/libraries/UniswapV4Lib.solsrc/interfaces/UniswapV4.sol
📚 Learning: 2025-11-11T19:43:16.018Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.018Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
test/mainnet-fork/Uniswapv4.t.solsrc/MainnetController.solsrc/libraries/UniswapV4Lib.sol
📚 Learning: 2025-10-16T17:40:33.927Z
Learnt from: supercontracts
Repo: sparkdotfi/spark-alm-controller PR: 170
File: test/mocks/Mocks.sol:25-134
Timestamp: 2025-10-16T17:40:33.927Z
Learning: In the spark-alm-controller repository, test mock contracts like MockTokenkReturnNull do not need to include ERC20 standard events (Transfer, Approval) for testing purposes. Full ERC20 compliance with events is not required for test mocks.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
🔇 Additional comments (7)
lib/uniswap-v4-core (1)
1-1: Submodule configuration is correct, but commit pinning requires verification.Configuration verification passed:
.gitmodulesis correctly configured with proper URL and pathfoundry.tomlremapping (@uniswap/v4-core/=lib/uniswap-v4-core/) is consistent- Submodule path and status are properly aligned
However, the pinned commit
59d3ecf53afa9264a16bba0e38f4c5d2231f80bcdoes not correspond to any tagged release. The only GitHub tag for Uniswap v4-core is v4.0.0 (tagged Jan 21, 2025), and its commit hash differs from the one pinned here.Confirm that:
- This non-release commit pinning is intentional (e.g., for testing, pre-release evaluation, or specific bug fixes).
- The rationale is documented or agreed upon with the team.
- There is a plan to migrate to a tagged release version once available.
.solhint.json (1)
10-73: Note the security-relevant rule disables.The configuration intentionally disables several security-focused linting rules:
avoid-call-valueandavoid-low-level-calls(lines 10-11): Permit low-level calls required for patterns like Permit2no-inline-assembly(line 72): Allows assembly blocksnot-rely-on-time(line 73): Permits time-dependent logicThese are standard for DeFi projects but require careful review of the actual low-level calls and assembly usage in the implementation.
foundry.toml (1)
21-21: LGTM!The Uniswap V4 remapping is correctly configured and follows the project's pattern for external dependencies.
.gitmodules (1)
50-55: LGTM!The Uniswap V4 submodule additions are correctly configured with official repository URLs and align with the remapping in foundry.toml.
src/interfaces/Common.sol (2)
4-12: LGTM!The IERC20Like interface provides the minimal necessary functions for token operations used in the integration.
14-18: LGTM!The IPermit2Like interface correctly defines the approve function signature matching Permit2's specification (uint160 for amount, uint48 for expiration).
src/interfaces/UniswapV4.sol (1)
9-21: LGTM!The IPositionManagerLike interface correctly defines the essential functions for interacting with Uniswap V4's Position Manager, including pool/position queries, liquidity modification, and ownership checks.
0122e2c to
6c26dfc
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
foundry.toml (1)
13-22: Add remap for@uniswap/v4-peripheryto match Uniswap importsYou’ve added a remap for
@uniswap/v4-core, butsrc/interfaces/UniswapV4.solstill imports v4‑periphery via a long relative path. For consistency and portability, add a matching remap and use both remapped paths in the interface.Example:
remappings = [ @@ - '@uniswap/v4-core/=lib/uniswap-v4-core/' + '@uniswap/v4-core/=lib/uniswap-v4-core/', + '@uniswap/v4-periphery/=lib/uniswap-v4-periphery/' ]
♻️ Duplicate comments (3)
lib/uniswap-v4-periphery (1)
1-1: Submodule commit hash remains invalid; prior critical issue unresolved.The commit hash
60cd93803ac2b7fa65fd6cd351fd5fd4cc8c9db5does not exist in the official Uniswap v4-periphery repository, as confirmed in the prior review. This will cause the build/test to fail when initializing the submodule. Update to a valid commit from the official repository (e.g.,3779387e5d296f39df543d23524b050f89a62917), and ensure coordination with thelib/uniswap-v4-coresubmodule for API compatibility.src/interfaces/UniswapV4.sol (1)
4-7: Prefer remapped Uniswap imports instead of long relative pathsThese imports bypass your Foundry remappings and hard‑code the repo layout:
import { PoolId } from "../../lib/uniswap-v4-core/src/types/PoolId.sol"; import { PoolKey } from "../../lib/uniswap-v4-core/src/types/PoolKey.sol"; import { PositionInfo } from "../../lib/uniswap-v4-periphery/src/libraries/PositionInfoLibrary.sol";Now that you have a remap for v4‑core (and can add one for v4‑periphery), it’s cleaner to use the remapped paths.
Suggested change (after adding the v4‑periphery remap in
foundry.toml):-import { PoolId } from "../../lib/uniswap-v4-core/src/types/PoolId.sol"; -import { PoolKey } from "../../lib/uniswap-v4-core/src/types/PoolKey.sol"; - -import { PositionInfo } from "../../lib/uniswap-v4-periphery/src/libraries/PositionInfoLibrary.sol"; +import { PoolId } from "@uniswap/v4-core/src/types/PoolId.sol"; +import { PoolKey } from "@uniswap/v4-core/src/types/PoolKey.sol"; +import { PositionInfo } from "@uniswap/v4-periphery/src/libraries/PositionInfoLibrary.sol";src/MainnetController.sol (1)
635-752: RestorenonReentranton Uniswap V4 relayer entrypoints
mintPositionUniswapV4isexternal nonReentrant, but the three sibling entrypoints:
increaseLiquidityUniswapV4burnPositionUniswapV4decreaseLiquidityUniswapV4are now plain
externaland still delegate toUniswapV4Lib, which ultimately reaches Uniswap V4 pools where arbitrary hook contracts can execute.Even with
_checkRole(RELAYER), a RELAYER‑level aggregator contract can be invoked from a hook and re‑enter these functions within the same transaction, giving a hook‑controlled call graph access to additional privileged paths and to your rate‑limit updates. The rest of the relayer surface is consistently guarded withnonReentrant, so these three functions stand out as the only Uniswap V4 paths without that protection.I recommend re‑adding
nonReentrantto align with the rest of the contract and to close this hook‑driven reentrancy window.Suggested diff:
- function increaseLiquidityUniswapV4( + function increaseLiquidityUniswapV4( bytes32 poolId, uint256 tokenId, uint128 liquidityIncrease, uint256 amount0Max, uint256 amount1Max ) - external + external nonReentrant { @@ - function burnPositionUniswapV4( + function burnPositionUniswapV4( bytes32 poolId, uint256 tokenId, uint256 amount0Min, uint256 amount1Min ) - external + external nonReentrant { @@ - function decreaseLiquidityUniswapV4( + function decreaseLiquidityUniswapV4( bytes32 poolId, uint256 tokenId, uint128 liquidityDecrease, uint256 amount0Min, uint256 amount1Min ) - external + external nonReentrant {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (15)
.gitmodules(1 hunks).lintstagedrc(1 hunks).prettierignore(1 hunks).prettierrc(1 hunks).solhint.json(1 hunks)foundry.toml(1 hunks)lib/uniswap-v4-core(1 hunks)lib/uniswap-v4-periphery(1 hunks)src/MainnetController.sol(8 hunks)src/RateLimitHelpers.sol(1 hunks)src/interfaces/Common.sol(1 hunks)src/interfaces/UniswapV4.sol(1 hunks)src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Curve.t.sol(0 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
💤 Files with no reviewable changes (1)
- test/mainnet-fork/Curve.t.sol
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
src/interfaces/UniswapV4.solsrc/libraries/UniswapV4Lib.solsrc/MainnetController.soltest/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
src/libraries/UniswapV4Lib.solsrc/MainnetController.soltest/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-10-16T16:47:34.627Z
Learnt from: supercontracts
Repo: sparkdotfi/spark-alm-controller PR: 170
File: src/MainnetController.sol:1153-1181
Timestamp: 2025-10-16T16:47:34.627Z
Learning: In src/MainnetController.sol, the USDS token always returns true on successful transfer operations, so it does not require the empty return data handling that some non-standard ERC20 tokens (like USDT) need.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: coverage
- GitHub Check: test
- GitHub Check: build
🔇 Additional comments (22)
.lintstagedrc (1)
1-8: LGTM! Standard pre-commit configuration.The lint-staged setup correctly maps file patterns to formatting and linting scripts. Solidity files will be processed by both prettier and solhint, which is the expected workflow.
.prettierignore (1)
1-5: LGTM! Appropriate exclusions for formatting.The ignore patterns correctly exclude third-party code (node_modules and Foundry's lib directory) from Prettier formatting.
.prettierrc (1)
1-24: LGTM! Consistent formatting configuration.The Prettier setup correctly enables Solidity support and uses sensible formatting defaults. The 120-character print width aligns with the solhint max-line-length rule in .solhint.json.
.solhint.json (3)
10-11: Verify intentional disabling of low-level call guards.The
avoid-call-valueandavoid-low-level-callsrules are disabled, removing safety guardrails against potentially unsafe low-level operations. While this is likely intentional for a proxy/controller contract that requires low-level control, ensure that:
- Code using
.call{value:...}()includes proper reentrancy protection- Low-level calls are limited to specific, well-audited use cases
- Return values are checked appropriately
72-73: Verify intentional disabling of assembly and timestamp guards.The
no-inline-assemblyandnot-rely-on-timerules are disabled. While assembly and timestamp usage may be necessary for optimization and protocol logic respectively, ensure:
- Inline assembly is limited, well-documented, and audited
- Timestamp dependencies account for miner manipulation risks (typically ±15 second variance)
1-90: Overall configuration is comprehensive.The solhint configuration extends recommended rules and adds appropriate constraints for Solidity development. The disabled security rules align with the needs of a low-level controller contract but warrant careful review during code audits.
lib/uniswap-v4-core (1)
1-1: No action required—submodule commit is verified.The pinned commit
59d3ecf53afa9264a16bba0e38f4c5d2231f80bccorresponds to the official Uniswap v4-core v1.0.2 release, committed by a Uniswap developer and GPG-signed. This is a stable, legitimate dependency pin with no security concerns.src/interfaces/Common.sol (1)
1-18: Interfaces look correct and match intended external contracts
IERC20LikeandIPermit2Likeare minimal but sufficient for your use cases, andIPermit2Like.approvematches the current Permit2 signature (no return value). I don’t see issues with these definitions.src/RateLimitHelpers.sol (1)
6-20: Rate‑limit key helpers are simple and soundAll helpers consistently derive keys as
keccak256(abi.encode(key, …)), keeping domain separation via the leadingkeyand avoiding storage layout surprises. No issues from a correctness or security perspective.src/libraries/UniswapV4Lib.sol (1)
251-303: Rate‑limit decrease computation assumes net outflow on increases and 1:1 normalized value
_increaseLiquidity:
- Uses clamped subtraction on normalized balances before/after to derive
rateLimitDecrease, which safely handles the (unlikely) case of net token inflow when adding liquidity.- Immediately calls
triggerRateLimitDecreasewithmakeBytes32Key(commonParams.rateLimitId, commonParams.poolId)and then fully revokes approvals via_approvePositionManager(..., 0).This is correct under the documented assumption that the tokens are treated as economically equal once normalized to 18 decimals (e.g., USDC/USDT), and matches the repository’s design of using normalized (not USD) values for rate limits.
Based on learnings
test/mainnet-fork/Uniswapv4.t.sol (12)
121-128: Rate‑limit configuration insetUpmatches library’s rate‑limit keying
setUpconfigures deposit/withdraw limits using_DEPOSIT_LIMIT_KEYand_WITHDRAW_LIMIT_KEY, which arekeccak256(abi.encode(limitId, _POOL_ID)). This aligns with the library’s use ofRateLimitHelpers.makeBytes32Key(rateLimitId, poolId)and the expectation that both directions share a bytes32 key derived from(rateLimitId, poolId).The per‑test assertions on
rateLimits.getCurrentRateLimit(...)effectively verify that rate‑limit deltas match the normalized balance changes computed insideUniswapV4Lib.Based on learnings
134-207: mintPosition tests provide good coverage of guards and allowance‑based slippageThe
mintPositionUniswapV4_*tests collectively check:
- Reentrancy guard via
_setControllerEntered()andReentrancyGuardrevert.- Role gating for non‑relayers.
- Tick range sanity (lower bound, upper bound, and max range).
- “Slippage” via allowance caps: tests deliberately expect generic
vm.expectRevert()when actual spend exceedsamount0Max/amount1Max.This matches the library/controller design where per‑operation slippage is enforced via Permit2 allowances rather than explicit on‑chain ratios. No issues spotted here.
262-281: End‑to‑end mint test correctly asserts ownership, liquidity, allowances, and rate‑limit deltas
test_mintPositionUniswapV4is a strong integration test:
- Confirms the Position Manager owner is
almProxyand thatgetPositionLiquidityequalsresult.liquidityIncrease.- Asserts both token→Permit2 and Permit2→PositionManager allowances are zero via
_assertZeroAllowances, validating the approval reset logic in_approvePositionManager.- Compares the rate‑limit decrease against normalized 18‑dec amounts (
_to18From6Decimals) based on actual spend, matching the library’s normalization behavior.This is an effective regression guard for future changes in approval handling or rate‑limit math.
Based on learnings
287-404: increaseLiquidity tests mirror mint tests and validate ownership + poolId guardsThe
increaseLiquidityUniswapV4_*tests cover:
- Access control for non‑relayers.
- Enforcement of
"MC/non-proxy-position"when the NFT is transferred away from the proxy.- Enforcement of
"MC/tokenId-poolId-mismatch"when the providedpoolIddoesn’t match the position’s pool.- Allowance‑based bounds on
amount0Max/amount1Max, and correct rate‑limit deltas on successful increases, including allowance reset checks.Together with mint tests, this gives good confidence that the increase flow is safe, correctly keyed to the intended pool, and interacts properly with rate‑limits.
410-511: burnPosition tests validate poolId checks, min‑amount slippage, and withdraw rate‑limitsThe
burnPositionUniswapV4_*tests:
- Enforce relayer role.
- Exercise
"MC/tokenId-poolId-mismatch"for wrongpoolId.- Assert Uniswap’s
SlippageCheck.MinimumAmountInsufficienterror whenamount0Min/amount1Minexceed forecasted outputs.- In the happy path, assert that position liquidity goes to zero and that the withdraw rate‑limit delta matches normalized amounts actually received.
Given the intentional design choice to not enforce
ownerOfinburnPosition, these tests correctly focus on poolId matching, slippage, and rate‑limit behavior rather than ownership.Based on learnings
517-624: decreaseLiquidity tests nicely complement burn tests and verify partial liquidity removalFor
decreaseLiquidityUniswapV4_*:
- Access control and poolId mismatch cases mirror the burn tests.
- Slippage checks for both tokens use half the minted liquidity and assert the correct
MinimumAmountInsufficientbehavior.- The success test verifies:
- NFT remains owned by
almProxy.- Liquidity decreases by the requested fraction (half in this case).
- Withdraw rate‑limit delta matches normalized amounts received.
This provides good coverage of partial unwind behavior and confirms that rate‑limit accounting for decreases is symmetric with burns.
630-667: Tick‑limit tests ensure admin gating and correct configuration event semanticsThe
setUniswapV4tickLimits_*tests:
- Confirm reentrancy protection.
- Enforce that only the admin (DEFAULT_ADMIN_ROLE) can set tick limits.
- Validate
"MC/invalid-ticks"behavior for bad ranges.- Assert the
UniswapV4TickLimitsSetevent contents and that the stored limits match expectations.This is important because these per‑pool tick limits are a core defense against pathological LP ranges and front‑running scenarios in the attack tests.
673-729: Story test is a solid end‑to‑end scenario tying together mint, increase, decrease, burn, swaps, and rate‑limits
test_uniswapV4_story1:
- Configures realistic tick limits.
- Exercises mint → increase → decrease → burn across time, with intervening swaps that move the pool price.
- Checks that both deposit and withdraw rate‑limits track the total normalized tokens moved over the lifecycle.
This serves as a high‑level “does everything still work together?” test and complements the unit‑style tests nicely.
Based on learnings
761-1158: Attack tests give excellent coverage of front‑run/back‑run scenarios across price regimesThe suite of
test_uniswapV4_attack_*functions:
- Systematically explores baseline vs. frontrun/backrun cases when the true price is mid, above, or below the expected tick range.
- Measures PnL for the attacker and LP using concrete amounts, showing how much value can be extracted when the relayer assumes stale prices.
- Includes a defended variant (
*_defended) demonstrating how tighteramount{0,1}Maxsettings cause the manipulated mint to revert due to insufficient allowance.These tests are very valuable as regression checks against subtle changes in controller logic or Uniswap V4 behavior. The hard‑coded expected amounts do make them somewhat brittle to changes in pool state or fork block, but given
_getBlock()is overridden to a fixed historical block that’s an acceptable trade‑off for precision here.
1208-1359: Helper flows_mintPosition/_increasePosition/_burnPositionare consistent with PositionManager semanticsThe helpers:
- Use
nextTokenId()before minting and assume the minted tokenId equals that value, which matches the current PositionManager implementation (tokenId = nextTokenId++).- Compute amounts spent/received as simple balance differences, which works given the direction of funds in mint/increase/burn operations.
- Reuse Position Manager’s
getPoolAndPositionInfoto derive ticks, avoiding re‑encoding pool keys in tests.No correctness issues seen. If PositionManager semantics change in the future, the tokenId assumption is the main coupling point to revisit.
1405-1455: Liquidity → amount helpers match standard Uniswap formulas and include input validation
_getAmount0ForLiquidity,_getAmount1ForLiquidity, and_getAmountsForLiquidity:
- Implement the usual Q64.96 formulas with
FullMath.mulDiv, matching Uniswap’s LiquidityAmounts logic.- Enforce
sqrtPriceAX96 < sqrtPriceBX96with explicitrequires, which helps catch misconfigured ranges early in tests.- Correctly handle the three regions (price below, within, above range) in
_getAmountsForLiquidity.These helpers provide a trustworthy “oracle” for the forecasted amounts used in slippage and limit calculations in tests.
1586-1633: Swap helper correctly wires Permit2 + Universal Router for test purposes
_swap:
- Treats the pool as symmetric USDC/USDT and chooses
zeroForOnebased ontokenIn.- Uses
dealto fund the account, sets a Permit2 allowance, and executes a single V4 swap via the Universal Router withSWAP_EXACT_IN_SINGLE + SETTLE_ALL + TAKE_ALL.- Computes
amountOutby simple balance delta.This looks correct for exercising price movement and attack scenarios in a forked test context. Since this uses live mainnet contracts via forking, any future changes to router or pool behavior would surface as test failures, which is desirable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
♻️ Duplicate comments (3)
src/MainnetController.sol (1)
675-739: Reentrancy and access control on Uniswap V4 relayer functions now match threat modelAll four Uniswap V4 relayer entrypoints (
increaseLiquidityUniswapV4,burnPositionUniswapV4,decreaseLiquidityUniswapV4, plus the existingmintPositionUniswapV4) are nowexternal nonReentrantand RELAYER‑gated, closing the hook‑driven reentrancy path flagged in an earlier review. Delegation toUniswapV4Libcentralises poolId/ownership checks and rate-limit accounting, which is consistent with the rest of the controller design.Given the intentional choice that
burnPosition/decreasePositionin the library only enforce poolId (not ownership) so that any approved burn/decrease routes underlying tokens to the proxy, no additional checks are needed here; just ensure this behavior remains clearly documented in the library to prevent future “fixes” that would break the economic model. Based on learningssrc/libraries/UniswapV4Lib.sol (2)
29-101: Position entrypoints and poolId/ownership checks match the intended security modelThe four external entrypoints cleanly separate responsibilities:
mintPositionfetches thePoolKeyfrom the Position Manager, buildsMINT_POSITION + SETTLE_PAIRcalldata, and delegates to_increaseLiquidity, which handles approvals and rate-limit accounting.increasePositionenforces bothownerOf(tokenId) == proxyand_requirePoolIdMatch(poolId, tokenId), ensuring added liquidity remains owned by the proxy and the tokenId belongs to the expected pool.burnPosition/decreasePositiononly require a poolId match and rely on Position Manager approvals, intentionally allowing any approved burn/decrease for that pool to route tokens toproxyeven if the proxy is not the NFT owner. This is consistent with the design decision that non‑owner burns are economically beneficial to the system.From an attack‑surface perspective, this means a misconfigured approval could allow a relayer to burn someone else’s NFT position, but the proceeds still land on the ALM proxy, not the attacker. Given that trade‑off, it would be helpful to keep a short comment here (or cross‑reference to higher‑level docs) explaining why burn/decrease omit ownership checks while increase enforces them, to prevent future regressions. Based on learnings
Also applies to: 103-167
173-209: Permit2 approval pattern and rate‑limit accounting are careful; consider defensive clamp on decreasesThe
_approvePositionManager+_increaseLiquidityflow is well thought‑out: best‑effort zeroing of token→Permit2, strict approve with return‑data check, a per‑call Permit2 approval to the Position Manager withexpiration = block.timestamp, then post‑call cleanup by resetting approvals. Combined with pre/post balance sampling and normalized clamped subtraction, deposit rate‑limit consumption is tightly bounded per call.
_decreaseLiquiditymirrors the balance‑differential pattern for withdrawals, but uses rawendingBalance - startingBalancewhen computing normalized deltas. This is correct for the expected case (net inflow on decrease/burn) and will revert if a future hook ever induces net outflow during a “decrease” call. If you prefer to treat such a path as “no rate‑limit credit” instead of a hard revert, you could reuse_clampedSubon normalized sums here as previously suggested; not critical, but slightly more future‑proof.Also applies to: 211-258, 260-292
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (5)
lib/uniswap-v4-core(1 hunks)lib/uniswap-v4-periphery(1 hunks)src/MainnetController.sol(8 hunks)src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
src/MainnetController.solsrc/libraries/UniswapV4Lib.soltest/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
src/MainnetController.solsrc/libraries/UniswapV4Lib.soltest/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-10-16T16:47:34.627Z
Learnt from: supercontracts
Repo: sparkdotfi/spark-alm-controller PR: 170
File: src/MainnetController.sol:1153-1181
Timestamp: 2025-10-16T16:47:34.627Z
Learning: In src/MainnetController.sol, the USDS token always returns true on successful transfer operations, so it does not require the empty return data handling that some non-standard ERC20 tokens (like USDT) need.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: coverage
- GitHub Check: build
- GitHub Check: test
🔇 Additional comments (6)
lib/uniswap-v4-core (1)
1-1: Submodule reference verified as secure.Uniswap V4 Core was released as an audited, stable protocol on January 31, 2025, after nine independent audits and a large security competition. The submodule URL points to the official Uniswap repository, and the commit is pinned to a specific version, which is best practice for reproducibility and dependency management. No concerns identified.
src/libraries/UniswapV4Lib.sol (1)
18-24: Limit IDs and hard‑coded Uniswap mainnet addresses are consistent but mainnet‑specific
LIMIT_DEPOSIT/LIMIT_WITHDRAWconstants and their use as prefixes formakeBytes32Keyalign with howMainnetControllerand tests derive per‑pool rate‑limit keys, so the wiring is coherent end‑to‑end.The
_PERMIT2and_POSITION_MANAGERaddresses match Uniswap’s documented Ethereum mainnet deployments, but they hard‑code this library to mainnet. That’s appropriate forMainnetController, but if you ever reuseUniswapV4Libon other networks, you’ll need a different deployment or a configurable address pattern.test/mainnet-fork/Uniswapv4.t.sol (3)
102-127: Rate-limit keys in tests correctly mirror library/controller wiringThe test constants
_LIMIT_DEPOSIT/_LIMIT_WITHDRAWand derived keys_DEPOSIT_LIMIT_KEY/_WITHDRAW_LIMIT_KEYusekeccak256(abi.encode(limitId, _POOL_ID)), matching howUniswapV4Libconstructs keys viaRateLimitHelpers.makeBytes32Key.setUpseeds both limits on the sharedrateLimitscontract, so subsequent assertions ongetCurrentRateLimitare exercising the same paths as production.This alignment between tests and on-chain key derivation is important for catching any future drift in how per‑pool limits are encoded.
134-425: Good coverage of Uniswap V4 relayer guards, tick limits, slippage, and rate-limit deltasThe
mintPositionUniswapV4_*,increaseLiquidityUniswapV4_*,burnPositionUniswapV4_*,decreaseLiquidityUniswapV4_*, andsetUniswapV4tickLimits_*tests together cover:
- Reentrancy protection via
_setControllerEntered()andReentrancyGuardReentrantCallexpectations on every V4 entrypoint and the admin setter.- Access control for RELAYER and DEFAULT_ADMIN_ROLE callers.
- Tick‑range validation and the
UniswapV4TickLimitsSetevent plus mapping reads.- PoolId correctness via
"MC/tokenId-poolId-mismatch"for mismatched NFT/pool combinations.- Slippage enforcement on min/max amounts using
SlippageCheck.MinimumAmountInsufficientand generic “insufficient allowance” reverts when actual usage exceedsamount{0,1}Max.- Rate-limit consumption symmetry by asserting that
initialLimit - currentLimitequals the 18‑dec normalized sum of token deltas.This suite gives strong confidence that the controller’s Uniswap V4 surface behaves correctly under both happy paths and common misconfigurations.
Also applies to: 430-675, 681-722
1263-1390: Helper math and router/Permit2 scaffolding correctly support the testsThe helpers for quoting liquidity (
_getAmount0ForLiquidity,_getAmount1ForLiquidity,_getAmountsForLiquidity,_quoteLiquidity) closely follow the standard Uniswap v3/v4 formulas usingFullMathandTickMath, and the 6‑→18 decimal conversion via_to18From6Decimalsmatches the system‑wide normalization assumption.
_swap’s use of Universal Router + Permit2 (per‑call approvals withexpiration = block.timestamp) plus balance‑delta checks provides a realistic price‑movement mechanism for the tests, and_getBlock’s fixed return ensures deterministic forking behavior. Together these helpers make the higher‑level tests precise without introducing additional stateful complexity into the controller.Also applies to: 1392-1688
src/MainnetController.sol (1)
955-957: Interface ordering is correct; no changes needed.The
quoteOFTfunction inILayerZero(src/interfaces/ILayerZero.sol:56) returns(OFTLimit memory, OFTFeeDetail[] memory oftFeeDetails, OFTReceipt memory). Your destructuring( , , OFTReceipt memory receipt )correctly capturesOFTReceiptas the third return value, and usingreceipt.amountReceivedLDto setsendParams.minAmountLDis the intended behavior.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (9)
src/libraries/UniswapV4Lib.sol (5)
183-219: Permit2 approval pattern and rate-limit accounting on deposit side look correct and well-contained
_approvePositionManagerfirst best‑effort zeros the token→Permit2 allowance via a low‑levelproxy.call, then mandates success for the non‑zeroapprove, then grants Permit2→PositionManager allowance withexpiration = uint48(block.timestamp)so approvals are transient to the current block/tx._increaseLiquiditysnapshots balances before/after, uses_clampedSubon normalized balances to compute the actual deposit (defensively handling any net inflow), triggers the per‑poolLIMIT_DEPOSITrate‑limit, and then calls_approvePositionManager(proxy, token, 0)to fully revoke both allowances. The flow is tightly scoped and matches the tests’ expectations around zeroed approvals.Also applies to: 221-268
18-26: Interactive Uniswap V4 flows are structurally sound; clarify burn/decrease ownership model
mintPositionenforces per‑poolTickLimits, derivesPoolKeyfrompoolId, and routes through_increaseLiquidity, whileincreasePositionadds an explicitownerOf(tokenId) == proxycheck plus a poolId match. In contrast,burnPosition/decreasePositiononly call_requirePoolIdMatchand then_decreaseLiquidity, intentionally allowing any approved position in the same pool to be burned/decreased so underlying tokens flow toproxy. That asymmetry is by design and economically favorable to the system, but it’s non‑obvious; adding a brief comment nearburnPosition/decreasePositionexplaining that ownership is intentionally not enforced (poolId + approvals only) would help future reviewers avoid “fixing” it incorrectly.Based on learnings
Also applies to: 35-177
270-302: Withdraw-side rate-limit accounting assumes net inflow; optional to mirror_clampedSubfor extra safety
_decreaseLiquiditycomputesrateLimitDecreasefromendingBalance{0,1} - startingBalance{0,1}and normalizes the deltas. Given this path only ever pulls tokens from the pool into the proxy (no approvals granted to move tokens out),ending >= startingshould always hold, so the raw subtraction is safe with Solidity 0.8 checked arithmetic and matches the tests’ expectations exactly. If you want future‑proofing against unforeseen hook behavior, you could mirror the_increaseLiquiditypattern by using_clampedSubon normalized sums instead, turning any pathological net outflow into “no rate-limit credit” rather than a revert.
467-472: Normalized-balance helper assumes tokens withdecimals() <= 18and standard ERC20 behavior
_getNormalizedBalancescales viabalance * 1e18 / 10 ** IERC20Like(token).decimals(), which works as intended for the 6‑decimal stables you’re targeting but will undercount tokens withdecimals() > 18and rely ondecimals()being implemented and non‑reverting. That’s acceptable given the current scope, but consider either documenting that this library is only meant for tokens withdecimals() <= 18and standard ERC20 semantics, or adding a defensive check if you expect to broaden token support later.Based on learnings
474-482: PoolId handling correctly mirrors V4 PositionManager internals but is tightly coupled
_getPoolKeyreadspoolKeys(bytes25(poolId)), and_requirePoolIdMatchcompareskeccak256(abi.encode(poolKey))to the externalpoolId, which matches Uniswap V4’s currentPoolIdencoding and how the tests derive/verify poolIds. This is intentionally coupled to the deployed PositionManager’s implementation; a short comment noting thatpoolIdis expected to bekeccak256(abi.encode(poolKey))and that thebytes25cast mirrors the V4 mapping key would make that dependency explicit for future maintainers.test/mainnet-fork/Uniswapv4.t.sol (3)
747-803: Story and attack scenario tests provide valuable adversarial coverage of Uniswap V4 economic behaviorThe story test and
test_uniswapV4_attack_*suite run through realistic sequences of mints, swaps via the V4 router, increases, decreases, and burns under different price path assumptions (mid/above/below, front‑run/back‑run combinations), then assert on concrete USDC/USDT flows and rate-limit deltas. They demonstrate that rate-limit accounting tracks actual normalized token movements and quantify MEV-style PnL impact under adverse order flow, which is exactly the kind of adversarial coverage this integration needs.Also applies to: 835-1232
809-833: Diagnostic price-logging tests and helpers are verbose but safely isolatedThe two
test_uniswapV4_logPriceAndTicks_*tests arevm.skip(true)and only used for manual diagnostics, and_logCurrentPriceAndTickperforms detailed fixed-point formatting of the 18‑dec price and current tick. This is intentionally verbose but confined to skipped tests, so it doesn’t affect CI performance; if you ever unskip these, you might want to reduce iterations/logging volume, but no change is needed now.Also applies to: 1547-1631
1648-1654: Allowance assertion messages are slightly token-specific but otherwise fine
_assertZeroAllowancescorrectly verifies both Permit2’s allowance and the token’s allowance to Permit2 are zero, but the error strings mention “usdc” even whentokencould be something else. This is purely cosmetic; if you touch this helper again, consider swapping the messages for generic ones (e.g."permit2 allowance non-zero"/"token->permit2 allowance non-zero") to avoid confusion when debugging failures for other tokens.src/MainnetController.sol (1)
133-133: Tick-limit setter correctly constrains ranges; consider documenting “(0,0,0) disables minting”
setUniswapV4TickLimitsenforcestickLowerMin <= tickUpperMax, writes all three fields, and emitsUniswapV4TickLimitsSet, which matches how_checkTickLimitsis used inUniswapV4Lib. Combined with_checkTickLimits’stickLimits-not-setguard,(0,0,0)effectively disables new mints while existing positions remain usable. It would be helpful to mention this “zero tuple = pool disabled for minting” semantics in NatSpec/comments so operators don’t misinterpret(0,0,0)as a valid narrow range.Also applies to: 317-336
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (3)
src/MainnetController.sol(7 hunks)src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
src/MainnetController.solsrc/libraries/UniswapV4Lib.soltest/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
src/MainnetController.solsrc/libraries/UniswapV4Lib.soltest/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-10-16T16:47:34.627Z
Learnt from: supercontracts
Repo: sparkdotfi/spark-alm-controller PR: 170
File: src/MainnetController.sol:1153-1181
Timestamp: 2025-10-16T16:47:34.627Z
Learning: In src/MainnetController.sol, the USDS token always returns true on successful transfer operations, so it does not require the empty return data handling that some non-standard ERC20 tokens (like USDT) need.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: coverage
- GitHub Check: build
- GitHub Check: test
🔇 Additional comments (12)
src/MainnetController.sol (4)
27-27: Uniswap V4 wiring (import, limit IDs, tick-limit mapping) looks consistentImporting
UniswapV4Lib, exposingLIMIT_UNISWAP_V4_DEPOSIT/WITHDRAWfrom the library, and storing per‑poolTickLimitsinuniswapV4TickLimitsall line up with howUniswapV4Libexpects to be called (bytes32 poolId keys,makeBytes32Key(LIMIT_*, poolId)in the library, and tests setting rate limits with the same scheme). I don’t see type or key‑mismatch risks here.Also applies to: 158-159, 201-203
629-656: mintPositionUniswapV4 entrypoint is well-gated and delegates safety checks to UniswapV4LibThis relayer function is
external nonReentrant, RELAYER‑gated, and passes the controller’s proxy, rate‑limits address, poolId, tick range, liquidity, max amounts, and the entireuniswapV4TickLimitsmapping intoUniswapV4Lib.mintPosition. That cleanly centralizes tick validation, token accounting, and rate‑limit updates inside the library while keeping the controller surface minimal. No additional checks appear missing here.
658-720: Increase/burn/decrease Uniswap V4 entrypoints maintain consistent access control and reentrancy defensesAll three functions (
increaseLiquidityUniswapV4,burnPositionUniswapV4,decreaseLiquidityUniswapV4) areexternal nonReentrant, RELAYER‑gated, and simply forward to the correspondingUniswapV4Libcalls with proxy, rateLimits, poolId, tokenId, amounts, and min amounts. Ownership checks and poolId guards are handled in the library (increasePositionenforcesownerOf == proxy, whileburn/decreaseintentionally rely only on poolId match and approvals, per the established design). From the controller side, the wiring and protections look correct.Based on learnings
937-938: LayerZeroquoteOFTusage now matches OFTReceipt semanticsDestructuring
quoteOFTas( , , OFTReceipt memory receipt )and settingsendParams.minAmountLD = receipt.amountReceivedLDcorrectly uses the quoted minimum amount deliverable on the destination chain, instead of hard‑coding zero or reading the wrong tuple element. This closes a slippage hole in the bridge call while keeping the rest of the flow unchanged.src/libraries/UniswapV4Lib.sol (1)
308-321: Tick-limit checks correctly gate new mints and treat zeroed limits as “not set”
_checkTickLimitsrequires at least one oftickLowerMin,tickUpperMax, ormaxTickWidthto be non‑zero (so(0,0,0)is “not configured”), enforcestickLower >= tickLowerMin,tickUpper <= tickUpperMax, and bounds the width viauint256(int256(tickUpper) - int256(tickLower)) <= maxTickWidth. That gives admins a clean way to disable new mints for a pool by setting(0,0,0)and relies on the width check to reject any inverted tick ranges. Logic looks correct.test/mainnet-fork/Uniswapv4.t.sol (7)
20-27: Test harness mirrors production wiring and rate-limit keys correctlyThe test contract pulls in
UniswapV4LibandMainnetController, hard-codes the same Permit2/PositionManager/Router/StateView addresses and poolId as the library, and defines_DEPOSIT_LIMIT_KEY/_WITHDRAW_LIMIT_KEYaskeccak256(abi.encode(_LIMIT_*, _POOL_ID)), which matchesRateLimitHelpers.makeBytes32Keyusage.setUpseeds generous rate limits for that key pair. This gives good confidence that tests are exercising the same wiring and keys used in production.Also applies to: 102-117, 121-128
130-305: mintPositionUniswapV4 tests validate reentrancy, role gating, tick limits, slippage, and rate-limit accountingThis block of tests systematically checks: reentrancy guard behavior, RELAYER-only access, unset tick limits, each tick-bound failure mode, amount0/1 max overshoot via Uniswap V4 slippage reverts, and a successful mint that asserts zeroed Permit2/token allowances plus exact
LIMIT_DEPOSITconsumption based on normalized token deltas. That gives strong coverage of the mint entrypoint’s safety and accounting.
306-443: increaseLiquidityUniswapV4 tests comprehensively cover ownership, poolId, slippage, and rate-limitsThese tests verify reentrancy, RELAYER gating, the
MC/non-proxy-positionownership check,MC/tokenId-poolId-mismatch,amount0/1Maxovershoots, and a happy path where liquidity, ownership, allowances, and deposit rate-limit deltas all match expectations. They tightly couple to the library’sincreasePositionbehavior and confirm the asymmetric ownership model (increase requires proxy ownership) is enforced.
445-565: burnPositionUniswapV4 tests confirm poolId guarding, SlippageCheck behavior, and withdraw rate-limitsThe burn tests assert reentrancy and RELAYER-only access, require the tokenId to match the supplied poolId, and use
SlippageCheck.MinimumAmountInsufficientexpectations to validate the underlying Uniswap V4 slippage guard. The happy-path test then checks position liquidity is fully burned and that theLIMIT_WITHDRAWrate-limit is decremented by the normalized sum of tokens actually received, matching_decreaseLiquidity’s accounting.
567-694: decreaseLiquidityUniswapV4 tests mirror burn coverage for partial withdrawalsThese tests mirror the burn suite for partial decreases: reentrancy and RELAYER-only checks, poolId mismatch, per-token min slippage failures, and a successful decrease that verifies ownership retained by the proxy, updated liquidity amount, and precise withdraw rate-limit consumption based on received amounts. This gives good confidence in partial withdraw behavior and its interaction with rate limits.
700-741: Admin tick-limit tests validate reentrancy, role, input validation, and event + storage wiringThe
setUniswapV4TickLimitstests ensure the function is nonReentrant, restricted to the admin role, rejectstickLowerMin > tickUpperMax, emits the expectedUniswapV4TickLimitsSetevent with configured values, and thatuniswapV4TickLimitsstorage returns exactly those values. The_assertReentrancyGuardWrittenToTwiceassertion also confirms the setter integrates with the same guard as other admin calls.
1282-1477: Helper functions give clear observability into token flows and align with library semanticsThe
_mintPosition,_increasePosition,_burnPosition,_decreasePosition, and_swaphelpers all compute before/after token balances around the controller calls, enforce proper prank contexts (relayer vs SPARK_PROXY vs external accounts), and return structured results used in assertions._swapuses the same PoolKey lookup and Permit2 pattern that the library relies on. This structure makes it easy to reason about exact token deltas and ticks in tests, and it matches the production integration patterns.Also applies to: 1664-1707
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
♻️ Duplicate comments (7)
test/mainnet-fork/Uniswapv4.t.sol (3)
763-819: Story and attack scenarios provide valuable adversarial coverageThe
test_uniswapV4_story1and the varioustest_uniswapV4_attack_*cases:
- Walk realistic sequences of mints/increases/decreases/burns interleaved with large swaps via the V4 router.
- Quantify LP PnL under different front‑run/back‑run paths (mid→above/below, below/above→mid, etc.).
- Assert on rate‑limit deltas in normalized 18‑decimals, matching the library’s design decision.
No issues spotted; these are a strong complement to unit‑style tests and double as documentation of economic behavior under adversarial order flow.
Also applies to: 851-1134, 115-1325
825-849: Verbose price/tick logging helpers are fine as skipped diagnostics
test_uniswapV4_logPriceAndTicks_*and_logCurrentPriceAndTickare intentionally diagnostic:
- Tests are
vm.skip(true)so they don’t impact CI._logCurrentPriceAndTick’s formatting ladder is long but logically consistent with_getPrice.No changes needed; just be aware that un‑skipping these tests would be slow and noisy in CI.
If you ever re-enable these, consider replacing the
ifladder with a single formatting helper and limiting iterations for CI‑friendliness.Also applies to: 1683-1767
1784-1790: Assertion messages in_assertZeroAllowancesare token‑specific but helper is genericThe helper accepts an arbitrary
tokenaddress, but the assertion messages are hard‑coded as"permit2 usdc allowance"and"token usdc allowance", which can be misleading when called with non‑USDC tokens.If you touch this helper again, consider making the messages token‑agnostic (e.g.
"permit2 allowance non-zero"/"token->permit2 allowance non-zero").- assertEq(allowance, 0, "permit2 usdc allowance"); - - assertEq(IERC20Like(token).allowance(address(almProxy), _PERMIT2), 0, "token usdc allowance"); + assertEq(allowance, 0, "permit2 allowance non-zero"); + + assertEq( + IERC20Like(token).allowance(address(almProxy), _PERMIT2), + 0, + "token->permit2 allowance non-zero" + );src/libraries/UniswapV4Lib.sol (4)
183-219: Permit2 approval flow is subtle but appears correct; consider documenting expiration intent
_approvePositionManager:
- Best‑effort zeroes the token→Permit2 allowance via a low‑level
proxy.call(IALMProxy.doCall(...approve(...,0))), ignoring failure.- For
amount != 0, enforces ERC‑20 approve success by checking the return data is empty or decodes totrue.- Unconditionally grants Permit2’s allowance to
_POSITION_MANAGERwithexpiration = uint48(block.timestamp), making the Permit2 approval transient to the current block/tx.- Callers later clear approvals again by calling
_approvePositionManager(proxy, token, 0).The pattern looks sound; the only suggestion is to add a brief comment noting that
expiration == block.timestampis deliberate to make Permit2 approvals as short‑lived as possible.If you haven’t already, you may want to cross‑check the current Permit2
approvesignature and expiration semantics against the deployed version you target to ensure no interface drift.
270-302: Use clamped subtraction in_decreaseLiquidityfor symmetry and future‑proofing
_decreaseLiquiditycurrently computes:rateLimitDecrease = _getNormalizedBalance(token0, endingBalance0 - startingBalance0) + _getNormalizedBalance(token1, endingBalance1 - startingBalance1);This assumes
endingBalance{0,1} >= startingBalance{0,1}and will revert on any net outflow during a “decrease” (e.g., if future hooks or integrations ever cause tokens to leave the proxy in this path). In contrast,_increaseLiquidityalready uses_clampedSubfor the symmetric case.For consistency and extra safety, consider mirroring
_increaseLiquidity:- rateLimitDecrease = - _getNormalizedBalance(token0, endingBalance0 - startingBalance0) + - _getNormalizedBalance(token1, endingBalance1 - startingBalance1); + rateLimitDecrease = _clampedSub( + _getNormalizedBalance(token0, endingBalance0) + + _getNormalizedBalance(token1, endingBalance1), + _getNormalizedBalance(token0, startingBalance0) + + _getNormalizedBalance(token1, startingBalance1) + );That would treat unexpected net outflows as “no rate‑limit credit” instead of reverting.
If you keep the current subtraction, it’s worth documenting the invariant that Uniswap V4 decrease/burn flows must always be net in‑flows to the proxy under your chosen actions/hook configuration.
469-474: Normalization assumesdecimals() <= 18and a standard ERC‑20decimalsimplementation
_getNormalizedBalancescales via:return balance * 1e18 / (10 ** IERC20Like(token).decimals());This:
- Works as intended for 6‑decimals stables (USDC/USDT) and other
decimals() <= 18tokens.- Will truncate towards zero for tokens with
decimals() > 18and can overflow10 ** decimalsifdecimalsis ever very large.- Assumes
decimals()is implemented and non‑reverting.Given the learnings that this integration is for known stable tokens and uses normalized, not USD‑denominated, limits, behavior is acceptable. If you expect to broaden token support later, consider either constraining to
decimals() <= 18or documenting that requirement.If you consider supporting tokens beyond USDC/USDT, it’s worth reviewing the exact
decimals()of those assets and updating this helper (or adding a guard) accordingly.
476-484: PoolId handling closely follows PositionManager internals; document coupling
_getPoolKeyand_requirePoolIdMatch:
- Cast the external
bytes32 poolIdtobytes25to indexpoolKeys(bytes25(poolId)).- Compare
keccak256(abi.encode(poolKey))against the caller‑suppliedpoolIdfor tokenId validation.This matches Uniswap V4’s current pattern (PoolId =
keccak256(abi.encode(poolKey))andpoolKeyskeyed by a truncated representation), but tightly couples this library to that internal encoding.A short comment here explaining that:
poolIdis expected to bekeccak256(abi.encode(poolKey))for the deployed_POSITION_MANAGER, and- the
bytes25cast mirrors its internal mapping key,would help future maintainers avoid “simplifying” this in a way that breaks compatibility.
If you ever upgrade PositionManager or target another chain, double‑check that its PoolId/poolKeys encoding still matches these assumptions.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (2)
src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
test/mainnet-fork/Uniswapv4.t.solsrc/libraries/UniswapV4Lib.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
test/mainnet-fork/Uniswapv4.t.solsrc/libraries/UniswapV4Lib.sol
📚 Learning: 2025-10-16T16:47:34.627Z
Learnt from: supercontracts
Repo: sparkdotfi/spark-alm-controller PR: 170
File: src/MainnetController.sol:1153-1181
Timestamp: 2025-10-16T16:47:34.627Z
Learning: In src/MainnetController.sol, the USDS token always returns true on successful transfer operations, so it does not require the empty return data handling that some non-standard ERC20 tokens (like USDT) need.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: build
- GitHub Check: coverage
- GitHub Check: test
🔇 Additional comments (4)
test/mainnet-fork/Uniswapv4.t.sol (2)
102-128: End‑to‑end coverage of Uniswap V4 controller entrypoints looks strongThese tests exercise all four relayer entrypoints (mint/increase/burn/decrease), including:
- Reentrancy guard and role checks.
- Tick‑limit configuration and enforcement.
- Amount‑bound behavior via
_getIncreasePositionMaxAmounts/_getDecreasePositionMinAmounts, consistent with the “efficiency‑ratio” slippage semantics.- Rate‑limit accounting through
_DEPOSIT_LIMIT_KEY/_WITHDRAW_LIMIT_KEYkeyed off(_LIMIT_* , _POOL_ID).This gives good confidence that the MainnetController↔UniswapV4Lib↔PositionManager integration behaves as intended on a mainnet fork for the target pool.
If you want an extra sanity check, verify on the fork that
RateLimitHelpers.makeBytes32Keymatches the test’s_DEPOSIT_LIMIT_KEY/_WITHDRAW_LIMIT_KEYderivation for this pool so limits are aligned across contracts.Also applies to: 297-320, 434-459
169-261: Reentrancy and access‑control tests correctly guard all new controller entrypointsThe reentrancy tests (
_setControllerEntered+ReentrancyGuardReentrantCall) and the role checks for relayer/admin roles around all Uniswap V4 entrypoints give good coverage of the intended protections at the MainnetController layer.These tests should catch regressions if reentrancy guards or AccessControl roles are accidentally relaxed in the future.
It’s worth double‑checking that every new Uniswap V4 relayer method in
MainnetController.solis decorated with the same non‑reentrant + role modifiers that these tests expect.Also applies to: 388-459, 561-581, 587-710, 716-757
src/libraries/UniswapV4Lib.sol (2)
35-177: Uniswap V4 interaction pattern and guards look coherent; main assumptions are explicitThe four public helpers:
- Mint: enforces tick‑limits via
_checkTickLimits, derivesPoolKeyfrompoolId, and routes through_increaseLiquiditywith Permit2 approvals and rate‑limit accounting keyed by(LIMIT_DEPOSIT, poolId).- Increase: requires
ownerOf(tokenId) == proxyand_requirePoolIdMatch, then reuses_increaseLiquidity.- Burn / decrease: rely on
_requirePoolIdMatchplus Position Manager approvals, intentionally omitting ownership checks so any approved position for the same pool can be burned/decreased and tokens routed toproxy(as per project learnings).- All flows normalize token balances to 1e18 and treat USDC/USDT as equal‑valued for rate limiting (also per learnings).
I don’t see a functional bug here; the main security model hinges on:
- Correct
poolIdwiring (must be the canonicalkeccak256(abi.encode(poolKey))for the target PositionManager).- Only trusted callers having the relayer/controller privileges that can invoke these helpers.
Please confirm that:
- The
poolIdpassed fromMainnetControlleris exactlykeccak256(abi.encode(poolKey))for_POSITION_MANAGERon this network.- Only the intended relayer/controller can reach these functions (e.g. via
onlyRole(RELAYER)).
308-323: Tick‑limit checks are strict and make (0,0,0) a sentinel; ensure that’s desired
_checkTickLimits:
- Treats
(0,0,0)as “not set” and reverts"MC/tickLimits-not-set".- Requires
tickLower < tickUpper, enforcestickLower >= tickLowerMin,tickUpper <= tickUpperMax, and caps width viamaxTickWidth.This means:
- You can’t deliberately set a zero‑width range or disable width limits (since
maxTickWidthmust be ≥ the actual width).- Setting
(0,0,0)after initial provisioning fully disables the pool for new mints in this helper, which matches how tests use it.All good as long as the sentinel behavior is intentional; consider mentioning this in controller‑level docs so operators understand that
(0,0,0)means “no Uniswap V4 activity allowed for this pool”.
| ); | ||
|
|
||
| require(tickLower >= limits.tickLowerMin, "MC/tickLower-too-low"); | ||
| require(tickUpper <= limits.tickUpperMax, "MC/tickUpper-too-high"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
require lower <= upper
| deal(address(usdt), address(almProxy), usdt.balanceOf(address(almProxy)) + amount1Forecasted + 1); | ||
|
|
||
| vm.prank(relayer); | ||
| vm.expectRevert(); // Insufficient allowance. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should include stdError here if thats whats failing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's token specific (i.e. based on how that version of USDT and/or USDC was written).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
src/MainnetController.sol(7 hunks)src/interfaces/UniswapV4.sol(1 hunks)src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
🧰 Additional context used
🧠 Learnings (5)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
src/interfaces/UniswapV4.solsrc/MainnetController.soltest/mainnet-fork/Uniswapv4.t.solsrc/libraries/UniswapV4Lib.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
src/MainnetController.soltest/mainnet-fork/Uniswapv4.t.solsrc/libraries/UniswapV4Lib.sol
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: coverage
- GitHub Check: test
- GitHub Check: build
🔇 Additional comments (7)
src/interfaces/UniswapV4.sol (1)
4-28: Interfaces match library and test usage; no changes neededThe IPositionManagerLike and IUniversalRouterLike signatures line up with how UniswapV4Lib and the fork tests call them (getPoolAndPositionInfo, poolKeys(bytes25), modifyLiquidities, execute). I don’t see any inconsistencies or missing methods for the current integration surface.
src/MainnetController.sol (3)
133-137: Tick‑limits and Uniswap V4 limit IDs are wired coherently
- LIMIT_UNISWAP_V4_{DEPOSIT,WITHDRAW,SWAP} correctly mirror UniswapV4Lib’s LIMIT_* constants, so RateLimits keys are consistent between controller, library, and tests.
- uniswapV4TickLimits is admin‑set via setUniswapV4TickLimits (DEFAULT_ADMIN_ROLE, nonReentrant) and enforced in the library’s _checkTickLimits, meaning per‑pool mint ranges are centrally governed while increase/decrease remain unrestricted by ticks, as intended.
- Setting (0,0,0) for a pool effectively disables new mints via the library’s “tickLimits-not-set” guard, matching how tests use this as a “no new mints” switch.
Looks sound from a control‑surface and configuration perspective.
Also applies to: 158-160, 202-203, 318-337
634-701: Uniswap V4 mint/increase/decrease entrypoints respect roles, reentrancy, and library invariants
- All three functions are RELAYER‑only and nonReentrant, which is important given V4 hooks can execute arbitrary code and attempt to re‑enter.
- They delegate all Uniswap‑specific logic (Permit2 approvals, PositionManager.modifyLiquidities, poolId checks, tick‑limit enforcement, and rate‑limit accounting) to UniswapV4Lib, keeping this contract’s surface small and easier to audit.
- increaseLiquidityUniswapV4 and decreaseLiquidityUniswapV4 rely on the library’s poolId/owner checks (owner enforced only on increase, intentionally omitted on decrease per design) and per‑pool rate‑limits; tests exercise both the happy paths and the “non‑proxy position” / “tokenId‑poolId‑mismatch” failure modes.
Given the current design goals, the controller side of the V4 liquidity flows looks correct and appropriately constrained.
703-721: Swap wiring to Uniswap V4 and slippage/rate‑limit controls look correct
- swapUniswapV4 is RELAYER‑only, nonReentrant, and delegates to UniswapV4Lib.swap, which performs:
- A
maxSlippage != 0guard (so swaps are disabled unless configured),- A semantic check that
amountOutMinis consistent with the configuredmaxSlippage,- A V4 Universal Router call using Permit2, and
- A per‑pool rate‑limit decrease keyed on (LIMIT_SWAP, poolId).
- The controller’s maxSlippage lookup via
maxSlippages[address(uint160(uint256(poolId)))]matches how tests set the slippage for this pool, and the library enforces the “max-slippage-not-set” / “amount-out-min-too-low” reverts which the tests assert on.From the controller’s side, the swap integration with Uniswap V4 plus slippage and rate‑limit checks behaves as intended.
test/mainnet-fork/Uniswapv4.t.sol (1)
149-1574: Tests exercise Uniswap V4 integration, hooks, rate‑limits, and attack scenarios thoroughly
- Reentrancy tests (
test_*_reentrancy) use_setControllerEntered()to assert that all Uniswap V4 entrypoints respect the controller’snonReentrantguard, which is key against malicious V4 hooks.- Role‑based tests (
revertsForNonRelayer,revertsWhenTickLimitsNotSet,revertsWhenTokenIsNotForPool, etc.) validate that only RELAYER and admin roles can touch the new flows and that poolId/tokenId/tick invariants are enforced.- Rate‑limit tests around
_DEPOSIT_LIMIT_KEY,_WITHDRAW_LIMIT_KEY, and_SWAP_LIMIT_KEYconfirm that the library’s normalized‑balance accounting for deposit/withdraw/swap matches the external RateLimits contract behavior.- The story/attack tests (
test_uniswapV4_story1andtest_uniswapV4_attack_*) exercise realistic front‑run/back‑run price moves on the real USDC/USDT V4 pool, demonstrating the economic impact while still validating that rate‑limits and tick‑limits behave deterministically.Overall this suite gives strong end‑to‑end assurance that the new Uniswap V4 integration behaves correctly and that the intended defenses (ticks, slippage, rate‑limits, reentrancy guards) actually hold under adversarial flows.
src/libraries/UniswapV4Lib.sol (2)
40-150: Uniswap V4 liquidity helpers correctly wrap PositionManager with tick/rate‑limit and owner safeguards
mintPositionenforces per‑poolTickLimitsvia_checkTickLimitsbefore buildingMINT_POSITION + SETTLE_PAIRcalldata, then uses_increaseLiquidityto:
- Approve token0/token1 to Permit2 and PositionManager,
- Measure pre/post balances at the proxy,
- Compute a normalized deposit delta (with
_clampedSubto ignore pathological “net in” cases), and- Trigger a rate‑limit decrease keyed on
(LIMIT_DEPOSIT, poolId), followed by clearing approvals.increasePositionadds an explicitownerOf(tokenId) == proxycheck plus_requirePoolIdMatch, so added liquidity always accrues to the proxy’s NFT and only for the intended pool.decreasePositionrelies on_requirePoolIdMatchand PositionManager’s own approvals, then routes all proceeds back to the proxy and credits(LIMIT_WITHDRAW, poolId)based on normalized balance deltas, matching how the tests compute expected rate‑limit usage.This structure gives a clear, well‑bounded integration surface to Uniswap V4: ticks, ownership, and per‑pool limits are checked here, while MainnetController only handles roles and dispatch.
219-304: Permit2 approval pattern is subtle but implemented correctly for PositionManager flows
_approveWithPermit2:
- Best‑effort zeros the token→Permit2 allowance via a low‑level
proxy.call(IALMProxy.doCall(...approve(...,0))), explicitly ignoring failure.- When
amount != 0, performs a mandatoryIERC20Like.approve(_PERMIT2, amount)throughIALMProxy.doCall, requiring either empty ortruereturn data to handle both ERC20 variants.- Grants a Permit2 allowance for
(user=proxy, token, spender, amount, expiration=block.timestamp), intentionally limiting spender use to the current transaction._increaseLiquiditythen:
- Calls
_approveWithPermit2for token0 and token1 with positive amounts beforemodifyLiquidities,- Measures normalized pre/post balances to drive
(LIMIT_DEPOSIT, poolId)rate‑limits, and- Calls
_approveWithPermit2(..., 0)afterward to clear both token→Permit2 and Permit2→PositionManager allowances.Tests validate that allowances are returned to zero after mint/increase flows. Given the interaction with external ERC20s and Permit2, this pattern looks correct and appropriately defensive.
test/mainnet-fork/Uniswapv4.t.sol
Outdated
|
|
||
| mainnetController.swapUniswapV4(_POOL_ID, address(usdc), 1_000_000e6, 999_281e6); | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can change this to 6 hours to be more realistic, same for all warps
| tickLower : -30, | ||
| tickUpper : -10, | ||
| liquidity : 1_000_000_000e6, | ||
| amount0Max : amount0Max, | ||
| amount1Max : amount1Max | ||
| }); | ||
| } | ||
|
|
||
| function test_uniswapV4_attack_baseline_priceAbove_wideTicks() public { | ||
| // Setup the pool and the controller. | ||
| vm.startPrank(SPARK_PROXY); | ||
| mainnetController.setUniswapV4TickLimits(_POOL_ID, -200, 200, 200); // Allow wider tick range. | ||
| rateLimits.setRateLimitData(_DEPOSIT_LIMIT_KEY, 20_000_000e18, uint256(20_000_000e18) / 1 days); | ||
| rateLimits.setRateLimitData(_WITHDRAW_LIMIT_KEY, 20_000_000e18, uint256(20_000_000e18) / 1 days); | ||
| vm.stopPrank(); | ||
|
|
||
| /******************************************************************************************/ | ||
| /*** Add Liquidity (Current price is expected to be above the range) ***/ | ||
| /******************************************************************************************/ | ||
|
|
||
| assertEq(_getCurrentTick(), -7); | ||
|
|
||
| IncreasePositionResult memory increaseResult = _mintPosition(-200, -10, 1_000_000_000e6, type(uint160).max, type(uint160).max); | ||
|
|
||
| assertEq(increaseResult.amount0Spent, 0); | ||
| assertEq(increaseResult.amount1Spent, 9_449_821.223798e6); | ||
| assertEq(increaseResult.amount1Spent + increaseResult.amount0Spent, 9_449_821.223798e6); | ||
|
|
||
| /******************************************************************************************/ | ||
| /*** Remove Liquidity ***/ | ||
| /******************************************************************************************/ | ||
|
|
||
| assertEq(_getCurrentTick(), -7); | ||
|
|
||
| DecreasePositionResult memory decreaseResult = _decreasePosition(increaseResult.tokenId, 1_000_000_000e6, 0, 0); | ||
|
|
||
| assertEq(decreaseResult.amount0Received, 0); | ||
| assertEq(decreaseResult.amount1Received, 9_449_821.223797e6); | ||
| assertEq(decreaseResult.amount0Received + decreaseResult.amount1Received, 9_449_821.223797e6); // Lost 0 USD. | ||
| } | ||
|
|
||
| function test_uniswapV4_attack_priceAboveToBelow_wideTicks() public { | ||
| // Setup the pool and the controller. | ||
| vm.startPrank(SPARK_PROXY); | ||
| mainnetController.setUniswapV4TickLimits(_POOL_ID, -200, 200, 200); // Allow wider tick range. | ||
| rateLimits.setRateLimitData(_DEPOSIT_LIMIT_KEY, 20_000_000e18, uint256(20_000_000e18) / 1 days); | ||
| rateLimits.setRateLimitData(_WITHDRAW_LIMIT_KEY, 20_000_000e18, uint256(20_000_000e18) / 1 days); | ||
| vm.stopPrank(); | ||
|
|
||
| /******************************************************************************************/ | ||
| /*** Frontrun ***/ | ||
| /******************************************************************************************/ | ||
|
|
||
| assertEq(_getCurrentTick(), -7); | ||
|
|
||
| uint256 amountOut1 = _externalSwap(_alice, address(usdc), 3_020_000e6); | ||
|
|
||
| /******************************************************************************************/ | ||
| /*** Add Liquidity (Current price was expected to be above the range, but is below) ***/ | ||
| /******************************************************************************************/ | ||
|
|
||
| assertEq(_getCurrentTick(), -501); | ||
|
|
||
| IncreasePositionResult memory increaseResult = _mintPosition(-200, -10, 1_000_000_000e6, type(uint160).max, type(uint160).max); | ||
|
|
||
| assertEq(increaseResult.amount0Spent, 9_549_562.082877e6); // Expected 0 | ||
| assertEq(increaseResult.amount1Spent, 0); // Expected 9_449_821.223798e6 | ||
| assertEq(increaseResult.amount1Spent + increaseResult.amount0Spent, 9_549_562.082877e6); // Expected 9_449_821.223798e6 | ||
|
|
||
| /******************************************************************************************/ | ||
| /*** Backrun ***/ | ||
| /******************************************************************************************/ | ||
|
|
||
| assertEq(_getCurrentTick(), -501); | ||
|
|
||
| uint256 amountOut2 = _externalSwap(_alice, address(usdt), uint128(amountOut1)); | ||
|
|
||
| assertEq(amountOut2, 3_067_685.526025e6); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These should be moved into a dedicated section for user actions where assertions are also performed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Other functions should be consolidated in here as well
949f62f to
9ca1d11
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/libraries/UniswapV4Lib.sol (1)
348-358: Consider defensive subtraction for consistency.Unlike
_increaseLiquiditywhich uses_clampedSubto handle theoretical edge cases,_decreaseLiquidityperforms raw subtraction (endingBalance - startingBalance). While theDECREASE_LIQUIDITY + TAKE_PAIRflow should always increase the proxy's balance, using consistent defensive patterns across similar operations would prevent potential reverts from unexpected edge cases (e.g., exotic hooks or fee-on-transfer interactions).- uint256 rateLimitDecrease = - _getNormalizedBalance(token0, endingBalance0 - startingBalance0) + - _getNormalizedBalance(token1, endingBalance1 - startingBalance1); + // Account for the theoretical possibility of spending tokens when removing liquidity + // (e.g., due to hooks or other edge cases) by using clamped subtraction. + uint256 rateLimitDecrease = + _getNormalizedBalance(token0, _clampedSub(endingBalance0, startingBalance0)) + + _getNormalizedBalance(token1, _clampedSub(endingBalance1, startingBalance1));
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (11)
.gitmodules(1 hunks)foundry.toml(1 hunks)lib/uniswap-v4-core(1 hunks)lib/uniswap-v4-periphery(1 hunks)src/MainnetController.sol(7 hunks)src/RateLimitHelpers.sol(1 hunks)src/interfaces/Common.sol(1 hunks)src/interfaces/UniswapV4.sol(1 hunks)src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Curve.t.sol(0 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
💤 Files with no reviewable changes (1)
- test/mainnet-fork/Curve.t.sol
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
src/libraries/UniswapV4Lib.solsrc/interfaces/UniswapV4.soltest/mainnet-fork/Uniswapv4.t.solsrc/MainnetController.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
src/libraries/UniswapV4Lib.soltest/mainnet-fork/Uniswapv4.t.solsrc/MainnetController.sol
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-10-16T16:47:34.627Z
Learnt from: supercontracts
Repo: sparkdotfi/spark-alm-controller PR: 170
File: src/MainnetController.sol:1153-1181
Timestamp: 2025-10-16T16:47:34.627Z
Learning: In src/MainnetController.sol, the USDS token always returns true on successful transfer operations, so it does not require the empty return data handling that some non-standard ERC20 tokens (like USDT) need.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: coverage
- GitHub Check: build
- GitHub Check: test
🔇 Additional comments (39)
lib/uniswap-v4-periphery (1)
1-1: Original review comment is based on incorrect assumption—v4.0.0 tag does not exist in Uniswap v4-periphery.The prior review suggested pinning v4-periphery to
v4.0.0to match v4-core, but v4-periphery has no published releases or tags. Using the HEAD commit is likely the correct approach for this repository, as it does not maintain formal release tags like v4-core does. The version mismatch concern is invalid given the different release management practices between the two projects.Likely an incorrect or invalid review comment.
foundry.toml (2)
13-22: Remapping syntax is correct and consistent.The new @uniswap/v4-core remapping follows the established pattern, is syntactically correct, and does not conflict with existing remappings.
21-21: The review comment based on an unfounded assumption—no v4-periphery imports or directory exist in the codebase.Verification shows:
- No imports from
@uniswap/v4-peripheryfound in any Solidity files- The
lib/uniswap-v4-peripherydirectory does not exist in the repository- The
@uniswap/v4-coreremapping at line 21 is correctly added and sufficient for the actual codebase needsThe original concern assumed v4-periphery would be used directly, but the code either uses only custom interfaces or imports only from v4-core. The remapping addition is correct as-is.
Likely an incorrect or invalid review comment.
lib/uniswap-v4-core (2)
1-1: Acknowledge intentional Uniswap V4 design decisions per prior review.Per the PR objectives and retrieved learnings, the prior automated review identified several design-level considerations—including missing ownership checks in burnPosition/decreasePosition, poolId→address casting, and bytes32→bytes25 truncation. The PR author has documented that these are intentional alignments with Uniswap V4 Position Manager design and do not represent security concerns in the context of this system's risk controls (tick-range limits, slippage checks, and rate limiting).
Ensure that the submodule commit does not introduce unexpected changes that would affect these already-reviewed and documented design decisions.
Based on learnings, the ownership omission in burnPosition and decreasePosition is an intentional design decision where burning someone else's position routes tokens to the proxy and is considered beneficial. Similarly, rate limits normalize token amounts to 18 decimals rather than USD values—also intentional.
1-1: Submodule configuration verified and correct.The commit hash
e50237c43811bd9b526eff40f26772152a42dabais valid, reachable (tagged asv4.0.0in the Uniswap V4 Core repository), and the submodule is properly registered with the correct Foundry remapping. No issues identified..gitmodules (1)
50-55: LGTM!The submodule additions for Uniswap V4 core and periphery are correctly configured with official Uniswap repository URLs. Submodule commit SHAs will pin the exact versions used.
src/interfaces/Common.sol (1)
1-18: LGTM!Clean interface definitions that expose only the required ERC20 and Permit2 functions needed for the Uniswap V4 integration. The named return values improve readability.
src/RateLimitHelpers.sol (1)
6-20: LGTM!The refactored helpers with generic parameter names (
a,b) and the newmakeBytes32Keyfunction provide a cleaner, more consistent API for key construction. The newmakeBytes32Keydirectly supports thepoolId-based rate limiting in UniswapV4Lib.src/interfaces/UniswapV4.sol (1)
9-28: LGTM!The interface definitions correctly expose the required Uniswap V4 PositionManager and UniversalRouter functions. The function ordering (state-mutating
modifyLiquiditiesfirst, then view functions) follows a reasonable convention.src/libraries/UniswapV4Lib.sol (11)
17-34: LGTM!The struct and constants are well-defined. The rate limit identifiers use unique keccak256 hashes to prevent collisions. The mainnet addresses are documented with their source.
40-78: LGTM!The
mintPositionfunction correctly validates tick limits before building calldata and delegating to_increaseLiquidity. The proxy is set as the NFT recipient in_getMintCalldata, ensuring ownership.
80-119: LGTM!The
increasePositionfunction correctly enforces ownership (ownerOf(tokenId) == proxy) before allowing liquidity increase. This ensures the proxy retains ownership of the increased liquidity.
121-158: LGTM!The intentional omission of ownership validation is well-documented in the inline comment (lines 132-134). Based on learnings, this design allows decreasing any position (for which the proxy has approval) with tokens flowing to the proxy, which benefits the system.
160-223: LGTM!The
swapfunction correctly implements:
- Slippage validation using the project's inverted semantics (minimum efficiency ratio)
- Protocol-level validation of
tokenInvia Uniswap V4'sCurrencyNotSettledrevert- Permit2 approval flow for the router
- Rate limiting based on normalized input amount
The swap rate limit tracks only
tokenInamount, which is a reasonable simplification for stablecoin pools.
229-272: LGTM!The Permit2 approval pattern is well-implemented:
- Best-effort zeroing via low-level call handles non-standard tokens (e.g., USDT)
- Mandatory success path validates return data for both
bool-returning and void ERC20sexpiration = uint48(block.timestamp)creates transient Permit2 approvals limited to the current block/transaction
274-323: LGTM!The
_increaseLiquidityimplementation:
- Uses
_clampedSubto defensively handle edge cases where tokens might be received- Documents the known limitation of equal-value token assumption (line 302-303)
- Properly cleans up Permit2 approvals after the operation
365-377: LGTM!The
_checkTickLimitsfunction correctly validates:
- Configuration is set (
maxTickSpacing != 0)- Tick ordering (
tickLower < tickUpper- strict inequality ensures non-zero range)- Bounds (
tickLower >= tickLowerMin,tickUpper <= tickUpperMax)- Width constraint with proper casting to handle negative tick values
379-385: LGTM!Clean utility functions.
_clampedSubprovides safe subtraction to prevent underflow.
387-496: LGTM!The calldata construction functions correctly encode:
- Appropriate action sequences for each operation (MINT/INCREASE/DECREASE with settlement)
- The proxy as recipient for mint and decrease operations
block.timestampas deadline for immediate execution
498-515: LGTM!These utility functions correctly implement:
- Balance normalization to 18 decimals (acceptable for the known 6-decimal stablecoins in use)
bytes25cast matching Uniswap V4's internalpoolKeysmapping key formatpoolIdverification using consistentkeccak256(abi.encode(poolKey))encodingsrc/MainnetController.sol (7)
133-138: LGTM!The event declaration is well-structured with
poolIdindexed for efficient filtering.
163-165: LGTM!Exposing the library constants as public state variables maintains consistency with other rate limit identifiers and provides external visibility.
207-209: LGTM!Per-pool tick limits storage is appropriately keyed by
bytes32 poolId.
323-346: LGTM!The validation logic correctly handles both the "disabled" state (all zeros) and valid configurations. The requirement that
tickLowerMin < tickUpperMax(strict inequality) properly rejects zero-width ranges.
643-666: LGTM!Proper access control, reentrancy protection, and clean delegation to
UniswapV4Lib.mintPosition. The parameter ordering has been corrected withtickLowerbeforetickUpper.
668-710: LGTM!Both functions have proper
RELAYERaccess control andnonReentrantprotection. The reentrancy guard issue from previous review has been addressed. Based on learnings, the intentional omission of ownership validation indecreaseLiquidityUniswapV4is a design decision—decreasing someone else's position would route tokens to the proxy, which benefits the system.
948-948: LGTM!Minor syntactic cleanup to the destructuring assignment; no functional change.
test/mainnet-fork/Uniswapv4.t.sol (12)
26-35: LGTM!The
IERC20Likeinterface intentionally omits the return type onapproveto handle non-conformant tokens (as noted in the comment). This is appropriate for test compatibility.
129-137: LGTM!The limit key constants correctly mirror the derivation pattern used in
UniswapV4Lib, ensuring test assertions match production behavior.
201-241: LGTM!Good boundary testing for tick ordering: verifies revert when
tickLower > tickUpper, revert whentickLower == tickUpper, and success whentickLower < tickUpper.
480-495: LGTM!This test correctly verifies that
increaseLiquidityUniswapV4rejects positions not owned by the proxy, which is the expected security control for increasing liquidity on existing positions.
780-808: LGTM!Both partial and full decrease tests properly verify balance changes, rate limit consumption, and position liquidity updates. The tests correctly use
_getDecreasePositionMinAmountsfor slippage protection.
839-853: LGTM!Tests correctly verify two layers of slippage protection:
- Controller-level check (
"MC/amountOutMin-too-low") ensuresamountOutMin >= amountIn * maxSlippage- Router-level check (
V4TooLittleReceived) ensures actual output meets minimumBoth token swap directions are also covered.
Also applies to: 866-889
1015-1071: LGTM!
test_uniswapV4_story1provides excellent end-to-end coverage: a complete round-trip of mint → increase → decrease → close with external swaps interspersed, verifying rate limit accounting at each step.
1103-1187: LGTM!The attack scenario tests provide valuable documentation of MEV exposure under various price manipulation conditions. The "defended" tests (
test_uniswapV4_attack_priceAboveToBelow_defended,test_uniswapV4_attack_priceAboveToBelow_defended_wideTicks) demonstrate that theamount0Max/amount1Maxparameters and tick spacing limits effectively mitigate sandwich attacks.Also applies to: 1520-1564
1722-1772: LGTM!The
_mintPositionhelper is well-structured with comprehensive assertions: verifies amounts spent are within max, rate limit is correctly consumed, position ownership, position liquidity, and allowances are zeroed. This consolidates assertions for cleaner tests.
2123-2129: LGTM!The assertion messages have been updated to be token-agnostic (
"permit2 allowance not 0","allowance to permit2 not 0"), addressing the previous review feedback.
2135-2137: Future block number for fork testing.The test uses block
23470490(September 29, 2025), which is a future block. This appears intentional to test against a specific pool state. Consider adding a comment explaining why this specific block was chosen if there are particular state requirements.
2139-2182: LGTM!The
_externalSwaphelper correctly implements the Uniswap V4 swap flow via Universal Router: token dealing, Permit2 approval chain, command encoding withSWAP_EXACT_IN_SINGLE+SETTLE_ALL+TAKE_ALLactions, and proper balance delta tracking.
src/MainnetController.sol
Outdated
| function swapUniswapV4( | ||
| bytes32 poolId, | ||
| address tokenIn, | ||
| uint128 amountIn, | ||
| uint128 amountOutMin | ||
| ) | ||
| external nonReentrant | ||
| { | ||
| _checkRole(RELAYER); | ||
|
|
||
| UniswapV4Lib.swap({ | ||
| proxy : address(proxy), | ||
| rateLimits : address(rateLimits), | ||
| poolId : poolId, | ||
| tokenIn : tokenIn, | ||
| amountIn : amountIn, | ||
| amountOutMin : amountOutMin, | ||
| maxSlippage : maxSlippages[address(uint160(uint256(poolId)))] | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding inline documentation for the poolId to address casting.
The maxSlippages lookup on line 729 uses address(uint160(uint256(poolId))), which truncates the bytes32 pool ID to 160 bits. Per PR discussion, this aligns with Uniswap V4's design, but consider adding a brief inline comment explaining this casting rationale for future maintainers.
UniswapV4Lib.swap({
proxy : address(proxy),
rateLimits : address(rateLimits),
poolId : poolId,
tokenIn : tokenIn,
amountIn : amountIn,
amountOutMin : amountOutMin,
+ // NOTE: poolId is cast to address (lower 160 bits) to align with Uniswap V4's design
maxSlippage : maxSlippages[address(uint160(uint256(poolId)))]
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function swapUniswapV4( | |
| bytes32 poolId, | |
| address tokenIn, | |
| uint128 amountIn, | |
| uint128 amountOutMin | |
| ) | |
| external nonReentrant | |
| { | |
| _checkRole(RELAYER); | |
| UniswapV4Lib.swap({ | |
| proxy : address(proxy), | |
| rateLimits : address(rateLimits), | |
| poolId : poolId, | |
| tokenIn : tokenIn, | |
| amountIn : amountIn, | |
| amountOutMin : amountOutMin, | |
| maxSlippage : maxSlippages[address(uint160(uint256(poolId)))] | |
| }); | |
| } | |
| function swapUniswapV4( | |
| bytes32 poolId, | |
| address tokenIn, | |
| uint128 amountIn, | |
| uint128 amountOutMin | |
| ) | |
| external nonReentrant | |
| { | |
| _checkRole(RELAYER); | |
| UniswapV4Lib.swap({ | |
| proxy : address(proxy), | |
| rateLimits : address(rateLimits), | |
| poolId : poolId, | |
| tokenIn : tokenIn, | |
| amountIn : amountIn, | |
| amountOutMin : amountOutMin, | |
| // NOTE: poolId is cast to address (lower 160 bits) to align with Uniswap V4's design | |
| maxSlippage : maxSlippages[address(uint160(uint256(poolId)))] | |
| }); | |
| } |
🤖 Prompt for AI Agents
In src/MainnetController.sol around lines 712 to 731, the lookup
maxSlippages[address(uint160(uint256(poolId)))] truncates the bytes32 poolId to
a 160-bit address without explanation; add a concise inline comment immediately
above that expression (or assign the cast to a well-named local variable like
address poolAddr = address(uint160(uint256(poolId)));) stating that Uniswap V4
encodes the pool's address in the low 160 bits of poolId and that this cast
intentionally extracts that address for the maxSlippages mapping, so future
maintainers understand the rationale and avoid accidental refactors.
| function test_setUniswapV4tickLimits_revertsForNonRelayer() external { | ||
| vm.expectRevert( | ||
| abi.encodeWithSelector( | ||
| IAccessControl.AccessControlUnauthorizedAccount.selector, | ||
| _unauthorized, | ||
| mainnetController.DEFAULT_ADMIN_ROLE() | ||
| ) | ||
| ); | ||
|
|
||
| vm.prank(_unauthorized); | ||
| mainnetController.setUniswapV4TickLimits(bytes32(0), 0, 0, 0); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test name is misleading: function checks DEFAULT_ADMIN_ROLE, not RELAYER.
The test is named test_setUniswapV4tickLimits_revertsForNonRelayer but verifies DEFAULT_ADMIN_ROLE access control. Consider renaming to test_setUniswapV4tickLimits_revertsForNonAdmin for clarity.
- function test_setUniswapV4tickLimits_revertsForNonRelayer() external {
+ function test_setUniswapV4tickLimits_revertsForNonAdmin() external {🤖 Prompt for AI Agents
In test/mainnet-fork/Uniswapv4.t.sol around lines 956 to 967, the test currently
asserts that calling setUniswapV4TickLimits reverts due to missing
DEFAULT_ADMIN_ROLE but the function name says "NonRelayer"; rename the test to
reflect ADMIN role (e.g., test_setUniswapV4tickLimits_revertsForNonAdmin) and
update any references or test runner expectations accordingly so the name
matches the asserted access control check.
9ca1d11 to
6c06cbb
Compare
|
Coverage after merging sc-1234-uniswap-v4-support into dev will be
Coverage Report
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
♻️ Duplicate comments (1)
test/mainnet-fork/Uniswapv4.t.sol (1)
796-807: Test name is misleading.The test is named
test_setUniswapV4tickLimits_revertsForNonRelayerbut verifiesDEFAULT_ADMIN_ROLEaccess control (Line 801). Consider renaming totest_setUniswapV4tickLimits_revertsForNonAdminfor clarity.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (11)
.gitmodules(1 hunks)foundry.toml(1 hunks)lib/uniswap-v4-core(1 hunks)lib/uniswap-v4-periphery(1 hunks)src/MainnetController.sol(7 hunks)src/RateLimitHelpers.sol(1 hunks)src/interfaces/Common.sol(1 hunks)src/interfaces/UniswapV4.sol(1 hunks)src/libraries/UniswapV4Lib.sol(1 hunks)test/mainnet-fork/Curve.t.sol(0 hunks)test/mainnet-fork/Uniswapv4.t.sol(1 hunks)
💤 Files with no reviewable changes (1)
- test/mainnet-fork/Curve.t.sol
🧰 Additional context used
🧠 Learnings (6)
📓 Common learnings
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, the burnPosition and decreasePosition functions in UniswapV4Lib.sol intentionally do not validate ownership because burning someone else's position would cause the underlying tokens to flow to the proxy, which benefits the system rather than harming it.
Applied to files:
src/interfaces/UniswapV4.solsrc/libraries/UniswapV4Lib.soltest/mainnet-fork/Uniswapv4.t.solsrc/MainnetController.sol
📚 Learning: 2025-11-11T19:43:16.032Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 0
File: :0-0
Timestamp: 2025-11-11T19:43:16.032Z
Learning: In spark-alm-controller, rate limits for Uniswap positions sum both tokens normalized to 18 decimals, which doesn't account for actual USD values. This is a known design decision where normalized balance is used rather than USD-denominated values.
Applied to files:
src/libraries/UniswapV4Lib.soltest/mainnet-fork/Uniswapv4.t.solsrc/MainnetController.sol
📚 Learning: 2025-11-04T17:15:21.481Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Aave.t.sol:59-63
Timestamp: 2025-11-04T17:15:21.481Z
Learning: In the spark-alm-controller repository, test files in test/base-fork/ use ForkTestBase from test/base-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to writing the reentrancy guard state to foreignController, while test files in test/mainnet-fork/ use ForkTestBase from test/mainnet-fork/ForkTestBase.t.sol where _setControllerEntered() defaults to mainnetController.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-11-04T17:15:39.489Z
Learnt from: deluca-mike
Repo: sparkdotfi/spark-alm-controller PR: 185
File: test/base-fork/Morpho.t.sol:122-126
Timestamp: 2025-11-04T17:15:39.489Z
Learning: In the spark-alm-controller repository, tests in the `test/base-fork/` directory use `ForkTestBase` from `test/base-fork/ForkTestBase.t.sol`, where the `_setControllerEntered()` helper function defaults to setting the reentrancy guard state on `foreignController`. Tests in `test/mainnet-fork/` use their own `ForkTestBase` which defaults to `mainnetController`.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
📚 Learning: 2025-10-16T16:47:34.627Z
Learnt from: supercontracts
Repo: sparkdotfi/spark-alm-controller PR: 170
File: src/MainnetController.sol:1153-1181
Timestamp: 2025-10-16T16:47:34.627Z
Learning: In src/MainnetController.sol, the USDS token always returns true on successful transfer operations, so it does not require the empty return data handling that some non-standard ERC20 tokens (like USDT) need.
Applied to files:
test/mainnet-fork/Uniswapv4.t.sol
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: build
- GitHub Check: coverage
- GitHub Check: test
🔇 Additional comments (32)
src/RateLimitHelpers.sol (1)
6-20: LGTM! Generic parameter names and new bytes32 helper align with reusable rate-limit key construction.The refactoring to generic parameter names (
a,binstead ofasset,destination,domain) increases reusability while maintaining type safety throughabi.encode. The newmakeBytes32Keyhelper supports Uniswap V4 poolId (bytes32) integration and follows the same collision-resistant pattern as the existing helpers.lib/uniswap-v4-periphery (1)
1-1: I need to check what release tags actually exist on v4-periphery:I need to verify if the current pinned commit is the latest HEAD on v4-periphery:
The original review comment's major issue is invalid. The v4-periphery contracts in this repository are still in development—no v4.0.0 release tag exists because the project has not published formal releases, unlike v4-core. Pinning to a valid commit (3779387e5d296f39df543d23524b050f89a62917) is the correct approach for active development code. No changes are required.
Likely an incorrect or invalid review comment.
.gitmodules (1)
50-55: LGTM! Standard submodule additions.The two new Uniswap V4 submodules are correctly configured with official repository URLs and follow the same format as existing submodules in the project.
src/interfaces/UniswapV4.sol (1)
9-21: Interface looks complete for the use case.The
IPositionManagerLikeinterface provides the necessary functions for Uniswap V4 position management:
modifyLiquiditiesfor updating positionsgetPoolAndPositionInfofor querying position detailspoolKeysfor pool lookupownerOffor ownership validationlib/uniswap-v4-core (1)
1-1: Pinned Uniswap v4 core via submodule commit; verify provenance and review upstream diffThis file only pins the
uniswap-v4-coresubmodule to commite50237c4…. Locally this is fine, but security/behavior now depends entirely on that upstream commit. Please ensure:
- The commit is from the expected official Uniswap repository and tag/branch.
- You’ve reviewed the upstream diff between the prior pinned commit (if any) and
e50237c4…for breaking changes or security-relevant behavior.src/interfaces/Common.sol (1)
1-18: LGTM!The interfaces are minimal, focused, and correctly defined for their intended use in UniswapV4Lib.sol. The function signatures align with standard ERC20 and Permit2 patterns.
src/libraries/UniswapV4Lib.sol (10)
16-30: LGTM!The library structure, TickLimits struct, and constants are well-defined. The hard-coded mainnet addresses align with Uniswap V4 official deployments per the documentation comment.
35-73: LGTM!The
mintPositionfunction correctly validates tick limits before minting, constructs appropriate calldata, and delegates to_increaseLiquidityfor execution and rate-limit accounting. The proxy is set as the NFT recipient in_getMintCalldata.
75-114: LGTM!The
increasePositionfunction correctly enforces ownership validation (Lines 87-90) to ensure the proxy retains ownership of the increased liquidity. The poolId match check (Line 92) prevents cross-pool misconfigurations.
116-153: LGTM!The
decreasePositionfunction intentionally omits ownership checks as documented in the comment (Lines 127-129). Per the design decision, decreasing another account's position routes underlying tokens to the proxy, which benefits the system. The poolId validation remains as the primary guard.Based on learnings.
159-202: LGTM!The
_approveWithPermit2function implements a robust approval pattern:
- Best-effort zeroing of existing allowance (Lines 167-178) with documented rationale for ignoring failures.
- Mandatory approval with return-value handling for both standard and non-compliant ERC20s (Lines 180-191).
- Transient Permit2 approval via
expiration = uint48(block.timestamp)limiting the permit to the current block (Line 199).
204-253: LGTM!The
_increaseLiquidityfunction correctly:
- Approves tokens before operation and cleans up after (Lines 216-217, 251-252).
- Uses balance deltas for accurate accounting (Lines 220-228).
- Applies clamped subtraction for edge cases (Lines 234-239).
- Documents the token valuation assumption (Lines 232-233).
The rate-limit accounting sums normalized balances rather than USD values, which is a known design decision per learnings.
255-289: LGTM!The
_decreaseLiquidityfunction correctly handles the withdrawal flow:
- No Permit2 approval needed since tokens flow inward.
- Balance delta accounting captures actual token increases.
- Direct subtraction (Lines 281-282) is safe under the standard V4 flow where
endingBalance >= startingBalance.The token valuation assumption is documented (Lines 278-279), consistent with the deposit side.
295-311: LGTM!The
_checkTickLimitsfunction correctly validates:
- Configuration is set (
maxTickSpacing != 0).- Ticks are ordered (
tickLower < tickUpper- strict inequality is correct for Uniswap V4).- Ticks within admin-defined bounds.
- Position width within configured maximum.
The
_clampedSubhelper is a clean implementation for safe subtraction.
317-432: LGTM!The calldata builder functions correctly encode Uniswap V4 action sequences:
_getMintCalldata: MINT_POSITION + SETTLE_PAIR_getIncreaseLiquidityCallData: INCREASE_LIQUIDITY + CLOSE_CURRENCY × 2_getDecreaseLiquidityCallData: DECREASE_LIQUIDITY + TAKE_PAIRThe
_getNormalizedBalancefunction correctly handles 6-decimal tokens (USDC/USDT). Thedecimals <= 18assumption is appropriate for the intended stable pairs.
434-446: LGTM!The
_getPoolKeyand_requirePoolIdMatchfunctions correctly interface with Uniswap V4's internal encoding:
bytes25cast mirrors the Position Manager's internal mapping key format.keccak256(abi.encode(poolKey))comparison validates pool membership correctly.src/MainnetController.sol (7)
133-138: LGTM!The
UniswapV4TickLimitsSetevent is well-structured withpoolIdindexed for efficient event filtering. The event parameters capture all relevant tick limit configuration.
163-164: LGTM!The rate-limit identifiers are correctly bound to
UniswapV4Libconstants, ensuring consistency between the controller and library. TheuniswapV4TickLimitsmapping provides per-pool tick configuration.Also applies to: 206-208
322-345: LGTM!The
setUniswapV4TickLimitsfunction correctly:
- Enforces admin-only access with
nonReentrant.- Validates either all-zero (disabled) or valid tick configuration (
maxTickSpacing > 0andtickLowerMin < tickUpperMax).- Setting
(0, 0, 0)effectively disables new mints while allowing existing positions to be managed.
642-665: LGTM!The
mintPositionUniswapV4function correctly gates access withRELAYERrole andnonReentrant, then delegates toUniswapV4Lib.mintPositionwhich handles tick validation, Permit2 approvals, and rate-limit accounting.
667-687: LGTM!The
increaseLiquidityUniswapV4function correctly enforcesRELAYERrole andnonReentrantmodifier. The library function handles ownership validation and rate-limit accounting.
689-709: LGTM!The
decreaseLiquidityUniswapV4function correctly enforcesRELAYERrole andnonReentrantmodifier. The intentional omission of ownership validation in the library is documented and aligns with the design decision.
926-927: LGTM!The destructuring adjustment correctly extracts only the needed
OFTReceiptfrom thequoteOFTreturn values.test/mainnet-fork/Uniswapv4.t.sol (9)
26-35: LGTM!The local
IERC20Likeinterface intentionally omits the bool return onapprove(as documented in the comment at Line 28) to accommodate non-conformant tokens during testing. This is a pragmatic choice for test flexibility.
131-419: LGTM!Comprehensive test coverage for
mintPositionUniswapV4:
- Access control and reentrancy tests.
- Boundary condition tests for tick limits (misordered, too low, too high, too wide) with both failure and success assertions.
- Slippage protection tests for both tokens.
- Rate-limit boundary testing.
- Happy path with reentrancy guard state verification.
421-597: LGTM!Thorough test coverage for
increaseLiquidityUniswapV4:
- Ownership validation test (Line 456-471) correctly verifies proxy must own the position.
- PoolId mismatch test verifies cross-pool protection.
- Slippage and rate-limit boundary tests included.
599-784: LGTM!Complete test coverage for
decreaseLiquidityUniswapV4:
- Correctly omits ownership test (intentional design per learnings).
- Min amount slippage tests verify
SlippageCheck.MinimumAmountInsufficientrevert.- Both partial (50%) and full decrease scenarios are tested with rate-limit verification.
825-841: LGTM!The
setUniswapV4TickLimitshappy path test correctly verifies event emission and state updates.
847-910: LGTM!The
test_uniswapV4_story1provides excellent end-to-end coverage:
- Full lifecycle: mint → increase → decrease → close.
- Time warps (90 days) and external swaps simulate realistic market conditions.
- Rate-limit accounting verified at each step.
- Clear NatSpec documentation of the scenario.
912-1523: LGTM!Comprehensive adversarial test coverage:
- Baseline scenarios establish expected behavior at different price positions.
- Attack scenarios (front-run/back-run patterns) quantify MEV/sandwich attack impact.
- Defense tests (
*_defended) demonstrate tick-width limits and slippage caps effectively blocking attack vectors.- Wide-tick scenarios illustrate the increased risk of broader ranges.
These tests serve as both validation and documentation of the economic security model.
1561-1611: LGTM!The
_mintPositionhelper includes comprehensive assertions:
- Token amounts within limits (Lines 1600-1601).
- Rate-limit accounting verification (Line 1603).
- Position ownership and liquidity checks (Lines 1605-1607).
- Zero-allowance verification for security (Lines 1609-1610).
This pattern is consistently applied across all position operation helpers.
1926-1969: LGTM!The
_externalSwaphelper correctly implements external swap functionality via the Universal Router:
- Proper action encoding (SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL).
- Correct Permit2 approval flow.
- Returns actual amount out from balance delta.
| '@layerzerolabs/lz-evm-messagelib-v2/=lib/layerzero-v2/packages/layerzero-v2/evm/messagelib/', | ||
| 'solidity-bytes-utils/=lib/solidity-bytes-utils/', | ||
| 'forge-std/=lib/forge-std/src/', | ||
| '@uniswap/v4-core/=lib/uniswap-v4-core/' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Add v4-periphery remapping for consistency.
Only the v4-core remapping is added, but the codebase also imports from v4-periphery (see src/interfaces/UniswapV4.sol line 7). To support the suggested remapped import pattern and maintain consistency, add a remapping for v4-periphery as well.
Apply this diff to add the missing remapping:
'@uniswap/v4-core/=lib/uniswap-v4-core/'
+ '@uniswap/v4-periphery/=lib/uniswap-v4-periphery/'
]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| '@uniswap/v4-core/=lib/uniswap-v4-core/' | |
| '@uniswap/v4-core/=lib/uniswap-v4-core/' | |
| '@uniswap/v4-periphery/=lib/uniswap-v4-periphery/' | |
| ] |
🤖 Prompt for AI Agents
In foundry.toml around line 21, the file only contains a remapping for
'@uniswap/v4-core/=lib/uniswap-v4-core/' while imports also reference
v4-periphery; add a parallel remapping entry for
'@uniswap/v4-periphery/=lib/uniswap-v4-periphery/' so the periphery imports
resolve correctly, placing it alongside the existing remapping and ensuring
whitespace/format matches existing remapping style.
| import { PoolId } from "../../lib/uniswap-v4-core/src/types/PoolId.sol"; | ||
| import { PoolKey } from "../../lib/uniswap-v4-core/src/types/PoolKey.sol"; | ||
|
|
||
| import { PositionInfo } from "../../lib/uniswap-v4-periphery/src/libraries/PositionInfoLibrary.sol"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Missing v4-periphery remapping in foundry.toml.
The past review comment suggests using remapped imports including @uniswap/v4-periphery/, but foundry.toml only adds the @uniswap/v4-core/ remapping. To enable the suggested refactor, add the v4-periphery remapping to foundry.toml.
Add this remapping to foundry.toml (in the remappings array):
'@uniswap/v4-periphery/=lib/uniswap-v4-periphery/'
---
🏁 Script executed:
```shell
#!/bin/bash
# Find and examine foundry.toml to check remappings
find . -name "foundry.toml" -type f -exec cat {} \;
Length of output: 1445
🏁 Script executed:
#!/bin/bash
# Search for v4-periphery imports in the codebase
rg "v4-periphery" -n -B2 -A2Length of output: 1988
🏁 Script executed:
#!/bin/bash
# Specifically check the imports in src/interfaces/UniswapV4.sol
cat src/interfaces/UniswapV4.solLength of output: 754
Add missing v4-periphery remapping to foundry.toml for import consistency.
The codebase uses v4-periphery imports in src/interfaces/UniswapV4.sol, src/libraries/UniswapV4Lib.sol, and test files, but all use relative paths (../../lib/uniswap-v4-periphery/...) while v4-core already has a remapping. Add this to the remappings array in foundry.toml:
'@uniswap/v4-periphery/=lib/uniswap-v4-periphery/'This enables converting the three files' v4-periphery imports to use the remapped path pattern for consistency.
🤖 Prompt for AI Agents
In src/interfaces/UniswapV4.sol lines 4-7, the imports use a relative path to
v4-periphery while v4-core already uses a remapping; add the remapping entry
'@uniswap/v4-periphery/=lib/uniswap-v4-periphery/' to the remappings array in
foundry.toml and then update the v4-periphery imports in this file (and the
other occurrences in src/libraries/UniswapV4Lib.sol and relevant test files) to
use the remapped import pattern @uniswap/v4-periphery/... instead of
../../lib/uniswap-v4-periphery/ to ensure consistent import resolution.
| library RateLimitHelpers { | ||
|
|
||
| function makeAddressKey(bytes32 key, address asset) internal pure returns (bytes32) { | ||
| return keccak256(abi.encode(key, asset)); | ||
| function makeAddressKey(bytes32 key, address a) internal pure returns (bytes32) { | ||
| return keccak256(abi.encode(key, a)); | ||
| } | ||
|
|
||
| function makeAddressAddressKey(bytes32 key, address asset, address destination) internal pure returns (bytes32) { | ||
| return keccak256(abi.encode(key, asset, destination)); | ||
| function makeAddressAddressKey(bytes32 key, address a, address b) internal pure returns (bytes32) { | ||
| return keccak256(abi.encode(key, a, b)); | ||
| } | ||
|
|
||
| function makeUint32Key(bytes32 key, uint32 domain) internal pure returns (bytes32) { | ||
| return keccak256(abi.encode(key, domain)); | ||
| function makeBytes32Key(bytes32 key, bytes32 a) internal pure returns (bytes32) { | ||
| return keccak256(abi.encode(key, a)); | ||
| } | ||
|
|
||
| function makeUint32Key(bytes32 key, uint32 a) internal pure returns (bytes32) { | ||
| return keccak256(abi.encode(key, a)); | ||
| } | ||
|
|
||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Consider adding library-level documentation.
While the implementations are straightforward, a brief comment block explaining that these helpers generate collision-resistant rate-limit keys via keccak256(abi.encode(...)) would improve maintainability.
Example:
/// @notice Internal helpers for building collision-resistant rate-limit keys
/// @dev Each helper encodes a base key with typed parameters using abi.encode
/// and hashes the result to produce a unique 32-byte identifier
library RateLimitHelpers {🤖 Prompt for AI Agents
In src/RateLimitHelpers.sol around lines 4 to 22, add a short library-level
NatSpec comment block above the library declaration that explains these are
internal helpers that build collision-resistant rate-limit keys by encoding
inputs with abi.encode and hashing with keccak256; include @notice and @dev tags
mentioning that each helper combines a base key with typed parameters and
returns a 32-byte identifier to improve maintainability and intent clarity.
Summary by CodeRabbit
New Features
Interfaces
Tests
Chores
✏️ Tip: You can customize this high-level summary in your review settings.