Skip to content

Research notes

Chainlink Cross-Rates: Scale Both Feeds

Work through an 8-decimal and 18-decimal cross-rate, an intermediate-rounding counterexample, and separate checks for both report timestamps.

Tall ivory vessel and wide ceramic bowl joined by a lime bridge on a dark studio tabletop
Illustration for research notes; examples in this article are educational.

A Chainlink cross-rate divides two prices expressed in a common quote currency after accounting for each feed’s decimals. For A/USD divided by B/USD, the result is B per A. Choose the output scale explicitly, round according to the consumer’s rule, and validate both answers and both update timestamps before using the derived rate.

Write the units before choosing the division

An A/USD price describes USD per whole A. A B/USD price describes USD per whole B. Dividing the first by the second cancels USD and leaves B per A: the reference value of one A expressed in B. Reversing the division answers a different question, A per B.

Chainlink’s EVM consumer guide uses this construction to derive BTC/EUR from BTC/USD and EUR/USD. The useful habit is to write “quote units per base unit” beside each input, then check the units left after cancellation. Pair labels alone can conceal an inverted calculation.

The common denominator must also mean the same thing. USD and USDT are different units. A redemption ratio and a market price can measure different relationships even when their labels look compatible. Chainlink’s feed-selection guidance distinguishes exchange-rate feeds from market-price feeds. Verify the intended asset, denomination and methodology before combining numbers.

Price precision belongs to the feed. Chainlink’s decimal-precision documentation explains how to interpret integer answers using each feed’s decimal count and instructs consumers to read that precision. The token contract’s decimal count is a separate quantity; the ERC-20 amount guide explains that scale. A token using eighteen decimals does not establish the price feed’s scale or the desired cross-rate precision.

A cross-rate with eight and eighteen input decimals

Consider two fictional assets, A and B. Every rate and feed configuration in this example is hypothetical. Assume both inputs are positive market-price references in USD per whole asset and have passed the validity checks discussed below.

Input fieldA/USD feedB/USD feed
Raw answer25001234567862500987654321098765432
Feed decimals818
USD per whole asset2500.1234567862500.987654321098765432

The human-readable calculation is 2500.12345678 divided by 62500.987654321098765432. It gives approximately 0.040001343188488802 B per A. This is a derived reference, not a promise that selling one A will deliver that quantity of B after spreads, fees or execution costs.

For exact integer arithmetic, retain both raw answers. Call them a and b; call their decimal counts dA and dB. Choose a third decimal count, dOut, for the output. A mathematical construction for positive inputs is:

  • Numerator N = a × 10^(dB + dOut).
  • Denominator D = b × 10^dA.
  • Output integer q = floor(N / D).

Display q using dOut decimal places. This construction performs one final rounding operation on the ratio. It is a mathematical specification; fixed-width contract arithmetic still needs bounded intermediate values.

For an eight-decimal output here, the scale factors simplify to q = floor(a × 10^18 / b). The exponent is eighteen because the denominator feed has eighteen decimals, the output has eight, and the numerator feed already has eight.

Chosen output precisionOutput integerDisplayed B per A
8 decimals40001340.04000134
18 decimals400013431884888020.040001343188488802

The two rows describe the same ratio at different output resolutions. Extra displayed places do not improve the underlying observations. At eight decimals, the discarded portion is about 0.00000000318849 B per A, below one output unit of 0.00000001.

Reversing the original inputs and recomputing gives 24.99916053 A per B at eight decimals with rounding down. Keep that unit attached to the number. Inverting the already rounded output introduces another rounding step, so calculate the reverse from the original inputs when it is needed.

Where integer arithmetic changes the answer

Dividing the two raw answers directly produces zero under integer division in this fixture. Even multiplying the numerator by the eight-decimal output scale first still produces zero. The missing ten-place adjustment between the input scales has not disappeared. A fractional raw-answer ratio would be ten billion times smaller than the intended human-readable cross-rate if it were interpreted without that adjustment.

Discarding both USD fractions first is another mistake: 2500 divided by 62500 gives 0.04, or an eight-place output integer of 4000000. That differs from the correctly scaled 4000134. Round the result at the declared output boundary rather than silently changing the input prices.

Chainlink’s illustrative converter rescales each input to the selected decimal count before division. Its sample reads discard the timestamp fields; this scale demonstration does not validate report age. Downscaling an input can also discard information before the final quotient. That approach and the single-rounding specification above therefore need separate expected results at sensitive boundaries.

For a second, deliberately constructed hypothetical fixture, let A/USD be 1.00000001 at eight decimals and B/USD be 1.000000019999999999 at eighteen decimals. An eight-place output from the full ratio rounds down to 0.99999999 B per A. Truncating B/USD to eight places first makes both inputs 1.00000001; dividing then yields 1.00000000. The difference is one output unit, caused by intermediate truncation.

This is an implementation choice to test explicitly. Solidity 0.8.30’s division rules round runtime integer division toward zero, which is rounding down for these positive inputs. That does not choose the economically appropriate rounding direction for every consumer.

Multiplication can overflow before division reduces a result. Bound supported answers and decimal counts, account for signed-to-unsigned conversions, and use arithmetic designed and reviewed for the required intermediate range. Off-chain, preserve exact integers or decimal text; the API precision guide explains why a floating-point conversion can lose digits before any scale adjustment.

Two valid answers still need two valid timestamps

The EVM latestRoundData() result contains an answer and updatedAt for that feed. Chainlink defines updatedAt as when the round was updated; its API reference also marks answeredInRound deprecated. A copied legacy check involving that field is not a substitute for understanding the current feed and checking its update time.

For this positive-price use case, reject either answer if it is zero or negative before division or conversion to an unsigned type. A missing response, reverted read or unacceptable timestamp makes the derived rate unavailable. Checking only the denominator avoids some arithmetic failures while still allowing an invalid numerator to produce a misleading price.

Each timestamp needs its own acceptance decision. Do not assign a newly calculated ratio the current time and thereby make its inputs appear fresh. Chainlink warns that combining feeds can introduce error even with a common denominator.

Use this hypothetical consumer policy at 12:00:00 UTC on 28 October 2026:

  • A/USD may be at most 120 seconds old.
  • B/USD may be at most 300 seconds old.
  • Their update timestamps may differ by at most 240 seconds.
  • Boundaries are inclusive; reject zero or future timestamps before computing ages.

These are invented application limits, not actual feed heartbeats or recommended defaults. Each row below is an independent scenario; only timestamps change, and all other checks are assumed to pass.

A update, UTCB update, UTCA ageB ageTime gapPolicy result
11:59:4011:59:1020 s50 s30 sPass
11:59:4011:54:5920 s301 s281 sReject B age and gap
11:57:5911:59:40121 s20 s101 sReject A age
11:59:5011:55:1010 s290 s280 sReject gap
11:59:4012:00:0120 sInvalidNot usedReject future B time
11:58:0011:55:00120 s300 s180 sPass at age boundaries

The fourth row passes both age limits but fails the separate alignment rule. Conversely, equal timestamps could pass the gap check while both answers are stale. These are distinct tests. Even a small update-time gap does not prove that all underlying market observations occurred simultaneously.

For an on-chain consumer, evaluate ages against the execution block’s timestamp. For an off-chain review, retain the observation block and its timestamp as well as the local receipt time. The oracle staleness guide explains why heartbeat, deviation and consumer age limits answer different questions.

Save enough evidence to reproduce the ratio

Keep the two input records alongside the output. Reading both at the same identified block avoids mixing chain states, but cannot force the feeds to publish together. A later execution needs validity evaluated against its own decision context.

Worksheet fieldWhat to record
Intended resultB per A, with both assets identified
Input identitiesNetwork, two proxy addresses and each pair’s methodology
Original answersBoth exact signed integers and both feed decimal counts
Report evidenceEach round ID and update timestamp, plus observation block hash and time
Consumer policySeparate age limits, time-gap limit and rejection behavior
Calculation policyOutput decimals, intermediate scaling and rounding direction
ResultExact output integer, display units and every failed gate

Preserve each round identifier in its own feed record. The proxy phase and round-ID guide helps interpret that identity; matching identifiers across feeds should not be treated as time alignment. Before adapting this EVM tuple worksheet to another delivery path, compare Data Feeds with Data Streams and verify the applicable report format.

Exercise the implementation with the two fixtures above, reversed inputs, a valid positive ratio smaller than one output unit, and an answer that exceeds the supported arithmetic range. Test a failed read, zero answer, negative answer, future timestamp and each inclusive time boundary separately. A computed zero from a tiny valid ratio needs explicit handling; it does not establish that the asset is worthless.

Chainlink places data-quality and contingency logic with the application developer. When either input fails, retain the reason and follow the defined unavailable-data policy. Before using a derived cross-rate, make its receipt reconstruct three things: which way the division ran, how all three scales were applied, and why both original reports were accepted.

Keep the context with the example.

Each worked example is illustrative, with its sources and assumptions in the article. Read our editorial standards for authorship, verification and corrections.