5/5
## Handling Decimal Precision in Smart Contracts: Normalizing Price Feed Data Smart contracts often need to interact with real-world data, such as asset prices provided by oracles like Chainlink. However, a common challenge arises when performing calculations involving values that use different fixed-point decimal precisions. This lesson explores this issue using a practical example: converting an Ether (ETH) amount to its US Dollar (USD) equivalent within a Vyper smart contract. ### The Challenge: Mismatched Decimal Precision Smart contracts primarily operate using integers. To represent fractional values like currency amounts, they rely on a system of fixed-point arithmetic, where a number represents a value scaled by a certain power of 10. For instance, $10.50 might be represented as the integer `1050` with an understanding that there are 2 decimal places. Consider a "Buy Me a Coffee" style smart contract where users send ETH, and the contract must verify if the received amount meets a minimum threshold specified in USD (e.g., $5). To do this, the contract needs the current ETH/USD exchange rate. We can fetch this rate using a Chainlink Price Feed. Let's look at how we might retrieve the price in Vyper: ```vyper # Interface for the Chainlink Price Feed (AggregatorV3Interface) interface PriceFeed: def latestAnswer() -> int256: view def decimals() -> uint8: view # Assuming self.price_feed is an instance of the PriceFeed interface # @internal - This is an internal function, not meant for external calls def get_raw_eth_to_usd_rate(): # Fetch the latest price from the Chainlink oracle price: int256 = staticcall self.price_feed.latestAnswer() # Example raw price returned: 336551000000 # This represents the price with a specific number of decimals ``` The `latestAnswer()` function returns the price as an integer (`int256`). However, this integer represents a value with a fixed number of decimal places. By consulting the Chainlink documentation or calling the `decimals()` function on the specific ETH/USD feed contract (often viewable on Etherscan), we find it uses **8 decimal places**. So, a raw value like `336551000000` actually represents $3,365.51000000. Now, let's consider the Ether sent by the user. When a user sends ETH to a contract, the amount is accessible via `msg.value`. This value is denominated in Wei, the smallest unit of Ether. The relationship between Ether and Wei is: 1 Ether = 1,000,000,000,000,000,000 Wei (1 * 10^18 Wei) This means that `msg.value` effectively represents an Ether amount with **18 decimal places**. Here lies the problem: * The ETH/USD price from Chainlink has **8** decimals. * The ETH amount (`msg.value`) has **18** decimals (as it's in Wei). We cannot directly multiply or compare these two values because their underlying scales are different. Performing arithmetic operations without aligning their precision would lead to significantly incorrect results. ### The Solution: Precision Normalization To perform accurate calculations, we must first normalize the values to a common decimal precision. In this scenario, since Ether amounts are typically handled with 18 decimals (Wei), it's often convenient to adjust the price feed data to match this 18-decimal standard. Normalization involves scaling one of the numbers so that it represents its value using the target number of decimal places. To convert the 8-decimal price to an 18-decimal representation, we need to add 10 extra decimal places (18 - 8 = 10). Mathematically, this is achieved by multiplying the raw price integer by 10 raised to the power of 10 (`10 ** 10`). Let's modify our function to accept the Ether amount (in Wei) and perform this normalization: ```vyper # Interface for the Chainlink Price Feed (AggregatorV3Interface) interface PriceFeed: def latestAnswer() -> int256: view def decimals() -> uint8: view # Assuming self.price_feed is an instance of the PriceFeed interface # @internal def get_eth_to_usd_rate_normalized(eth_amount: uint256): # eth_amount is in Wei (18 decimals) # Fetch the raw price from Chainlink raw_price: int256 = staticcall self.price_feed.latestAnswer() # This raw_price has 8 decimals as confirmed by the feed's decimals() function. # Example: 336551000000 represents $3,365.51000000 # Normalize the price to 18 decimals to match Wei's precision. # We need to add (18 - 8) = 10 decimal places. # Multiply by 10**10. # Note: Ensure intermediate calculations don't overflow. # We cast raw_price to uint256 assuming the price is positive. # Handle potential negative prices if necessary based on oracle behavior. normalized_eth_price: uint256 = convert(raw_price, uint256) * (10**10) # normalized_eth_price now represents the ETH/USD rate scaled to 18 decimals. # Example: 336551000000 * 10**10 = 3365510000000000000000 # Now, normalized_eth_price (18 decimals) and eth_amount (18 decimals) # have the same precision and can be used in further calculations, # such as finding the USD value of eth_amount: # usd_value = (eth_amount * normalized_eth_price) / (10**18) ``` In the code above: 1. We fetch the `raw_price` which has 8 decimals. 2. We calculate the required scaling factor: `10 ** (18 - 8)` which is `10 ** 10`. 3. We multiply the `raw_price` (after converting to `uint256` for compatibility, assuming a positive price) by `10 ** 10`. 4. The result, `normalized_eth_price`, now represents the ETH price per USD, but scaled up as if it had 18 decimal places. With both the `eth_amount` (in Wei) and the `normalized_eth_price` represented using 18 decimals, we can now proceed with further calculations, such as determining the USD value of the received Ether, knowing that the units are compatible. ### Key Takeaway When working with fixed-point numbers in smart contracts, especially when integrating external data sources like price feeds or interacting with different token standards (like ERC20 tokens which can have varying decimals), always verify the decimal precision of each value. Before performing arithmetic operations like multiplication, division, addition, subtraction, or comparison, ensure the numbers are normalized to the same decimal precision to prevent calculation errors and potential vulnerabilities. Multiplying a value by `10 ** N` effectively increases its precision by `N` decimal places.
Smart contracts often need to interact with real-world data, such as asset prices provided by oracles like Chainlink. However, a common challenge arises when performing calculations involving values that use different fixed-point decimal precisions. This lesson explores this issue using a practical example: converting an Ether (ETH) amount to its US Dollar (USD) equivalent within a Vyper smart contract.
Smart contracts primarily operate using integers. To represent fractional values like currency amounts, they rely on a system of fixed-point arithmetic, where a number represents a value scaled by a certain power of 10. For instance, $10.50 might be represented as the integer 1050
with an understanding that there are 2 decimal places.
Consider a "Buy Me a Coffee" style smart contract where users send ETH, and the contract must verify if the received amount meets a minimum threshold specified in USD (e.g., $5). To do this, the contract needs the current ETH/USD exchange rate.
We can fetch this rate using a Chainlink Price Feed. Let's look at how we might retrieve the price in Vyper:
The latestAnswer()
function returns the price as an integer (int256
). However, this integer represents a value with a fixed number of decimal places. By consulting the Chainlink documentation or calling the decimals()
function on the specific ETH/USD feed contract (often viewable on Etherscan), we find it uses 8 decimal places. So, a raw value like 336551000000
actually represents $3,365.51000000.
Now, let's consider the Ether sent by the user. When a user sends ETH to a contract, the amount is accessible via msg.value
. This value is denominated in Wei, the smallest unit of Ether. The relationship between Ether and Wei is:
1 Ether = 1,000,000,000,000,000,000 Wei (1 * 10^18 Wei)
This means that msg.value
effectively represents an Ether amount with 18 decimal places.
Here lies the problem:
The ETH/USD price from Chainlink has 8 decimals.
The ETH amount (msg.value
) has 18 decimals (as it's in Wei).
We cannot directly multiply or compare these two values because their underlying scales are different. Performing arithmetic operations without aligning their precision would lead to significantly incorrect results.
To perform accurate calculations, we must first normalize the values to a common decimal precision. In this scenario, since Ether amounts are typically handled with 18 decimals (Wei), it's often convenient to adjust the price feed data to match this 18-decimal standard.
Normalization involves scaling one of the numbers so that it represents its value using the target number of decimal places. To convert the 8-decimal price to an 18-decimal representation, we need to add 10 extra decimal places (18 - 8 = 10). Mathematically, this is achieved by multiplying the raw price integer by 10 raised to the power of 10 (10 ** 10
).
Let's modify our function to accept the Ether amount (in Wei) and perform this normalization:
In the code above:
We fetch the raw_price
which has 8 decimals.
We calculate the required scaling factor: 10 ** (18 - 8)
which is 10 ** 10
.
We multiply the raw_price
(after converting to uint256
for compatibility, assuming a positive price) by 10 ** 10
.
The result, normalized_eth_price
, now represents the ETH price per USD, but scaled up as if it had 18 decimal places.
With both the eth_amount
(in Wei) and the normalized_eth_price
represented using 18 decimals, we can now proceed with further calculations, such as determining the USD value of the received Ether, knowing that the units are compatible.
When working with fixed-point numbers in smart contracts, especially when integrating external data sources like price feeds or interacting with different token standards (like ERC20 tokens which can have varying decimals), always verify the decimal precision of each value. Before performing arithmetic operations like multiplication, division, addition, subtraction, or comparison, ensure the numbers are normalized to the same decimal precision to prevent calculation errors and potential vulnerabilities. Multiplying a value by 10 ** N
effectively increases its precision by N
decimal places.
An essential explanation to Handling Decimal Precision in Smart Contracts: Normalizing Price Feed Data - Delve into the common problem of mismatched decimal precision when using external data like Chainlink price feeds (8 decimals) with native blockchain values like Ether (18 decimals). Discover how to normalize these values in Vyper to enable accurate calculations.
Previous lesson
Previous
Next lesson
Next
Give us feedback
Course Overview
About the course
The basics of blockchain transactions, how to send and receive money on a blockchain network.
How to write Python based smart contracts using Vyper.
How to read and understand Vyper smart contracts.
Vyper data structures, arrays, structs, hash maps.
How to build a smart contract application and deploy on ZKsync with Moccasin.
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
On-chain Data Analyst
$59,000 - $139,000 (avg. salary)
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Last updated on June 11, 2025
Duration: 2h 08min
Duration: 2h 32min
Duration: 24min
Course Overview
About the course
The basics of blockchain transactions, how to send and receive money on a blockchain network.
How to write Python based smart contracts using Vyper.
How to read and understand Vyper smart contracts.
Vyper data structures, arrays, structs, hash maps.
How to build a smart contract application and deploy on ZKsync with Moccasin.
Smart Contract Auditor
$100,000 - $200,000 (avg. salary)
On-chain Data Analyst
$59,000 - $139,000 (avg. salary)
DeFi Developer
$75,000 - $200,000 (avg. salary)
Smart Contract Engineer
$100,000 - $150,000 (avg. salary)
Web3 developer
$60,000 - $150,000 (avg. salary)
Web3 Developer Relations
$85,000 - $125,000 (avg. salary)
Last updated on June 11, 2025