Files
nick-doc/11 - Testing/Payment Safety Edge Cases.md
Siavash Sameni 67244223ec docs: add sub-project service docs + sync vault 2026-06-08
Add 10 - Services/ docs for all sub-projects: backend, frontend, scanner,
deployment (new), update amanat-assist. Update Scanner Architecture,
Telegram Mini App flow, and Activity Log. Add payment safety edge cases.
2026-06-08 16:23:00 +04:00

8.9 KiB

title, tags, created
title tags created
Payment Safety Edge Cases
testing
payment
safety
aml
scanner
edge-cases
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.tsscreenPaymentForAml()
  • src/services/payment/amnScanner/directBalancePaymentService.tsfundDirectBalancePayment() 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.tsBSCTransactionVerifier.verifyTransfer()
  • src/services/payment/amnScanner/directBalancePaymentService.tsprocessDirectBalanceWebhook()

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_assetdistinguishable 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 === 0wrong_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.tsparseTransferLogs(), 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=falsegap
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()requireEoaSendereth_getCodecontract_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:

cd backend && node_modules/.bin/jest __tests__/payment-edge-cases.test.ts --no-coverage