Skip to content

Commit

Permalink
fix: losing precision on small amounts
Browse files Browse the repository at this point in the history
  • Loading branch information
arthurgeron committed Feb 3, 2025
1 parent d136330 commit 966a120
Show file tree
Hide file tree
Showing 2 changed files with 15 additions and 14 deletions.
4 changes: 4 additions & 0 deletions packages/app/src/systems/Core/utils/convertToUsd.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ describe('Convert to USD', () => {
);
expect(formatted).toBe('$10.00');
});
it('should not lose precision when dealing with really small amounts', () => {
const { formatted } = convertToUsd(bn(1), DECIMAL_FUEL, MOCK_ETH_RATE);
expect(formatted).toBe('$0.000002');
});
});
25 changes: 11 additions & 14 deletions packages/app/src/systems/Core/utils/convertToUsd.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type BN, bn } from 'fuels';

const DEFAULT_MIN_PRECISION = 2;
const EXTRA_PRECISION_DIGITS = 10;

export function convertToUsd(
amount: BN,
Expand All @@ -9,27 +10,23 @@ export function convertToUsd(
): { value: number; formatted: string } {
if (!rate) return { value: 0, formatted: '$0' };

const precisionFactor = 10 ** DEFAULT_MIN_PRECISION;

// Used for the output fixed-point
// Use a higher extra precision multiplier to preserve small fractions.
const extraPrecision = 10 ** EXTRA_PRECISION_DIGITS;
// This is used for the fixed output decimals.
const outFactor = 10 ** DEFAULT_MIN_PRECISION;

// Scaled rate. Instead of rate * outFactor (which is what we did before), we use an extra multiplier to keep the fractional part.
// Since BN handles numbers as integers, we need to round the result to avoid losing precision on smaller values.
// Scale the rate to fixed-point.
const rateFixed = Math.round(rate * outFactor);

// We multiply the numerator by an extra precision factor in order to not lose the fractional part during division
// Our intended fixed-point integer `(representing value * (precisionFactor * outFactor))` is (amount * rateFixed * precisionFactor) / (10^decimals)
// Essentially ` (amount/10^decimals) * rate ` but in fixed-point arithmetic.
// And then we recover the actual value by dividing by (precisionFactor * outFactor).
const numerator = amount.mul(bn(rateFixed)).mul(bn(precisionFactor));
// Multiply the numerator by the extra precision factor to avoid truncation.
// Numerator represents: amount * rateFixed * extraPrecision.
const numerator = amount.mul(bn(rateFixed)).mul(bn(extraPrecision));
const denominator = bn(10).pow(bn(decimals));
const scaledUsdBN = numerator.div(denominator);

// Convert to a number:
// Here scaledUsdBN represents value * (precisionFactor * outFactor)
// so we must divide by (precisionFactor * outFactor) to get the actual value.
const value = Number(scaledUsdBN.toString()) / (precisionFactor * outFactor);
// scaledUsdBN now represents the value multiplied by (extraPrecision * outFactor)
// Recover the actual value by dividing by that factor.
const value = Number(scaledUsdBN.toString()) / (extraPrecision * outFactor);

if (value === 0) return { value, formatted: '$0' };

Expand Down

0 comments on commit 966a120

Please sign in to comment.