Solidity Liquidation Mechanics
Liquidation is the forced repayment of an unhealthy loan by a third party. When a position health factor falls below 1, anyone may repay part of the debt and take collateral at a discount.
A healthy position has a health factor above 100
Collateral of 100 at a price of 100 is worth 10000, against a debt of 5000. The health factor is 160, so the position is healthy.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract LendingPosition {
address public owner;
uint256 public collateral;
uint256 public debt;
uint256 public price;
mapping(address => uint256) public liquidatorCollateral;
constructor(uint256 collateral_, uint256 debt_, uint256 price_) {
owner = msg.sender;
collateral = collateral_;
debt = debt_;
price = price_;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
price = newPrice;
}
function healthFactor() public view returns (uint256) {
return (collateral * price * 80) / debt;
}
function liquidate(uint256 repayAmount) public returns (uint256 taken) {
require(healthFactor() < 100, "position is healthy");
require(repayAmount <= debt, "repay too much");
uint256 base = repayAmount / price;
taken = (base * 110) / 100;
require(taken <= collateral, "not enough collateral");
debt -= repayAmount;
collateral -= taken;
liquidatorCollateral[msg.sender] += taken;
}
}
A healthy position cannot be liquidated
The health factor is 160, which is above 100. A liquidation attempt reverts and nothing changes.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract LendingPosition {
address public owner;
uint256 public collateral;
uint256 public debt;
uint256 public price;
mapping(address => uint256) public liquidatorCollateral;
constructor(uint256 collateral_, uint256 debt_, uint256 price_) {
owner = msg.sender;
collateral = collateral_;
debt = debt_;
price = price_;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
price = newPrice;
}
function healthFactor() public view returns (uint256) {
return (collateral * price * 80) / debt;
}
function liquidate(uint256 repayAmount) public returns (uint256 taken) {
require(healthFactor() < 100, "position is healthy");
require(repayAmount <= debt, "repay too much");
uint256 base = repayAmount / price;
taken = (base * 110) / 100;
require(taken <= collateral, "not enough collateral");
debt -= repayAmount;
collateral -= taken;
liquidatorCollateral[msg.sender] += taken;
}
}
A price drop makes the position unhealthy
When the price falls from 100 to 25, the collateral value falls to 2500. The health factor falls to 40, which is below 100.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract LendingPosition {
address public owner;
uint256 public collateral;
uint256 public debt;
uint256 public price;
mapping(address => uint256) public liquidatorCollateral;
constructor(uint256 collateral_, uint256 debt_, uint256 price_) {
owner = msg.sender;
collateral = collateral_;
debt = debt_;
price = price_;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
price = newPrice;
}
function healthFactor() public view returns (uint256) {
return (collateral * price * 80) / debt;
}
function liquidate(uint256 repayAmount) public returns (uint256 taken) {
require(healthFactor() < 100, "position is healthy");
require(repayAmount <= debt, "repay too much");
uint256 base = repayAmount / price;
taken = (base * 110) / 100;
require(taken <= collateral, "not enough collateral");
debt -= repayAmount;
collateral -= taken;
liquidatorCollateral[msg.sender] += taken;
}
}
Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract LendingPosition {
address public owner;
uint256 public collateral;
uint256 public debt;
uint256 public price;
mapping(address => uint256) public liquidatorCollateral;
constructor(uint256 collateral_, uint256 debt_, uint256 price_) {
owner = msg.sender;
collateral = collateral_;
debt = debt_;
price = price_;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function setPrice(uint256 newPrice) public onlyOwner {
price = newPrice;
}
function healthFactor() public view returns (uint256) {
return (collateral * price * 80) / debt;
}
function liquidate(uint256 repayAmount) public returns (uint256 taken) {
require(healthFactor() < 100, "position is healthy");
require(repayAmount <= debt, "repay too much");
uint256 base = repayAmount / price;
taken = (base * 110) / 100;
require(taken <= collateral, "not enough collateral");
debt -= repayAmount;
collateral -= taken;
liquidatorCollateral[msg.sender] += taken;
}
}
Key points
- Health factor: A health factor compares collateral value against debt, with 100 meaning exactly covered at the safety threshold.
- Liquidation threshold: The liquidation threshold is the safety fraction, here 80, applied to collateral value before comparing it with debt.
- Liquidator: A liquidator is anyone who repays part of an unhealthy debt in exchange for collateral.
- Discount: A discount is the extra collateral a liquidator receives above the repaid value, here 10 percent.
- Healthy position: A healthy position has a health factor of at least 100 and cannot be liquidated.
- Restatement: Liquidation lets a stranger repay an unhealthy loan and take discounted collateral for doing so.
In this example, the position starts with 100 collateral and 5000 debt at a price of 100. After the price drops to 25, the liquidator repays 1000 of debt. The base collateral for 1000 at a price of 25 is 40, plus a 10 percent discount gives 44, so the debt falls to 4000, the collateral falls to 56, and the liquidator holds 44.
Note
Liquidation uses a mock oracle price in this lesson, and a real protocol would read its price from a defended feed.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A position has a health factor of 40. Can anyone liquidate it?