Skip to content

fix(tokens): convert the buy amount out of the entry currency - #1523

Merged
bmc08gt merged 1 commit into
code/cashfrom
fix/buy-gate-entry-currency
Sep 21, 2026
Merged

bmc08gt merged 1 commit into
code/cashfrom
fix/buy-gate-entry-currency

Conversation

@bmc08gt

@bmc08gt bmc08gt commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

A user in Nigeria with about $0.87 of USDF could not buy at any amount. Entering ₦500 against a balance the same screen displayed as "₦1,330.74 available" produced Insufficient Balance.

Cause

checkFundingAmount compares the typed amount against transactionLimit(), which is built from tokenCoordinator.tokenBalances and is therefore USD-denominated. It read the typed amount through the private enteredAmount getter:

private val enteredAmount: Fiat
    get() = Fiat(
        fiat = amountDelegate.state.value.enteredAmount,
        currencyCode = stateFlow.value.tokenBalance.currencyCode,  // USD
    )

That stamps the raw number with the token balance's currency rather than converting it. The user types 500 naira and the gate evaluates 500 > 0.87.

Fiat.valueGreaterThan compares toDouble() with no currency check, so nothing catches the mismatch.

This is a single wrong accessor, not two implementations that drifted. Every other consumer of the amount builds it against the rate's currency — the submit path at lines 927, 938, 1005, 1043, 1256 and 1317, and the sibling validator checkBalanceLimit, which converts via rateToUsd already. checkFundingAmount was the only caller of the mislabeled getter, which is now unused.

Fix

Convert before comparing, matching checkBalanceLimit. ₦500 at roughly ₦1,530/$ becomes $0.33 and clears the $0.87 ceiling.

A missing rate now blocks rather than passes. Rate.ignore carries Double.MIN_VALUE, so converting with it collapses any amount to nearly zero and would clear every ceiling.

Scope

Every non-USD user on the Get/Buy path, on 4503. Severity tracks the exchange rate: NGN and INR block almost any entry, while EUR and GBP sit close enough to 1.0 that the gate mostly behaved, which is likely how this survived. USD is arithmetically unaffected — fx is 1.0 and the conversion is identity. Add Money is unaffected: that branch returns maxSendPerDay in the entry currency.

The same class of bug was fixed for INR in #1173, in the display path. That change corrected maxAmountFlow, which is why the screenshot shows a correct "₦1,330.74 available" above a gate that disagrees with it.

Tests

checkFundingAmount and transactionLimit had no coverage. Three tests added to SwapViewModelErrorTest:

  • an NGN entry inside the converted balance passes, and fires no bottom bar
  • an NGN entry above the converted balance still blocks, guarding against over-correction
  • a missing rate blocks, at an amount that would otherwise clear the raw ceiling

The first fails against the unfixed gate; I confirmed that by reverting the source and re-running. The third fails too. The second passes either way — for NGN the raw number always exceeds its own converted value, so no entry can distinguish the two code paths in that direction. It is a guard, not a regression test, and I have left it in as one.

Full module suite: 114 tests, no failures.

Follow-ups, not in this PR

The gate and the "available" hint compute the same ceiling from independent paths with different limit sources (sendLimitFor versus maxToAdd), which is what let them disagree. Deriving one from the other would prevent a repeat, but it changes daily-limit behavior and deserves its own change.

A currency guard on Fiat.valueGreaterThan and min would have turned this into a loud failure instead of a silent one.

The buy gate compares the typed amount against a ceiling derived from token
balances, which are USD-denominated. It read that amount through the
`enteredAmount` getter, which stamps the raw number with the token balance's
currency without converting it, so 500 naira was compared as $500 and blocked
against a balance worth about $0.87.

Any currency trading well below 1:1 is affected, which is why this reached us
from Nigeria and, earlier, from India. USD is unaffected, and so is Add Money,
which returns its limit in the entry currency already.

Convert through `rateToUsd` before the comparison, the way `checkBalanceLimit`
and every downstream amount already do. A missing rate blocks rather than
passes: `Rate.ignore` carries `Double.MIN_VALUE`, so converting with it would
collapse the amount to nearly zero and clear every ceiling.
@bmc08gt bmc08gt self-assigned this Sep 21, 2026
@github-actions github-actions Bot added type: fix Bug fix area: tokens Token accounts, balances, token info labels Sep 21, 2026
@bmc08gt
bmc08gt merged commit 15948d9 into code/cash Sep 21, 2026
3 checks passed
@bmc08gt
bmc08gt deleted the fix/buy-gate-entry-currency branch September 23, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: tokens Token accounts, balances, token info type: fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant