Hello! Welcome to your next lesson in the "Fungible Tokens & DeFi Primitives" module.
Introduction
In our last lesson, we built a staking contract, where users deposit tokens to earn rewards over time. This introduced the core pattern of a contract holding user funds and managing individual balances.
Today, we'll build on that foundation to create another, even more powerful DeFi primitive: a lending pool. While staking is about earning yield on assets you hold, lending protocols allow you to use those assets as collateral to borrow other assets. This unlocks capital efficiency and is the basis for leverage, stablecoin minting, and many other financial strategies in DeFi.
Our focus will be on the essential mechanics of an over-collateralized lending protocol, similar to foundational versions of Aave or Compound.
Learning Outcome: By the end of this lesson, you will be able to implement core lending pool functions for depositing collateral and borrowing assets.
We'll start with the key theoretical concepts, see how they manifest in a real-world application, and then dive into the Solidity implementation.
1. The Core Concepts of Over-Collateralized Lending
In traditional finance, loans are often underwritten based on identity and credit history. In the anonymous world of DeFi, this isn't possible. Instead, trust is replaced by economic security in the form of over-collateralization: to borrow something of value, you must first lock up collateral of even greater value.
This simple principle gives rise to a few critical metrics that govern every lending protocol.
DeFi Lending Protocols - Smart Contracts and Decentralized Finance
To understand the mechanics of a lending pool, we first need to define its core terminology. This video from the Center for Innovative Finance provides an excellent conceptual overview.
Please watch the first two sections of the video (from 00:00 to 11:48). The first part introduces the need for collateralization, and the second part is crucial as it defines the key terms we will be using throughout this lesson.
Key Terminology Summary
As you saw in the video, a user's interaction with a lending protocol is defined by these key parameters:
- Position: The entirety of a user's collateral and debt with the protocol.
- Loan-to-Value (LTV): A ratio that determines the maximum borrowing power of a specific collateral asset. For example, if ETH has an LTV of 80%, for every $100 worth of ETH you deposit as collateral, you can borrow up to $80 worth of other assets. Different assets have different LTVs based on their perceived risk and volatility.
- Liquidation Threshold: A second, higher ratio that determines when a position is considered under-collateralized and at risk of being liquidated. The gap between the LTV and the Liquidation Threshold acts as a safety buffer for the borrower.
- Health Factor: A single number that represents the safety of your loan. It's calculated as the ratio of the value of your collateral (adjusted by the liquidation threshold) to the value of your debt. A Health Factor greater than 1 is safe. If it drops below 1, your position can be liquidated by anyone to repay your debt, often incurring a penalty.
Our goal today is to implement the functions that allow a user to create and manage a position, keeping these rules in mind.
2. The User Experience: A Practical Look
Before we write the code, let's see how these concepts are presented to a user. As a front-end developer, you know that abstract backend logic must be translated into a clear user interface.
How to Lend and Borrow Crypto Using AAVE
This video by Richard Hsu demonstrates the user flow for supplying and borrowing on Aave, one of the largest DeFi lending protocols. Notice how the UI directly exposes the concepts we just discussed.
Watch from 03:31 to 06:12. This will walk you through: Supplying USDC as collateral. Understanding the LTV and Liquidation Threshold for that asset. Borrowing ETH against the collateral and seeing the resulting Health Factor.
This demo makes it clear what our smart contract needs to enable:
- A function to accept collateral (
supply). - A function to allow borrowing (
borrow). - A system of internal accounting to track each user's collateral and debt.
- Logic to enforce the LTV and check the Health Factor.
3. Solidity Implementation: Building the Lending Pool
Now, let's build the smart contract. We'll follow the structure of a popular tutorial that builds a simplified, over-collateralized lending contract. In this example, users will deposit ETH as collateral and borrow an ERC-20 token called CORN.
This structure is a great learning tool because it isolates the core logic we need to focus on.
Build an Over-Collateralized Lending Platform in Solidity
The Speedrun Ethereum challenge 'Build an Over-Collateralized Lending Platform' provides a clear, step-by-step guide to implementing the core functions. We will walk through its logic.
Please read through Checkpoints 2, 3, and 4. You don't need to set up the full project, but focus on understanding the code snippets and the logic behind them. We will break them down below.
Let's analyze the key functions from the resource.
A. Depositing and Withdrawing Collateral
This is the entry point for any user. The logic is very similar to the stake function from our previous lesson.
-
State Variable:
mapping(address => uint256) public s_userCollateral;
This mapping tracks how much ETH collateral each user has deposited. -
addCollateral():
This function allows a user to deposit ETH.function addCollateral() public payable { if (msg.value == 0) { revert Lending__InvalidAmount(); } s_userCollateral[msg.sender] += msg.value; // Add sent ETH to user's collateral balance emit CollateralAdded(msg.sender, msg.value, i_cornDEX.currentPrice()); }It's a
payablefunction that takes the sent ETH (msg.value) and credits it to the sender's balance in thes_userCollateralmapping. -
withdrawCollateral(uint256 amount):
This allows a user to retrieve their collateral, provided they have enough and it's not securing an active loan (we'll enforce this later).function withdrawCollateral(uint256 amount) public { if (amount == 0 || s_userCollateral[msg.sender] < amount) { revert Lending__InvalidAmount(); } // Important: We will later add a check here to ensure withdrawing // doesn't make an existing loan unhealthy. s_userCollateral[msg.sender] -= amount; (bool success, ) = payable(msg.sender).call{value: amount}(""); if (!success) { revert Lending__TransferFailed(); } emit CollateralWithdrawn(msg.sender, amount, i_cornDEX.currentPrice()); }
B. Valuing Assets and Checking Position Health
To enforce borrowing limits, the contract must know the value of the collateral and the debt. This requires a price feed.
For this lesson, the example uses a simple DEX contract as a price oracle. This is not secure for production (it's easily manipulated), but it's perfect for learning the mechanics.
-
calculateCollateralValue(address user):
This function converts the user's ETH collateral into its equivalent value in theCORNtoken.function calculateCollateralValue(address user) public view returns (uint256) { uint256 collateralAmount = s_userCollateral[user]; // price is returned from the oracle in CORN per 1 ETH (with 18 decimals) uint256 price = i_cornDEX.currentPrice(); return (collateralAmount * price) / 1e18; } -
_validatePosition(address user):
This internal function acts as our health check. It calculates the user's position ratio and reverts if it's unsafe (i.e., if their Health Factor is below the minimum).function _validatePosition(address user) internal view { // isLiquidatable checks if the ratio of collateral value to debt value // is below the minimum required threshold (e.g., 150%). if (isLiquidatable(user)) { revert Lending__UnsafePositionRatio(); } }This function is the heart of the protocol's safety mechanism.
C. Borrowing and Repaying
With the safety checks in place, we can now implement the borrow function.
-
State Variable:
mapping(address => uint256) public s_userBorrowed;
This mapping tracks how muchCORNeach user has borrowed. -
borrowCorn(uint256 borrowAmount):
This is the core function for taking out a loan.function borrowCorn(uint256 borrowAmount) public { if (borrowAmount == 0) { revert Lending__InvalidAmount(); } // 1. Update user's debt balance s_userBorrowed[msg.sender] += borrowAmount; // 2. CRITICAL: Check if the new position is safe _validatePosition(msg.sender); // 3. If safe, transfer the borrowed tokens bool success = i_corn.transfer(msg.sender, borrowAmount); if (!success) { revert Lending__BorrowingFailed(); } emit AssetBorrowed(msg.sender, borrowAmount, i_cornDEX.currentPrice()); }The order of operations is vital: we optimistically update the user's debt, check if the resulting position is valid, and only then transfer the funds. If
_validatePositionreverts, the entire transaction fails, and the state changes (like the updated debt) are rolled back.
The repayCorn function is the reverse: the user approves the contract to spend their CORN, the function uses transferFrom to pull the tokens back, and the user's s_userBorrowed balance is decreased.
Conclusion
Today you've implemented the fundamental logic of an over-collateralized lending pool. You've moved from the theory of collateralization to the practical implementation of the contracts that power this essential DeFi primitive.
Key Takeaways:
- Core Functions: A lending pool is built around
depositCollateral,withdrawCollateral,borrow, andrepayfunctions. - Over-Collateralization: Loans are secured by ensuring the value of a user's collateral is significantly higher than the value of their debt.
- Health Check is Crucial: The most important logic in a
borrowfunction is the safety check (_validatePosition) that prevents a user from borrowing too much and putting their position (and the protocol) at risk. - Price Feeds are Essential: The entire system relies on knowing the relative prices of assets. The security and reliability of this price feed are paramount.
Next Steps:
Our implementation used a simple, insecure price feed from a DEX. In a real-world scenario, this would be a critical vulnerability. In our next lesson, we will address this directly by learning how to integrate a price oracle (e.g., Chainlink) to calculate an account's collateralization ratio, making our lending protocol far more robust and secure.
Can't find a good explanation? Sign up and we'll make it for you
Sign up