Skip to content

added test cases for ERC1271 delegated wallet signature verification#1627

Draft
0xDEnYO wants to merge 7 commits intomainfrom
ERC1271-testing
Draft

added test cases for ERC1271 delegated wallet signature verification#1627
0xDEnYO wants to merge 7 commits intomainfrom
ERC1271-testing

Conversation

@0xDEnYO
Copy link
Contributor

@0xDEnYO 0xDEnYO commented Feb 11, 2026

Which Jira task belongs to this PR?

https://lifi.atlassian.net/browse/LF-15521

Why did I implement it this way?

Checklist before requesting a review

Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)

  • I have checked that any arbitrary calls to external contracts are validated and or restricted
  • I have checked that any privileged calls (i.e. storage modifications) are validated and or restricted
  • I have ensured that any new contracts have had AT A MINIMUM 1 preliminary audit conducted on by <company/auditor>

@lifi-action-bot lifi-action-bot marked this pull request as draft February 11, 2026 03:05
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Warning

Rate limit exceeded

@0xDEnYO has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 0 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

This PR adds comprehensive support for multiple permit standards (EIP-2612, EIP-3009, Permit2) to the Permit2Proxy contract, introduces new token permit interfaces, adds fork-based ERC-1271 tests on Base and Arbitrum chains, updates script helpers, and provides extensive documentation for integration workflows.

Changes

Cohort / File(s) Summary
Permit2Proxy Core Expansion
src/Periphery/Permit2Proxy.sol
Added 8 new public functions (callDiamondWithEIP2612Signature variants, callDiamondWithEIP3009Signature variants, callDiamondWithPermit2, callDiamondWithPermit2Witness, getPermit2MsgHash) and internal helper _maxApproveAndExecuteCalldata to centralize execution logic; introduces support for EIP-2612, EIP-3009 receiveWithAuthorization, and Permit2 witness flows with refined error handling.
Token Permit Interfaces
src/Interfaces/IERC20Permit7597.sol, src/Interfaces/IERC20TransferWithAuthorization.sol
New interface definitions for ERC-7597 (opaque signature permit) and ERC-3009 (receiveWithAuthorization with dual overloads); enable support for non-standard permit signatures and contract signers.
Fork Tests — ERC-1271 Validation
test/solidity/Periphery/Permit2Proxy/BaseERC1271Fork.t.sol, test/solidity/Periphery/Permit2Proxy/CoinbaseERC1271Fork.t.sol
Comprehensive fork tests on Base and Arbitrum chains covering EIP-7702 delegation, EIP-712 domain retrieval, replay-safe hashing, Permit2 digest generation, wallet funding/approval flows, isValidSignature verification, and diamond swap integration; includes 5–6 test cases per file with error handling.
Permit2Proxy Test Suite
test/solidity/Periphery/Permit2Proxy/Permit2Proxy.t.sol
Large test contract (1327 lines) covering EIP-2612, EIP-3009, Permit2, and Coinbase-delegated flows with nonce progression, replay prevention, signer validation, and integration with bridging paths; includes mainnet forks and wallet-based signing.
Testing Utilities
test/solidity/utils/ERC1271Wallet.sol
Minimal ERC-1271 wallet implementation with optional delegation support; stores owner and validates signatures locally or delegates via staticcall.
Documentation and Configuration
docs/Permit2Proxy-WalletCalldataGuide.md, opcodes-selectors_base_delegate_contract.md, .cursor/rules/400-solidity-tests.mdc, deployments/arbitrum.staging.json
New Permit2Proxy integration guide (178 lines) with calldata encoding examples, opcode/selector documentation (102 lines), test naming rule clarification, and Permit2Proxy address update in staging deployment.
Script Helpers
script/playgroundHelpers.sh
Added decodeSelector4byte and decodeSelectors4byte bash functions to query 4byte.directory API with rate limiting; enables offline selector resolution.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • #1553: Modifies script/playgroundHelpers.sh with related helper function updates for selector decoding and API interactions.
  • #910: Modifies src/Periphery/Permit2Proxy.sol with overlapping EIP-2612 and permit-related execution paths (callDiamondWithEIP2612Signature, getPermit2MsgHash).
  • #1257: Modifies src/Periphery/Permit2Proxy.sol with changes to diamond call logic and msg.value forwarding that intersect with the new _maxApproveAndExecuteCalldata execution paths.

Suggested labels

AuditRequired, enhancement, testing

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description does not fill in the template sections. The 'Why did I implement it this way?' section is completely empty, and the checklist items are unchecked with no elaboration on the changes made. Complete the 'Why did I implement it this way?' section with implementation rationale, and check off or explain the checklist items. Provide context on the changes and testing approach.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main additions: test cases for ERC1271 delegated wallet signature verification, which is supported by the test files and documentation changes throughout the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ERC1271-testing

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (1)
  • JIRA integration encountered authorization issues. Please disconnect and reconnect the integration in the CodeRabbit UI.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@lifi-action-bot
Copy link
Collaborator

Test Coverage Report

Line Coverage: 86.77% (2979 / 3433 lines)
Function Coverage: 90.32% ( 476 / 527 functions)
Branch Coverage: 65.49% ( 484 / 739 branches)
Test coverage (86.77%) is above min threshold (83%). Check passed.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

🤖 Fix all issues with AI agents
In `@test/solidity/CoinbaseERC1271Fork.t.sol`:
- Around line 394-407: The test currently asserts exact balance equality after
calling IPermit2Proxy(PERMIT2_PROXY_ARBITRUM).callDiamondWithPermit2 by checking
IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM) == amount which is
fragile; change it to record uint256 before =
IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM) prior to
vm.prank(wallet) and the call, then assert that
IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM) == before + amount (or
assertEq(delta, amount)) so you verify the balance delta rather than exact
absolute balance.
🧹 Nitpick comments (2)
test/solidity/CoinbaseERC1271Fork.t.sol (2)

83-88: Minor: UNISWAP_ARBITRUM and USDC_ARBITRUM are not labeled.

Other constants like USDT_ARBITRUM, PERMIT2, etc. are labeled for trace readability, but UNISWAP_ARBITRUM and USDC_ARBITRUM (used in the swap test) are missing labels.

Suggested addition
         vm.label(PERMIT2, "PERMIT2");
         vm.label(PERMIT2_PROXY_ARBITRUM, "PERMIT2_PROXY_ARBITRUM");
+        vm.label(UNISWAP_ARBITRUM, "UNISWAP_ARBITRUM");
+        vm.label(USDC_ARBITRUM, "USDC_ARBITRUM");

251-264: Missing result-length guard (unlike the first test).

testFork_CoinbaseERC1271_IsValidSignature_ReturnsMagicValue (line 224) checks result.length < 4 before decoding, but this test accesses bytes4(result) directly. For consistency and clearer diagnostics on failure, consider adding the same guard.

Suggested fix
         if (!success) revert IsValidSignatureCallFailed();
+        if (result.length < 4) revert InvalidResultLength(result.length);
 
         assertEq(

Comment on lines +394 to +407
vm.prank(wallet);
IPermit2Proxy(PERMIT2_PROXY_ARBITRUM).callDiamondWithPermit2(
diamondCalldata,
permit,
signature
);

// Proxy should have received the USDT (then approved diamond); assert proxy balance or diamond call succeeded.
assertEq(
IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM),
amount,
"Permit2Proxy should hold USDT after permit transfer"
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Exact-balance assertion is fragile — should assert balance delta instead.

The proxy may already hold some USDT from real Arbitrum transactions at the forked block. assertEq(balance, amount) will fail if there is any pre-existing balance. Record the balance before the call and assert the increase.

Suggested fix
+        uint256 proxyUsdtBefore = IERC20(USDT_ARBITRUM).balanceOf(
+            PERMIT2_PROXY_ARBITRUM
+        );
+
         vm.prank(wallet);
         IPermit2Proxy(PERMIT2_PROXY_ARBITRUM).callDiamondWithPermit2(
             diamondCalldata,
             permit,
             signature
         );

-        // Proxy should have received the USDT (then approved diamond); assert proxy balance or diamond call succeeded.
-        assertEq(
-            IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM),
+        // Proxy should have received the USDT (then approved diamond).
+        assertEq(
+            IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM) -
+                proxyUsdtBefore,
             amount,
             "Permit2Proxy should hold USDT after permit transfer"
         );
🤖 Prompt for AI Agents
In `@test/solidity/CoinbaseERC1271Fork.t.sol` around lines 394 - 407, The test
currently asserts exact balance equality after calling
IPermit2Proxy(PERMIT2_PROXY_ARBITRUM).callDiamondWithPermit2 by checking
IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM) == amount which is
fragile; change it to record uint256 before =
IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM) prior to
vm.prank(wallet) and the call, then assert that
IERC20(USDT_ARBITRUM).balanceOf(PERMIT2_PROXY_ARBITRUM) == before + amount (or
assertEq(delta, amount)) so you verify the balance delta rather than exact
absolute balance.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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

🤖 Fix all issues with AI agents
In `@opcodes-selectors.md`:
- Line 46: The entry listing "`0x55299b49` | `UpgradeFailed()` (event)" is
incorrect because `UpgradeFailed()` is a custom error selector (4-byte) not an
event (32-byte topic); update the document to classify `0x55299b49` as a custom
error selector for `UpgradeFailed()` (or remove the "(event)" label), and ensure
any surrounding explanation in opcodes-selectors.md clarifies that 4-byte
selectors map to functions/errors while events use 32-byte topic hashes.
- Around line 79-82: The description for the excluded selector 0x4e487b71 is
incorrect: replace the primary label "Error(string)" with "Panic(uint256)" and
optionally add the correct selector for Error(string) (0x08c379a0) for clarity;
update the explanatory sentence around the 0x4e487b71 entry (and keep the
existing parenthetical) so it consistently identifies 0x4e487b71 as
Panic(uint256) and not Error(string).

In `@test/solidity/BaseERC1271Fork.t.sol`:
- Around line 463-467: Replace the exact-balance assertion that checks
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) == amount with a delta check:
read and store the pre-transfer balance from
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) into a variable (e.g.,
preBalance), execute the permit transfer, then assert that the new balance minus
preBalance equals the expected amount (or use assertTrue(newBalance >=
preBalance + amount) if overflow-protection needed). Update the assertion that
references PERMIT2_PROXY_BASE, amount, and IERC20/USDT_BASE accordingly so the
test verifies an increase rather than an absolute equality.
- Around line 227-257: The Base test's _fundWalletAndApprovePermit2() issues
only a single approve(PERMIT2, max) via BATCH_EXECUTE_SELECTOR which breaks for
Tether-like tokens that require approve(0) before changing non-zero allowances;
update _fundWalletAndApprovePermit2() to perform two batch execute calls from
wallet: first encode/execute approve(PERMIT2, 0) and handle its ok/returnData
exactly like the existing call, then encode/execute approve(PERMIT2,
type(uint256).max) and keep the existing success checks and final balance
assertion; mirror the CoinbaseERC1271Fork.t.sol pattern/comments to document
"USDT requires approve(0) before approve(max)".
🧹 Nitpick comments (4)
script/playgroundHelpers.sh (1)

2076-2078: Minor: -sS flags are negated by 2>/dev/null.

curl -sS means "silent but show errors on stderr", but 2>/dev/null discards stderr, making -S a no-op. If error visibility on curl failures is intentional (per -S), remove 2>/dev/null; otherwise simplify to curl -s -L.

Optional simplification
-  RESPONSE=$(curl -sS -L "${FOURBYTE_URL}/?hex_signature=${SELECTOR}" 2>/dev/null || echo '{"count":0}')
+  RESPONSE=$(curl -s -L "${FOURBYTE_URL}/?hex_signature=${SELECTOR}" 2>/dev/null || echo '{"count":0}')
.cursor/commands/analyze-unverified-contract.md (1)

72-83: Section labels (D, B, C, A) are confusing — consider sequential numbering.

The text says "Use this order for speed" but labels them D → B → C → A instead of 1 → 2 → 3 → 4. This makes it hard to follow the intended priority. Renumber to match the described execution order.

test/solidity/CoinbaseERC1271Fork.t.sol (1)

50-67: Significant code duplication with BaseERC1271Fork.t.sol — consider a shared base contract.

Both test files duplicate: EIP-712 type hashes, _permit2Digest, _encodeSignature, _signPermit2Permit, _signPermit2PermitForSpender, _replaySafeHashFromWallet, _buildSwapTokensGenericCalldata, and all four interfaces (IERC20, IPermit2ProxyView, IPermit2Proxy, IDiamondLoupe).

Extract shared logic into a common base (e.g. test/solidity/utils/BaseERC1271Test.sol) that both CoinbaseERC1271Fork and BaseERC1271Fork inherit from. Chain-specific constants and delegation setup would remain in the concrete contracts.

Also applies to: 148-179, 353-401, 571-595

test/solidity/BaseERC1271Fork.t.sol (1)

120-156: _fetchDomainFromDelegator silently falls through if chainId from the decoded domain doesn't match the fork's chain — consider a sanity check.

If eip712Domain() returns a stale or incorrect chainId (e.g., the implementation was deployed on a different chain and returns a hardcoded value), fetchedDomainSeparator will be built with the wrong chain ID, and all subsequent ERC-1271 signature verifications will silently fail. Adding a require(chainId == block.chainid) or a warning log would make debugging easier if the fork block or delegation target changes.

Comment on lines +79 to +82
## Excluded

- **`0xffffffff`** – Used as sentinel/mask in the bytecode (not a function selector).
- **`0x4e487b71`** – `Error(string)`; used for revert encoding, not dispatch. (4byte also lists as `Panic(uint256)`.)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

0x4e487b71 is Panic(uint256), not Error(string).

The excluded-selector note says this is Error(string), but Error(string) is 0x08c379a0. The selector 0x4e487b71 is actually Panic(uint256) (as your own parenthetical note acknowledges). Fix the primary description to avoid confusion.

Suggested fix
-- **`0x4e487b71`** – `Error(string)`; used for revert encoding, not dispatch. (4byte also lists as `Panic(uint256)`.)
+- **`0x4e487b71`** – `Panic(uint256)`; used for compiler-generated panic reverts (e.g. arithmetic overflow), not function dispatch.
📝 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.

Suggested change
## Excluded
- **`0xffffffff`** – Used as sentinel/mask in the bytecode (not a function selector).
- **`0x4e487b71`**`Error(string)`; used for revert encoding, not dispatch. (4byte also lists as `Panic(uint256)`.)
## Excluded
- **`0xffffffff`** – Used as sentinel/mask in the bytecode (not a function selector).
- **`0x4e487b71`**`Panic(uint256)`; used for compiler-generated panic reverts (e.g. arithmetic overflow), not function dispatch.
🤖 Prompt for AI Agents
In `@opcodes-selectors.md` around lines 79 - 82, The description for the excluded
selector 0x4e487b71 is incorrect: replace the primary label "Error(string)" with
"Panic(uint256)" and optionally add the correct selector for Error(string)
(0x08c379a0) for clarity; update the explanatory sentence around the 0x4e487b71
entry (and keep the existing parenthetical) so it consistently identifies
0x4e487b71 as Panic(uint256) and not Error(string).

Comment on lines +227 to +257
function _fundWalletAndApprovePermit2() internal {
uint256 amount = 100 * 1e6; // 100 USDT (6 decimals); use 1000 if whale has sufficient balance
vm.prank(USDT_WHALE_BASE);
if (!IERC20(USDT_BASE).transfer(wallet, amount))
revert WhaleTransferFailed();

bytes memory approveCalldata = abi.encodeWithSelector(
bytes4(keccak256("approve(address,uint256)")),
PERMIT2,
type(uint256).max
);
BatchExecuteCall[] memory calls = new BatchExecuteCall[](1);
calls[0] = BatchExecuteCall({
target: USDT_BASE,
value: uint256(0),
data: approveCalldata
});

vm.prank(wallet);
(bool ok, bytes memory returnData) = wallet.call(
abi.encodeWithSelector(BATCH_EXECUTE_SELECTOR, calls)
);
if (!ok) {
if (returnData.length > 0) LibUtil.revertWith(returnData);
revert ApproveViaExecuteFailed();
}

uint256 walletBalance = IERC20(USDT_BASE).balanceOf(wallet);
if (walletBalance != amount)
revert WalletShouldHaveUSDT(amount, walletBalance);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for Base USDT address references
rg "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2|USDT_BASE" test/solidity/ --type sol -B2 -A2

Repository: lifinance/contracts

Length of output: 90


🏁 Script executed:

# Search for approve patterns around USDT in test files, specifically looking for approve(0) patterns
rg "approve.*\(.*0.*\).*approve|approve.*0.*then|approve.*0.*before" test/solidity/ --type sol -B2 -A2

Repository: lifinance/contracts

Length of output: 90


🏁 Script executed:

# Look at the BaseERC1271Fork.t.sol file to see if there are other USDT handling patterns and the Arbitrum reference
cat -n test/solidity/BaseERC1271Fork.t.sol | head -100

Repository: lifinance/contracts

Length of output: 5684


🏁 Script executed:

# Search for Arbitrum USDT handling to compare with Base
rg "USDT.*Arbitrum|arbitrum.*USDT|0x.*USDT" test/solidity/ --type sol -B3 -A3 | head -60

Repository: lifinance/contracts

Length of output: 90


🏁 Script executed:

# Check if there's a reference to how the Arbitrum version handles approve(0)
rg -n "_fundWalletAndApprovePermit2|approve.*0.*max|Arbitrum" test/solidity/BaseERC1271Fork.t.sol -B3 -A3

Repository: lifinance/contracts

Length of output: 2010


🏁 Script executed:

fd "CoinbaseERC1271Fork" test/

Repository: lifinance/contracts

Length of output: 102


🏁 Script executed:

# Search for approve patterns in test files (without -type since it was failing)
grep -r "approve.*0.*approve\|approve.*0.*type" test/ --include="*.sol" -n

Repository: lifinance/contracts

Length of output: 837


🏁 Script executed:

# Look for USDT approvals in Base-related tests
grep -r "0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2\|USDT_BASE" test/ --include="*.sol" -B5 -A5

Repository: lifinance/contracts

Length of output: 14496


🏁 Script executed:

# Find CoinbaseERC1271Fork and examine its approve pattern
find test/ -name "*CoinbaseERC1271*" -o -name "*Coinbase*Fork*"

Repository: lifinance/contracts

Length of output: 102


🏁 Script executed:

# Search for approve(0) patterns specifically in USDT contexts
grep -r "approve.*0\)" test/ --include="*.sol" -B3 -A1 | grep -A3 -B3 "USDT\|0xfde4"

Repository: lifinance/contracts

Length of output: 86


🏁 Script executed:

# Get the _fundWalletAndApprovePermit2 function from CoinbaseERC1271Fork.t.sol
sed -n '184,220p' test/solidity/CoinbaseERC1271Fork.t.sol

Repository: lifinance/contracts

Length of output: 1473


🏁 Script executed:

# Look for approve(0) pattern in the Arbitrum version
grep -n "approve.*0\|approve.*max" test/solidity/CoinbaseERC1271Fork.t.sol -B2 -A2

Repository: lifinance/contracts

Length of output: 444


🏁 Script executed:

# Check if the tests in BaseERC1271Fork actually run successfully by looking for test execution patterns
cat -n test/solidity/BaseERC1271Fork.t.sol | tail -100

Repository: lifinance/contracts

Length of output: 3824


🏁 Script executed:

# Search for any comments or documentation about Base USDT vs Arbitrum USDT differences
grep -r "Base.*USDT\|USDT.*Base\|0xfde4" . --include="*.sol" --include="*.md" --include="*.ts" -B2 -A2

Repository: lifinance/contracts

Length of output: 6464


🏁 Script executed:

# Check the full context around the _fundWalletAndApprovePermit2 in both files side-by-side to understand the difference
echo "=== ARBITRUM VERSION ===" && sed -n '184,250p' test/solidity/CoinbaseERC1271Fork.t.sol && echo -e "\n=== BASE VERSION ===" && sed -n '227,257p' test/solidity/BaseERC1271Fork.t.sol

Repository: lifinance/contracts

Length of output: 3719


Add approve(0) before approve(max) in _fundWalletAndApprovePermit2() to match the Arbitrum pattern.

Base USDT (0xfde4...) is Tether's token, which has the same non-standard approve() behavior as Arbitrum USDT. The parallel CoinbaseERC1271Fork.t.sol correctly implements two sequential calls—reset to 0, then set to max—with an explicit comment: "USDT requires approve(0) before approve(max)". The Base version skips the zero reset and only issues one batch execute call. This will fail if the wallet ever has a non-zero allowance to Permit2. Update _fundWalletAndApprovePermit2() to issue two batch execute calls: one for approve(PERMIT2, 0), then one for approve(PERMIT2, type(uint256).max).

🤖 Prompt for AI Agents
In `@test/solidity/BaseERC1271Fork.t.sol` around lines 227 - 257, The Base test's
_fundWalletAndApprovePermit2() issues only a single approve(PERMIT2, max) via
BATCH_EXECUTE_SELECTOR which breaks for Tether-like tokens that require
approve(0) before changing non-zero allowances; update
_fundWalletAndApprovePermit2() to perform two batch execute calls from wallet:
first encode/execute approve(PERMIT2, 0) and handle its ok/returnData exactly
like the existing call, then encode/execute approve(PERMIT2, type(uint256).max)
and keep the existing success checks and final balance assertion; mirror the
CoinbaseERC1271Fork.t.sol pattern/comments to document "USDT requires approve(0)
before approve(max)".

Comment on lines +463 to +467
assertEq(
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE),
amount,
"Permit2Proxy should hold USDT after permit transfer"
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same fragile exact-balance assertion as the Coinbase test — use a delta check.

Same issue as CoinbaseERC1271Fork.t.sol line 446: the proxy may already hold USDT at the forked block. Record balance before and assert the increase.

Suggested fix
+        uint256 proxyUsdtBefore = IERC20(USDT_BASE).balanceOf(
+            PERMIT2_PROXY_BASE
+        );
+
         vm.prank(wallet);
         IPermit2Proxy(PERMIT2_PROXY_BASE).callDiamondWithPermit2(
             diamondCalldata,
             permit,
             signature
         );

-        assertEq(
-            IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE),
+        assertEq(
+            IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) -
+                proxyUsdtBefore,
             amount,
             "Permit2Proxy should hold USDT after permit transfer"
         );
🤖 Prompt for AI Agents
In `@test/solidity/BaseERC1271Fork.t.sol` around lines 463 - 467, Replace the
exact-balance assertion that checks
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) == amount with a delta check:
read and store the pre-transfer balance from
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) into a variable (e.g.,
preBalance), execute the permit transfer, then assert that the new balance minus
preBalance equals the expected amount (or use assertTrue(newBalance >=
preBalance + amount) if overflow-protection needed). Update the assertion that
references PERMIT2_PROXY_BASE, amount, and IERC20/USDT_BASE accordingly so the
test verifies an increase rather than an absolute equality.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In @.cursor/rules/400-solidity-tests.mdc:
- Around line 17-19: Update the naming rule to accept the repository’s existing
fork and failure prefixes: add `testFork_` and `testFail_` to the allowed
prefixes list and document an exception that `testFork_` tests may contain
additional internal underscores (e.g., allow names like
`testFork_BaseERC1271_IsValidSignature_ReturnsMagicValue`), and update the
examples in the rule text to reflect these additions so the rule no longer
conflicts with the tests in this PR.

In `@src/Interfaces/IERC20TransferWithAuthorization.sol`:
- Around line 9-10: The NatSpec `@dev` comment in
IERC20TransferWithAuthorization.sol is broken by a regular '//' on the
continuation line; change the comment prefix for the continuation from '//' to
'///' so the entire explanation about transferWithAuthorization and
receiveWithAuthorization is included in generated docs (look for the comment
referencing transferWithAuthorization and receiveWithAuthorization and replace
the second-line '//' with '///').

In `@test/solidity/BaseERC1271Fork.t.sol`:
- Line 90: The test file disables all fork tests by setting the internal
constant _FORK_TESTS_DISABLED = true which causes each test to return
immediately; replace this pattern by either removing the constant and enabling
tests, switch the flag to false, or (preferably) change the per-test
early-return pattern to use Foundry's skipping mechanism (e.g., call
vm.skip(true) or use a Foundry profile/label) and add a short comment
documenting the reason for skipping so fork tests run or are explicitly skipped
instead of silently being disabled by _FORK_TESTS_DISABLED in
BaseERC1271Fork.t.sol.

In `@test/solidity/Periphery/Permit2Proxy/BaseERC1271Fork.t.sol`:
- Line 5: The import path uses a direct lib/ path instead of the remapped alias;
update the import of ISignatureTransfer so it uses the remapping
`permit2/interfaces/ISignatureTransfer.sol` (replace the existing `import {
ISignatureTransfer } from "lib/Permit2/src/interfaces/ISignatureTransfer.sol";`
with the remapped path) to match the project's remappings and other tests.
- Around line 89-93: The constant _FORK_TESTS_DISABLED is currently true which
causes setUp() to return early and disables all testFork_* tests; change
_FORK_TESTS_DISABLED to false (or replace it with a dynamic check using an
environment flag via vm.envOr or similar) so setUp() creates the fork state and
allows testFork_* functions to run, ensuring you update the constant or the
conditional in setUp() accordingly (refer to _FORK_TESTS_DISABLED and setUp()).
- Around line 481-485: Instead of asserting the absolute balance equals amount,
capture the pre-call balance and assert the delta equals amount: read
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) into a beforeBalance variable
before the permit transfer, perform the transfer, then read afterBalance and
replace assertEq(IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE), amount, ...)
with assertEq(afterBalance - beforeBalance, amount, ...) so the test passes even
if PERMIT2_PROXY_BASE had a pre-existing USDT balance.

In `@test/solidity/Periphery/Permit2Proxy/CoinbaseERC1271Fork.t.sol`:
- Line 5: Change the import to use the permit2 remapping instead of the
hardcoded lib path: update the import that references ISignatureTransfer (the
import line importing ISignatureTransfer) to use "permit2/..." (match the same
remapping used in BaseERC1271Fork.t.sol) so the import reads from the remapped
permit2 package rather than "lib/Permit2/...".
🧹 Nitpick comments (10)
src/Interfaces/IERC20Permit7597.sol (1)

4-7: Consider adding a reference link to ERC-7597.

Per coding guidelines, when copying from or shadowing an external protocol, add a short NatSpec note with a link referencing the original. A link to the ERC-7597 specification would help future readers.

test/solidity/utils/ERC1271Wallet.sol (1)

14-14: owner can be immutable.

owner is set once in the constructor and never modified. Making it immutable would be consistent with DELEGATE_TO and slightly cheaper on reads.

Proposed fix
-    address public owner;
+    address public immutable owner;
test/solidity/BaseERC1271Fork.t.sol (1)

606-609: Inline IERC20 interface is duplicated across test files.

This same minimal IERC20 interface (and IPermit2ProxyView) is also defined in test/solidity/Periphery/Permit2Proxy.t.sol. Consider extracting a shared test utility interface (e.g., in test/solidity/utils/Interfaces.sol) and importing it from both files to reduce duplication.

As per coding guidelines: "Flag duplicate code that should use existing helpers/libraries."

test/solidity/Periphery/Permit2Proxy.t.sol (1)

1134-1180: Double-use revert test: consider adding assertBalanceChange or an explicit assertion after the first call.

The first callDiamondWithEIP3009Signature call (line 1154) is expected to succeed but has no assertion verifying it did. If it silently fails for an unexpected reason, the second call's vm.expectRevert might match an unrelated revert, making the test pass vacuously. A brief assertion (e.g., verifying the nonce was consumed or the balance changed) after the first call would strengthen this test.

src/Periphery/Permit2Proxy.sol (2)

78-127: Parameter naming: public function parameters lack underscore prefix.

The existing callDiamondWithEIP2612Signature(v,r,s) and all new public functions (callDiamondWithEIP3009Signature, etc.) use parameters like tokenAddress, amount, deadline instead of _tokenAddress, _amount, _deadline. This deviates from the codebase convention (e.g. callDiamondWithPermit2 at Line 273 uses _diamondCalldata, _permit, _signature).

Since the existing v,r,s overload already shipped without underscores this is likely intentional for consistency within the EIP-2612/3009 family, but it creates an inconsistency with the Permit2 functions in the same contract.


13-17: Consider adding bug-bounty custody documentation.

Per coding guidelines, contracts in src/ should explicitly document whether they hold/custody funds. This proxy does transiently hold tokens (between transferFrom/receiveWithAuthorization and the diamond call). A brief NatSpec note like "This contract transiently holds tokens during permit+bridge flows; funds should not persist beyond a single transaction" would clarify intent for auditors and bug bounty hunters.

test/solidity/Periphery/Permit2Proxy/Permit2Proxy.t.sol (2)

360-413: Mid-test fork switch replaces all setUp() state — fragile test isolation.

test_CanExecuteCalldataUsingEip2612CoinbaseWalletUsdc calls vm.createSelectFork at Line 365, which switches from the mainnet fork (set in setUp) to an Arbitrum fork. After this, permit2Proxy, uniPermit2, and all state from setUp() are stale. The test correctly creates a new local proxyWithBytes, but if someone later adds assertions referencing the original permit2Proxy, they'd get unexpected results.

Consider adding a brief comment at the fork-switch explaining that setUp state is intentionally replaced, or extracting this into a separate test contract (similar to the CoinbaseERC1271Fork pattern).


1029-1032: Constant defined between helper functions — minor organizational nit.

EIP3009_RECEIVE_WITH_AUTHORIZATION_TYPEHASH at Line 1029 is wedged between the _setCoinbaseDelegationAndInitialize helper and the _generateEIP3009ReceiveWithAuthorizationDigest helper. Consider moving it up to the constants section (near Line 22-46) for consistency.

test/solidity/Periphery/Permit2Proxy/BaseERC1271Fork.t.sol (1)

606-627: Interfaces duplicated across fork test files.

IERC20, IPermit2ProxyView, IPermit2Proxy, and IDiamondLoupe are defined identically in both BaseERC1271Fork.t.sol and CoinbaseERC1271Fork.t.sol. Consider extracting them into a shared test helper (e.g., test/solidity/utils/ForkTestInterfaces.sol) to reduce duplication. That said, per coding guidelines, test interfaces may be defined inline, so this is optional.

test/solidity/Periphery/Permit2Proxy/CoinbaseERC1271Fork.t.sol (1)

76-98: setUp() always creates a fork — tests will fail or be slow without ETH_NODE_URI_ARBITRUM.

Unlike BaseERC1271Fork which has _FORK_TESTS_DISABLED, this test always forks Arbitrum at block 410M. If the RPC is unavailable or slow, all tests in this contract will fail or timeout. Consider adding a similar guard or using vm.envOr to conditionally skip, especially since this is a draft PR.

Comment on lines +17 to +19
- **Exactly one underscore** in the function name: between the prefix and the test name (e.g. `test_CanExecuteCalldata`, `testRevert_WhenCalledWithInvalidCalldata`). No extra underscores inside the test name.
- **Prefixes:** `test_` (success), `testRevert_` (failure), `testBase_` (base).
- **Test name (after the underscore):** CamelCase, no internal underscores (e.g. `CanExecuteCalldataUsingEip2612SignatureUsdc`, not `can_execute_calldata_using_eip2612_signature_usdc`).
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

New naming rule contradicts test names in this PR.

The rule mandates exactly one underscore and only three prefixes (test_, testRevert_, testBase_), but the fork tests added in this same PR use testFork_ as a prefix with multiple internal underscores (e.g., testFork_BaseERC1271_IsValidSignature_ReturnsMagicValue). Additionally, the repo historically uses testFail_ per its style guide.

Consider either:

  1. Adding testFork_ (and testFail_) to the allowed prefixes, or
  2. Renaming the fork tests to comply (e.g., test_ForkBaseERC1271IsValidSignatureReturnsMagicValue).
🤖 Prompt for AI Agents
In @.cursor/rules/400-solidity-tests.mdc around lines 17 - 19, Update the naming
rule to accept the repository’s existing fork and failure prefixes: add
`testFork_` and `testFail_` to the allowed prefixes list and document an
exception that `testFork_` tests may contain additional internal underscores
(e.g., allow names like
`testFork_BaseERC1271_IsValidSignature_ReturnsMagicValue`), and update the
examples in the rule text to reflect these additions so the rule no longer
conflicts with the tests in this PR.

Comment on lines +9 to +10
/// @dev transferWithAuthorization is not used by LI.FI since anyone may call this on the token,
// so it can be front-run. We only call receiveWithAuthorization.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

NatSpec continuation uses // instead of ///.

Line 10 switches from /// (NatSpec) to // (regular comment), so the rest of the @dev explanation won't appear in generated docs. Use /// for the continuation.

Proposed fix
     /// `@dev` transferWithAuthorization is not used by LI.FI since anyone may call this on the token,
-    //       so it can be front-run. We only call receiveWithAuthorization.
+    ///      so it can be front-run. We only call receiveWithAuthorization.
🤖 Prompt for AI Agents
In `@src/Interfaces/IERC20TransferWithAuthorization.sol` around lines 9 - 10, The
NatSpec `@dev` comment in IERC20TransferWithAuthorization.sol is broken by a
regular '//' on the continuation line; change the comment prefix for the
continuation from '//' to '///' so the entire explanation about
transferWithAuthorization and receiveWithAuthorization is included in generated
docs (look for the comment referencing transferWithAuthorization and
receiveWithAuthorization and replace the second-line '//' with '///').

bytes32 internal fetchedMessageTypeHash;

/// @dev Set true to deactivate fork tests (e.g. for coverage); set false to re-enable. No vm.skip (older forge-std).
bool internal constant _FORK_TESTS_DISABLED = true;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

All fork tests are disabled — no test coverage is provided by this file.

_FORK_TESTS_DISABLED = true causes every test to return immediately in its first line. While this may be intentional for CI/coverage runs, it means the PR doesn't actually execute any of these tests. Consider using a more standard approach like vm.skip(true) or a Foundry profile/label to selectively run fork tests, and document the intention clearly.

🤖 Prompt for AI Agents
In `@test/solidity/BaseERC1271Fork.t.sol` at line 90, The test file disables all
fork tests by setting the internal constant _FORK_TESTS_DISABLED = true which
causes each test to return immediately; replace this pattern by either removing
the constant and enabling tests, switch the flag to false, or (preferably)
change the per-test early-return pattern to use Foundry's skipping mechanism
(e.g., call vm.skip(true) or use a Foundry profile/label) and add a short
comment documenting the reason for skipping so fork tests run or are explicitly
skipped instead of silently being disabled by _FORK_TESTS_DISABLED in
BaseERC1271Fork.t.sol.

pragma solidity ^0.8.17;

import { Test } from "forge-std/Test.sol";
import { ISignatureTransfer } from "lib/Permit2/src/interfaces/ISignatureTransfer.sol";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Import path should use the permit2/ remapping instead of a direct lib/ path.

Line 5 uses lib/Permit2/src/interfaces/ISignatureTransfer.sol while the rest of the codebase (and the other test files in this PR) use the permit2/interfaces/ISignatureTransfer.sol remapping. As per coding guidelines, use remappings defined in remappings.txt.

Suggested fix
-import { ISignatureTransfer } from "lib/Permit2/src/interfaces/ISignatureTransfer.sol";
+import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol";
📝 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.

Suggested change
import { ISignatureTransfer } from "lib/Permit2/src/interfaces/ISignatureTransfer.sol";
import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol";
🤖 Prompt for AI Agents
In `@test/solidity/Periphery/Permit2Proxy/BaseERC1271Fork.t.sol` at line 5, The
import path uses a direct lib/ path instead of the remapped alias; update the
import of ISignatureTransfer so it uses the remapping
`permit2/interfaces/ISignatureTransfer.sol` (replace the existing `import {
ISignatureTransfer } from "lib/Permit2/src/interfaces/ISignatureTransfer.sol";`
with the remapped path) to match the project's remappings and other tests.

Comment on lines +89 to +93
/// @dev Set true to deactivate fork tests (e.g. for coverage); set false to re-enable. No vm.skip (older forge-std).
bool internal constant _FORK_TESTS_DISABLED = true;

function setUp() public {
if (_FORK_TESTS_DISABLED) return;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

All fork tests are disabled — ensure this is intentional for the draft PR.

_FORK_TESTS_DISABLED = true means none of the testFork_* functions in this file will execute. Combined with the early return in setUp(), no fork state is created. This is fine for a draft, but should be toggled to false (or replaced with a more dynamic mechanism like vm.envOr) before merging.

🤖 Prompt for AI Agents
In `@test/solidity/Periphery/Permit2Proxy/BaseERC1271Fork.t.sol` around lines 89 -
93, The constant _FORK_TESTS_DISABLED is currently true which causes setUp() to
return early and disables all testFork_* tests; change _FORK_TESTS_DISABLED to
false (or replace it with a dynamic check using an environment flag via vm.envOr
or similar) so setUp() creates the fork state and allows testFork_* functions to
run, ensuring you update the constant or the conditional in setUp() accordingly
(refer to _FORK_TESTS_DISABLED and setUp()).

Comment on lines +481 to +485
assertEq(
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE),
amount,
"Permit2Proxy should hold USDT after permit transfer"
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Exact-balance assertion is fragile — assert the balance delta instead.

Same issue as flagged in the Coinbase fork test: the deployed PERMIT2_PROXY_BASE may already hold USDT at the forked block. assertEq(balance, amount) will fail if there's any pre-existing balance. Record the balance before the call and assert the increase.

Suggested fix
+        uint256 proxyUsdtBefore = IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE);
+
         vm.prank(wallet);
         IPermit2Proxy(PERMIT2_PROXY_BASE).callDiamondWithPermit2(
             diamondCalldata,
             permit,
             signature
         );

         assertEq(
-            IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE),
+            IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) - proxyUsdtBefore,
             amount,
             "Permit2Proxy should hold USDT after permit transfer"
         );
🤖 Prompt for AI Agents
In `@test/solidity/Periphery/Permit2Proxy/BaseERC1271Fork.t.sol` around lines 481
- 485, Instead of asserting the absolute balance equals amount, capture the
pre-call balance and assert the delta equals amount: read
IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE) into a beforeBalance variable
before the permit transfer, perform the transfer, then read afterBalance and
replace assertEq(IERC20(USDT_BASE).balanceOf(PERMIT2_PROXY_BASE), amount, ...)
with assertEq(afterBalance - beforeBalance, amount, ...) so the test passes even
if PERMIT2_PROXY_BASE had a pre-existing USDT balance.

pragma solidity ^0.8.17;

import { Test } from "forge-std/Test.sol";
import { ISignatureTransfer } from "lib/Permit2/src/interfaces/ISignatureTransfer.sol";
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Import path should use the permit2/ remapping.

Same as BaseERC1271Fork.t.sol — use the remapping for consistency.

Suggested fix
-import { ISignatureTransfer } from "lib/Permit2/src/interfaces/ISignatureTransfer.sol";
+import { ISignatureTransfer } from "permit2/interfaces/ISignatureTransfer.sol";
🤖 Prompt for AI Agents
In `@test/solidity/Periphery/Permit2Proxy/CoinbaseERC1271Fork.t.sol` at line 5,
Change the import to use the permit2 remapping instead of the hardcoded lib
path: update the import that references ISignatureTransfer (the import line
importing ISignatureTransfer) to use "permit2/..." (match the same remapping
used in BaseERC1271Fork.t.sol) so the import reads from the remapped permit2
package rather than "lib/Permit2/...".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This file will be helpful for testing once we have a verified delegator contract or more info about how it works

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants