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

Unbounded Time Scaling in Oracle Smoothing Function Allows Price Manipulation

Summary

The ScrvusdOracleV2.vy oracle uses a linear time-based approximation for price smoothing that allows exploitation through timing of transactions. An attacker can delay interactions with the oracle to force larger price movements than intended, potentially allowing value extraction from stableswap pools using this oracle.

Vulnerability Details

The oracle's _smoothed_price function uses a linear approximation based on the time elapsed since the last update to calculate the maximum allowed price change:

max_change: uint256 = (
self.max_price_increment * (block.timestamp - self.last_update) * last_price // 10**18
)

The above code assumes that price changes should scale linearly with time, but lacks a cap on the maximum time delta used in calculations. This results in no minimum update frequency being enforced.

Impact

This vulnerability can lead to significant fund loss in stableswap pools that rely on this oracle for pricing. An attacker can:

  1. Wait for a period where the oracle hasn't been updated

  2. Observe the actual price change of scrvUSD during this period

  3. If the price has moved beyond what would typically be allowed, exploit the oracle's permissive bounds after the delay

  4. Execute trades against the stableswap pool at manipulated prices

  5. Extract value from the price discrepancy

Proof of Concept

  1. Add the following test function to ScrvusdOracleV2.vy:

@external
def test_time_manipulation(last_price: uint256, raw_price: uint256, time_delta: uint256) -> uint256:
# Simulate time passing
self.last_update = block.timestamp - time_delta
# Call the vulnerable function
return self._smoothed_price(last_price, raw_price)
  1. Deploy the modified contract and make these calls:

# Get the default max_price_increment (should be 2 * 10**12)
cast call [CONTRACT_ADDRESS] "max_price_increment()"
# Call test_time_manipulation with a 1-day time delta
cast call [CONTRACT_ADDRESS] "test_time_manipulation(uint256,uint256,uint256)" 1000000000000000000 1050000000000000000 86400
# Call test_time_manipulation with a 7-day time delta
cast call [CONTRACT_ADDRESS] "test_time_manipulation(uint256,uint256,uint256)" 1000000000000000000 1050000000000000000 604800

With max_price_increment = 2 * 10**12:

  • 1-day calculation: 2 * 10**12 * 86400 * 1e18 / 1e18 = 172,800,000,000,000 (0.0173%)

  • 7-day calculation: 2 * 10**12 * 604800 * 1e18 / 1e18 = 1,209,600,000,000,000 (0.121%)

With a 7-day gap, the price can move significantly more than would be allowed with frequent updates. This would allow for an attacker to time their interactions to maximize price slippage.

Tools Used

Manual Testing

Recommendations

One major fix would be to add a maximum time delta to limit the effect of long periods without updates:

@view
def _smoothed_price(last_price: uint256, raw_price: uint256) -> uint256:
# Cap the time delta to avoid excessive price movements
MAX_TIME_DELTA: constant(uint256) = 86400 # 1 day
time_delta: uint256 = min(block.timestamp - self.last_update, MAX_TIME_DELTA)
max_change: uint256 = (
self.max_price_increment * time_delta * last_price // 10**18
)
# -max_change <= (raw_price - last_price) <= max_change
if unsafe_sub(raw_price + max_change, last_price) > 2 * max_change:
return last_price + max_change if raw_price > last_price else last_price - max_change
return raw_price
Updates

Lead Judging Commences

0xnevi Lead Judge
3 months ago
0xnevi Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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