Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to your course on Ethereum development.
Introduction
In our last lesson, we successfully integrated a Chainlink price oracle, giving our lending protocol the crucial ability to value a user's collateral. We built the getCurrentRatio function, which acts as a health monitor for every borrowing position. This function tells us when a position becomes undercollateralized and risky.
Today, we will answer the question: "What do we do about it?"
This lesson focuses on implementing the most critical safety mechanism in any lending protocol: liquidation. This is the process that protects the protocol and its lenders from losses when a borrower's position becomes too risky due to market volatility.
Learning Outcome: By the end of this lesson, you will be able to implement the liquidation logic to repay underwater debt and seize collateral.
We will cover the theory behind liquidations, the economic incentives that drive them, and the step-by-step process of building a liquidate function in Solidity. Most importantly, we will explore the many subtle but critical vulnerabilities that can arise in liquidation logic, a topic of paramount importance for secure smart contract development.
1. The "What, Why, and Who" of Liquidation
Before we write any code, let's establish a firm conceptual understanding of the liquidation process.
- What is it? Liquidation is the forced repayment of a portion of an undercollateralized loan. In return for repaying the debt, the liquidator is allowed to purchase the borrower's collateral at a discount.
- Why is it necessary? It's the protocol's primary defense against accumulating "bad debt"—loans where the value of the collateral is less than the value of the debt. Without liquidation, a market crash could render the entire protocol insolvent.
- Who performs it? Liquidations are not triggered automatically by the protocol. They are initiated by external actors, often called "liquidators," "keepers," or "searchers." These are typically bots run by sophisticated users who are economically incentivized to monitor the blockchain for liquidatable positions.
To see these concepts in action, let's review a high-level overview of the liquidation process in Compound, one of DeFi's foundational lending protocols.
Please read the section titled "Liquidation". Focus on the key terms: liquidation discount, close factor, and the description of how an arbitrageur (liquidator) interacts with the protocol.
The key takeaway is that liquidation is a public, incentivized function. We need to design it so that it's profitable for someone to call it precisely when the protocol needs them to. The liquidation discount (or incentive) is the liquidator's reward, and the close factor limits how much of a position can be liquidated at once, preventing a total loss for the borrower from a single transaction.
2. The Anatomy of a Liquidation Function
Let's translate this theory into a Solidity function. We will model our function based on the battle-tested design of Compound's liquidateBorrow.
The Compound V2 documentation provides a clear function signature and explanation for liquidateBorrow. This is an excellent template for our own implementation.
Focus on the CErc20 section under "Liquidate Borrow". Study the function signature liquidateBorrow(address borrower, uint amount, address collateral) and the description of its parameters. We will adapt this for our contract.
Drawing from that structure, let's build our own liquidate function. We'll assume our contract has the following from previous lessons:
MINIMUM_HEALTH_FACTOR: A constant, e.g.,1e18(representing 100%). A user is liquidatable if their health factor is below this.LIQUIDATION_INCENTIVE: The bonus collateral given to liquidators, e.g.,105e16(representing a 5% bonus, or 1.05).s_userCollateral[user]ands_userBorrowed[user]: Mappings tracking user balances.getHealthFactor(user): The function we built in the last lesson (renamed fromgetCurrentRatiofor clarity) that returns a user's collateral-to-debt ratio.
Here is a "happy path" implementation of the liquidation logic:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "./LendingPool.sol"; // Assuming our previous contract
contract LiquidationLogic is LendingPool {
uint256 public constant LIQUIDATION_INCENTIVE = 105e16; // 1.05, or 5% bonus
uint256 public constant CLOSE_FACTOR = 5e17; // 0.5, or 50%
event Liquidation(
address indexed liquidator,
address indexed borrower,
uint256 repayAmount,
address collateralToken,
uint256 seizedCollateralAmount
);
function liquidate(address _borrower, uint256 _repayAmount, address _collateralTokenAddress) external {
// 1. CHECKS
uint256 healthFactor = getHealthFactor(_borrower);
require(healthFactor < MINIMUM_HEALTH_FACTOR, "Position is not liquidatable");
uint256 maxRepay = (s_userBorrowed[_borrower] * CLOSE_FACTOR) / 1e18;
require(_repayAmount > 0 && _repayAmount <= maxRepay, "Invalid repay amount");
// 2. EFFECTS
// Calculate how much collateral to seize
uint256 collateralToSeizeInUsd = (_repayAmount * LIQUIDATION_INCENTIVE) / 1e18;
uint256 collateralPrice = getEthPrice(); // From previous lesson (8 decimals)
uint256 collateralToSeizeInWei = (collateralToSeizeInUsd * 1e18) / (collateralPrice / 1e10); // Adjust for decimals
// Update borrower's state
s_userBorrowed[_borrower] -= _repayAmount;
s_userCollateral[_borrower] -= collateralToSeizeInWei;
// 3. INTERACTIONS
// Pull the repaid debt token from the liquidator
IERC20 debtToken = IERC20(s_debtTokenAddress);
bool success = debtToken.transferFrom(msg.sender, address(this), _repayAmount);
require(success, "Repayment transfer failed");
// Send the seized collateral to the liquidator
IERC20 collateralToken = IERC20(_collateralTokenAddress);
collateralToken.transfer(msg.sender, collateralToSeizeInWei);
emit Liquidation(msg.sender, _borrower, _repayAmount, _collateralTokenAddress, collateralToSeizeInWei);
}
}
This function follows the Checks-Effects-Interactions pattern, a fundamental security principle we'll cover in more detail later.
- Checks: It first verifies that the position is unhealthy and the repayment amount is valid.
- Effects: It then performs all internal state changes (updating the borrower's debt and collateral balances).
- Interactions: Finally, it interacts with other contracts (transferring tokens). This order prevents a class of attacks called reentrancy.
3. The Dangers of Liquidation: A Deep Dive into Vulnerabilities
The function above works for the ideal case. However, in the adversarial environment of Ethereum, "happy path" implementations are insufficient and dangerous. Liquidation functions are notorious for having subtle bugs that can be exploited.
To build robust systems, you must think like an attacker. The following video is an excellent masterclass on the common pitfalls and vulnerabilities found in real-world liquidation logic.
You Have To Know These 13 Critical Liquidation Vulnerabilities
Owen Thurm, a security researcher, provides a detailed breakdown of 13 critical liquidation vulnerabilities. Understanding these is non-negotiable for any serious DeFi developer. We will focus on the most common and impactful ones.
Please watch the following segments. For each, focus on understanding how a seemingly small oversight can break the entire liquidation mechanism. Insolvent vs. Liquidable (09:23 - 15:09): This is a critical distinction. Understand why a protocol wants to liquidate before insolvency. Liquidation Reverts (Group): Blacklisted Tokens (15:24 - 20:08): How can a token feature block liquidation? Zero Transfers (20:08 - 22:20): What happens if a token reverts on zero-amount transfers? Insolvency & Fees (24:47 - 30:31): How can math errors (like underflow) when a position is deeply negative cause the liquidation to fail? Logic & Incentive Errors: Not Accounting for Fees (22:20 - 24:47): What happens if your health factor calculation doesn't include accrued interest? Not Prioritizing Caller Fee (39:17 - 41:58): Why is ensuring the liquidator gets paid a critical part of protocol safety?
Let's summarize the key lessons from that analysis:
- Liquidable vs. Insolvent: A position is liquidable when its health factor drops below a threshold (e.g., 150%). It is insolvent when the collateral value is literally less than the debt value (health factor < 100%). The entire point of the liquidation threshold and incentive is to get positions closed before they become insolvent and cause a loss to the protocol.
- Reverts are the Enemy: Any condition that causes the
liquidatefunction to revert unexpectedly is a potential attack vector. If an attacker can create a position that is liquidatable but cannot be liquidated, they can push it into insolvency, knowing the protocol can't protect itself. This is why handling edge cases like blacklisted tokens, zero-amount transfers, or arithmetic underflows during insolvency is so important. - Incentives Must Be Guaranteed: The system relies on profit-seeking liquidators. If there's a scenario where a liquidator's fee could become zero or they are not paid reliably, they will not service the protocol. No liquidators means no liquidations, which leads to bad debt.
4. Building a More Robust Liquidation Bot
While our focus is on the on-chain contract, it's insightful to see how the other side—the liquidation bot—operates. Understanding the off-chain logic gives you a complete picture of the ecosystem.
Build a Compound Liquidation Bot with Email Alerts powered by Alchemy
This video from Alchemy demonstrates how to build a liquidation bot for the Compound protocol. You don't need to absorb all the code, but pay attention to the overall logic.
Watch the sections on Liquidation Mechanics (06:37 - 11:05) and the Bot Logic Walkthrough (12:53 - 32:04). Focus on: How the bot identifies underwater accounts. The concept of simulating a transaction before sending it to check for profitability. How the bot executes the absorb and buyCollateral (or in our case, liquidate) functions.
The bot's logic reinforces our design goals: our liquidate function must be efficient, predictable, and reliably profitable for the caller when a position is unhealthy. The use of simulation APIs (like alchemy_simulateAssetChanges) by bots means that any ambiguity or unhandled edge case in our contract will be detected and likely avoided by liquidators, leaving the protocol vulnerable.
Conclusion
Today, you've implemented one of the most complex and high-stakes components of a DeFi protocol. Moving beyond a simple implementation to understand the security landscape and economic incentives surrounding liquidations is what separates a novice from an expert smart contract developer.
Key Takeaways:
- Liquidation is an Incentivized Process: It relies on external liquidators who are rewarded with a discount on the borrower's collateral for repaying debt.
- The Goal is to Avoid Insolvency: Liquidations are triggered before a loan becomes a net loss for the protocol.
- Robustness is Paramount: Your liquidation logic must gracefully handle numerous edge cases (insolvency math, token behaviors, fee calculations) to prevent reverts, which can block liquidations and lead to bad debt.
- Checks-Effects-Interactions: This pattern is critical for preventing reentrancy attacks in functions like
liquidate.
Next Steps:
We have now built the complete core logic for a simplified lending protocol: depositing collateral, borrowing an asset, calculating account health, and liquidating unsafe positions. The on-chain engine is complete.
In the next lesson, "Architect the data flow for a DeFi front-end," we will shift our focus. We'll explore how to design a front-end application that can effectively communicate with our smart contracts, handle asynchronous events like transaction confirmations, and provide a seamless user experience—a topic that aligns perfectly with your background in front-end development.