🎓 A1Learner
🔍
0 of 45 lessons completed

Solidity Project: Lending Protocol

A lending protocol is a contract that lends tokens against deposited collateral and charges interest over time. This project combines deposits, a borrow ratio, interest, repayment, and liquidation into one working sketch.

Collateral backs a loan up to a fixed ratio

A borrower deposits collateral first. The pool then lends at most 75 percent of the collateral value.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    address public owner;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory tokenName) {
        name = tokenName;
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function mint(address to, uint256 amount) public onlyOwner {
        balanceOf[to] += amount;
        totalSupply += amount;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        allowance[from][msg.sender] -= amount;
        return true;
    }
}

contract LendingPool {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    uint256 public price;
    address public owner;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    mapping(address => uint256) public lastAccrued;

    constructor(ToyToken collateralToken_, ToyToken borrowToken_) {
        collateralToken = collateralToken_;
        borrowToken = borrowToken_;
        price = 2;
        owner = msg.sender;
    }

    function setPrice(uint256 newPrice) public {
        require(msg.sender == owner, "not owner");
        price = newPrice;
    }

    function depositCollateral(uint256 amount) public {
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }

    function borrow(uint256 amount) public {
        uint256 value = collateral[msg.sender] * price;
        uint256 maxBorrow = (value * 75) / 100;
        require(debt[msg.sender] + amount <= maxBorrow, "over ratio");
        debt[msg.sender] += amount;
        lastAccrued[msg.sender] = block.timestamp;
        borrowToken.transfer(msg.sender, amount);
    }

    function accrue(address user) public {
        uint256 elapsed = block.timestamp - lastAccrued[user];
        if (elapsed > 0 && debt[user] > 0) {
            uint256 interest = (debt[user] * elapsed) / 1000;
            debt[user] += interest;
        }
        lastAccrued[user] = block.timestamp;
    }

    function repay(uint256 amount) public {
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }

    function withdraw(uint256 amount) public {
        require(debt[msg.sender] == 0, "debt open");
        require(collateral[msg.sender] >= amount, "low collateral");
        collateral[msg.sender] -= amount;
        collateralToken.transfer(msg.sender, amount);
    }

    function liquidate(address user, uint256 repayAmount) public {
        uint256 value = collateral[user] * price;
        uint256 limit = (value * 75) / 100;
        require(debt[user] > limit, "healthy");
        borrowToken.transferFrom(msg.sender, address(this), repayAmount);
        debt[user] -= repayAmount;
        uint256 seized = (repayAmount * 110) / (100 * price);
        require(seized <= collateral[user], "low collateral");
        collateral[user] -= seized;
        collateralToken.transfer(msg.sender, seized);
    }
}

Interest grows with warped time at a fixed rate

The pool accrues interest as debt times elapsed seconds divided by 1000. Warping the clock forward makes the growth visible.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    address public owner;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory tokenName) {
        name = tokenName;
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function mint(address to, uint256 amount) public onlyOwner {
        balanceOf[to] += amount;
        totalSupply += amount;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        allowance[from][msg.sender] -= amount;
        return true;
    }
}

contract LendingPool {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    uint256 public price;
    address public owner;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    mapping(address => uint256) public lastAccrued;

    constructor(ToyToken collateralToken_, ToyToken borrowToken_) {
        collateralToken = collateralToken_;
        borrowToken = borrowToken_;
        price = 2;
        owner = msg.sender;
    }

    function setPrice(uint256 newPrice) public {
        require(msg.sender == owner, "not owner");
        price = newPrice;
    }

    function depositCollateral(uint256 amount) public {
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }

    function borrow(uint256 amount) public {
        uint256 value = collateral[msg.sender] * price;
        uint256 maxBorrow = (value * 75) / 100;
        require(debt[msg.sender] + amount <= maxBorrow, "over ratio");
        debt[msg.sender] += amount;
        lastAccrued[msg.sender] = block.timestamp;
        borrowToken.transfer(msg.sender, amount);
    }

    function accrue(address user) public {
        uint256 elapsed = block.timestamp - lastAccrued[user];
        if (elapsed > 0 && debt[user] > 0) {
            uint256 interest = (debt[user] * elapsed) / 1000;
            debt[user] += interest;
        }
        lastAccrued[user] = block.timestamp;
    }

    function repay(uint256 amount) public {
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }

    function withdraw(uint256 amount) public {
        require(debt[msg.sender] == 0, "debt open");
        require(collateral[msg.sender] >= amount, "low collateral");
        collateral[msg.sender] -= amount;
        collateralToken.transfer(msg.sender, amount);
    }

    function liquidate(address user, uint256 repayAmount) public {
        uint256 value = collateral[user] * price;
        uint256 limit = (value * 75) / 100;
        require(debt[user] > limit, "healthy");
        borrowToken.transferFrom(msg.sender, address(this), repayAmount);
        debt[user] -= repayAmount;
        uint256 seized = (repayAmount * 110) / (100 * price);
        require(seized <= collateral[user], "low collateral");
        collateral[user] -= seized;
        collateralToken.transfer(msg.sender, seized);
    }
}

A price drop makes a position liquidatable

When the collateral price falls, the same debt can pass the 75 percent limit. A liquidator then repays part of the debt and takes collateral at a bonus.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    address public owner;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory tokenName) {
        name = tokenName;
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function mint(address to, uint256 amount) public onlyOwner {
        balanceOf[to] += amount;
        totalSupply += amount;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        allowance[from][msg.sender] -= amount;
        return true;
    }
}

contract LendingPool {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    uint256 public price;
    address public owner;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    mapping(address => uint256) public lastAccrued;

    constructor(ToyToken collateralToken_, ToyToken borrowToken_) {
        collateralToken = collateralToken_;
        borrowToken = borrowToken_;
        price = 2;
        owner = msg.sender;
    }

    function setPrice(uint256 newPrice) public {
        require(msg.sender == owner, "not owner");
        price = newPrice;
    }

    function depositCollateral(uint256 amount) public {
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }

    function borrow(uint256 amount) public {
        uint256 value = collateral[msg.sender] * price;
        uint256 maxBorrow = (value * 75) / 100;
        require(debt[msg.sender] + amount <= maxBorrow, "over ratio");
        debt[msg.sender] += amount;
        lastAccrued[msg.sender] = block.timestamp;
        borrowToken.transfer(msg.sender, amount);
    }

    function accrue(address user) public {
        uint256 elapsed = block.timestamp - lastAccrued[user];
        if (elapsed > 0 && debt[user] > 0) {
            uint256 interest = (debt[user] * elapsed) / 1000;
            debt[user] += interest;
        }
        lastAccrued[user] = block.timestamp;
    }

    function repay(uint256 amount) public {
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }

    function withdraw(uint256 amount) public {
        require(debt[msg.sender] == 0, "debt open");
        require(collateral[msg.sender] >= amount, "low collateral");
        collateral[msg.sender] -= amount;
        collateralToken.transfer(msg.sender, amount);
    }

    function liquidate(address user, uint256 repayAmount) public {
        uint256 value = collateral[user] * price;
        uint256 limit = (value * 75) / 100;
        require(debt[user] > limit, "healthy");
        borrowToken.transferFrom(msg.sender, address(this), repayAmount);
        debt[user] -= repayAmount;
        uint256 seized = (repayAmount * 110) / (100 * price);
        require(seized <= collateral[user], "low collateral");
        collateral[user] -= seized;
        collateralToken.transfer(msg.sender, seized);
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract ToyToken {
    string public name;
    address public owner;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;

    constructor(string memory tokenName) {
        name = tokenName;
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    function mint(address to, uint256 amount) public onlyOwner {
        balanceOf[to] += amount;
        totalSupply += amount;
    }

    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        allowance[from][msg.sender] -= amount;
        return true;
    }
}

contract LendingPool {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    uint256 public price;
    address public owner;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    mapping(address => uint256) public lastAccrued;

    constructor(ToyToken collateralToken_, ToyToken borrowToken_) {
        collateralToken = collateralToken_;
        borrowToken = borrowToken_;
        price = 2;
        owner = msg.sender;
    }

    function setPrice(uint256 newPrice) public {
        require(msg.sender == owner, "not owner");
        price = newPrice;
    }

    function depositCollateral(uint256 amount) public {
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }

    function borrow(uint256 amount) public {
        uint256 value = collateral[msg.sender] * price;
        uint256 maxBorrow = (value * 75) / 100;
        require(debt[msg.sender] + amount <= maxBorrow, "over ratio");
        debt[msg.sender] += amount;
        lastAccrued[msg.sender] = block.timestamp;
        borrowToken.transfer(msg.sender, amount);
    }

    function accrue(address user) public {
        uint256 elapsed = block.timestamp - lastAccrued[user];
        if (elapsed > 0 && debt[user] > 0) {
            uint256 interest = (debt[user] * elapsed) / 1000;
            debt[user] += interest;
        }
        lastAccrued[user] = block.timestamp;
    }

    function repay(uint256 amount) public {
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }

    function withdraw(uint256 amount) public {
        require(debt[msg.sender] == 0, "debt open");
        require(collateral[msg.sender] >= amount, "low collateral");
        collateral[msg.sender] -= amount;
        collateralToken.transfer(msg.sender, amount);
    }

    function liquidate(address user, uint256 repayAmount) public {
        uint256 value = collateral[user] * price;
        uint256 limit = (value * 75) / 100;
        require(debt[user] > limit, "healthy");
        borrowToken.transferFrom(msg.sender, address(this), repayAmount);
        debt[user] -= repayAmount;
        uint256 seized = (repayAmount * 110) / (100 * price);
        require(seized <= collateral[user], "low collateral");
        collateral[user] -= seized;
        collateralToken.transfer(msg.sender, seized);
    }
}

Key points

  • Collateral: Collateral is tokens a borrower locks up to back a loan.
  • Borrow ratio: The borrow ratio caps a loan at 75 percent of the collateral value.
  • Interest: Interest grows the debt by a fixed fraction for each second that passes.
  • Repayment: Repayment lowers the debt, and withdrawal opens only once the debt is zero.
  • Liquidation: Liquidation lets someone repay an unhealthy loan and take collateral at a bonus.

In this example, the borrower deposits 100 collateral and borrows 100, then time warps forward and interest raises the debt to 110. The borrower tops up 10 borrowed tokens, repays the full 110, and withdraws the original 100 collateral with no debt left.

Note

The price here is set by the owner in a toy contract, so it stands in for a real price feed.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

A borrower deposits 100 collateral tokens at price 2. What is the largest loan the 75 percent ratio allows?