DEV Community

Cover image for πŸ’° Day 23 of #30DaysOfSolidity β€” Build a DeFi Lending & Borrowing System (Aave-like dApp)
Saurav Kumar
Saurav Kumar

Posted on

πŸ’° Day 23 of #30DaysOfSolidity β€” Build a DeFi Lending & Borrowing System (Aave-like dApp)

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
Enter fullscreen mode Exit fullscreen mode

βš™οΈ Setup

forge init foundry-project
cd foundry-project
forge install OpenZeppelin/openzeppelin-contracts --no-commit
Enter fullscreen mode Exit fullscreen mode

🧩 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ“Š 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);
}
Enter fullscreen mode Exit fullscreen mode

πŸ’Ή 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

🏦 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ§ͺ 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

βš™οΈ foundry.toml

[profile.default]
src = "src"
out = "out"
libs = ["lib"]
Enter fullscreen mode Exit fullscreen mode

πŸš€ Run the Project

forge build
forge test -vv
Enter fullscreen mode Exit fullscreen mode

🧠 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)