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.
This commit is contained in:
206
11 - Testing/Payment Safety Edge Cases.md
Normal file
206
11 - Testing/Payment Safety Edge Cases.md
Normal file
@@ -0,0 +1,206 @@
|
||||
---
|
||||
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
|
||||
```
|
||||
Reference in New Issue
Block a user