Summary
The ScrvusdOracleV2.vy
contract suffers from significant precision loss in price calculations due to non-standard division ordering in multiple functions. This can lead to incorrect price calculations, especially for large numbers or when dealing with small price differences.
Vulnerability Details
In ScrvusdOracleV2.vy
, several price calculation functions perform division operations in a way that can lead to precision loss:
In _raw_price()
:
return self._total_assets(parameters) * 10**18
In price inversion calculations:
return self._price_v1() if _i == 0 else 10**36
In _smoothed_price()
:
max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price
)
Critical issues:
Division before multiplication in price calculations
Potential for truncation in intermediate results
Non-standardized scaling factors across calculations
Missing precision guards for edge cases
Inconsistent rounding behavior
Impact
-
Financial Impact:
Incorrect price calculations leading to mispriced assets
Accumulating errors in long-term price tracking
Potential for arbitrage due to precision mismatches
-
System Risks:
Loss of funds due to incorrect liquidations
Unfair trading opportunities
System instability during high-value transactions
Proof of Concept
def demonstrate_precision_loss():
oracle = ScrvusdOracleV2.deploy()
large_assets = 10**30
large_supply = 10**12
price1 = large_assets * 10**18 // large_supply
small_assets = 100
small_supply = 10**18
price2 = small_assets * 10**18 // small_supply
price = 10**18
inverted = 10**36 // price
re_inverted = 10**36 // inverted
assert price != re_inverted
Tools Used
Manual code review
Mathematical analysis
Recommendations
Implement Safe Price Calculation Library:
@view
def _safe_price_calc(
numerator: uint256,
denominator: uint256,
scale: uint256
) -> uint256:
"""
@notice Safely calculate price with proper scaling
@param numerator The top number in division
@param denominator The bottom number in division
@param scale The desired decimal scale
"""
assert denominator != 0, "Division by zero"
# Handle small numbers
if numerator < scale:
return (numerator * scale)
# Handle large numbers
return (numerator
Implement Price Normalization:
@view
def _normalize_price(
price: uint256,
input_decimals: uint256,
output_decimals: uint256
) -> uint256:
"""
@notice Normalize price between different decimal scales
"""
if input_decimals == output_decimals:
return price
if input_decimals > output_decimals:
diff: uint256 = input_decimals - output_decimals
return price
diff: uint256 = output_decimals - input_decimals
return price * 10**diff
Add Precision Guards:
struct PrecisionConfig:
min_price: uint256
max_price: uint256
decimals: uint256
@view
def _check_price_precision(
price: uint256,
config: PrecisionConfig
) -> bool:
"""
@notice Verify price is within acceptable precision bounds
"""
assert price >= config.min_price, "Price below minimum"
assert price <= config.max_price, "Price above maximum"
# Check significant digits
significant_digits: uint256 = 0
temp: uint256 = price
while temp > 0:
temp = temp // 10
significant_digits += 1
assert significant_digits <= config.decimals, "Too many significant digits"
return True
Implement Revised Price Calculations:
@view
def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
"""
@notice Price replication with improved precision
"""
parameters: PriceParams = self._obtain_price_params(parameters_ts)
total_assets: uint256 = self._total_assets(parameters)
total_supply: uint256 = self._total_supply(parameters, ts)
return self._safe_price_calc(
total_assets,
total_supply,
10**18
)
@view
def _invert_price(price: uint256) -> uint256:
"""
@notice Safely invert price maintaining precision
"""
assert price != 0, "Cannot invert zero price"
# Use normalized calculation
PRICE_SCALE: constant(uint256) = 10**18
return self._safe_price_calc(
PRICE_SCALE * PRICE_SCALE,
price,
PRICE_SCALE
)
Add Price Validation Checks:
@view
def _validate_price_calculation(
price: uint256,
previous_price: uint256
) -> bool:
"""
@notice Validate price calculation results
"""
# Configuration
config: PrecisionConfig = PrecisionConfig({
min_price: 10**12, # 0.000001 in 18 decimals
max_price: 10**24, # 1,000,000 in 18 decimals
decimals: 18
})
# Basic validation
assert self._check_price_precision(price, config)
if previous_price > 0:
max_change: uint256 = previous_price * 2 # 100% change
min_change: uint256 = previous_price
assert price <= max_change, "Price change too large"
assert price >= min_change, "Price change too small"
return True
These improvements provide:
Safe mathematical operations
Consistent precision handling
Proper decimal scaling
Validation checks
Protection against edge cases
The implementation should use all these mechanisms together to ensure accurate and safe price calculations while maintaining precision throughout all operations.