Skip to content
Request an audit

‹ All findings

API3 oracle timestamp can be set to future timestamp and block API3 Oracle usage to make code revert in underflow

MediumINIT Capital·Code4rena · Money market · 15th December, 2023CodebaseM-11

In the Api3OracleReader.sol, the code assumes tha the timestamp returned from oracle is always in the past.

    function getPrice_e36(address _token) external view returns (uint price_e36) {
        // load and check
        DataFeedInfo memory dataFeedInfo = dataFeedInfos[_token];
        _require(dataFeedInfo.dataFeedId != bytes32(0), Errors.DATAFEED_ID_NOT_SET);
        _require(dataFeedInfo.maxStaleTime != 0, Errors.MAX_STALETIME_NOT_SET);

        // get price and token's decimals
        uint decimals = uint(IERC20Metadata(_token).decimals());
        // return price per token with 1e18 precisions
        // e.g. 1 BTC = 35000 * 1e18 in USD_e18 unit
        (int224 price, uint timestamp) = IApi3ServerV1(api3ServerV1).readDataFeedWithId(dataFeedInfo.dataFeedId);

        // check if the last updated is not longer than the max stale time
        _require(block.timestamp - timestamp Note the check:

// e.g. 1 BTC = 35000 * 1e18 in USD_e18 unit (int224 price, uint timestamp) = IApi3ServerV1(api3ServerV1).readDataFeedWithId(dataFeedInfo.dataFeedId);

// check if the last updated is not longer than the max stale time _require(block.timestamp - timestamp If timestamp is greater than block.timestamp, the transaction will revert and block oracle lookup on APi3OracleReader.sol.

The relayer on api3 side can update both oracle price timestamp and value.

Let us go over how the price is updated in Api3 code:

The function readDataFeedWithID basically just read the data from struct _dataFeeds[beaconId], in this function

    /// @notice Reads the data feed with ID
    /// @param dataFeedId Data feed ID
    /// @return value Data feed value
    /// @return timestamp Data feed timestamp
    function _readDataFeedWithId(
        bytes32 dataFeedId
    ) internal view returns (int224 value, uint32 timestamp) {
        DataFeed storage dataFeed = _dataFeeds[dataFeedId];
        (value, timestamp) = (dataFeed.value, dataFeed.timestamp);
        require(timestamp > 0, "Data feed not initialized");
    }

When the relayer updates the oracle data, first we are calling processBeaconUpdate.

Note that is a modifier onlyValidateTimestamp.

  function processBeaconUpdate(
        bytes32 beaconId,
        uint256 timestamp,
        bytes calldata data
    )
        internal
        onlyValidTimestamp(timestamp)
        returns (int224 updatedBeaconValue)
    {
        updatedBeaconValue = decodeFulfillmentData(data);
        require(
            timestamp > _dataFeeds[beaconId].timestamp,
            "Does not update timestamp"
        );
        _dataFeeds[beaconId] = DataFeed({
            value: updatedBeaconValue,
            timestamp: uint32(timestamp)
        });
    }

The check ensures that only when the timstamp is from more than 1 hour, the update reverts.

    /// @dev Reverts if the timestamp is from more than 1 hour in the future
    modifier onlyValidTimestamp(uint256 timestamp) virtual {
        unchecked {
            require(
                timestamp What does this mean?

The timestamp of an oracle can be set to the future within an hour, the relayer does not have to be malicious, it is a normal part of updating data.

Suppose the current timestamp is 10000

1 hour = 3600 seconds

If the relayer set timestamp to 12000, the price update will go through

But the code:

_require(block.timestamp - timestamp Will revert when current timestamp is 10001.

10001 - 12000 will revert in underflow.

Recommended Mitigation Steps

If the oracle timestamp comes from the future, the code should consider it not stale.

Can change the code to:

if (block.timestamp > timestamp) {
 _require(block.timestamp - timestamp **[fez-init (INIT) confirmed](https://github.com/code-423n4/2023-12-initcapital-findings/issues/4#issuecomment-1869777419)**