5/5
## Handling Value Conversion and Precision with Integer Division in Vyper When building smart contracts, especially those interacting with real-world values like currency prices, accurately handling numbers with different decimal precisions is crucial. Ethereum uses Wei (1 ETH = 10<sup>18</sup> Wei) for its native currency, giving it 18 decimal places. However, external data sources, like Chainlink price feeds for ETH/USD, often use a different number of decimals (e.g., 8). This lesson explains how to perform calculations involving these different precisions using scaling and integer division in Vyper. **The Challenge: Comparing Apples and Oranges (Different Precisions)** Imagine a smart contract function, perhaps `fund()`, where users send ETH (`msg.value`). You want to ensure they send *at least* a certain USD value, say $5. How do you compare the ETH sent (in Wei, 18 decimals) with a USD target? You need the current ETH/USD price. Let's say a Chainlink price feed provides this, but it returns the price with 8 decimals. For example, if 1 ETH = $3365.51, the feed might return the integer `336551000000`. You cannot directly multiply `msg.value` by this price feed value because their implied decimal points don't align. **Aligning Precision: Scaling the Price Feed Data** To perform meaningful multiplication, we need both numbers to effectively have the same number of decimal places. Since ETH/Wei uses 18 decimals, a common practice is to adjust the price feed value to also represent 18 decimals. If the price feed has 8 decimals and we need 18, we need to add 10 decimal places. We do this by multiplying the price feed value by 10<sup>10</sup>. ```vyper # Assume self.price_feed.latestAnswer() returns the ETH/USD price with 8 decimals price: int256 = staticcall self.price_feed.latestAnswer() # e.g., 336551000000 ($3365.51) # Convert to uint256 and scale up to 18 decimals # We need 18 decimals total, the price has 8, so multiply by 10**(18-8) = 10**10 eth_price_scaled_to_18_decimals: uint256 = convert(price, uint256) * (10**10) # eth_price_scaled_to_18_decimals is now e.g., 3365510000000000000000 ``` Now, `eth_price_scaled_to_18_decimals` represents the price of 1 ETH in USD, but scaled up so that it has 18 decimal places, just like Wei. Its effective unit is `(USD * 10**18) / (ETH * 10**18)`, or simply `ScaledUSD / Wei`. **Calculating the Equivalent Value: Multiplication and Precision Adjustment** With both the ETH amount (`eth_amount`, typically `msg.value` in Wei) and the price (`eth_price_scaled_to_18_decimals`) effectively using 18 decimal places, we can multiply them to find the equivalent USD value. `eth_amount (Wei)` * `eth_price_scaled_to_18_decimals (ScaledUSD / Wei)` = `Result (ScaledUSD)` However, there's a catch: when you multiply two numbers that are scaled by 10<sup>18</sup>, the resulting number is scaled by 10<sup>18</sup> * 10<sup>18</sup> = 10<sup>36</sup>. This result has far too much precision. We typically want our final USD value to also be represented with 18 decimals (like "USD Wei") for consistency. **Introducing Integer Division (`//`)** To reduce the precision from 36 decimals back down to the desired 18 decimals, we use integer division (`//`). Integer division in Vyper (and Python) performs division and *discards* any fractional part (remainder), returning only the whole number (integer) part. It **truncates**, it does not round. We divide the high-precision result of our multiplication by 10<sup>18</sup>: ```vyper # eth_amount has 18 decimals (Wei) # eth_price_scaled_to_18_decimals has 18 decimals (ScaledUSD / Wei) eth_amount: uint256 = msg.value # Example input # Multiplication results in a number with 18 + 18 = 36 decimals product: uint256 = eth_price_scaled_to_18_decimals * eth_amount # Use integer division to reduce precision from 36 back to 18 # Divide by 10**18 to remove the extra 18 decimal places eth_amount_in_usd_18_decimals: uint256 = product // (10**18) # return eth_amount_in_usd_18_decimals ``` **Understanding Integer Division (`//`) Behavior** It's crucial to understand that `//` truncates. Consider these examples: * `4 // 3` results in `1` (because 4 / 3 = 1.333...) * `6 // 3` results in `2` (because 6 / 3 = 2.0) * `7 // 3` results in `2` (because 7 / 3 = 2.333...) * `8 // 3` results in `2` (because 8 / 3 = 2.666...) You can test this directly in Vyper: ```vyper @external @view def divide_me(number: uint256) -> uint256: # Returns the integer part of number / 3 return number // 3 ``` Calling `divide_me(7)` will return `2`. This deterministic truncation is essential in smart contracts, where floating-point arithmetic is avoided due to potential non-deterministic behavior across different machines, which would break consensus. Fixed-point arithmetic, using integers scaled by powers of 10 and adjusted with integer division, provides the necessary precision and determinism. **Putting It All Together: The `fund()` Example** Now we can implement the logic in our `fund()` function to check if the received ETH meets the minimum USD requirement. Let's assume `self.minimum_usd` is also stored with 18 decimals (e.g., `5 * (10**18)` for $5). ```vyper # Internal helper function to perform the conversion @internal @view def _get_eth_to_usd_rate(_eth_amount: uint256) -> uint256: price: int256 = staticcall self.price_feed.latestAnswer() # 8 decimals eth_price_scaled: uint256 = convert(price, uint256) * (10**10) # Scale to 18 decimals # Multiply amount (18 dec) by price (18 dec) -> 36 decimals # Divide by 10**18 using integer division to get 18 decimals usd_value: uint256 = (eth_price_scaled * _eth_amount) // (10**18) return usd_value # Function where users send ETH @payable @external def fund(): # Calculate the USD value of the ETH sent (msg.value) usd_value_of_eth: uint256 = self._get_eth_to_usd_rate(msg.value) # Ensure the USD value meets the minimum requirement (assuming minimum_usd also has 18 decimals) assert usd_value_of_eth >= self.minimum_usd, "You must spend more ETH!" # ... (rest of the funding logic) ... ``` **Key Takeaways** 1. **Precision Awareness:** Always know the number of decimal places your numerical values represent (e.g., Wei vs. price feeds). 2. **Scaling for Alignment:** Before multiplying or dividing numbers with different precisions, scale one or both by multiplying with powers of 10 (`10**N`) to align their effective decimal points. 3. **Multiplication Increases Precision:** Multiplying two numbers scaled by 10<sup>X</sup> and 10<sup>Y</sup> results in a number scaled by 10<sup>X+Y</sup>. 4. **Integer Division (`//`) for Reduction:** Use `// (10**N)` to reduce the precision of a scaled number, discarding the fractional part (truncation). 5. **Determinism:** This fixed-point arithmetic approach ensures deterministic results suitable for the blockchain environment. 6. **Order of Operations:** Use parentheses `()` to ensure multiplication and division occur in the intended order, especially in complex expressions like `(a * b) // c`. By carefully managing decimal precision using scaling and integer division, you can perform accurate and safe value conversions and comparisons within your Vyper smart contracts.
When building smart contracts, especially those interacting with real-world values like currency prices, accurately handling numbers with different decimal precisions is crucial. Ethereum uses Wei (1 ETH = 1018 Wei) for its native currency, giving it 18 decimal places. However, external data sources, like Chainlink price feeds for ETH/USD, often use a different number of decimals (e.g., 8). This lesson explains how to perform calculations involving these different precisions using scaling and integer division in Vyper.
The Challenge: Comparing Apples and Oranges (Different Precisions)
Imagine a smart contract function, perhaps fund()
, where users send ETH (msg.value
). You want to ensure they send at least a certain USD value, say $5. How do you compare the ETH sent (in Wei, 18 decimals) with a USD target?
You need the current ETH/USD price. Let's say a Chainlink price feed provides this, but it returns the price with 8 decimals. For example, if 1 ETH = $3365.51, the feed might return the integer 336551000000
. You cannot directly multiply msg.value
by this price feed value because their implied decimal points don't align.
Aligning Precision: Scaling the Price Feed Data
To perform meaningful multiplication, we need both numbers to effectively have the same number of decimal places. Since ETH/Wei uses 18 decimals, a common practice is to adjust the price feed value to also represent 18 decimals.
If the price feed has 8 decimals and we need 18, we need to add 10 decimal places. We do this by multiplying the price feed value by 1010.
Now, eth_price_scaled_to_18_decimals
represents the price of 1 ETH in USD, but scaled up so that it has 18 decimal places, just like Wei. Its effective unit is (USD * 10**18) / (ETH * 10**18)
, or simply ScaledUSD / Wei
.
Calculating the Equivalent Value: Multiplication and Precision Adjustment
With both the ETH amount (eth_amount
, typically msg.value
in Wei) and the price (eth_price_scaled_to_18_decimals
) effectively using 18 decimal places, we can multiply them to find the equivalent USD value.
eth_amount (Wei)
* eth_price_scaled_to_18_decimals (ScaledUSD / Wei)
= Result (ScaledUSD)
However, there's a catch: when you multiply two numbers that are scaled by 1018, the resulting number is scaled by 1018 * 1018 = 1036. This result has far too much precision. We typically want our final USD value to also be represented with 18 decimals (like "USD Wei") for consistency.
Introducing Integer Division (//
)
To reduce the precision from 36 decimals back down to the desired 18 decimals, we use integer division (//
). Integer division in Vyper (and Python) performs division and discards any fractional part (remainder), returning only the whole number (integer) part. It truncates, it does not round.
We divide the high-precision result of our multiplication by 1018:
Understanding Integer Division (//
) Behavior
It's crucial to understand that //
truncates. Consider these examples:
4 // 3
results in 1
(because 4 / 3 = 1.333...)
6 // 3
results in 2
(because 6 / 3 = 2.0)
7 // 3
results in 2
(because 7 / 3 = 2.333...)
8 // 3
results in 2
(because 8 / 3 = 2.666...)
You can test this directly in Vyper:
Calling divide_me(7)
will return 2
.
This deterministic truncation is essential in smart contracts, where floating-point arithmetic is avoided due to potential non-deterministic behavior across different machines, which would break consensus. Fixed-point arithmetic, using integers scaled by powers of 10 and adjusted with integer division, provides the necessary precision and determinism.
Putting It All Together: The fund()
Example
Now we can implement the logic in our fund()
function to check if the received ETH meets the minimum USD requirement. Let's assume self.minimum_usd
is also stored with 18 decimals (e.g., 5 * (10**18)
for $5).
Key Takeaways
Precision Awareness: Always know the number of decimal places your numerical values represent (e.g., Wei vs. price feeds).
Scaling for Alignment: Before multiplying or dividing numbers with different precisions, scale one or both by multiplying with powers of 10 (10**N
) to align their effective decimal points.
Multiplication Increases Precision: Multiplying two numbers scaled by 10X and 10Y results in a number scaled by 10X+Y.
Integer Division (//
) for Reduction: Use // (10**N)
to reduce the precision of a scaled number, discarding the fractional part (truncation).
Determinism: This fixed-point arithmetic approach ensures deterministic results suitable for the blockchain environment.
Order of Operations: Use parentheses ()
to ensure multiplication and division occur in the intended order, especially in complex expressions like (a * b) // c
.
By carefully managing decimal precision using scaling and integer division, you can perform accurate and safe value conversions and comparisons within your Vyper smart contracts.
An essential guide to Handling Value Conversion and Precision with Integer Division in Vyper - Master fixed-point arithmetic in Vyper by scaling values with different decimals (like Wei and oracle prices) and using integer division (`//`) to manage precision. Ensure accurate and deterministic value comparisons and calculations in your smart contracts.
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