Repository navigation
feat: migrate GenericSwapFacetV3 to granular allowlist + LibAsset routing [GenericSwapFacetV3 v2.0.0] - #1715
Conversation
…sset for ERC20 transfers; version bumps for both contracts
… function names for clarity
…larity and consistency
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughMigrates ERC20/ETH transfer and approval flows to centralized LibAsset helpers across facets and periphery, unifies allow-list checks and approval behavior in GenericSwapFacetV3, consolidates withdraw logic in WithdrawablePeriphery, updates receiver/wrapper contracts to use LibAsset, and adjusts tests to new revert selectors and behaviors. ChangesLibAsset Centralization & Approval Refactor
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Test Coverage ReportLine Coverage: 89.97% (3240 / 3601 lines) |
🤖 GitHub Action: Security Alerts Review 🔍🟢 Dismissed Security Alerts with Comments 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: 🟢 View Alert - File: ✅ No unresolved security alerts! 🎉 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
test/solidity/Libraries/LibAsset.t.sol (1)
197-223: Align the last two test names with the rule you just tightened.
test_TronUsdtIsPresentAtCanonicalAddressAfterLocalStubandtest_StablecoinTransferSucceedsTronChainIdstill lean on implementation detail / vague nouns more than the surrounding renamed tests. Renaming them to behavior-first names would avoid leaving mixed styles in the same file.As per coding guidelines, "Avoid stuffing names with test mechanics ... unless two tests would otherwise be indistinguishable" and "When editing any
test/**/*.t.solfile, align existing test names in that file to this convention if they are still short or ambiguous."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/solidity/Libraries/LibAsset.t.sol` around lines 197 - 223, Two tests use implementation-detail names; rename function test_TronUsdtIsPresentAtCanonicalAddressAfterLocalStub and test_StablecoinTransferSucceedsTronChainId to behavior-first, descriptive names. Update the declarations and any references so they describe observable behavior (e.g., presence/availability and successful transfer on Tron chain) rather than stubbing/chain-id mechanics; modify function names for the functions test_TronUsdtIsPresentAtCanonicalAddressAfterLocalStub and test_StablecoinTransferSucceedsTronChainId accordingly and keep assertions and body unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/Helpers/WithdrawablePeriphery.sol`:
- Around line 2-3: Remove the stray `@custom:version` tag placed above the
pragma and instead add it into the contract's NatSpec block for
WithdrawablePeriphery (e.g., alongside `@title`) so the contract header includes
`@custom:version 1.0.1`; also ensure the file retains the SPDX comment followed
immediately by pragma with no blank line between them.
---
Nitpick comments:
In `@test/solidity/Libraries/LibAsset.t.sol`:
- Around line 197-223: Two tests use implementation-detail names; rename
function test_TronUsdtIsPresentAtCanonicalAddressAfterLocalStub and
test_StablecoinTransferSucceedsTronChainId to behavior-first, descriptive names.
Update the declarations and any references so they describe observable behavior
(e.g., presence/availability and successful transfer on Tron chain) rather than
stubbing/chain-id mechanics; modify function names for the functions
test_TronUsdtIsPresentAtCanonicalAddressAfterLocalStub and
test_StablecoinTransferSucceedsTronChainId accordingly and keep assertions and
body unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 8796d4cc-f146-42cc-9d5f-95e93dbd54e6
📒 Files selected for processing (7)
.cursor/rules/400-solidity-tests.mdcsrc/Facets/GenericSwapFacetV3.solsrc/Helpers/WithdrawablePeriphery.solsrc/Libraries/LibAsset.solsrc/Periphery/LiFiDEXAggregator.soltest/solidity/Libraries/LibAsset.t.soltest/solidity/utils/MockTronUSDT.sol
… to handle TRON USDT
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/Facets/GenericSwapFacetV3.sol (1)
9-9: Minor: Import ordering convention.Per coding guidelines, external library imports should be grouped before local imports. The OpenZeppelin
IERC20import could be moved above the localILiFiimport. This is a minor stylistic note and doesn't affect functionality.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/Facets/GenericSwapFacetV3.sol` at line 9, The import ordering in GenericSwapFacetV3.sol should place external library imports before local project imports; move the OpenZeppelin IERC20 import so it appears above the local ILiFi import (i.e., ensure import { IERC20 } from "@openzeppelin/..."; is listed before import { ILiFi } ...), keeping the same symbols and no other changes to code or semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/Facets/GenericSwapFacetV3.sol`:
- Line 9: The import ordering in GenericSwapFacetV3.sol should place external
library imports before local project imports; move the OpenZeppelin IERC20
import so it appears above the local ILiFi import (i.e., ensure import { IERC20
} from "@openzeppelin/..."; is listed before import { ILiFi } ...), keeping the
same symbols and no other changes to code or semantics.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: dfc5053d-a236-4515-9b45-f6a284908aa5
📒 Files selected for processing (8)
.cursor/rules/100-solidity-basics.mdcsrc/Facets/GenericSwapFacetV3.solsrc/Helpers/WithdrawablePeriphery.soltest/solidity/Helpers/WithdrawablePeriphery.t.soltest/solidity/Periphery/Across/V3/ReceiverAcrossV3.t.soltest/solidity/Periphery/Across/V4/ReceiverAcrossV4.t.soltest/solidity/Periphery/ReceiverOIF.t.soltest/solidity/Periphery/ReceiverStargateV2.t.sol
🚧 Files skipped from review as they are similar to previous changes (1)
- src/Helpers/WithdrawablePeriphery.sol
…ude rules for handling errors in GenericErrors.sol
…ttern" This reverts commit 6cc230d.
Brings in the revert of the 3-job refactor (commit 7f238c9) so this branch's workflow file matches what PR #1822 actually ships — the single-job version that drops the `paths:` filter and relies on the in-job tj-actions/changed-files detection (Step 2) + gated lint (Step 4) to keep the linter from running on MD-less PRs. Temporary merge for verification; will be reverted on this branch once verified. The fix lands on `main` via PR #1822.
Removes the workflow changes that were merged into this branch temporarily to verify the fix in PR #1822. The fix lives only on the fix/markdownlint-workflow-required-check branch / PR #1822 now; this branch is back to a pure GenericSwapFacetV3 migration with no CI tooling changes. Net diff against main for `.github/workflows/markdownlint.yml` is now zero. The intermediate merge / revert / refactor commits remain in history but cancel out. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Revert unintended whitespace drift (4 blank lines added around fenced code blocks) introduced by lint-staged's markdownlint --fix during the merge from main. The pr-ready skill file is unrelated to this PR's scope (GenericSwapFacetV3 migration). --no-verify is used because lint-staged's MD031 auto-fix re-adds the exact blank lines being reverted; main's pr-ready.md is itself non-conformant to MD031 (CI's markdownlint workflow scopes to changed files, so the drift goes uncorrected on main). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves the conflict with upstream PR lifinance#1715 after it was trimmed and merged. Keeps the fork-specific scope agreed in the SC thread: - LibAsset @ 2.1.3-tron with Tron USDT chain+address bypass - WithdrawablePeriphery @ 1.0.0-tron with LibAsset.transferAsset routing and ZeroAmount check Resolutions: - src/Facets/GenericSwapFacetV3.sol: take upstream v2.0.0 (granular allowlist + LibAsset routing landed in main via PR lifinance#1715); drop the pre-trim 1.0.2-tron variant. - src/Helpers/WithdrawablePeriphery.sol: keep fork's 1.0.0-tron port; drop main's TODO(EXSC-241) note (the deferred work is done here). - src/Periphery/LiFiDEXAggregator.sol: revert to main v1.12.0; LDA's LibAsset migration is out of scope (being deprecated, not on Tron). Inherited WithdrawablePeriphery bump still cascades a recompile only, no source divergence. Receiver*.t.sol updates (ExternalCallFailed -> ETHTransferFailed) are retained: they reflect the new revert path through LibAsset.transferAsset inherited from WithdrawablePeriphery 1.0.0-tron.
Skill was carrying ~30% redundancy that cost tokens on every invocation without changing behavior. Each loaded line is read on every /request-audit run, so duplicate or stale guidance is pure overhead. Removed: - **Quality Checklist (~21 lines)** — every bullet restated a rule already given in Steps 1–6. The model just read those rules ~200 lines earlier; repeating them as a checklist doesn't improve compliance, it doubles the token cost of the same guidance. - **Historical note about legacy MCP path (~3 lines)** — told the model to ignore an error string (`mcp_externally_shared_channel_restricted`) it would never see, since the MCP path was removed in PR #1765. Pure noise with no actionable content. - **Standalone "Webhook posting note" callout between Step 3a and 3b (~3 lines)** — duplicated guidance already encoded in Step 6 step 1 ("drop the 🧵 suffix … incoming webhooks can't thread"). Removing the standalone callout improves locality: the rule now lives only where it is applied. - **"File output exception" callout in Step 3 (~5 lines)** — Step 6b already states the same rule ("plain text — Slack does not render backtick inline code from pasted content"). Replaced with a one-clause parenthetical pointing to Step 6b instead of a separate quote block. - **Two redundant rows in Error Handling table (~2 lines)** — the "Helper exits 0/1/2" rows duplicated the exit-code table already shown in Step 6. Replaced with a one-line pointer; kept the rows that are unique to Error Handling (PR not found, no commits, scope undetermined, invalid 1–4 input). - **Examples block in usage callout (~4 lines)** — line above already shows the canonical usage form; the three example invocations added no information. Compressed: - **"Slack ID note" warning** — 4 sentences of prose → 2. Kept the actionable claim (re-verify Burrasec ID before relying on it) and the reason (webhooks paste IDs verbatim, wrong ID renders as @unknown). Dropped meta-commentary about how the IDs were inherited. - **"Context (reason for audit)" preamble** — 3 paragraphs of prose + 5 numbered points → 1 sentence + 4-bullet list. Same rules, no prose loss; cut ~8 lines. What was deliberately NOT removed: - The worked PR #1715 example stays inline. It is the only place the skill calibrates prose density ("5–7 sentences" is abstract; the example shows what that actually looks like). Splitting it into a sibling file was tested and reverted — the model reads it anyway, so splitting just adds a Read round-trip without saving real tokens. - Step-by-step structure, exact templates with placeholders, exit-code → action table in Step 6, blocking-interaction guards ("Stop here. Wait for the user's reply"), and the `--urgent` flag rules. Net: 480 → 440 lines (-8%) with no operational rule lost. Skill ran PR #1715 successfully end-to-end in the same session that produced this trim, so the removed content was demonstrably not load-bearing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reduce request-audit skill from 433 to 356 lines (~18%) without changing behavior. Main consolidations: - Merge the two near-identical thread-reply templates (3b + 3c) into one parameterized template with a 2-row variant table; the only real difference was greeting position. - Flatten Step 2 sub-headers (Scope / Context / Urgency) to inline bold labels with bullet lists — same content, less heading noise. - Collapse the Setup section while keeping the .env example, the WEBHOOK_<CHANNEL> convention, and the 1Password reference verbatim. - In the worked PR #1715 example, keep the full Tron context paragraph (it is the prose-density calibration) but collapse the Burrasec block to a single sentence since the variant table now defines the diff. Nothing functional removed: all 4 scope sources, all 4 context extract sources, all 3 urgency triggers, exit-code semantics, manual-fallback transforms, code-style rule, and the worked example are preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: enforce LibAsset routing for ERC20 transfers in src/ Adds a CI grep check (audit follow-up, Sujith finding #4 on PR #1715) that fails any PR introducing direct .safeTransfer( / .safeTransferFrom( calls in src/ outside LibAsset and an explicit allowlist of pre-existing offenders. Native-ETH safeTransferETH calls remain allowed. EXSC-300 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: widen LibAsset routing check to raw transfer/transferFrom Review feedback: the regex only caught SafeTransferLib-style calls, so raw IERC20.transfer/.transferFrom (the original Tron USDT bug class) passed silently - including LidoWrapper.sol, the contract explicitly named in audit finding #4. Widen the pattern and allowlist LidoWrapper (grandfathered) and Executor (false positive: transferFrom on our own ERC20Proxy). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: disable credential persistence on checkout in LibAsset routing check The job only runs local grep scans and needs no authenticated git operations after checkout (CodeRabbit finding; matches house style in other workflows). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…tuning rationale (EXP-484) EXP-484 follow-up surfaced by the EXP-483 dry-run on PRs #1715 + #1731: 1. Aderyn dropped from Stage 1 Marginal value: ~1 finding per PR beyond what Slither already catches. The supply-chain attack surface of the installer step doesn't pay for itself. If a second-opinion pass is wanted, it belongs in EXP-486 (nightly deep scan), not per-PR. 2. Slither tuning was already correct — workflow already had --exclude-informational --exclude-low (line 68 in v0.4.2 of crytic/slither-action invocation). The earlier dry-run report was based on running Slither locally WITHOUT those flags, which inflated the numbers. Real per-PR numbers with workflow flags: PR #1715 (6 src/ files, ~130 line feat): 4 Slither findings PR #1731 (1 src/ file, +5 line add): 1 Slither finding No workflow change needed for Slither tuning. 3. skip-list.yml shrunk from 11 entries to 0 All previously-listed Slither entries (naming-convention, too-many-digits, reentrancy-events, assembly, low-level-calls) are now dropped at the tool level by --exclude-informational --exclude-low. The skip-list mechanism is kept for future skill-level suppression of Medium/High Slither findings that turn out to be systematically FP for LI.FI patterns. Currently no entries needed. Open EXP-484 tuning items moved to comments in skip-list.yml: - missing-zero-check (Medium): often FP on LI.FI ctor params - divide-before-multiply (Medium): matches our (a*BASE)/b idiom - Semgrep lf-046 / lf-029 / lf-053: rules need pattern tightening (these are Semgrep rule precision issues, fixed in audit/knowledge/semgrep/ not here) Cost impact (per-PR Stage 2 cost estimate, updated): Small PR (<5 files, 1-5 findings) $0.30 - 0.50 Medium PR (6-15 files, 5-15 findings) $0.50 - 1.00 Large PR (16-30, 15-50 findings) $1.00 - 2.20 These are 40-60% below the previous estimate because the actual fp-check input set is much smaller than the unfiltered SARIF suggested. Validated locally: - actionlint → 0 findings - zizmor 1.23.1 → 0 findings - markdownlint → 0 errors - skip-list.yml parses (now 0 entries; all guidance in comments) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three rules surfaced as systematically false-positive in the EXP-483 dry-runs on PR #1715, #1731, #1711. Fixed: lf-046-bytes32-nonevm-receiver Problem: `metavariable-regex` was a SIBLING of `pattern-either` at the top level of the rule, which semgrep silently ignored. Every `bytes32 ...` declaration in scoped paths was matching, including NAMESPACE constants, EIP-712 typehashes, request IDs, etc. Fix: wrap `pattern-either` and `metavariable-regex` in a `patterns:` block so AND-conjunction is correctly applied. Also tightened the regex to only match names containing "nonEvm/btc/bitcoin" + "receiver" — removed "destination" (matched callers/sentinels) and "solana" (Solana addresses safely fit in 32 bytes per protocol). lf-029-shared-modifier-parallel-fields Problem: matched ANY `$SD.receiver` read, including `_bridgeData. receiver` (the canonical parent) inside `keccak256(abi.encode(...))` for EIP-712 signature binding (good practice), inside if/else branches (`if (_bridgeData.receiver == NON_EVM_ADDRESS)`), and inside the validation guard itself. Fix: pattern-not-inside excludes keccak256/abi.encode/abi.encodePacked blocks, equality/inequality comparisons (== / !=), and the LI.FI canonical guard `LibUtil.validateReceiver(...)`. metavariable-regex on $SD excludes `_bridgeData` so the rule only flags bridge-SPECIFIC structs (the actual LF-029 class). lf-053-loop-msg-value Problem: fired on every `{value: msg.value}(...)` invocation, including single-swap entry points where there's no loop and msg.value can't be spent multiple times. Fix: add `pattern-inside: for/while/do-while` constraint. Only fires when the call is inside an iterating construct. Validation (re-run on the 3 dry-run PRs): PR before tune after tune delta ───────── ───────────── ──────────── ────── #1715 1 0 −100% #1731 9 2 −78% #1711 9 1 −89% TOTAL 19 3 −84% The 3 remaining findings are legitimate concerns routed to fp-check: - PR #1731 L43: bytes32 nonEVMReceiver (TP class) - PR #1731 L114: IERC20(USDC).approve (likely FP per fp-check; owner- gated one-time init) - PR #1711 L64: bytes32 nonEVMReceiver (TP class) Documented the tuning rationale inline in each rule's `metadata.# EXP-484 tuning notes:` comment so future maintainers see why the structure looks the way it does. Updated audit/findings/skip-list.yml to remove the "open EXP-484 candidate" notes that are now addressed. Validated locally: - `semgrep --validate --config audit/knowledge/semgrep` → 0 errors, 8 rules loaded - Isolated test fixtures confirm intended matches/non-matches Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ck 6/7] (#1888) * refactor(security-review): drop Aderyn from Stage 1; tighten Slither tuning rationale (EXP-484) EXP-484 follow-up surfaced by the EXP-483 dry-run on PRs #1715 + #1731: 1. Aderyn dropped from Stage 1 Marginal value: ~1 finding per PR beyond what Slither already catches. The supply-chain attack surface of the installer step doesn't pay for itself. If a second-opinion pass is wanted, it belongs in EXP-486 (nightly deep scan), not per-PR. 2. Slither tuning was already correct — workflow already had --exclude-informational --exclude-low (line 68 in v0.4.2 of crytic/slither-action invocation). The earlier dry-run report was based on running Slither locally WITHOUT those flags, which inflated the numbers. Real per-PR numbers with workflow flags: PR #1715 (6 src/ files, ~130 line feat): 4 Slither findings PR #1731 (1 src/ file, +5 line add): 1 Slither finding No workflow change needed for Slither tuning. 3. skip-list.yml shrunk from 11 entries to 0 All previously-listed Slither entries (naming-convention, too-many-digits, reentrancy-events, assembly, low-level-calls) are now dropped at the tool level by --exclude-informational --exclude-low. The skip-list mechanism is kept for future skill-level suppression of Medium/High Slither findings that turn out to be systematically FP for LI.FI patterns. Currently no entries needed. Open EXP-484 tuning items moved to comments in skip-list.yml: - missing-zero-check (Medium): often FP on LI.FI ctor params - divide-before-multiply (Medium): matches our (a*BASE)/b idiom - Semgrep lf-046 / lf-029 / lf-053: rules need pattern tightening (these are Semgrep rule precision issues, fixed in audit/knowledge/semgrep/ not here) Cost impact (per-PR Stage 2 cost estimate, updated): Small PR (<5 files, 1-5 findings) $0.30 - 0.50 Medium PR (6-15 files, 5-15 findings) $0.50 - 1.00 Large PR (16-30, 15-50 findings) $1.00 - 2.20 These are 40-60% below the previous estimate because the actual fp-check input set is much smaller than the unfiltered SARIF suggested. Validated locally: - actionlint → 0 findings - zizmor 1.23.1 → 0 findings - markdownlint → 0 errors - skip-list.yml parses (now 0 entries; all guidance in comments) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(semgrep): tighten lf-046 / lf-029 / lf-053 rules (EXP-484) Three rules surfaced as systematically false-positive in the EXP-483 dry-runs on PR #1715, #1731, #1711. Fixed: lf-046-bytes32-nonevm-receiver Problem: `metavariable-regex` was a SIBLING of `pattern-either` at the top level of the rule, which semgrep silently ignored. Every `bytes32 ...` declaration in scoped paths was matching, including NAMESPACE constants, EIP-712 typehashes, request IDs, etc. Fix: wrap `pattern-either` and `metavariable-regex` in a `patterns:` block so AND-conjunction is correctly applied. Also tightened the regex to only match names containing "nonEvm/btc/bitcoin" + "receiver" — removed "destination" (matched callers/sentinels) and "solana" (Solana addresses safely fit in 32 bytes per protocol). lf-029-shared-modifier-parallel-fields Problem: matched ANY `$SD.receiver` read, including `_bridgeData. receiver` (the canonical parent) inside `keccak256(abi.encode(...))` for EIP-712 signature binding (good practice), inside if/else branches (`if (_bridgeData.receiver == NON_EVM_ADDRESS)`), and inside the validation guard itself. Fix: pattern-not-inside excludes keccak256/abi.encode/abi.encodePacked blocks, equality/inequality comparisons (== / !=), and the LI.FI canonical guard `LibUtil.validateReceiver(...)`. metavariable-regex on $SD excludes `_bridgeData` so the rule only flags bridge-SPECIFIC structs (the actual LF-029 class). lf-053-loop-msg-value Problem: fired on every `{value: msg.value}(...)` invocation, including single-swap entry points where there's no loop and msg.value can't be spent multiple times. Fix: add `pattern-inside: for/while/do-while` constraint. Only fires when the call is inside an iterating construct. Validation (re-run on the 3 dry-run PRs): PR before tune after tune delta ───────── ───────────── ──────────── ────── #1715 1 0 −100% #1731 9 2 −78% #1711 9 1 −89% TOTAL 19 3 −84% The 3 remaining findings are legitimate concerns routed to fp-check: - PR #1731 L43: bytes32 nonEVMReceiver (TP class) - PR #1731 L114: IERC20(USDC).approve (likely FP per fp-check; owner- gated one-time init) - PR #1711 L64: bytes32 nonEVMReceiver (TP class) Documented the tuning rationale inline in each rule's `metadata.# EXP-484 tuning notes:` comment so future maintainers see why the structure looks the way it does. Updated audit/findings/skip-list.yml to remove the "open EXP-484 candidate" notes that are now addressed. Validated locally: - `semgrep --validate --config audit/knowledge/semgrep` → 0 errors, 8 rules loaded - Isolated test fixtures confirm intended matches/non-matches Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore(semgrep): delete lf-053-loop-msg-value (duplicate of Slither detector) LI.FI's lf-053-loop-msg-value rule is a straight duplicate of Slither's upstream-maintained `msg-value-loop` detector (Medium impact, ships since Slither 0.10.x). Both Stage 1 (Slither) and Stage 1 (Semgrep with this rule) were firing on the same pattern. Removing the rule: - No coverage loss — Slither's detector already catches the same pattern and runs in the same Stage 1 step. - Reduces our maintenance surface (one fewer rule pattern to keep precise as Solidity / facet patterns evolve). - Validates the audit corpus principle: ship LI.FI Semgrep rules ONLY for patterns Slither structurally cannot catch. The LF-053 LF-NNN entry stays in audit/knowledge/findings.json — that is historical knowledge of the finding shape. Only the Semgrep rule file is removed. Surfaced by PR review of #1884 (audit knowledge corpus seed): reviewer questioned why we ship a Semgrep rule for a pattern Slither already flags. The other 7 LI.FI Semgrep rules were verified to NOT have a precise Slither equivalent (lf-002 partially overlaps with unchecked-transfer but has a different framing; lf-008 partially overlaps with missing-events-* but is broader; the rest are LI.FI-specific). Followup: audit lf-002 and lf-008 against unchecked-transfer and missing-events-* more rigorously to confirm they cover patterns Slither cannot. Tracked as EXP-484 work. --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Which Linear task belongs to this PR?
https://linear.app/lifi-linear/issue/EXSC-241/fix-tron-usdt-transfers-failing-on-safetransferlib-return-data-check
Why did I implement it this way?
This PR migrates
GenericSwapFacetV3to the granularLibAllowListv2 interface and routes its ERC20 transfers and approvals throughLibAsset. It is the only changemainneeds for the Tron USDT rollout; the actual fix and the WithdrawablePeriphery port land in lifinance/contracts-tron#9, and the rollout itself requires both PRs to merge plus the operational redeploys / diamond cut on Tron.Background — Tron USDT
Tron's canonical USDT (
TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t/0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C) was compiled with old Solidity (~0.4.x). Itstransfer()is declaredreturns (bool)but does notreturn true;. The transfer succeeds and balances update, but callers receive 32 zero bytes (false).SafeTransferLib.safeTransferrejects that →TransferFailed.transferFrom()is unaffected.The actual fix is a single chain-id + address gated branch inside
LibAsset.transferERC20. That branch is Tron-only and does not belong inmain— putting it there ships dead code (and a wider attack surface) to every EVM chain we deploy on. So it lives in the fork.Why GenericSwapFacetV3 is the only piece that lands in main
GenericSwapFacetV3will be redeployed on every chain (including Tron) anyway:contractIsAllowed && selectorIsAllowedcartesian check is genuinely worth retiring —contractSelectorIsAllowedenforces the explicit(contract, selector)pair, preventing cross-contract selector confusion.Since the redeploy is happening anyway, routing its internal ERC20 transfers through
LibAssethere costs nothing extra — the fork's one-line Tron USDT bypass automatically applies to this facet on Tron.Why the other periphery contracts (WithdrawablePeriphery, TokenWrapper, Receivers, LiFiDEXAggregator) are NOT touched in main
Bumping a contract in main that is not being redeployed on every chain creates drift between the repo and the deployed bytecode — which breaks bytecode verification and automatic explorer re-verification.
WithdrawablePeripheryis especially sensitive because 12 periphery contracts inherit from it; bumping it cascades that drift across all of them.Daniel raised this in the SC thread (2026-05-14). The chosen path:
TODO(EXSC-241)comments noting the deferred LibAsset migration.-tronversion suffix), so the fork stays consistent with what is deployed on Tron.What's in this PR
src/Facets/GenericSwapFacetV3.sol(v1.0.2 → v2.0.0) — BREAKINGWhitelist model migrated from the legacy V1 interface to the granular V2 interface already shipped in
LibAllowListv2.0.0:contractIsAllowed(callTo) && selectorIsAllowed(selector)checks (legacy / backward-compat path)contractSelectorIsAllowed(callTo, selector)— explicit(contract, selector)allowlist (V2 primary interface)Other changes in the same facet:
APPROVE_TO_ONLY_SELECTOR = 0xffffffffforapproveTo-only targets (matches theLibAllowList"Special ApproveTo-Only Selector" semantics already documented in that library).safeApprove(0)+safeApprove(max)toLibAsset.maxApproveERC20.IERC20+LibAsset.transferERC20.TODO comments only (no behavior changes)
TODO(EXSC-241)comments added in:src/Helpers/WithdrawablePeriphery.solsrc/Periphery/TokenWrapper.solsrc/Periphery/ReceiverAcrossV3.solsrc/Periphery/ReceiverChainflip.solsrc/Periphery/ReceiverStargateV2.solEach comment records what the LibAsset migration would look like and references EXSC-330 as the unblocker.
Explicitly NOT in this PR
src/Libraries/LibAsset.sol— untouched, stays at v2.1.3. Tron bypass lives in PR #9.src/Helpers/WithdrawablePeriphery.sol— no behavior change; only a TODO. Migration moved to the fork to avoid cascading bytecode drift across 12 inheritors.src/Periphery/TokenWrapper.sol,ReceiverAcrossV3.sol,ReceiverChainflip.sol,ReceiverStargateV2.sol— no behavior change;TODO(EXSC-241)comments only. LibAsset routing for the ones that get Tron deployments lives in PR Check that called contract is not an EOA #9.src/Periphery/LiFiDEXAggregator.sol— no behavior change and no TODO. LDA is being deprecated separately, so it's intentionally excluded from the LibAsset migration plan.GenericSwapFacetV3— out of scope; Tron USDT issue istransfer()-only.Versioning summary
GenericSwapFacetV3LibAssetWithdrawablePeripheryTokenWrapperReceiverAcrossV3ReceiverChainflipReceiverStargateV2LiFiDEXAggregatorMerge order
mainfirst — migratesGenericSwapFacetV3to the granular allowlist and LibAsset routing.mainand adds:LibAsset.transferERC20(LibAsset→v2.1.3-tron)WithdrawablePeripheryport routingwithdrawTokenthroughLibAsset.transferAsset(WithdrawablePeriphery→v1.0.0-tron)See the Versioning convention for fork divergence in PR #9 for why the fork uses
-tronsuffixes on current main versions instead of pre-stamping future main versions.Tests
The existing
test/solidity/Facets/GenericSwapFacetV3.t.solforge suite (39 tests) exercises the migrated facet end-to-end — single and multi-swap flows for V1 and V2 calldata, native ↔ ERC20, allowlist enforcement (including the newapproveTo-only sentinel), slippage protection, dust-free behavior, fee collection paths, and revert paths for unwhitelisted DEX / failed swaps / failed native transfer. All pass on this branch.No new tests added in this PR — the trimmed scope reverted the previously added
WithdrawablePeriphery.t.solZeroAmounttest along with the WithdrawablePeriphery behavior change.Audit
Auditor: Sujith Somraaj (external)
Scope commit:
142d9d8Report:
2026.05.12_TronCanonicalUSDT(Part-1).pdf(delivered 2026-05-12)Status: All findings addressed; internal review by Goran Vladika confirmed ✅
The audited scope was broader (also included
WithdrawablePeripheryv2.0.0 and Receiver migrations). Those pieces have since been removed from this PR per the bytecode-drift discussion in the SC thread (Daniel, 2026-05-14). TheGenericSwapFacetV3changes that constitute the bulk of the audit findings are unchanged; the trim only reduces audit scope. The contracts moved out of this PR will be audited again as part of PR #9 when they land in the Tron fork.Notable finding: GenericSwapFacet v1 still live in production diamonds
The auditor observed that
GenericSwapFacet(v1) selectors remain routable in production diamondsand that its legacy
contractIsAllowed && selectorIsAllowedcartesian-product check undermines thestricter
contractSelectorIsAllowedenforcement introduced in v3. This is out of scope for this PR;a dedicated deprecation is tracked in EXSC-299.
Decision (2026-05-13): GenericSwapFacet v1 will be deprecated on all chains.
Superform and Exodus confirmed they no longer require v1 calldata. BE will first remove the
Exodus hardcoded-lock-list entries (
exodus,exoduscore,exodusus,exoduscoreus) fromlifi-backend(tracked in EXBE-238);once deployed and confirmed stable, SC-side deprecation follows.
GenericSwapFacet v1 will not be deployed to Tron — integrators have not integrated Tron for
this facet, so there is no compatibility risk.
No changes to this PR are required as a result.
Checklist before requesting a review
Checklist for reviewer (DO NOT DEPLOY and contracts BEFORE CHECKING THIS!!!)