DeFiLayer 1Layer 2
14,723 OP
View results
Submission Details
Severity: medium
Invalid

Precision Loss in Price Calculation Due to Integer Division

Summary

The ScrvusdOracleV2.vy::\_raw\_price uses integer division (//) to compute the price as (self._total_assets(parameters) * 10**18) // self._total_supply(parameters, ts). This operation truncates fractional remainders, resulting in a loss of precision in the calculated price. While the impact is reduced in typical stableswap-ng pools with large values, it remains a notable design flaw that could affect accuracy in scenarios with smaller asset or supply values.

Vulnerability Details

The _raw_price function calculates the price per share by multiplying total\_assets by 10**18 (to scale to 18 decimals) and then dividing by total_supply using Vyper’s integer division operator (//). Integer division discards any remainder, unlike floating-point division, which retains fractional parts.

This truncation occurs because Vyper lacks native fixed-point arithmetic support, unlike Solidity libraries such as PRBMath. The result feeds into price methods (_price_v0, _price_v1, _price_v2), and while _smoothed_price mitigates sudden jumps, it doesn’t correct the underlying precision loss in _raw_price. The POC test demonstrates this by comparing the contract’s output to a higher-precision calculation scaled by 10**36.

https://github.com/CodeHawks-Contests/2025-03-curve/blob/198820f0c30d5080f75073243677ff716429dbfd/contracts/scrvusd/oracles/ScrvusdOracleV2.vy#L140-L152

@view
@external
def raw_price(
_i: uint256 = 0, _ts: uint256 = block.timestamp, _parameters_ts: uint256 = block.timestamp
) -> uint256:
"""
@notice Get approximate `scrvUSD.pricePerShare()` without smoothening
@param _i 0 (default) for `pricePerShare()` and 1 for `pricePerAsset()`
@param _ts Timestamp at which to see price (only near period is supported)
"""
p: uint256 = self._raw_price(_ts, _parameters_ts)
return p if _i == 0 else 10**36 // p

Add this function to tests/scrvusd/oracle/unitary/test_v2.py:

def test_precision_loss_with_small_values(soracle, verifier):
"""
Test to demonstrate precision loss in _raw_price with small values using integer comparisons.
"""
# Set up small values where precision loss is evident
total_assets = 100
total_supply = 51
# Calculate expected price with integer division (mimics contract behavior)
expected_price = (total_assets * 10**18) // total_supply
# Calculate a higher precision price by scaling further before division
higher_precision_price = (total_assets * 10**36) // total_supply
# Update the oracle with mock parameters
# Assuming PriceParams order: [total_debt, total_idle, total_supply, full_profit_unlock_date, ...]
# Set total_debt = 0, total_idle = total_assets to make total_assets = total_idle
ts = boa.env.evm.patch.timestamp
parameters = [0, total_assets, total_supply, ts + 7 * 86400, 0, 0, 0]
with boa.env.prank(verifier):
soracle.update_price(parameters, ts, 1)
# Fetch the raw price from the oracle
raw_price = soracle.raw_price()
# Verify that the contract's raw price matches the expected integer division result
assert raw_price == expected_price, f"Expected {expected_price}, got {raw_price}"
# Check for a remainder to confirm precision loss occurs
remainder = (total_assets * 10**18) % total_supply
assert remainder != 0, "No precision loss possible with these values (division is exact)"
print(f"remainder(10^18 precision): {remainder}")
# Demonstrate precision loss: scaling raw_price should be less than higher_precision_price
# because integer division truncated the result
assert raw_price * 10**18 < higher_precision_price, "Precision loss not detected"
# Optional debugging output
print(f"Contract raw price (10^18 precision): {raw_price}")
print(f"Higher precision price (scaled back from 10^36): {higher_precision_price / 10**18}")

Then run: pytest -s tests/scrvusd/oracle/unitary/test_v2.py

output:

Audit-Github/2025-03-curve$ pytest -s tests/scrvusd/oracle/unitary/test_v2.py
============================================================= test session starts ==============================================================
collected 7 items
tests/scrvusd/oracle/unitary/test_v2.py .....Expected price (integer division): 1960784313725490196
Raw price from oracle: 1960784313725490196
Expected price (higher precision): 1960784313725490196078431372549019607
.remainder(10^18 precision): 4
Contract raw price (10^18 precision): 1960784313725490196
Higher precision price (scaled back from 10^36): 1.9607843137254902e+18

POC Output Analysis: The test output confirms the issue:

  • Expected price (integer division): 1960784313725490196

  • Raw price from oracle: 1960784313725490196

    These two numbers are identical. Why? Because:

    • The test calculates (100 * 10**18) // 51 = 1960784313725490196 in Python, mimicking the contract’s logic.

    • The contract does the same calculation internally and returns the same value.

  • Higher precision price (scaled back from 10^36): 1.9607843137254902e+18 (1960784313725490196.0784313725490196…)

    The vulnerability becomes clear when you look at the higher precision value (1960784313725490196.07843). This shows what the price could be if the contract didn’t truncate, highlighting the lost .07843 * 10^18.

Impact

The precision loss is small, and in typical pools with large values, the relative error becomes negligible. However, In edge cases with smaller pools (e.g., new or low-liquidity pools), this loss could accumulate, slightly skewing prices and affecting trading decisions or profit calculations. For instance, if a pool operates with total_assets = 100 and total_supply = 51, repeated small inaccuracies could mislead traders or liquidity providers.

Tools Used

Recommendations

Increase Scaling Factor: Multiply by a higher factor (e.g., 10**36) before division, then scale back, to retain more precision:

price = (self._total_assets(parameters) * 10**36 // self._total_supply(parameters, ts)) / 10**18

Document Precision Limits: Add comments or documentation noting the expected precision loss and advise pool integrators to account for this in edge cases.

Updates

Lead Judging Commences

0xnevi Lead Judge 5 months ago
Submission Judgement Published
Invalidated
Reason: Lack of quality
Assigned finding tags:

[invalid] finding-precision-loss

All values will be scaled to a combined of 36 decimals before division (be it price-related values or totalSupply). Considering the 18 decimals of all values, no realistic values were presented in any duplicates to proof a substantial impact on precision loss.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.