From 2c93151390b7e0da5b46e745e298cf73007bd0c0 Mon Sep 17 00:00:00 2001 From: gomes <17035424+gomesalexandre@users.noreply.github.com> Date: Mon, 17 Aug 2026 22:38:48 +0200 Subject: [PATCH] feat(sdk-core): currency-safe CurrencyAmount comparisons greaterThan/equalTo/lessThan fell through to Fraction with no currency check, so comparing amounts of different currencies silently compared raw numerators (usdcAmount.greaterThan(daiAmount) returned a meaningless result). add/subtract/multiply already guard this with a CURRENCY invariant. Override the three comparisons to apply the same invariant when the other side is a CurrencyAmount, so a cross-currency comparison throws. The base Fraction | BigintIsh signature is kept so existing callers comparing against a raw amount (e.g. amount.equalTo(ZERO) with a JSBI zero) keep working - narrowing to CurrencyAmount | 0 as the issue suggests breaks those legit call sites (verified against v2-sdk). closes #53 --- .changeset/currency-safe-comparisons.md | 5 +++ .../entities/fractions/currencyAmount.test.ts | 33 +++++++++++++++++++ .../src/entities/fractions/currencyAmount.ts | 20 +++++++++++ 3 files changed, 58 insertions(+) create mode 100644 .changeset/currency-safe-comparisons.md diff --git a/.changeset/currency-safe-comparisons.md b/.changeset/currency-safe-comparisons.md new file mode 100644 index 000000000..aba57cb6b --- /dev/null +++ b/.changeset/currency-safe-comparisons.md @@ -0,0 +1,5 @@ +--- +"@uniswap/sdk-core": minor +--- + +`CurrencyAmount.greaterThan/equalTo/lessThan` now throw a `CURRENCY` invariant when compared against a `CurrencyAmount` of a different currency, matching the existing behavior of `add`/`subtract`. Previously these fell through to `Fraction` and silently compared raw numerators across currencies. Comparisons against a raw amount (number, `JSBI`, `Fraction`) are unchanged. diff --git a/sdks/sdk-core/src/entities/fractions/currencyAmount.test.ts b/sdks/sdk-core/src/entities/fractions/currencyAmount.test.ts index f4f18c7b3..09be95033 100644 --- a/sdks/sdk-core/src/entities/fractions/currencyAmount.test.ts +++ b/sdks/sdk-core/src/entities/fractions/currencyAmount.test.ts @@ -112,4 +112,37 @@ describe('CurrencyAmount', () => { expect(amount.toExact()).toEqual('0.00123') }) }) + + describe('#comparisons', () => { + const ADDRESS_TWO = '0x0000000000000000000000000000000000000002' + const tokenA = new Token(1, ADDRESS_ONE, 18) + const tokenB = new Token(1, ADDRESS_TWO, 18) + + it('compares amounts of the same currency', () => { + const two = CurrencyAmount.fromRawAmount(tokenA, 2) + const one = CurrencyAmount.fromRawAmount(tokenA, 1) + expect(two.greaterThan(one)).toBe(true) + expect(one.lessThan(two)).toBe(true) + expect(one.equalTo(CurrencyAmount.fromRawAmount(tokenA, 1))).toBe(true) + }) + + it('still compares against a raw amount (number, JSBI) without a currency', () => { + const amount = CurrencyAmount.fromRawAmount(tokenA, 1) + const zero = CurrencyAmount.fromRawAmount(tokenA, 0) + expect(amount.greaterThan(0)).toBe(true) + expect(zero.equalTo(0)).toBe(true) + // raw BigintIsh (e.g. a JSBI ZERO constant) must keep working, not just the 0 literal + expect(zero.equalTo(JSBI.BigInt(0))).toBe(true) + expect(amount.greaterThan(JSBI.BigInt(0))).toBe(true) + expect(zero.lessThan(amount)).toBe(true) + }) + + it('throws when comparing different currencies', () => { + const a = CurrencyAmount.fromRawAmount(tokenA, 1) + const b = CurrencyAmount.fromRawAmount(tokenB, 1) + expect(() => a.greaterThan(b)).toThrow('CURRENCY') + expect(() => a.lessThan(b)).toThrow('CURRENCY') + expect(() => a.equalTo(b)).toThrow('CURRENCY') + }) + }) }) diff --git a/sdks/sdk-core/src/entities/fractions/currencyAmount.ts b/sdks/sdk-core/src/entities/fractions/currencyAmount.ts index dd96dfe60..0689a31d9 100644 --- a/sdks/sdk-core/src/entities/fractions/currencyAmount.ts +++ b/sdks/sdk-core/src/entities/fractions/currencyAmount.ts @@ -66,6 +66,26 @@ export class CurrencyAmount extends Fraction { return CurrencyAmount.fromFractionalAmount(this.currency, divided.numerator, divided.denominator) } + // Comparisons keep the base `Fraction | BigintIsh` signature so existing + // callers (e.g. `amount.equalTo(ZERO)` against a raw zero) keep working, and + // add the same currency-safety invariant that add/subtract enforce whenever + // the other side is itself a CurrencyAmount - so a cross-currency comparison + // throws instead of silently comparing raw numerators. + public lessThan(other: Fraction | BigintIsh): boolean { + if (other instanceof CurrencyAmount) invariant(this.currency.equals(other.currency), 'CURRENCY') + return super.lessThan(other) + } + + public equalTo(other: Fraction | BigintIsh): boolean { + if (other instanceof CurrencyAmount) invariant(this.currency.equals(other.currency), 'CURRENCY') + return super.equalTo(other) + } + + public greaterThan(other: Fraction | BigintIsh): boolean { + if (other instanceof CurrencyAmount) invariant(this.currency.equals(other.currency), 'CURRENCY') + return super.greaterThan(other) + } + public toSignificant( significantDigits: number = 6, format?: object,