Skip to content

feat: migrate GenericSwapFacetV3 to granular allowlist + LibAsset routing [GenericSwapFacetV3 v2.0.0] - #1715

Merged
mirooon merged 40 commits into
mainfrom
exsc-241-fix-tron-usdt-transfers
May 20, 2026
Merged

mirooon merged 40 commits into
mainfrom
exsc-241-fix-tron-usdt-transfers

Conversation

@mirooon

@mirooon mirooon commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor

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 GenericSwapFacetV3 to the granular LibAllowList v2 interface and routes its ERC20 transfers and approvals through LibAsset. It is the only change main needs 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.

┌─────────────────────────────────────────────────────────────┐
│  PR #1715 (this PR — main contracts repo)                   │
│  GenericSwapFacetV3 v2.0.0:                                 │
│    • LibAllowList v2 granular (contract,selector) allowlist │
│    • ERC20 transfers/approvals through LibAsset             │
│  No changes to LibAsset itself; no other periphery touched. │
└────────────────────────┬────────────────────────────────────┘
                         │  rebase
                         ▼
┌─────────────────────────────────────────────────────────────┐
│  PR #9 (contracts-tron fork)                                │
│  • LibAsset v2.1.3-tron:  chain+address gated Tron USDT     │
│    bypass in transferERC20                                  │
│  • WithdrawablePeriphery v1.0.0-tron: route withdrawToken   │
│    through LibAsset.transferAsset                           │
└─────────────────────────────────────────────────────────────┘

Background — Tron USDT

Tron's canonical USDT (TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t / 0xa614f803B6FD780986A42c78Ec9c7f77e6DeD13C) was compiled with old Solidity (~0.4.x). Its transfer() is declared returns (bool) but does not return true;. The transfer succeeds and balances update, but callers receive 32 zero bytes (false). SafeTransferLib.safeTransfer rejects 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 in main — 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

GenericSwapFacetV3 will be redeployed on every chain (including Tron) anyway:

  • It's being audited for the granular-allowlist migration regardless.
  • The legacy contractIsAllowed && selectorIsAllowed cartesian check is genuinely worth retiring — contractSelectorIsAllowed enforces the explicit (contract, selector) pair, preventing cross-contract selector confusion.
  • Tron deployments need it.

Since the redeploy is happening anyway, routing its internal ERC20 transfers through LibAsset here 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. WithdrawablePeriphery is 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:

  • Main repo: leave these contracts as-is, with TODO(EXSC-241) comments noting the deferred LibAsset migration.
  • Tron fork: contracts that are actually redeployed on Tron carry the LibAsset routing change directly (under a -tron version suffix), so the fork stays consistent with what is deployed on Tron.
  • Unblocker: EXSC-330 — once deploy logs carry the commit hash, re-verification becomes recoverable from history and the drift is tolerable. At that point we can roll the full LibAsset migration into main.

What's in this PR

src/Facets/GenericSwapFacetV3.sol (v1.0.2 → v2.0.0) — BREAKING

Whitelist model migrated from the legacy V1 interface to the granular V2 interface already shipped in LibAllowList v2.0.0:

  • Before: independent contractIsAllowed(callTo) && selectorIsAllowed(selector) checks (legacy / backward-compat path)
  • After: paired contractSelectorIsAllowed(callTo, selector) — explicit (contract, selector) allowlist (V2 primary interface)

Other changes in the same facet:

  • New sentinel APPROVE_TO_ONLY_SELECTOR = 0xffffffff for approveTo-only targets (matches the LibAllowList "Special ApproveTo-Only Selector" semantics already documented in that library).
  • Approval logic switched from manual safeApprove(0) + safeApprove(max) to LibAsset.maxApproveERC20.
  • ERC20 transfers and balance reads moved to IERC20 + LibAsset.transferERC20.

TODO comments only (no behavior changes)

TODO(EXSC-241) comments added in:

  • src/Helpers/WithdrawablePeriphery.sol
  • src/Periphery/TokenWrapper.sol
  • src/Periphery/ReceiverAcrossV3.sol
  • src/Periphery/ReceiverChainflip.sol
  • src/Periphery/ReceiverStargateV2.sol

Each 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.
  • Approval and native-ETH paths outside GenericSwapFacetV3 — out of scope; Tron USDT issue is transfer()-only.

Versioning summary

Contract Before After this PR Notes
GenericSwapFacetV3 1.0.2 2.0.0 granular allowlist + LibAsset routing
LibAsset 2.1.3 2.1.3 unchanged — Tron bypass in fork only
WithdrawablePeriphery 1.0.0 1.0.0 TODO comment only
TokenWrapper 1.2.1 1.2.1 TODO comment only
ReceiverAcrossV3 1.1.0 1.1.0 TODO comment only
ReceiverChainflip 1.0.1 1.0.1 TODO comment only
ReceiverStargateV2 1.1.0 1.1.0 TODO comment only
LiFiDEXAggregator 1.12.0 1.12.0 unchanged — being deprecated separately, no TODO

Merge order

  1. This PR (feat: migrate GenericSwapFacetV3 to granular allowlist + LibAsset routing [GenericSwapFacetV3 v2.0.0] #1715) merges to main first — migrates GenericSwapFacetV3 to the granular allowlist and LibAsset routing.
  2. PR #9 in contracts-tron rebases on the updated main and adds:
    • the Tron USDT bypass in LibAsset.transferERC20 (LibAsset → v2.1.3-tron)
    • the WithdrawablePeriphery port routing withdrawToken through LibAsset.transferAsset (WithdrawablePeriphery → v1.0.0-tron)

See the Versioning convention for fork divergence in PR #9 for why the fork uses -tron suffixes on current main versions instead of pre-stamping future main versions.

Tests

The existing test/solidity/Facets/GenericSwapFacetV3.t.sol forge 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 new approveTo-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.sol ZeroAmount test along with the WithdrawablePeriphery behavior change.

Audit

Auditor: Sujith Somraaj (external)
Scope commit: 142d9d8
Report: 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 WithdrawablePeriphery v2.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). The GenericSwapFacetV3 changes 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 diamonds
and that its legacy contractIsAllowed && selectorIsAllowed cartesian-product check undermines the
stricter contractSelectorIsAllowed enforcement 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) from
lifi-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!!!)

  • 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 April 15, 2026 15:09
@lifi-action-bot lifi-action-bot changed the title EXSC-241 Fix Tron USDT transfers EXSC-241 Fix Tron USDT transfers [GenericSwapFacetV3 v1.1.0, WithdrawablePeriphery v1.0.1, LibAsset v2.2.0, LiFiDEXAggregator v1.13.0] Apr 15, 2026
@coderabbitai

coderabbitai Bot commented Apr 15, 2026 •

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Migrates 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.

Changes

LibAsset Centralization & Approval Refactor

Layer / File(s) Summary
Approval & Swap Logic
src/Facets/GenericSwapFacetV3.sol
Replaces dual allow-list checks with LibAllowList.contractSelectorIsAllowed; introduces APPROVE_TO_ONLY_SELECTOR; replaces conditional approve/reset logic with unconditional LibAsset.maxApproveERC20(...); migrates ERC20 calls to IERC20(...).balanceOf and LibAsset.transferERC20; version bump to 2.0.0.
Core Asset Helpers Adoption
src/Helpers/WithdrawablePeriphery.sol, src/Periphery/LiFiDEXAggregator.sol, src/Periphery/TokenWrapper.sol, src/Periphery/LidoWrapper.sol
Replaces direct SafeERC20/SafeTransferLib ERC20 transfer/transferFrom/approve calls with LibAsset.transferERC20, LibAsset.transferFromERC20, LibAsset.maxApproveERC20, or LibAsset.transferAsset; retains SafeTransferLib ETH path in LiFiDEXAggregator where applicable; updates NatSpec and @custom:version annotations.
Receiver Recovery Paths
src/Periphery/ReceiverAcrossV3.sol, src/Periphery/ReceiverChainflip.sol, src/Periphery/ReceiverStargateV2.sol
Swap-failure and recovery flows now return tokens via LibAsset.transferERC20(...) instead of safeTransfer; removes ExternalCallFailed import; updates comments about revert-on-zero-address behavior; version bumps.
Tests / Revert Expectations
test/solidity/Helpers/WithdrawablePeriphery.t.sol, test/solidity/Periphery/.../ReceiverAcrossV3.t.sol, .../ReceiverAcrossV4.t.sol, .../ReceiverOIF.t.sol, .../ReceiverStargateV2.t.sol, test/solidity/Periphery/LidoWrapper/LidoWrapper.t.sol
Adjusts tests to expect ETHTransferFailed() (and ZeroAmount where applicable) instead of ExternalCallFailed(); updates LidoWrapper tests to expect SafeTransferLib.TransferFromFailed when transferFrom reverts; adds necessary imports.
Peripheral Metadata & Misc
src/Periphery/*, claude-code
Minor @custom:version bumps and documentation additions across periphery contracts; updates submodule pointer for claude-code.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • lifinance/contracts#831: Related — modifies WithdrawablePeriphery and its tests with overlapping transfer/revert semantics.
  • lifinance/contracts#909: Related — similar migration of periphery and receiver contracts to LibAsset helpers.
  • lifinance/contracts#912: Related — changes approval/transfer flows and adopts LibAsset helper patterns used here.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title clearly and concisely summarizes the main breaking change: migration of GenericSwapFacetV3 to a granular allowlist and LibAsset routing, with the version bump to v2.0.0 highlighted. The title directly reflects the primary objective.
Description check ✅ Passed The PR description follows the template structure, provides comprehensive context (Linear task, implementation rationale, affected contracts with versions), includes detailed checklists, and documents scope, audit status, and merge order.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch exsc-241-fix-tron-usdt-transfers

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

lifi-action-bot commented Apr 15, 2026 •

Copy link
Copy Markdown
Collaborator

Test Coverage Report

Line Coverage: 89.97% (3240 / 3601 lines)
Function Coverage: 93.65% ( 502 / 536 functions)
Branch Coverage: 72.18% ( 597 / 827 branches)
Test coverage (89.97%) is above min threshold (83%). Check passed.

@mirooon
mirooon marked this pull request as ready for review April 15, 2026 15:16
@lifi-action-bot

lifi-action-bot commented Apr 15, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 GitHub Action: Security Alerts Review 🔍

🟢 Dismissed Security Alerts with Comments
The following alerts were dismissed with proper comments:

🟢 View Alert - File: src/Helpers/WithdrawablePeriphery.sol
🔹 Parameters passed to a constructor that are not validated for correct values may lead to contract creation in an undesired state. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/no-parameter-validation-in-constructor
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: Mostly as a style / deployment-safety nit, not as a meaningful exploit. We usually check owner in the child contract of WithdrawablePeriphery

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Performing a narrowing downcast may result in silent overflow due to bit truncation. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/unsafe-downcast
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: normal, correct way to read an ABI function selector: the first four bytes of callData are exactly a bytes4. There is no extra width to “overflow”; the conversion is intentional use of those four bytes as the selector.

🟢 View Alert - File: src/Helpers/WithdrawablePeriphery.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: withdrawToken is onlyOwner, so a malicious receiver can’t grief arbitrary users; the owner can pick a different _receiver if one reverts and the contract no longer does receiver.call{value: ...}("");

🟢 View Alert - File: src/Periphery/LiFiDEXAggregator.sol
🔹 External calls in a loop may lead to denial of service if those calls revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/calls-in-loop
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: swaps/wraps necessarily perform external calls per step, and if any step reverts the whole route reverts (atomicity)

🟢 View Alert - File: src/Helpers/WithdrawablePeriphery.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: it's safe, it's only admin function

🟢 View Alert - File: src/Helpers/WithdrawablePeriphery.sol
🔹 Functions restricted to a single owner may result in loss or abuse of contract functionality if the owner's private key is compromised. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/owner-single-point-of-failure
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: it's managed by multisig

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Performing a narrowing downcast may result in silent overflow due to bit truncation. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/unsafe-downcast
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: normal, correct way to read an ABI function selector: the first four bytes of callData are exactly a bytes4. There is no extra width to “overflow”; the conversion is intentional use of those four bytes as the selector.

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: callTo / approveTo are gated by LibAllowList. those callees are known routers

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: callTo / approveTo are gated by LibAllowList. those callees are known routers

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: callTo / approveTo are gated by LibAllowList. those callees are known routers

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: callTo / approveTo are gated by LibAllowList. those callees are known routers

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Parameters passed to a constructor that are not validated for correct values may lead to contract creation in an undesired state. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/no-parameter-validation-in-constructor
🔹 Dismiss Reason: Won't fix
🔹 Dismiss Comment: that's valid alert. It will be addressed in the next PR.

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: This function is not vulnerable to reentrancy. All external calls are made to whitelisted DEX contracts using a controlled low-level .call

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Reentrant functions which emit events after making an external call may lead to out-of-order events. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/reentrancy-events
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: Event emissions follow all state mutations and validations

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: This call intentionally sends native tokens using .call without a specific gas stipend because we are forwarding the full balance and handling failure with an explicit revert. The callee (receiver) is trusted and failures are caught via if (!success)

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: We explicitly check the success and the _receiver is user supplied

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: it executes a user-defined swap on a whitelisted contract and selector (LibAllowList)

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: This function is not vulnerable to reentrancy. All external calls are made to whitelisted DEX contracts using a controlled low-level .call

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: The target contract and selector are both whitelisted via LibAllowList to mitigate risk

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: The target contract and selector are both whitelisted via LibAllowList to mitigate risk

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 Making an external call without a gas budget may consume all of the transaction's gas, causing it to revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/call-without-gas-budget
🔹 Dismiss Reason: Won't fix
🔹 Dismiss Comment: This native token transfer uses .call to avoid the 2300 gas stipend limitation of transfer() and send(), which is incompatible with many smart contract wallets. We explicitly check the success flag and revert with a custom error if the call fails

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 External calls in a loop may lead to denial of service if those calls revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/calls-in-loop
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: We explicitly check the success and receiver is user supplied

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 External calls in a loop may lead to denial of service if those calls revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/calls-in-loop
🔹 Dismiss Reason: Won't fix
🔹 Dismiss Comment: if one swap fails, the whole transaction should revert, and that's acceptable by design

🟢 View Alert - File: src/Facets/GenericSwapFacetV3.sol
🔹 External calls in a loop may lead to denial of service if those calls revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/calls-in-loop
🔹 Dismiss Reason: Won't fix
🔹 Dismiss Comment: if one swap fails, the whole transaction should revert, and that's acceptable by design

🟢 View Alert - File: src/Periphery/LiFiDEXAggregator.sol
🔹 Casting a large uint to int (even at same byte width) may lead to overflow. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/uint-to-int-conversion
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: safe. The amountIn value comes from the pool’s reserves, which are much smaller than the maximum number allowed by int256

🟢 View Alert - File: src/Periphery/LiFiDEXAggregator.sol
🔹 Calling a function without checking the return value may lead to silent failures. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/unused-return-function-call
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: The return value from IUniswapV3Pool(pool).swap() is not needed because the result is handled by the callback function uniswapV3SwapCallback

🟢 View Alert - File: src/Periphery/LiFiDEXAggregator.sol
🔹 External calls in a loop may lead to denial of service if those calls revert. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/calls-in-loop
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: expected and safe. If the call fails, the whole transaction reverts,

🟢 View Alert - File: src/Libraries/LibAsset.sol
🔹 Calling transferFrom functionality with a parameterized from value may lead to theft or loss of tokens. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/arbitrary-transfer-from
🔹 Dismiss Reason: Won't fix
🔹 Dismiss Comment: This is a library and it's by design that the function is generic.

🟢 View Alert - File: src/Libraries/LibAsset.sol
🔹 The value of msg.value is constant in a transaction, and will not be reapplied in a loop. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/msg-value-reuse
🔹 Dismiss Reason: Won't fix
🔹 Dismiss Comment: That's fine. We do not expect msg.value to change. Also, if we deposit a native asset we will only do it once so this should be fine.

🟢 View Alert - File: src/Periphery/LiFiDEXAggregator.sol
🔹 Parameters passed to a constructor that are not validated for correct values may lead to contract creation in an undesired state. For more information, visit: http://detectors.olympixdevsectools.com/article/web3-vulnerability/no-parameter-validation-in-constructor
🔹 Dismiss Reason: False positive
🔹 Dismiss Comment: The _owner is already validated in the constructor and the priviledged array t is optiona

✅ No unresolved security alerts! 🎉

@lifi-action-bot
lifi-action-bot marked this pull request as draft April 15, 2026 15:17

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_TronUsdtIsPresentAtCanonicalAddressAfterLocalStub and test_StablecoinTransferSucceedsTronChainId still 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.sol file, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 602d4a9 and dfef8b8.

📒 Files selected for processing (7)
  • .cursor/rules/400-solidity-tests.mdc
  • src/Facets/GenericSwapFacetV3.sol
  • src/Helpers/WithdrawablePeriphery.sol
  • src/Libraries/LibAsset.sol
  • src/Periphery/LiFiDEXAggregator.sol
  • test/solidity/Libraries/LibAsset.t.sol
  • test/solidity/utils/MockTronUSDT.sol

Comment thread src/Helpers/WithdrawablePeriphery.sol Outdated
@mirooon
mirooon marked this pull request as ready for review April 16, 2026 08:06
@lifi-action-bot
lifi-action-bot marked this pull request as draft April 16, 2026 08:06
Comment thread src/Facets/GenericSwapFacetV3.sol Dismissed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 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 IERC20 import could be moved above the local ILiFi import. 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

📥 Commits

Reviewing files that changed from the base of the PR and between dfef8b8 and ad14a6e.

📒 Files selected for processing (8)
  • .cursor/rules/100-solidity-basics.mdc
  • src/Facets/GenericSwapFacetV3.sol
  • src/Helpers/WithdrawablePeriphery.sol
  • test/solidity/Helpers/WithdrawablePeriphery.t.sol
  • test/solidity/Periphery/Across/V3/ReceiverAcrossV3.t.sol
  • test/solidity/Periphery/Across/V4/ReceiverAcrossV4.t.sol
  • test/solidity/Periphery/ReceiverOIF.t.sol
  • test/solidity/Periphery/ReceiverStargateV2.t.sol
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Helpers/WithdrawablePeriphery.sol

@mirooon
mirooon marked this pull request as ready for review April 16, 2026 12:00
@lifi-action-bot lifi-action-bot changed the title EXSC-241 Fix Tron USDT transfers [GenericSwapFacetV3 v1.1.0, WithdrawablePeriphery v1.0.1, LibAsset v2.2.0, LiFiDEXAggregator v1.13.0] EXSC-241 Fix Tron USDT transfers [GenericSwapFacetV3 v2.0.0, WithdrawablePeriphery v1.0.1, LibAsset v2.2.0, LiFiDEXAggregator v1.13.0] Apr 16, 2026
@lifi-action-bot
lifi-action-bot marked this pull request as draft April 16, 2026 12:00
@mirooon
mirooon marked this pull request as ready for review April 16, 2026 12:03
@lifi-action-bot lifi-action-bot changed the title EXSC-241 Fix Tron USDT transfers [GenericSwapFacetV3 v2.0.0, WithdrawablePeriphery v1.0.1, LibAsset v2.2.0, LiFiDEXAggregator v1.13.0] EXSC-241 Fix Tron USDT transfers [GenericSwapFacetV3 v2.0.0, WithdrawablePeriphery v2.0.0, LibAsset v2.2.0, LiFiDEXAggregator v1.13.0] Apr 16, 2026
…ude rules for handling errors in GenericErrors.sol
Comment thread src/Facets/GenericSwapFacetV3.sol
Comment thread src/Helpers/WithdrawablePeriphery.sol Outdated
mirooon added 2 commits May 19, 2026 12:39
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>
@mirooon
mirooon merged commit 09c14a1 into main May 20, 2026
35 checks passed
@mirooon
mirooon deleted the exsc-241-fix-tron-usdt-transfers branch May 20, 2026 07:48
mirooon added a commit to lifinance/contracts-tron that referenced this pull request May 21, 2026
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.
mirooon added a commit that referenced this pull request Jun 4, 2026
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>
mirooon pushed a commit that referenced this pull request Jun 4, 2026
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>
0xDEnYO added a commit that referenced this pull request Jun 12, 2026
* 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>
gvladika added a commit that referenced this pull request Jun 15, 2026
…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>
gvladika added a commit that referenced this pull request Jun 15, 2026
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>
gvladika added a commit that referenced this pull request Jun 15, 2026
…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>
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.

5 participants