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 10 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.

Give us feedback!