#37276 [SC-Medium] Redstone's price feed is used incorrectly.

Submitted on Dec 1st 2024 at 11:03:07 UTC by @jasonxiale for IOP | Fluid Protocol

  • Report ID: #37276

  • Report Type: Smart Contract

  • Report severity: Medium

  • Target: https://github.com/Hydrogen-Labs/fluid-protocol/tree/main/contracts/oracle-contract/src/main.sw

  • Impacts:

    • Direct theft of any user funds, whether at-rest or in-motion, other than unclaimed yield

Description

Brief/Intro

In current implementation for oracle-contract.get_price, while function is_pyth_price_stale_or_outside_confidence return true in main.sw#L88, Redstone price feed will be used.

However there is a flaw when using Redstone price feed that might lead stale price being used.

Vulnerability Details

According to oracle-contract.get_price, when redstone is used, redstone.read_timestamp will be used as the price 's publish timestamp.

And redstone.read_timestamp will be checked against with current_time in main.sw#L107

The issue is that redstone.read_timestamp uses Unix timestamp, but Fule's timestamp function returns TAI64 format timestamp

 63 /// Get the TAI64 timestamp of a block at a given `block_height`.
 64 ///
 65 /// # Additional Information
 66 ///
 67 /// The TAI64 timestamp begins at 2^62 seconds before 1970, and ends at 2^62 seconds after 1970,
 68 /// with a TAI second defined as the duration of 9192631770 periods of the radiation corresponding
 69 /// to the transition between the two hyperfine levels of the ground state of the cesium atom.
 70 ///
 71 /// # Arguments
 72 ///
 73 /// * `block_height`: [u32] - The height of the block to get the timestamp of.
 74 ///
 75 /// # Returns
 76 ///
 77 /// * [u64] - The TAI64 timestamp of the block at `block_height`.
 78 ///
 79 /// # Examples
 80 ///
 81 /// ```sway
 82 /// use std::block::timestamp_of_block;
 83 ///
 84 /// fn foo() {
 85 ///     let timestamp_of_block_100 = timestamp_of_block(100u32);
 86 ///     log(timestamp_of_block_100);
 87 /// }
 88 /// ```
 89 pub fn timestamp_of_block(block_height: u32) -> u64 {
 90     asm(timestamp, height: block_height) {
 91         time timestamp height;
 92         timestamp: u64
 93     }
 94 }

From above code, we can see that timestamp() returns [The TAI64 timestamp of the current block.] (https://github.com/FuelLabs/sway/blob/6d9065b8d762a39eb475562426a2d4ed17d92d00/sway-lib-std/src/block.sw#L47)

  1. According to redstone_adapter.read_timestamp, the function uses storage.timestamp as return value.

  2. And storage.timestamp is written by redstone_adapter.sw#L139 in redstone_adapter.overwrite_prices function.

  3. redstone_adapter.overwrite_prices is called by redstone_adapter.write_prices in redstone_adapter.sw#L96

  4. Now we'll check how timestamp is returned in redstone_adapter.sw#L95

  5. In redstone_adapter.process_payload, a config var is created with config.block_timestamp set to get_unix_timestamp() get_unix_timestamp() is defined as:

pub fn get_unix_timestamp() -> u64 {
    timestamp() - TAI64_UNIX_ADJUSTMENT
}

From here, we know that config.block_timestamp is UNIX time.

  1. Back to redstone_adapter.process_payload, in redstone_adapter.sw#L150, process_input is called, and process_input is defined in processor.process_input.

 28 pub fn process_input(bytes: Bytes, config: Config) -> (Vec<u256>, u64) {
 29     config.check_parameters();
 30 
 31     let payload = Payload::from_bytes(bytes);
 32     let timestamp = config.validate_timestamps(payload); <<<--- Here we only care timestamp
 33 
 34     let matrix = get_payload_result_matrix(payload, config);
 35     let results = get_feed_values(matrix, config);
 36 
 37     config.validate_signer_count(results);
 38     (results.aggregated(), timestamp)
 39 }
  1. In processor.sw#L32, timestamp is returned by config.validate_timestamps function.

  2. config.validate_timestamps is defined in config_validation.sw#L27-L43

 27     fn validate_timestamps(self, payload: Payload) -> u64 {
 28         let first_timestamp = payload.data_packages.get(0).unwrap().timestamp;
 29         validate_timestamp(first_timestamp, self.block_timestamp * 1000);
 30 
... 
41 
 42         first_timestamp
 43     }
  1. In config_validation.sw#L28-L29, the payload's timestamp and self.block_timestamp(which is UNIX timestamp) are passed to validate_timestamp function.

  2. According to validate_timestamp's implemention, we can get see that payload's timestamp in step9 should be UNIX timestamp format.

  7 pub fn validate_timestamp(timestamp: u64, block_timestamp: u64) {
  8     if (block_timestamp > timestamp) {
  9         require(
 10             block_timestamp - timestamp <= MAX_DATA_TIMESTAMP_DELAY_SECONDS * 1000,
 11             RedStoneError::TimestampOutOfRange((false, block_timestamp, timestamp)),
 12         );
 13     }