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.
8.9 KiB
title, tags, created
| title | tags | created | ||||||
|---|---|---|---|---|---|---|---|---|
| Payment Safety 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 aPLATFORM_AML_REQUIRED=1env flag. - SDN list can be up to 24 h stale.
Relevant code
src/services/payment/safety/ofacProvider.ts— SDN download, parse, cachesrc/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
underpaidpayment status in the DB — payment stayspendinguntil 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_assetdirectBalancePaymentService.ts:createDirectBalancePayIntent()— capturesnativeBaselineBalancedirectBalancePaymentService.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 detectiondirectBalancePaymentService.ts:processDirectBalanceWebhook()— mismatch emitspayment-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.tsmarketplace/routes.ts
Remaining gaps
- No bytecode check in
screenPaymentForAml()itself — the AML layer cannot gate on sender type. - No seller-level
requireEoaSenderflag — 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_sendertransactionSafetyProvider.ts— readsTRANSACTION_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