Create your own
Lesson illustration

Integrating a Price Oracle for Collateralization Ratios

Hello! Welcome to your next lesson in the "Fungible Tokens & DeFi Primitives" module.

Introduction

In our last lesson, we built the foundational functions of a lending pool: depositing collateral and borrowing assets. However, we identified a critical weak point: our contract relied on a simple, insecure DEX for price information. In a real-world scenario, this would be a fatal flaw, as an attacker could easily manipulate the price to drain the protocol.

Today, we will fix that. We will replace our placeholder price feed with a robust, decentralized oracle network. This is a non-negotiable step for building secure DeFi applications.

Learning Outcome: By the end of this lesson, you will be able to integrate a price oracle (e.g., Chainlink) to calculate an account's collateralization ratio.

We'll explore why oracles are necessary, how to integrate Chainlink Price Feeds into a Solidity contract, and finally, how to use that data to implement the risk management logic at the heart of our lending protocol.

1. The Oracle Problem

Before we write any code, it's essential to understand why we need oracles. Blockchains like Ethereum are deterministic, isolated systems. For a transaction to be valid, every node on the network must be able to execute it and arrive at the exact same result. This creates a secure "walled garden," but it also means a smart contract has no native ability to access data from the outside world, like the current price of ETH on Binance or Coinbase. An attempt to make a standard API call would break consensus, as different nodes would get different results at different times.

This is known as the oracle problem. Oracles are the services that solve this problem by acting as a secure bridge, fetching external data and posting it on-chain for smart contracts to consume.

What is Lending Protocol? Definition, How It Works, DeFi ...

To understand the role of oracles within the context of a lending protocol, let's start with a high-level overview from the Cube.exchange guide on lending protocols.

Please read the following short sections: The bullet point on "Oracles" under the "Key concepts include" heading. The paragraph on "Price oracles and risk engine" under the "How It Works" section. The FAQ entry for "What is the role of oracles?" Focus on how these sections describe oracles as the mechanism for valuing collateral and triggering risk controls.

As the reading highlights, oracles are not just a "nice-to-have"; they are a core component of the protocol's risk engine. A lending protocol is only as secure as its price feed. This is why decentralized oracle networks like Chainlink are the industry standard. They aggregate data from numerous independent sources, making them highly resistant to manipulation.

2. Integrating a Chainlink Price Feed

Now for the practical part. We will integrate a Chainlink Price Feed to get the price of ETH in terms of USD. The process involves three main steps: importing the interface, initializing the feed with the correct address, and calling a function to get the latest price.

Chainlink - Price Oracle | DeFi

This short video from Smart Contract Programmer provides a concise, practical demonstration of how to integrate a Chainlink Price Feed. It covers all the essential steps we'll be implementing.

Watch from 00:33 to 02:39. Pay close attention to: The import statement for AggregatorV3Interface. How the price feed is initialized with an address in the constructor. The call to latestRoundData() and the crucial step of handling the returned value's decimals.

Let's formalize those steps with clean code examples, drawing from the structure of a well-architected stablecoin contract that faces the same requirements as our lending pool.

How to Create an Overcollaterized Stablecoin with Foundry

The QuickNode guide on creating a stablecoin provides excellent, clean code snippets for integrating Chainlink. We will use its structure as our reference.

Please read the sections "Imports", "Contract Initialization + State Variables", and "Fetch ETH/USD Price From Chainlink Oracle". Focus on the Solidity code blocks provided.

Step-by-Step Implementation

Let's break down the code from the resources you just reviewed.

1. Import the Interface

To interact with any contract, your contract needs to know its functions. This is what an interface provides—it's like an API definition or a TypeScript interface.

import "@chainlink/contracts/src/v0.8/shared/interfaces/AggregatorV3Interface.sol";

2. Declare and Initialize the Price Feed

First, declare a state variable of the interface type. Then, in your constructor, you'll initialize it with the on-chain address of the specific price feed you want to use (e.g., ETH/USD on the Sepolia testnet). You can find these addresses in the Chainlink documentation.

contract LendingPool {
    AggregatorV3Interface public priceFeed;

    constructor(address _priceFeedAddress) {
        priceFeed = AggregatorV3Interface(_priceFeedAddress);
    }
    // ... rest of the contract
}

3. Fetch the Price

The core interaction happens by calling the latestRoundData() function on the priceFeed instance. This function returns several values, but we are primarily interested in the price.

function getEthPrice() public view returns (uint256) {
    // latestRoundData() returns 5 values, but we only need the second one (price).
    // The other values are left blank to ignore them.
    (, int256 price, , , ) = priceFeed.latestRoundData();
    
    // Price can be negative in some edge cases, so we check for that.
    require(price > 0, "Invalid price");

    // The price is returned as an int256, so we cast it to uint256.
    return uint256(price);
}

A crucial detail here is handling decimals. The value returned by getEthPrice() is not a whole number of dollars. It's a fixed-point number. For the ETH/USD feed, the price has 8 decimal places. So, if getEthPrice() returns 250000000000, the actual price is $2,500.00000000. Forgetting this is a very common and costly source of bugs.

3. Calculating the Collateralization Ratio

Now that we can reliably fetch the price of our collateral (ETH), we can implement the most important risk management function in our lending pool: calculating a user's collateralization ratio.

The collateralization ratio is defined as:

Let's implement a function that calculates this for a given user.

How to Create an Overcollaterized Stablecoin with Foundry

The QuickNode guide provides a perfect implementation of a function to calculate the current ratio. This is the goal of our lesson.

Read the section "Current Ratio" and study the getCurrentRatio function. We will analyze its math below.

Analyzing the getCurrentRatio Function

Let's adapt the function from the article for our lending pool. Assume we have the following state variables from our previous lesson:

  • s_userCollateral[user] stores the user's ETH collateral in wei (18 decimals).
  • s_userBorrowed[user] stores the user's debt in the borrowed token (let's assume it's a USD-pegged stablecoin with 18 decimals).
// A constant for precision in percentage calculations (100% = 10000)
uint256 public constant RATIO_PRECISION = 10000;

function getCurrentRatio(address user) public view returns (uint256) {
    uint256 collateralAmount = s_userCollateral[user]; // In wei, 18 decimals
    uint256 debtAmount = s_userBorrowed[user]; // In stablecoin, 18 decimals

    if (debtAmount == 0) {
        // If there's no debt, the ratio is infinite. Return the max value.
        return type(uint256).max;
    }

    // 1. Get the ETH price from our oracle function
    uint256 ethPrice = getEthPrice(); // Has 8 decimals

    // 2. Calculate the total value of the collateral in USD (with 18 decimals)
    // (collateral_wei * price_usd_8dec) / 1e8 = collateral_usd_18dec
    uint256 collateralValue = (collateralAmount * ethPrice) / 1e8;

    // 3. Calculate the ratio, scaled by RATIO_PRECISION
    // (collateralValue_18dec * 10000) / debtAmount_18dec = ratio
    uint256 ratio = (collateralValue * RATIO_PRECISION) / debtAmount;
    
    return ratio;
}

Let's trace the math with an example:

  • User deposits 1 ETH (collateralAmount = 1e18).
  • ETH price is $2,500 (ethPrice = 2500 * 1e8).
  • collateralValue = (1e18 * 2500e8) / 1e8 = 2500e18. This represents $2,500 with 18 decimals of precision.
  • User borrows 1,000 USDC (debtAmount = 1000e18).
  • ratio = (2500e18 * 10000) / 1000e18 = 25000.
  • With RATIO_PRECISION of 10000 representing 100%, a return value of 25000 means the user's collateralization ratio is 250%.

This function is the brain of our protocol's safety. It can now be used in the borrow function to prevent users from taking on too much debt, and as we'll see in the next lesson, it's the trigger for liquidations.

Conclusion

Congratulations! You've taken a massive step towards building a production-ready DeFi protocol. By replacing a naive price feed with a robust Chainlink oracle, you've secured the most critical input to your lending pool's risk engine.

Key Takeaways:

  • The Oracle Problem: Smart contracts cannot natively access off-chain data, requiring a secure bridge known as an oracle.
  • Chainlink Integration: Integrating a Chainlink Price Feed involves importing the AggregatorV3Interface, initializing it with a specific on-chain address, and calling latestRoundData().
  • Decimal Precision is Critical: Price data from oracles comes in a fixed-point format. You must account for the correct number of decimals (e.g., 8 for ETH/USD) in all calculations to avoid catastrophic bugs.
  • Collateralization Ratio Calculation: By combining the on-chain collateral balance with the off-chain price from the oracle, you can calculate the total value of a user's collateral and determine their health factor or collateralization ratio.

Next Steps:

A healthy lending protocol must not only prevent users from opening unsafe positions but also have a mechanism to deal with positions that become unsafe due to market volatility. Our getCurrentRatio function tells us when a position is unsafe. In the next lesson, we will use this function to implement the liquidation logic to repay underwater debt and seize collateral, completing the core functionality of our lending protocol.

Can't find a good explanation? Sign up and we'll make it for you

Sign up