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

Timestamp Manipulation in update_price() Function

Summary

The ScrvusdOracleV2.vy contract's update_price() function accepts a user-provided timestamp (_ts) without validation. This timestamp is directly used in price calculations and stored in the contract state. Since there are no bounds or sanity checks on this timestamp, it could potentially be manipulated to influence price calculations, even though access to this function is protected by role-based access control.

Vulnerability Details

Severity: Medium

Files Affected:

  • contracts/scrvusd/oracles/ScrvusdOracleV2.vy

Functions Affected:

  • update_price()

In the oracle contract, the update_price() function accepts a timestamp parameter that is used for critical price calculations:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Allowing same block updates for fixing bad blockhash provided (if possible)
assert self.last_block_number <= _block_number, "Outdated"
self.last_block_number = _block_number
self.last_prices = [self._price_v0(), self._price_v1(), self._price_v2()]
self.last_update = block.timestamp
ts: uint256 = self.price_params_ts
current_price: uint256 = self._raw_price(ts, ts)
self.price_params = PriceParams(
total_debt=_parameters[0],
total_idle=_parameters[1],
total_supply=_parameters[2],
full_profit_unlock_date=_parameters[3],
profit_unlocking_rate=_parameters[4],
last_profit_update=_parameters[5],
balance_of_self=_parameters[6],
)
self.price_params_ts = _ts # <-- Storing the user-provided timestamp
new_price: uint256 = self._raw_price(_ts, _ts) # <-- Using it for price calculation
log PriceUpdate(new_price, _ts, _block_number)
if new_price > current_price:
return (new_price - current_price) * 10**18 // current_price
return (current_price - new_price) * 10**18 // current_price

While the function has access control (access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)), it doesn't:

  • Validate that _ts is reasonably close to block.timestamp

  • Check that _ts isn't in the future

  • Ensure _ts isn't older than self.price_params.last_profit_update

Additionally, this timestamp is used in price calculations through the _raw_price() function:

@view
def _raw_price(ts: uint256, parameters_ts: uint256) -> uint256:
"""
@notice Price replication from scrvUSD vault
"""
parameters: PriceParams = self._obtain_price_params(parameters_ts)
return self._total_assets(parameters) * 10**18 // self._total_supply(parameters, ts)

The timestamp affects how unlocked shares are calculated, which in turn impacts the total supply used in the price denominator:

@view
def _total_supply(p: PriceParams, ts: uint256) -> uint256:
# Need to account for the shares issued to the vault that have unlocked.
return p.total_supply - self._unlocked_shares(
p.full_profit_unlock_date,
p.profit_unlocking_rate,
p.last_profit_update,
p.balance_of_self,
ts, # This is where the timestamp is used
)

Impact

The timestamp manipulation vulnerability could lead to several issues:

  1. Price Manipulation: By providing carefully chosen timestamps, an authorized caller could influence the price calculation, potentially causing prices to be higher or lower than they should be.

  2. Unlocked Shares Calculation: The timestamp directly affects how many shares are considered "unlocked" in the _unlocked_shares() function, which impacts the total supply used in price calculations.

  3. Inconsistent Protocol State: Using manipulated timestamps could create inconsistencies between the oracle's view of the protocol state and the actual protocol state.

  4. Financial Exploitation: Since the price data from this oracle may be used for financial decisions (like asset valuations, liquidations, etc.), inaccurate prices could lead to direct financial losses for users.

  5. Historical Data Manipulation: A malicious actor could retroactively change how prices are calculated for past periods by using timestamps that don't align with the actual block times.

While the risk is mitigated somewhat by the role-based access control, it remains a significant concern as even authorized callers (including the verifier contracts) could potentially manipulate prices through timestamp selection.

Tools Used

  • Manual code review

Recommendations

  1. Add Timestamp Validation:

    • Implement bounds checking to ensure the provided timestamp is reasonable:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Add timestamp validation
assert _ts <= block.timestamp, "Timestamp in future"
assert block.timestamp - _ts <= 1 hours, "Timestamp too old"
assert _ts >= self.price_params.last_profit_update, "Timestamp before last update"
# Rest of the implementation
  1. Use Block Timestamp Instead:

    • Consider using the block's timestamp rather than a user-provided one:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _block_number: uint256
) -> uint256:
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Use block timestamp instead of parameter
_ts: uint256 = block.timestamp
# Rest of the implementation with _ts replaced by block.timestamp
  1. Verify Timestamp Against Block:

    • If the timestamp must be provided (e.g., to match the source block), verify it against block data:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
access_control._check_role(PRICE_PARAMETERS_VERIFIER, msg.sender)
# Verify timestamp against block (assuming a function to get block timestamp)
block_ts: uint256 = self._get_block_timestamp(_block_number)
assert _ts == block_ts, "Timestamp doesn't match block"
# Rest of the implementation
  1. Add Circuit Breaker for Suspicious Price Changes:

    • Implement a circuit breaker that can be triggered if price changes exceed certain thresholds:

@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
# Existing implementation
price_change: uint256 = 0
if new_price > current_price:
price_change = (new_price - current_price) * 10**18 // current_price
else:
price_change = (current_price - new_price) * 10**18 // current_price
# Circuit breaker for large price changes
if price_change > MAX_PRICE_CHANGE:
self._trigger_circuit_breaker()
log SuspiciousPriceChange(new_price, current_price, price_change)
return price_change
  1. Event Emission for Monitoring:

    • Add more detailed events to facilitate off-chain monitoring:

# Add a new event for timestamp tracking
event PriceUpdateWithTime:
new_price: uint256
oracle_ts: uint256 # Time when update was processed
param_ts: uint256 # Provided timestamp
block_number: uint256
@external
def update_price(
_parameters: uint256[ALL_PARAM_CNT], _ts: uint256, _block_number: uint256
) -> uint256:
# Existing implementation
# Log more detailed information
log PriceUpdateWithTime(new_price, block.timestamp, _ts, _block_number)
# Return price change

By implementing these recommendations, particularly the timestamp validation bounds, the contract would be more resilient against potential manipulation of price calculations through timestamp selection.

Updates

Lead Judging Commences

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

[invalid] finding-missing-proof-content-validation

- See [here]([https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle)](https://github.com/CodeHawks-Contests/2025-03-curve?tab=readme-ov-file#blockhash-oracle) on how it is used to verify storage variable - All state roots and proofs must be verified by the OOS `StateProofVerifier` inherited as `Verifier` (where the price values and params are extracted), so there is no proof that manipulating timestamp/inputs can affect a price update - It is assumed that the OOS prover will provide accurate data and the OOS verifier will verify the prices/max unlock time to be within an appropriate bound/values - There is a account existance check in L96 of `ScrvusdVerifierV1.sol`, in which the params for price updates are extracted from

Support

FAQs

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