--- title: Payment Safety Edge Cases tags: [testing, payment, safety, aml, scanner, edge-cases] created: 2026-06-08 --- # Payment Safety Edge Cases Automated tests live in `backend/__tests__/payment-edge-cases.test.ts` (38 tests, all passing). This document records the design rationale, current system behaviour, and remaining gaps for each of the five payment edge-case families. ## Edge Case Families ### 1 · Blacklisted / OFAC-Sanctioned Sender Wallet **How the system works** The OFAC SDN list is downloaded from US Treasury once per 24 h and cached locally. Address screening runs when the seller has opted in (`requireAmlCheck=true` on the seller offer). For on-chain (BSC Verifier) payments the buyer address comes from the ERC-20 Transfer log `from` field. For direct-balance (AMN Scanner) payments the buyer address must be stored in `amnScannerDirectBalance.buyerAddress` at intent-creation time. **Implemented behaviour** | Scenario | Result | |---|---| | OFAC-listed address + seller opted in | `block=true`, `reason=aml_sanctions` | | OFAC-listed address + seller NOT opted in | `block=false`, `reason=aml_not_required` | | OFAC provider unreachable + `amlBlockOnFailure=true` | `block=true` (fail-closed) | | OFAC provider unreachable + `amlBlockOnFailure=false` | `block=false`, `providerUnavailable=true` (fail-open) | | No buyer address (direct-balance, no tx hash) | `block=false`, `reason=no_buyer_address_to_screen` | | Direct-balance + `buyerAddress` stored + sanctioned | `block=true` via `fundDirectBalancePayment` AML gate | | BTC / XMR addresses in SDN XML | Ignored (only EVM `0x…` addresses parsed) | **Remaining gaps** - AML is opt-in per seller. A sanctioned address can pay any seller with `requireAmlCheck=false`. Platform-level mandatory screening could be added via a `PLATFORM_AML_REQUIRED=1` env flag. - SDN list can be up to 24 h stale. **Relevant code** - `src/services/payment/safety/ofacProvider.ts` — SDN download, parse, cache - `src/services/payment/safety/amlScreeningService.ts` — `screenPaymentForAml()` - `src/services/payment/amnScanner/directBalancePaymentService.ts` — `fundDirectBalancePayment()` AML gate --- ### 2 · Overpayment and Underpayment **How the system works** On-chain (BSC Verifier): amount ≥ expected → success; amount < expected → `insufficient_amount`. Direct-balance (webhook): delta ≥ expected → `funded=true`; delta < expected → `funded=false`. Overpay excess is accepted silently and remains locked at the derived destination address. **Implemented behaviour** | Scenario | Result | |---|---| | On-chain overpay (15 USDT vs 10 expected) | `success=true`, `actualAmount` returned | | On-chain exact match | `success=true` | | On-chain underpay by 1 wei | `failureReason=insufficient_amount`, both amounts returned | | Direct-balance overpay | `funded=true`, `reason=paid` | | Direct-balance exact match | `funded=true` | | Direct-balance underpay | `funded=false`, `reason=underpaid:delta=…,expected=…` **+ `payment-underpaid` socket event** with `shortfall` | | Direct-balance zero balance | `funded=false` | **Remaining gaps** - No dedicated `underpaid` payment status in the DB — payment stays `pending` until expiry. - Overpay excess locked at derived address with no automated recovery. **Relevant code** - `src/services/payment/decentralizedPaymentService.ts` — `BSCTransactionVerifier.verifyTransfer()` - `src/services/payment/amnScanner/directBalancePaymentService.ts` — `processDirectBalanceWebhook()` --- ### 3 · Native Coin (ETH / BNB) Sent Instead of ERC-20 **How the system works** Native coin transfers emit no ERC-20 Transfer log. On-chain verification sees no Transfer events. The AMN scanner watches a specific ERC-20 token balance; native coin does not affect it. **Implemented behaviour** | Scenario | Result | |---|---| | On-chain tx succeeded, empty logs | `failureReason=wrong_asset` ← **distinguishable from transfer_not_found** | | On-chain tx with only Approval log | `failureReason=transfer_not_found` | | Direct-balance webhook, ERC-20 balance unchanged | `funded=false` — scanner unaware of native coin arrival | | Direct-balance API check, native baseline captured | `warning=native_coin_detected:delta=N` when native balance increased | **Remaining gaps** - The direct-balance **webhook** path cannot detect native coin because the scanner only reports ERC-20 balance changes. A native-coin watch could be added to the scanner service separately. - Native coin locked at derived address — no automated sweep path. **`wrong_asset` vs `transfer_not_found`** | failureReason | Meaning | |---|---| | `wrong_asset` | Tx succeeded on-chain with zero logs — almost certainly native coin was sent | | `transfer_not_found` | Tx succeeded but logs exist yet none match token/recipient — likely wrong token or misconfigured tx | **Relevant code** - `decentralizedPaymentService.ts:verifyTransfer()` — `receipt.logs.length === 0` → `wrong_asset` - `directBalancePaymentService.ts:createDirectBalancePayIntent()` — captures `nativeBaselineBalance` - `directBalancePaymentService.ts:checkDirectBalancePayment()` — compares native balance to baseline --- ### 4 · Wrong ERC-20 Token Sent to Deposit Address **How the system works** On-chain: BSC Verifier detects token/recipient mismatches explicitly. Direct-balance webhook: the scanner reports the token address in the payload; mismatches are caught at intake. **Implemented behaviour** | Scenario | Result | |---|---| | On-chain: USDC sent when USDT expected | `failureReason=wrong_token`, `actualToken` populated | | On-chain: right token, wrong recipient | `failureReason=wrong_recipient`, `actualRecipient` populated | | On-chain: completely random token + address | `failureReason=transfer_not_found` | | Direct-balance webhook: wrong `tokenAddress` | `funded=false`, `reason=address-token-mismatch` **+ `payment-wrong-token` socket event** | | Direct-balance webhook: wrong `chainId` | `funded=false`, `reason=address-token-mismatch` **+ `payment-wrong-token` socket event** | | Direct-balance webhook: wrong `address` | `funded=false`, `reason=address-token-mismatch` **+ `payment-wrong-token` socket event** | **Remaining gaps** - Wrong-token funds are locked at the derived address — no automated recovery. **Relevant code** - `decentralizedPaymentService.ts` — `parseTransferLogs()`, `verifyTransfer()` wrong-token detection - `directBalancePaymentService.ts:processDirectBalanceWebhook()` — mismatch emits `payment-wrong-token` --- ### 5 · Smart-Contract Sender **How the system works** The ERC-20 Transfer log `from` field (topics[1]) is the immediate sender. Smart contracts (DEX routers, multisigs, mixers) appear here. The system has no built-in EOA detection — only OFAC-listed contract addresses are blocked. **Implemented behaviour** | Scenario | Result | |---|---| | Contract address in Transfer log | Surfaced as `evidence.from`, passed to AML | | OFAC-listed contract + seller opted in | `block=true` | | Unlisted contract (e.g. mixer) + not on OFAC | `block=false` — **gap** | | Gnosis Safe (legitimate multisig) | `block=false` — indistinguishable from mixer by address alone | | `requireEoaSender=true` + contract bytecode | `failureReason=contract_sender`, `success=false` | | `requireEoaSender=true` + EOA (empty bytecode) | `success=true` as normal | | `requireEoaSender` not set (default) | Contract sender passes — backward-compatible | **Enabling EOA enforcement** Set env flag at any scope (per-service or globally): ``` TRANSACTION_SAFETY_REQUIRE_EOA_SENDER=1 ``` This is wired to all 5 `verifyTransfer` call sites: - `transactionSafetyProvider.ts` (webhook safety evaluation) - `paymentController.ts` (new + re-verify web3 payment paths) - `paymentRoutes.ts` - `marketplace/routes.ts` **Remaining gaps** - No bytecode check in `screenPaymentForAml()` itself — the AML layer cannot gate on sender type. - No seller-level `requireEoaSender` flag — currently only a platform-wide env toggle. - Gnosis Safe and unlisted mixers remain indistinguishable at address level. A known-multisig factory allowlist would be required for principled permitting. **Relevant code** - `decentralizedPaymentService.ts:verifyTransfer()` — `requireEoaSender` → `eth_getCode` → `contract_sender` - `transactionSafetyProvider.ts` — reads `TRANSACTION_SAFETY_REQUIRE_EOA_SENDER` --- ## Test File Reference `backend/__tests__/payment-edge-cases.test.ts` | Section | Tests | Status | |---|---|---| | 1 · Blacklisted / OFAC wallet | 10 | ✓ all pass | | 2 · Overpayment and underpayment | 8 | ✓ all pass | | 3 · Native coin (ETH/BNB) | 4 | ✓ all pass | | 4 · Wrong ERC-20 token | 7 | ✓ all pass | | 5 · Smart-contract sender | 9 | ✓ all pass | | **Total** | **38** | **✓** | Tests labelled `GAP ·` document known system limitations that pass because they describe current (undesired) behaviour. Tests labelled `FIXED ·` confirm a gap was mitigated. Run: ```bash cd backend && node_modules/.bin/jest __tests__/payment-edge-cases.test.ts --no-coverage ```