Description
Five verifiers in apps/swap-service/src/verification/swap-verification.service.ts are stubs — each carries a // TODO: Implement on-chain/API verification and makes no network call — but they return verificationStatus: 'SUCCESS' with hasAffiliate derived from the row's own claimed bps:
// TODO: Implement on-chain/API verification for Cetus
const affiliateBps = swap.affiliateBps
const hasAffiliate = affiliateBps > 0
return { verificationStatus: 'SUCCESS', hasAffiliate, ... }
Affected: verifyCetus, verifySunio, verifyAvnu, verifyStonfi, verifyAcross.
That feeds straight into the payout gate:
// swaps.service.ts:467
const isAffiliateVerified = verificationResult.verificationStatus === 'SUCCESS' && verificationResult.hasAffiliate
// scripts/affiliate-payouts/utils.ts:122
if (!row.isAffiliateVerified) { ... } // excluded, otherwise payable
So a swap on one of these five is marked affiliate-verified on the strength of its own payload, and becomes payable, with nothing checked. The gate that exists to keep unverified swaps out of revenue reports them as verified.
Contrast with the honest no-ops in the same switch — BobGateway, Debridge, Test return noAffiliateResult('SUCCESS', 'Verification not implemented'), and verifyArbitrumBridge returns hasAffiliate: false. All four yield isAffiliateVerified = false and are correctly excluded. That is the behaviour the five stubs should have.
Not currently exploitable: none of the five is in public-api's ENABLED_SWAPPER_NAMES, so no quote can be minted for them today. But swap-service will verify such a row if one arrives by any route, and enabling any of these swappers would open the path silently.
Suggested fix
Short term, make the five behave like the other unimplemented ones — return hasAffiliate: false (or noAffiliateResult('SUCCESS', 'Verification not implemented')) so they cannot set isAffiliateVerified. One line each.
Longer term, implement real verification per swapper, or add a compile-time guard so a new SwapperName cannot default into a self-certifying branch.
Related
0x (verifyZrx) calls /trade-analytics/swap?txHash=… assuming it filters. It does not — the endpoint returns a paginated feed of ~200 recent trades across all chains plus a nextCursor, which the verifier never follows. So a 0x swap verifies only while it remains on the first page; older ones hit the not-found branch, which returns PENDING, leaving the swap re-polled every 30s indefinitely. Separate bug, same file.
CowSwap, Portals, Bebop, ButterSwap do make real calls and are not part of this issue.
Description
Five verifiers in
apps/swap-service/src/verification/swap-verification.service.tsare stubs — each carries a// TODO: Implement on-chain/API verificationand makes no network call — but they returnverificationStatus: 'SUCCESS'withhasAffiliatederived from the row's own claimed bps:Affected:
verifyCetus,verifySunio,verifyAvnu,verifyStonfi,verifyAcross.That feeds straight into the payout gate:
So a swap on one of these five is marked affiliate-verified on the strength of its own payload, and becomes payable, with nothing checked. The gate that exists to keep unverified swaps out of revenue reports them as verified.
Contrast with the honest no-ops in the same switch —
BobGateway,Debridge,TestreturnnoAffiliateResult('SUCCESS', 'Verification not implemented'), andverifyArbitrumBridgereturnshasAffiliate: false. All four yieldisAffiliateVerified = falseand are correctly excluded. That is the behaviour the five stubs should have.Not currently exploitable: none of the five is in public-api's
ENABLED_SWAPPER_NAMES, so no quote can be minted for them today. But swap-service will verify such a row if one arrives by any route, and enabling any of these swappers would open the path silently.Suggested fix
Short term, make the five behave like the other unimplemented ones — return
hasAffiliate: false(ornoAffiliateResult('SUCCESS', 'Verification not implemented')) so they cannot setisAffiliateVerified. One line each.Longer term, implement real verification per swapper, or add a compile-time guard so a new
SwapperNamecannot default into a self-certifying branch.Related
0x(verifyZrx) calls/trade-analytics/swap?txHash=…assuming it filters. It does not — the endpoint returns a paginated feed of ~200 recent trades across all chains plus anextCursor, which the verifier never follows. So a 0x swap verifies only while it remains on the first page; older ones hit the not-found branch, which returnsPENDING, leaving the swap re-polled every 30s indefinitely. Separate bug, same file.CowSwap,Portals,Bebop,ButterSwapdo make real calls and are not part of this issue.