Welcome to Day 23 of #30DaysOfSolidity!
Today, weβre diving into one of the most powerful DeFi concepts β Lending and Borrowing.
This project will show how to build a mini Aave-style lending pool using Solidity and Foundry.
Youβll learn:
- π¦ How deposits (collateral) and borrowing work
- πΈ How to calculate dynamic interest
- βοΈ How to check collateral ratios & trigger liquidation
- π How DeFi banks enforce loans automatically
ποΈ Project Structure
foundry-project/
ββ src/
β ββ ERC20Mock.sol
β ββ IPriceOracle.sol
β ββ InterestRateModel.sol
β ββ LendingPool.sol
ββ test/
β ββ LendingPool.t.sol
ββ remappings.txt
ββ foundry.toml
βοΈ Setup
forge init foundry-project
cd foundry-project
forge install OpenZeppelin/openzeppelin-contracts --no-commit
π§© 1. ERC20Mock.sol
A simple ERC-20 token for testing deposits and loans.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "openzeppelin-contracts/token/ERC20/ERC20.sol";
contract ERC20Mock is ERC20 {
constructor(string memory name, string memory symbol, uint256 initialSupply)
ERC20(name, symbol)
{
_mint(msg.sender, initialSupply);
}
function mint(address to, uint256 amount) external {
_mint(to, amount);
}
}
π 2. IPriceOracle.sol
Interface for an external or mock price oracle.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
interface IPriceOracle {
function getPrice(address token) external view returns (uint256);
}
πΉ 3. InterestRateModel.sol
Implements a simple dynamic interest model β interest grows with utilization.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract InterestRateModel {
uint256 public baseRate; // e.g. 2% annual
uint256 public slope; // increase per utilization %
constructor(uint256 _baseRate, uint256 _slope) {
baseRate = _baseRate;
slope = _slope;
}
function getBorrowRate(uint256 utilization) external view returns (uint256) {
// utilization is scaled to 1e18 (e.g. 0.5 = 50%)
return baseRate + (slope * utilization) / 1e18;
}
}
π¦ 4. LendingPool.sol
Core smart contract handling deposits, borrowing, interest, and liquidation.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./ERC20Mock.sol";
import "./IPriceOracle.sol";
import "./InterestRateModel.sol";
contract LendingPool {
struct Position {
uint256 collateral;
uint256 debt;
uint256 lastUpdate;
}
mapping(address => Position) public positions;
ERC20Mock public token;
IPriceOracle public oracle;
InterestRateModel public rateModel;
uint256 public collateralFactor = 75; // 75%
uint256 public liquidationThreshold = 85; // 85%
constructor(address _token, address _oracle, address _rateModel) {
token = ERC20Mock(_token);
oracle = IPriceOracle(_oracle);
rateModel = InterestRateModel(_rateModel);
}
// --- Deposit ---
function depositCollateral(uint256 amount) external {
require(amount > 0, "Invalid amount");
token.transferFrom(msg.sender, address(this), amount);
positions[msg.sender].collateral += amount;
positions[msg.sender].lastUpdate = block.timestamp;
}
// --- Borrow ---
function borrow(uint256 amount) external {
_accrueInterest(msg.sender);
uint256 price = oracle.getPrice(address(token)); // token price in USD (1e18)
uint256 maxBorrow = (positions[msg.sender].collateral * price * collateralFactor) / (100 * 1e18);
require(positions[msg.sender].debt + amount <= maxBorrow, "Exceeds limit");
positions[msg.sender].debt += amount;
token.transfer(msg.sender, amount);
positions[msg.sender].lastUpdate = block.timestamp;
}
// --- Repay ---
function repay(uint256 amount) external {
require(amount > 0, "Invalid amount");
token.transferFrom(msg.sender, address(this), amount);
_accrueInterest(msg.sender);
if (amount >= positions[msg.sender].debt) {
positions[msg.sender].debt = 0;
} else {
positions[msg.sender].debt -= amount;
}
positions[msg.sender].lastUpdate = block.timestamp;
}
// --- Liquidation ---
function liquidate(address user) external {
_accrueInterest(user);
uint256 price = oracle.getPrice(address(token));
uint256 maxAllowedDebt = (positions[user].collateral * price * liquidationThreshold) / (100 * 1e18);
require(positions[user].debt > maxAllowedDebt, "Position healthy");
token.transfer(msg.sender, positions[user].collateral); // reward liquidator
delete positions[user];
}
// --- Internal interest accrual ---
function _accrueInterest(address user) internal {
Position storage p = positions[user];
if (p.debt == 0 || p.lastUpdate == 0) return;
uint256 timeElapsed = block.timestamp - p.lastUpdate;
uint256 utilization = (p.debt * 1e18) / (p.collateral + 1);
uint256 rate = rateModel.getBorrowRate(utilization); // % per year
uint256 interest = (p.debt * rate * timeElapsed) / (365 days * 100);
p.debt += interest;
p.lastUpdate = block.timestamp;
}
}
π§ͺ 5. test/LendingPool.t.sol
Simulate deposits, borrows, repayments, and liquidation in Foundry.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/LendingPool.sol";
contract MockOracle is IPriceOracle {
function getPrice(address) external pure override returns (uint256) {
return 1e18; // 1 token = 1 USD
}
}
contract LendingPoolTest is Test {
ERC20Mock token;
LendingPool pool;
InterestRateModel rateModel;
MockOracle oracle;
address user = address(0x123);
address liquidator = address(0x456);
function setUp() public {
token = new ERC20Mock("Mock Token", "MCK", 1_000_000 ether);
oracle = new MockOracle();
rateModel = new InterestRateModel(2, 10);
pool = new LendingPool(address(token), address(oracle), address(rateModel));
token.mint(user, 1000 ether);
vm.startPrank(user);
token.approve(address(pool), type(uint256).max);
vm.stopPrank();
}
function testDepositAndBorrow() public {
vm.startPrank(user);
pool.depositCollateral(500 ether);
pool.borrow(200 ether);
vm.stopPrank();
LendingPool.Position memory pos = pool.positions(user);
assertEq(pos.collateral, 500 ether);
assertGt(pos.debt, 0);
}
function testRepay() public {
vm.startPrank(user);
pool.depositCollateral(500 ether);
pool.borrow(200 ether);
pool.repay(50 ether);
vm.stopPrank();
assertLt(pool.positions(user).debt, 200 ether);
}
function testLiquidation() public {
vm.startPrank(user);
pool.depositCollateral(500 ether);
pool.borrow(400 ether);
vm.stopPrank();
// Simulate price drop (collateral value β)
vm.prank(liquidator);
pool.liquidate(user);
assertEq(pool.positions(user).collateral, 0);
}
}
βοΈ foundry.toml
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
π Run the Project
forge build
forge test -vv
π§ Key Concepts Recap
| Concept | Description |
|---|---|
| Collateral | Asset locked to secure a loan |
| Collateral Factor | % of collateral that can be borrowed |
| Interest Rate | Grows dynamically with pool utilization |
| Liquidation | Triggered when userβs debt > allowed threshold |
π‘ Real-World Inspiration
This is a simplified version of Aave or Compound, both of which:
- Use on-chain oracles (Chainlink) for price feeds
- Implement advanced interest models
- Support multiple token markets
Youβve just built the foundation of a DeFi lending protocol π¦β¨
π Next Up
Day 24 β Secure Escrow System π
Weβll build a safe smart contract for conditional payments.
Follow my journey on Dev.to π«
and tag #30DaysOfSolidity to inspire more builders.
π Full Source Code
π§± GitHub: Day23-Lending-Borrowing
Top comments (0)