From 1f6b1c7e9dc9e3aae629dd5a01c19891fb9b980e Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:41:33 +0100 Subject: [PATCH 1/2] Reject direct REALU token deposits with AML FAIL (#3278) Add ASSET_INPUT_NOT_ALLOWED error that triggers CheckStatus.FAIL when the input asset is REALU. This prevents bypassing the intended BrokerBot swap flow by sending REALU tokens directly to a DFX deposit address. --- src/integration/sift/dto/sift.dto.ts | 1 + src/subdomains/core/aml/enums/aml-error.enum.ts | 6 ++++++ src/subdomains/core/aml/enums/aml-reason.enum.ts | 1 + src/subdomains/core/aml/services/aml-helper.service.ts | 4 +++- src/subdomains/supporting/payment/dto/transaction.dto.ts | 1 + 5 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/integration/sift/dto/sift.dto.ts b/src/integration/sift/dto/sift.dto.ts index 843a139f0b..d657d12cb6 100644 --- a/src/integration/sift/dto/sift.dto.ts +++ b/src/integration/sift/dto/sift.dto.ts @@ -1038,6 +1038,7 @@ export const SiftAmlDeclineMap: { [method in AmlReason]: DeclineCategory } = { [AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: DeclineCategory.RISKY, [AmlReason.INTERMEDIARY_WITHOUT_SENDER]: DeclineCategory.RISKY, [AmlReason.NAME_TOO_SHORT]: DeclineCategory.OTHER, + [AmlReason.ASSET_INPUT_NOT_ALLOWED]: DeclineCategory.INVALID, }; export interface ScoreRsponse { diff --git a/src/subdomains/core/aml/enums/aml-error.enum.ts b/src/subdomains/core/aml/enums/aml-error.enum.ts index b566c6bd56..e8839c8210 100644 --- a/src/subdomains/core/aml/enums/aml-error.enum.ts +++ b/src/subdomains/core/aml/enums/aml-error.enum.ts @@ -67,6 +67,7 @@ export enum AmlError { TRADE_APPROVAL_DATE_MISSING = 'TradeApprovalDateMissing', BANK_TX_CUSTOMER_NAME_MISSING = 'BankTxCustomerNameMissing', FORCE_MANUAL_CHECK = 'ForceManualCheck', + ASSET_INPUT_NOT_ALLOWED = 'AssetInputNotAllowed', } export const DelayResultError = [ @@ -328,4 +329,9 @@ export const AmlErrorResult: { amlCheck: CheckStatus.PENDING, amlReason: AmlReason.MANUAL_CHECK, }, + [AmlError.ASSET_INPUT_NOT_ALLOWED]: { + type: AmlErrorType.CRUCIAL, + amlCheck: CheckStatus.FAIL, + amlReason: AmlReason.ASSET_INPUT_NOT_ALLOWED, + }, }; diff --git a/src/subdomains/core/aml/enums/aml-reason.enum.ts b/src/subdomains/core/aml/enums/aml-reason.enum.ts index 5f55419276..1bdeadcb68 100644 --- a/src/subdomains/core/aml/enums/aml-reason.enum.ts +++ b/src/subdomains/core/aml/enums/aml-reason.enum.ts @@ -40,6 +40,7 @@ export enum AmlReason { VIRTUAL_IBAN_USER_MISMATCH = 'VirtualIbanUserMismatch', INTERMEDIARY_WITHOUT_SENDER = 'IntermediaryWithoutSender', NAME_TOO_SHORT = 'NameTooShort', + ASSET_INPUT_NOT_ALLOWED = 'AssetInputNotAllowed', } export const KycAmlReasons = [ diff --git a/src/subdomains/core/aml/services/aml-helper.service.ts b/src/subdomains/core/aml/services/aml-helper.service.ts index cf4a777d22..de2bbc2dab 100644 --- a/src/subdomains/core/aml/services/aml-helper.service.ts +++ b/src/subdomains/core/aml/services/aml-helper.service.ts @@ -1,5 +1,5 @@ import { Config, Environment } from 'src/config/config'; -import { Active } from 'src/shared/models/active'; +import { Active, isAsset } from 'src/shared/models/active'; import { Country } from 'src/shared/models/country/country.entity'; import { DisabledProcess, Process } from 'src/shared/services/process.service'; import { Util } from 'src/shared/utils/util'; @@ -52,6 +52,8 @@ export class AmlHelperService { ) return errors; + if (isAsset(inputAsset) && inputAsset.name === 'REALU') errors.push(AmlError.ASSET_INPUT_NOT_ALLOWED); + if ( !DisabledProcess(Process.TRADE_APPROVAL_DATE) && !entity.userData.tradeApprovalDate && diff --git a/src/subdomains/supporting/payment/dto/transaction.dto.ts b/src/subdomains/supporting/payment/dto/transaction.dto.ts index 2f7876c026..72a0fc48e1 100644 --- a/src/subdomains/supporting/payment/dto/transaction.dto.ts +++ b/src/subdomains/supporting/payment/dto/transaction.dto.ts @@ -115,6 +115,7 @@ export const TransactionReasonMapper: { [AmlReason.VIRTUAL_IBAN_USER_MISMATCH]: TransactionReason.UNKNOWN, [AmlReason.INTERMEDIARY_WITHOUT_SENDER]: TransactionReason.BANK_NOT_ALLOWED, [AmlReason.NAME_TOO_SHORT]: TransactionReason.KYC_DATA_NEEDED, + [AmlReason.ASSET_INPUT_NOT_ALLOWED]: TransactionReason.ASSET_NOT_AVAILABLE, }; export class UnassignedTransactionDto { From f58cdc9c058275130ba48c1adfc407586cab1512 Mon Sep 17 00:00:00 2001 From: TaprootFreak <142087526+TaprootFreak@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:42:39 +0100 Subject: [PATCH 2/2] Fix exchange order double-counting in saveTradingLog (#3281) For transfer/deposit commands between exchanges, the destination exchange balance updates (via WebSocket/API) before the order is marked complete. This caused inputAmount to be counted twice: once in liquidity (updated balance) and once in exchangeOrder (IN_PROGRESS order). Now only counts exchangeOrder when the action system matches the target asset's exchange (funds leaving), skipping when funds arrive from another exchange where the balance already reflects those funds. --- src/subdomains/supporting/log/log-job.service.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/subdomains/supporting/log/log-job.service.ts b/src/subdomains/supporting/log/log-job.service.ts index dc6e98587a..0ff2287301 100644 --- a/src/subdomains/supporting/log/log-job.service.ts +++ b/src/subdomains/supporting/log/log-job.service.ts @@ -454,10 +454,16 @@ export class LogJobService { const cryptoInput = [Blockchain.MONERO, Blockchain.LIGHTNING, Blockchain.ZANO].includes(curr.blockchain) ? 0 : pendingPayIns.reduce((sum, tx) => sum + (tx.asset.id === curr.id ? tx.amount : 0), 0); - const exchangeOrder = pendingExchangeOrders.reduce( - (sum, tx) => sum + (tx.pipeline.rule.targetAsset.id === curr.id ? tx.inputAmount : 0), - 0, - ); + const exchangeOrder = pendingExchangeOrders.reduce((sum, tx) => { + if (tx.pipeline.rule.targetAsset.id !== curr.id) return sum; + + // for transfer/deposit: only count when action.system matches the target asset's exchange + // (funds leaving this exchange, balance decreased). Skip when funds arrive from another + // exchange, as the destination balance already reflects those funds before order completion. + if (tx.action.command !== 'withdraw' && tx.action.system !== (curr.blockchain as string)) return sum; + + return sum + tx.inputAmount; + }, 0); const bridgeOrder = pendingBridgeOrders.reduce( (sum, tx) => sum + (tx.pipeline.rule.targetAsset.id === curr.id ? tx.inputAmount : 0), 0,