Chainlink Oracle Security Considerations

0xsaff

0xsaff

Posted on August 27, 2023

Chainlink Oracle Security Considerations

Chainlink allows smart contract developers to receive a wide variety of off-chain data, with the most commonly used features being receiving off-chain randomness and off-chain pricing data. Integrating your smart contracts with Chainlink provides a unique set of potential security vulnerabilities that attackers can exploit; here are the common vulnerabilities that smart contract developers & auditors need to look out for.

Not Checking For Stale Prices

Many smart contracts use Chainlink to request off-chain pricing data, but a common error occurs when the smart contract doesn’t check whether that data is stale. Consider this stale pricing data finding from Sherlock’s USSD audit:

// @audit no check for stale price data
(, int256 price, , , ) = priceFeedDAIETH.latestRoundData();

return
    (wethPriceUSD * 1e18) /
    ((DAIWethPrice + uint256(price) * 1e10) / 2);
Enter fullscreen mode Exit fullscreen mode

Oracle data feeds can return stale pricing data for a variety of reasons. If the returned pricing data is stale, this code will execute with prices that don’t reflect the current pricing resulting in a potential loss of funds for the user and/or the protocol. Smart contracts should always check the updatedAt parameter returned from latestRoundData() and compare it to a staleness threshold:

// @audit fixed to check for stale price data
(, int256 price, , uint256 updatedAt, ) = priceFeedDAIETH.latestRoundData();

if (updatedAt < block.timestamp - 60 * 60 /* 1 hour */) {
   revert("stale price feed");
}

return
    (wethPriceUSD * 1e18) /
    ((DAIWethPrice + uint256(price) * 1e10) / 2);
Enter fullscreen mode Exit fullscreen mode

The staleness threshold should correspond to the heartbeat of the oracle’s price feed. This can be found on Chainlink’s list of Ethereum mainnet price feeds by checking the “Show More Details” box, which will show the “Heartbeat” column for each feed. For networks other than Ethereum mainnet, make sure to select your desired L1/L2 on that page before reading the data columns.
More examples: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]

Not Checking For Down L2 Sequencer

When using Chainlink with L2 chains like Arbitrum, smart contracts must check whether the L2 Sequencer is down to avoid stale pricing data that appears fresh - Chainlink’s official documentation provides an example implementation. Smart contract auditors should look out for missing L2 sequencer activity checks when they see price code callinglatestRoundData() in projects that are to be deployed on L2s.
More examples: [1, 2, 3, 4, 5]

Same Heartbeat Used For Multiple Price Feeds

Smart contracts often use multiple oracle price feeds to track prices for multiple assets. It is an error to assume that the same time interval heartbeat can be used as a staleness check for every feed, as different feeds can have different heartbeats. Consider this code from JOJO’s Sherlock audit:

function getMarkPrice() external view returns (uint256 price) {
  int256 rawPrice;
  uint256 updatedAt;
  // @audit first feed
  (, rawPrice, , updatedAt, ) = IChainlink(chainlink).latestRoundData();

  // @audit second feed
  (, int256 USDCPrice,, uint256 USDCUpdatedAt,) = IChainlink(USDCSource).latestRoundData();

  require( // @audit feed #1 stale check using same heartbeatInterval
  block.timestamp - updatedAt <= heartbeatInterval,
  "ORACLE_HEARTBEAT_FAILED"
  );
           // @audit feed #2 stale check using same heartbeatInterval
  require(block.timestamp - USDCUpdatedAt <= heartbeatInterval, "USDC_ORACLE_HEARTBEAT_FAILED");
  uint256 tokenPrice = (SafeCast.toUint256(rawPrice) * 1e8) / SafeCast.toUint256(USDCPrice);
  return tokenPrice * 1e18 / decimalsCorrection;
}
Enter fullscreen mode Exit fullscreen mode

In this example, the first price feed has a heartbeat of 1 hour while the second has a heartbeat of 24 hours, so they require different heartbeats to be used in their staleness checks. The appropriate heartbeats can be found on Chainlink’s list of Ethereum mainnet price feeds by checking the “Show More Details” box, which will show the “Heartbeat” column for each feed. For networks other than Ethereum mainnet, make sure to select your desired L1/L2 on that page before reading the data columns.

Oracle Price Feeds Not Updated Frequently

Care must be taken when selecting which price oracle(s) to use; using an oracle price feed that isn’t updated frequently will result in calculations being performed with inaccurate prices that don’t reflect the true value of the asset.
Chainlink Oracles are currently the safest choice, but even then, care must be taken regarding which price feed to choose; similar price feeds can have different heartbeat & deviation thresholds; the longer the heartbeat & higher the deviation threshold, the more the oracle price can differ from the true, current price.
Smart contracts developers should use & auditors should check that price feeds with the lowest heartbeat & deviation thresholds are being used to ensure the oracle’s reported price is as close as possible to the true, current price.
More examples: [1, 2, 3, 4]

Request Confirmation < Depth of Chain Re-Orgs

When requesting randomness, the REQUEST_CONFIRMATION parameter must be greater than the depth of common chain re-organizations on the target chain(s) the contract is to be deployed on, as chain re-organizations re-order blocks & transactions, which can affect the returned randomness.
This could result in a winner becoming a loser or vice-versa due to the chain re-organization re-ordering the randomness request, resulting in a different randomness result. This parameter is found in the contract that inherits from VRFConsumerBaseV2:

contract VRFv2Consumer is VRFConsumerBaseV2 {
    // @audit REQUEST_CONFIRMATIONS = how many blocks confirmed
    // before receiving randomness. Must be greater than depth
    // of common chain reorganisations that occur on target chain.
    //
    // eg polygon has 5+ block re-orgs per day with depth > 3 blocks
    // and frequently has re-orgs with depth < 30 blocks
    //
    // when your transaction for requesting randomness from VRF is moved
    // to a different block then the returned randonmness can change
    // meaning the winner as determined by the returned randonmness
    // can also change!
    uint16 internal constant REQUEST_CONFIRMATIONS = 3;
Enter fullscreen mode Exit fullscreen mode

This parameter will often have the value of 3 because this is the default value in the official Chainlink tutorial, so is simply copied without much thought by developers. Smart contract developers & auditors should confirm whether the value of REQUEST_CONFIRMATIONS is suitable for the targeted chain(s) the smart contract will be deployed on. If the smart contract is to be deployed upon multiple chains, a different value for REQUEST_CONFIRMATIONS may be required for each deployment.
More examples: [1]

Assuming Oracle Price Precision

When working with Oracle price feeds, developers must account for different price feeds having different decimal precision; it is an error to assume that every price feed will report prices using the same precision. Generally, non-ETH pairs report using 8 decimals, while ETH pairs report using 18 decimals.
If precision is assumed, there is plenty of room for developer mistakes to be made since, for example, ETH/USD reports using 8 decimals, as it is considered a non-ETH pair since the price of ETH is being reported in USD. There are also price feeds such as AMPL/USD that report using 18 decimals which breaks the general rule that USD price feeds report in 8 decimals.
Smart contracts can call AggregatorV3Interface.decimals() to get the exact number of decimals for the price feed being called.
More examples: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Incorrect Oracle Price Feed Address

Some projects will hard-code oracle price feed addresses. Others will have addresses in deploy scripts to be set during contract deployment. Wherever the addresses are located, auditors should check that they point to the correct oracle price feed. Examine this code from Sherlock’s USSD contest:

// @audit correct address here, but wrong address in constructor
// chainlink btc/usd priceFeed 0xf4030086522a5beea4988f8ca5b36dbc97bee88c;
contract StableOracleWBTC is IStableOracle {
    AggregatorV3Interface priceFeed;

    constructor() {
        priceFeed = AggregatorV3Interface(
            // @audit wrong address; this is ETH/USD not BTC/USD !
            0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
        );
    }
Enter fullscreen mode Exit fullscreen mode

Here the correct address for the BTC/USD price feed appears in the comment, but in the constructor, the address of the ETH/USD price feed incorrectly appears. Auditors should verify that price feed addresses are correct by referring to Chainlink’s list of Ethereum mainnet price feeds. For projects being deployed on L2s or alternate L1s, auditors should verify that correct price feed addresses are being used for those networks.
More examples: [1]

Oracle Price Updates Can Be Front-Run

Some stablecoin protocols that allow users to deposit collateral and mint/burn stablecoin tokens based upon prices of collateral assets can be subject to having value extracted from the protocol by having their oracle updates sandwich attacked.
Oracle price updates may be too slow behind real-world prices due to only updating after a set deviation % of price change, plus attackers may see oracle updates in the mempool & front-run them. This is a complicated problem to solve; potential solutions involve:

For more information, check out Angle’s Research Series & Synthetix History with Frontrunning.
More examples: [1, 2]

Unhandled Oracle Revert Denial Of Service

Calls to Oracles could potentially revert, which may result in a complete Denial-of-Service to smart contracts which depend upon them. Chainlink multisigs can immediately block access to price feeds at will, so just because a price feed is working today does not mean it will continue to do so indefinitely. Smart contracts should handle this by:

  • wrapping calls to Oracles in try/catch blocks and dealing appropriately with any errors,
  • providing functionality to replace or update oracle feeds after they are configured.

If a configured Oracle feed has malfunctioned or ceased operating, but the smart contract does not have any alternative data source, nor does the contract allow updates to data sources, that contract will be permanently bricked.
This would be especially bad for stablecoin protocols and lending/borrowing platforms where large amounts of user value are stored in the form of collateral that would no longer be able to be withdrawn due to calls to the price oracles reverting.
More examples: [1, 2]

Unhandled Depeg Of Bridged Assets

Consider a Lending & Borrowing protocol where:

  • users can deposit a wrapped asset such as WBTC (wrapped BTC) and borrow against it,
  • the protocol uses Chainlink’s BTC/USD feed to price WBTC,

If the WBTC bridge is compromised and WBTC depegs from BTC, the protocol will continue to price WBTC using the BTC/USD price, even though WBTC will instantly become worth far less than native BTC due to the bridge compromise.
Users could then buy WBTC for a far lower value than native BTC, deposit it into the protocol, and borrow against it using the value of native BTC. This would allow attackers to drain the protocol in the event of a bridge compromise leading to a depeg event.
To help address this issue, the protocol could use Chainlink’s WBTC/BTC price feed to monitor for a depeg event.
More examples: [1]

Oracle Returns Incorrect Price During Flash Crashes

Chainlink price feeds have in-built minimum & maximum prices they will return; if during a flash crash, bridge compromise, or depegging event, an asset’s value falls below the price feed’s minimum price, the oracle price feed will continue to report the (now incorrect) minimum price.
An attacker could:

  • buy that asset using a decentralized exchange at the very low price,
  • deposit the asset into a Lending / Borrowing platform using Chainlink’s price feeds,
  • borrow against that asset at the minimum price Chainlink’s price feed returns, even though the actual price is far lower.

This attack would let the attacker drain value from Lending / Borrowing platforms. To help mitigate such an attack on-chain, smart contracts could check that minAnswer < receivedAnswer < maxAnswer.
This attack could also potentially be mitigated off-chain via off-chain monitoring, which compares Chainlink’s latest reported price to other off-chain sources such as centralized exchanges and/or liquid indexes which aggregate multiple off-chain price sources to produce one index price; if external sources are reporting prices lower than Chainlink’s minAnswer, off-chain monitoring could disable the smart contract’s price feed for that asset, forcing any transactions to revert.
Developers & Auditors can find Chainlink’s oracle feed [minAnswer, maxAnswer] values by:

More examples: [1, 2]

Placing Bets After Randomness Request

Market participants should not be able to place a bet or other input after a randomness request has been made, which will return the result of a lottery or other form of draw, as an attacker would be able to front-run the randomness response by inspecting the winning result then purchasing a ticket with the winning inputs.
More examples: [1]

Re-requesting Randomness

Re-requesting randomness allows VRF service providers to withhold fulfillment if the outcome is not favorable to them, wait for the re-request, then return the randomness only if it is now favorable to them.

💖 💪 🙅 🚩
0xsaff
0xsaff

Posted on August 27, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

What was your win this week?
weeklyretro What was your win this week?

November 29, 2024

Where GitOps Meets ClickOps
devops Where GitOps Meets ClickOps

November 29, 2024