🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Lending Concepts

Lending is borrowing tokens against tokens locked as collateral. Collateral is value locked in the contract that backs the loan, and borrowing stays capped by a collateral ratio.

Depositing collateral records the locked value

Depositing 200 collateral tokens records 200 locked. Nothing is borrowed yet.

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Lending {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    constructor(address col, address bor) {
        collateralToken = ToyToken(col);
        borrowToken = ToyToken(bor);
    }
    function depositCollateral(uint256 amount) public {
        require(amount > 0, "empty deposit");
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }
    function maxBorrow(address user) public view returns (uint256) {
        return collateral[user] * 50 / 100;
    }
    function borrow(uint256 amount) public {
        require(debt[msg.sender] + amount <= maxBorrow(msg.sender), "above limit");
        debt[msg.sender] += amount;
        borrowToken.transfer(msg.sender, amount);
    }
    function repay(uint256 amount) public {
        require(amount <= debt[msg.sender], "over repay");
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }
    function withdrawCollateral() public {
        require(debt[msg.sender] == 0, "debt open");
        uint256 amount = collateral[msg.sender];
        collateral[msg.sender] = 0;
        collateralToken.transfer(msg.sender, amount);
    }
}

Borrowing within the ratio succeeds

With 200 locked, borrowing 100 sits exactly at the limit. The debt reads 100.

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Lending {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    constructor(address col, address bor) {
        collateralToken = ToyToken(col);
        borrowToken = ToyToken(bor);
    }
    function depositCollateral(uint256 amount) public {
        require(amount > 0, "empty deposit");
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }
    function maxBorrow(address user) public view returns (uint256) {
        return collateral[user] * 50 / 100;
    }
    function borrow(uint256 amount) public {
        require(debt[msg.sender] + amount <= maxBorrow(msg.sender), "above limit");
        debt[msg.sender] += amount;
        borrowToken.transfer(msg.sender, amount);
    }
    function repay(uint256 amount) public {
        require(amount <= debt[msg.sender], "over repay");
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }
    function withdrawCollateral() public {
        require(debt[msg.sender] == 0, "debt open");
        uint256 amount = collateral[msg.sender];
        collateral[msg.sender] = 0;
        collateralToken.transfer(msg.sender, amount);
    }
}

Borrowing above the ratio is refused

Borrowing 101 against 200 locked passes the limit. The call is refused and the debt stays at 0.

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Lending {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    constructor(address col, address bor) {
        collateralToken = ToyToken(col);
        borrowToken = ToyToken(bor);
    }
    function depositCollateral(uint256 amount) public {
        require(amount > 0, "empty deposit");
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }
    function maxBorrow(address user) public view returns (uint256) {
        return collateral[user] * 50 / 100;
    }
    function borrow(uint256 amount) public {
        require(debt[msg.sender] + amount <= maxBorrow(msg.sender), "above limit");
        debt[msg.sender] += amount;
        borrowToken.transfer(msg.sender, amount);
    }
    function repay(uint256 amount) public {
        require(amount <= debt[msg.sender], "over repay");
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }
    function withdrawCollateral() public {
        require(debt[msg.sender] == 0, "debt open");
        uint256 amount = collateral[msg.sender];
        collateral[msg.sender] = 0;
        collateralToken.transfer(msg.sender, amount);
    }
}

Example

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

contract ToyToken {
    string public name;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    event Transfer(address indexed from, address indexed to, uint256 value);
    constructor(string memory tokenName, address holder, uint256 supply) {
        name = tokenName;
        totalSupply = supply;
        balanceOf[holder] = supply;
    }
    function transfer(address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[msg.sender] >= amount, "low balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, amount);
        return true;
    }
    function approve(address spender, uint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        return true;
    }
    function transferFrom(address from, address to, uint256 amount) public returns (bool) {
        require(to != address(0), "zero address");
        require(balanceOf[from] >= amount, "low balance");
        require(allowance[from][msg.sender] >= amount, "low allowance");
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
        emit Transfer(from, to, amount);
        return true;
    }
}


contract Lending {
    ToyToken public collateralToken;
    ToyToken public borrowToken;
    mapping(address => uint256) public collateral;
    mapping(address => uint256) public debt;
    constructor(address col, address bor) {
        collateralToken = ToyToken(col);
        borrowToken = ToyToken(bor);
    }
    function depositCollateral(uint256 amount) public {
        require(amount > 0, "empty deposit");
        collateralToken.transferFrom(msg.sender, address(this), amount);
        collateral[msg.sender] += amount;
    }
    function maxBorrow(address user) public view returns (uint256) {
        return collateral[user] * 50 / 100;
    }
    function borrow(uint256 amount) public {
        require(debt[msg.sender] + amount <= maxBorrow(msg.sender), "above limit");
        debt[msg.sender] += amount;
        borrowToken.transfer(msg.sender, amount);
    }
    function repay(uint256 amount) public {
        require(amount <= debt[msg.sender], "over repay");
        borrowToken.transferFrom(msg.sender, address(this), amount);
        debt[msg.sender] -= amount;
    }
    function withdrawCollateral() public {
        require(debt[msg.sender] == 0, "debt open");
        uint256 amount = collateral[msg.sender];
        collateral[msg.sender] = 0;
        collateralToken.transfer(msg.sender, amount);
    }
}

Key points

  • Collateral: tokens locked in the contract to back a loan.
  • Collateral ratio: the cap that ties the largest loan to the locked value.
  • Borrow limit: this contract lends at most half the locked collateral value.
  • Repayment: paying back a loan lowers the debt before collateral can leave.
  • Liquidation: selling collateral when a loan goes bad is described only and is out of scope for this course.

In this example, a borrower locks 400 collateral tokens, borrows 150, repays the full loan, and withdraws the collateral. The borrower ends with the starting collateral back and no debt left.

Note

both tokens in this lesson are toy tokens priced equally on a local chain only.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

With 400 collateral tokens locked, what is the largest loan in this contract?