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:
Siavash Sameni
2026-06-08 16:22:52 +04:00
parent 181e8e9c2f
commit 67244223ec
13 changed files with 2734 additions and 311 deletions

View 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
```