跳到内容

在 Linea 上构建简单的借贷智能合约

了解如何在 Linea 上构建和部署一个简单的借贷智能合约。

13 分钟阅读
在 Linea 上构建简单的借贷智能合约

去中心化金融(DeFi)通过消除中间人并使用智能合约自动处理交易,重塑了人们借贷资金的方式。过去需要银行和繁琐文件才能完成的事情,如今只需几次点击即可在链上实现。

Linea 通过使用零知识 rollup(zk-rollup)对交易进行批处理和压缩,帮助开发者构建 DeFi 应用程序,与以太坊主网相比,具有更好的可扩展性和更低的 gas 费用。

在本指南中,我们将逐步介绍如何在 Linea 上构建和部署一个简单的借贷智能合约。你将学习如何管理抵押品、发放贷款以及设置还款规则——这些都是 DeFi 借贷系统的核心功能。

学完本指南后,你将拥有一个可运行的智能合约,并对 DeFi 借贷的底层运作原理有深入的理解。无论你是区块链开发新手,还是希望提升技能的开发者,本分步指南都将为你提供在 Linea 上开始构建所需的工具。

前提条件

合约概述

本合约支持以下功能:

  1. 抵押品存入:用户可以向合约存入抵押品,相关信息安全存储在智能合约中。

  2. 借款:用户可以凭借已存入的抵押品申请贷款。

  3. 还款:用户可以偿还贷款以解锁其抵押品。

  4. 抵押品提取:用户可以提取未因活跃贷款而被锁定的抵押品。

项目设置

本项目将使用 Foundry 框架。安装 Foundry 后,运行以下命令以初始化新项目:

forge init lending

在 Vscode 或你喜欢的代码编辑器中打开 market 文件夹,并删除 scripts/counter.s.sol、src/counter.sol 和 test/counter.t.sol。

安装所有依赖项

forge install foundry-rs/forge-std --no-commit && forge install OpenZeppelin/openzeppelin-contracts --no-commit

借贷智能合约完整代码

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
import "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import "lib/openzeppelin-contracts/contracts/access/Ownable.sol";
contract LendingBorrowing is Ownable {
    struct Loan {
        uint256 amount; // Borrowed amount
        uint256 collateral; // Collateral amount
        bool isActive; // Loan status
    }
    IERC20 public immutable collateralToken;
    IERC20 public immutable lendingToken;
    uint256 public collateralFactor; // Percentage of collateral allowed to be borrowed (e.g., 50%)
    mapping(address => uint256) public collateralBalances; // User collateral balances
    mapping(address => Loan) public loans; // User loans
    event CollateralDeposited(address indexed user, uint256 amount);
    event CollateralWithdrawn(address indexed user, uint256 amount);
    event LoanTaken(address indexed user, uint256 amount);
    event LoanRepaid(address indexed user, uint256 amount);
    constructor(IERC20 _collateralToken, IERC20 _lendingToken, uint256 _collateralFactor) Ownable(msg.sender) {
        require(_collateralFactor <= 100, "Collateral factor must be <= 100");
        collateralToken = _collateralToken;
        lendingToken = _lendingToken;
        collateralFactor = _collateralFactor;
    }
    function setCollateralFactor(uint256 _newFactor) external onlyOwner {
        require(_newFactor <= 100, "Collateral factor must be <= 100");
        collateralFactor = _newFactor;
    }
    function depositCollateral(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        collateralBalances[msg.sender] += _amount;
        collateralToken.transferFrom(msg.sender, address(this), _amount);
        emit CollateralDeposited(msg.sender, _amount);
    }
    function withdrawCollateral(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        require(collateralBalances[msg.sender] >= _amount, "Insufficient collateral");
        uint256 maxWithdrawable = collateralBalances[msg.sender] - _loanRequiredCollateral(msg.sender);
        require(_amount <= maxWithdrawable, "Cannot withdraw collateral locked for a loan");
        collateralBalances[msg.sender] -= _amount;
        collateralToken.transfer(msg.sender, _amount);
        emit CollateralWithdrawn(msg.sender, _amount);
    }
    function takeLoan(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        require(loans[msg.sender].isActive == false, "Existing loan must be repaid first");
        uint256 maxLoan = (collateralBalances[msg.sender] * collateralFactor) / 100;
        require(_amount <= maxLoan, "Loan exceeds collateral limit");
        loans[msg.sender] = Loan({
            amount: _amount,
            collateral: collateralBalances[msg.sender],
            isActive: true
        });
        lendingToken.transfer(msg.sender, _amount);
        emit LoanTaken(msg.sender, _amount);
    }
    function repayLoan(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        Loan storage userLoan = loans[msg.sender];
        require(userLoan.isActive, "No active loan");
        require(_amount <= userLoan.amount, "Repay amount exceeds loan");
        lendingToken.transferFrom(msg.sender, address(this), _amount);
        userLoan.amount -= _amount;
        if (userLoan.amount == 0) {
            userLoan.isActive = false;
        }
        emit LoanRepaid(msg.sender, _amount);
    }
    function _loanRequiredCollateral(address _user) internal view returns (uint256) {
        Loan memory userLoan = loans[_user];
        if (!userLoan.isActive) return 0;
        return (userLoan.amount * 100) / collateralFactor;
    }
    function getLoanDetails(address _user) external view returns (uint256 amount, uint256 collateral, bool isActive) {
        Loan memory userLoan = loans[_user];
        return (userLoan.amount, userLoan.collateral, userLoan.isActive);
    }
}
复制

代码概述

Loan 结构体

struct Loan {
        uint256 amount; // Borrowed amount
        uint256 collateral; // Collateral amount
        bool isActive; // Loan status
    }
复制

Loan 结构体作为存储用户个人贷款详情的蓝图。结构体中的每个字段代表贷款状态和条件的关键信息。以下是各字段的说明:

  • Amount(金额):存储用户已借入的代币数量。该值用于追踪用户的欠款金额、确定还款义务,并验证贷款条件。

  • Collateral(抵押品):存储用户为担保贷款而存入的抵押品价值。它保护出借方的资金,并根据抵押因子帮助确定最大可借金额。

  • IsActive(是否活跃):表示贷款当前是否处于活跃状态。该字段用于追踪贷款状态,并防止用户在现有贷款未全额还清前申请新贷款。

状态变量

IERC20 public immutable collateralToken;
    IERC20 public immutable lendingToken;
    uint256 public collateralFactor; // Percentage of collateral allowed to be borrowed (e.g., 50%)
复制

上述代码片段定义了三个关键状态变量,每个变量在借贷平台的功能实现和安全保障中发挥特定作用。以下是这些变量的说明:

  • CollateralToken(抵押代币):代表用户作为贷款抵押品存入的代币。例如,若使用 USDC 作为抵押品,该变量存储 USDC 合约的区块链地址。

  • LendingToken(借贷代币):该变量代表平台向借款人出借的代币。若 USDT 为借贷代币,则存储 USDT 合约的区块链地址。

  • CollateralFactor(抵押因子):定义用户可借入的抵押品价值百分比。以整数表示,其中 100 代表抵押品价值的 100%。例如,若 collateralFactor 为 50,用户最多可借入其抵押品价值的 50%。这确保贷款处于超额抵押状态,以维护借贷系统的安全性。若用户存入价值 $1,000 的抵押品且 collateralFactor 为 50%,则其最大可借金额为 $500。

映射

mapping(address => uint256) public collateralBalances; // User collateral balances
 mapping(address => Loan) public loans; // User loans
复制
  • CollateralBalances 映射:追踪每个用户存入的抵押品数量。每个用户都有与其地址关联的唯一抵押品余额。该映射用于验证用户是否有足够的抵押品来申请贷款或提取资金。

  • Loans 映射:通过将用户地址与其贷款信息关联,追踪每个用户的活跃贷款详情。它确保用户在仍有活跃贷款时无法申请新贷款。

事件

 event CollateralDeposited(address indexed user, uint256 amount); 
 event CollateralWithdrawn(address indexed user, uint256 amount);
 event LoanTaken(address indexed user, uint256 amount);
 event LoanRepaid(address indexed user, uint256 amount);
复制
  • CollateralDeposited 事件 – 当用户向合约存入抵押品时触发。记录用户地址和存入金额。

  • CollateralWithdrawn 事件 – 当用户从合约提取抵押品时触发。记录用户地址和提取金额。

  • LoanTaken 事件 – 当用户申请贷款时触发。记录用户地址和借入金额。

  • LoanRepaid 事件 – 当用户偿还贷款时触发。记录用户地址和还款金额。

构造函数

 constructor(IERC20 _collateralToken, IERC20 _lendingToken, uint256 _collateralFactor) Ownable(msg.sender) {
        require(_collateralFactor <= 100, "Collateral factor must be <= 100");
        collateralToken = _collateralToken;
        lendingToken = _lendingToken;
        collateralFactor = _collateralFactor;
    }
复制

构造函数是 Solidity 中的特殊函数,仅在合约部署时运行一次。其目的是:

  • 设置合约的初始配置。

  • 为关键状态变量赋值。

  • 通过 Ownable 建立所有权和访问控制。

构造函数接受三个参数:_collateralToken、_lendingToken 和 _collateralFactor。

  • _collateralToken – 用作贷款抵押品的代币。

  • _lendingToken – 用户从系统借入的代币。

  • _collateralFactor – 决定最大借款额度的抵押品百分比,确保不超过 100%。

初始化时,构造函数将:

  • 将 _collateralToken 指定为抵押资产。

  • 将 _lendingToken 设置为借贷代币。

  • 配置 _collateralFactor 以建立抵押品与贷款的比率。

此外,Ownable(msg.sender) 确保合约部署者成为所有者,赋予其管理权限,例如在必要时修改 collateralFactor。

设置抵押因子

function setCollateralFactor(uint256 _newFactor) external onlyOwner {
        require(_newFactor <= 100, "Collateral factor must be <= 100");
        collateralFactor = _newFactor;
    }
复制

该函数是一项重要的管理功能,允许合约所有者修改 抵押因子,该因子控制抵押品与可借资金之间的比率。抵押因子 用于确定用户可凭其抵押品借入的最大金额。通过允许更新该因子,合约可以适应不断变化的市场条件或风险状况。

该函数标记为 external,意味着只能从合约外部调用。onlyOwner 修饰符确保只有合约所有者(通常是部署者)才能调用此函数,提供严格的访问控制。

该函数确保新的 抵押因子 是介于 0 到 100 之间的有效百分比,以防止设置不切实际或不安全的值。例如,超过 100 的因子可能允许用户借入超过其抵押品价值的金额。

该函数使用传入的 _newFactor 更新 collateralFactor 状态变量。

存入抵押品

 function depositCollateral(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        collateralBalances[msg.sender] += _amount;
        collateralToken.transferFrom(msg.sender, address(this), _amount);
        emit CollateralDeposited(msg.sender, _amount);
    }
复制

该函数是允许用户向合约存入抵押品的核心功能。这是用户申请贷款前的必要步骤,确保抵押品在合约中得到妥善记录和存储。

该函数验证用户提供的金额大于零,并将用户的抵押品余额增加存入金额。它使用 transferFrom 将代币从用户转移到合约,这要求用户在调用此函数前授权合约使用其代币。

该函数实现以下功能:

  • 抵押品存入:用户将指定数量的预定义代币(collateralToken)存入合约。该抵押品作为其申请贷款的担保。

  • 追踪抵押品余额:存入金额被添加到用户的抵押品余额中,并在 collateralBalances 映射中进行追踪。

  • 启用借款:抵押品作为计算用户可借金额的基础。

提取抵押品

function withdrawCollateral(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        require(collateralBalances[msg.sender] >= _amount, "Insufficient collateral");
        uint256 maxWithdrawable = collateralBalances[msg.sender] - _loanRequiredCollateral(msg.sender);
        require(_amount <= maxWithdrawable, "Cannot withdraw collateral locked for a loan");
        collateralBalances[msg.sender] -= _amount;
        collateralToken.transfer(msg.sender, _amount);
        emit CollateralWithdrawn(msg.sender, _amount);
    }
复制

该函数允许用户提取部分或全部已存入的抵押品,同时确保作为活跃贷款担保而被锁定的抵押品无法被提取。

该函数验证提取金额大于零,并确认用户有足够的可用抵押品来满足提取请求。它计算任何活跃贷款所锁定的抵押品部分,将用户的抵押品余额减少提取金额,并将请求的代币从合约转回给用户。

总结而言,该函数实现以下功能:

  • 抵押品提取:允许用户从合约中提取其可用抵押品。

  • 贷款保护:确保用户无法提取用于担保活跃贷款所需的抵押品。

  • 状态管理:在成功提取后更新用户的抵押品余额。

申请贷款

function takeLoan(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        require(loans[msg.sender].isActive == false, "Existing loan must be repaid first");
        uint256 maxLoan = (collateralBalances[msg.sender] * collateralFactor) / 100;
        require(_amount <= maxLoan, "Loan exceeds collateral limit");
        loans[msg.sender] = Loan({
            amount: _amount,
            collateral: collateralBalances[msg.sender],
            isActive: true
        });
        lendingToken.transfer(msg.sender, _amount);
        emit LoanTaken(msg.sender, _amount);
    }
复制

该函数允许用户根据其已存入的抵押品从合约借入代币。贷款金额受用户存入的抵押品和合约 抵押因子 的限制,确保借款保持在安全范围内。该函数在促进贷款的同时维护系统安全,发挥着关键作用。

该函数确保以下内容:

  • 贷款金额大于零。

  • 用户没有活跃贷款,防止同时存在多笔贷款。

  • 请求的贷款不超过基于抵押品的最大借款限额。

  • loans 映射更新用户的贷款详情。

  • 借入的代币从合约转移给用户。

总结而言,该函数实现以下功能:

  • 贷款发放:允许用户借入以其存入抵押品为担保的代币。

  • 抵押品管理:确保贷款金额在用户的借款额度内。

  • 贷款追踪:维护用户贷款记录,防止过度借贷并确保系统完整性。

偿还贷款

function repayLoan(uint256 _amount) external {
        require(_amount > 0, "Amount must be greater than zero");
        Loan storage userLoan = loans[msg.sender];
        require(userLoan.isActive, "No active loan");
        require(_amount <= userLoan.amount, "Repay amount exceeds loan");
        lendingToken.transferFrom(msg.sender, address(this), _amount);
        userLoan.amount -= _amount;
        if (userLoan.amount == 0) {
            userLoan.isActive = false;
        }
        emit LoanRepaid(msg.sender, _amount);
    }
复制

该函数允许用户偿还其活跃贷款,可部分偿还或全额偿还。它更新贷款状态并确保还款得到正确处理,有助于维护系统的完整性。

该函数执行以下检查和操作:

  • 确保还款金额大于零,以防止无效操作。

  • 获取用户的贷款详情,并在继续操作前确认其有活跃贷款。

  • 确保还款金额不超过剩余贷款余额。

  • 将还款金额从用户转移到合约,需要事先对指定金额进行授权。

  • 减少贷款余额,减去还款金额。

  • 若贷款已全额偿还,该函数将标记其为非活跃状态。

总结而言,该函数实现以下功能:

  • 贷款还款:允许借款人将借入金额归还给合约。

  • 贷款追踪:还款后更新贷款余额和状态。

  • 透明度:触发事件以在链上记录还款交易。

测试

在 src 文件夹中创建 Token.sol 文件,并将以下代码添加到该文件中。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {ERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
    constructor(string memory name, string memory symbol) ERC20(name, symbol){}
    function mint(address user, uint256 amount) public {
        _mint(user, amount);
    }
}
复制

在 test 文件夹中创建 lending.t.sol 文件,并将以下代码添加到该文件中。

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import { LendingBorrowing} from "../src/Lending.sol";
import { Token } from "../src/Token.sol";
contract LendingTest is Test {
     LendingBorrowing public lending;
     Token public collateral;
     Token public lendingToken;
     address public user1 = makeAddr("user1");
     address public user2 = makeAddr("user2");
     address public user3 = makeAddr("user3");

    function setUp() public {
        collateral = new Token("collateralToken", "ct");
        lendingToken = new Token("lendingToken", "lt");
        lending = new LendingBorrowing(collateral, lendingToken, 80);
        collateral.mint(user1, 100 ether);
        lendingToken.mint(address(lending), 1000 ether);
    }
    function test_DepositCollateral() public {
        vm.startPrank(user1);
        collateral.approve(address(lending), 100 ether);
        lending.depositCollateral(50 ether);

        assert(collateral.balanceOf(user1) == 50 ether);
        assert(collateral.balanceOf(address(lending)) == 50 ether);  
    }

    function test_TakeLoan() public {
        vm.startPrank(user1);
        collateral.approve(address(lending), 100 ether);
        lending.depositCollateral(50 ether);
        vm.expectRevert();
        lending.takeLoan(100 ether);
        vm.expectRevert();
        lending.takeLoan(49 ether);
        lending.takeLoan(40 ether);
        assert(lendingToken.balanceOf(user1) == 40 ether);
    }

    function test_RepayLoan() public {
        vm.startPrank(user1);
        collateral.approve(address(lending), 100 ether);
        lending.depositCollateral(50 ether);
        lending.takeLoan(40 ether);
        lendingToken.approve(address(lending), 40 ether);
        lending.repayLoan(40 ether);
        assert(lendingToken.balanceOf(user1) == 0 ether);
    }

    function test_WithdrawCollateral() public {
        vm.startPrank(user1);
        collateral.approve(address(lending), 100 ether);
        lending.depositCollateral(50 ether);
        lending.takeLoan(40 ether);
         lendingToken.approve(address(lending), 40 ether);
        lending.repayLoan(40 ether);
        lending.withdrawCollateral(50 ether);
        assert(lendingToken.balanceOf(user1) == 0 ether);
        assert(collateral.balanceOf(user1) == 100 ether);
    }

}
复制

测试设置

该函数在每个测试函数之前调用,用于初始化状态。

  • 部署代币合约:collateral 和 lendingToken 合约均被部署。

  • 部署 LendingBorrowing 合约:LendingBorrowing 合约以 collateral token、lendingToken 和 80% 的抵押贷款比率进行初始化(意味着用户最多只能借入其抵押品价值的 80%)。

  • 铸造代币:- 为用户 1 铸造 100 个抵押代币。

  • 向借贷合约铸造 1000 个借贷代币。

测试存入抵押品

该函数测试用户能否向借贷合约存入抵押品。用户 1 授权借贷合约最多使用其 100 个抵押代币,并向合约存入 50 个抵押代币。该函数验证:

  • 用户 1 的余额减少了 50 个代币。

  • 借贷合约的抵押品余额增加了 50 个代币。

测试申请贷款

该函数测试用户能否根据其已存入的抵押品申请贷款。用户 1 存入 50 个抵押代币并授权借贷合约使用。

  • 测试确保申请超过允许金额或低于所需最低金额(基于抵押品)的贷款会回滚。

  • 用户 1 成功申请了 40 个 lendingToken 代币的贷款。

  • 测试验证申请贷款后用户 1 收到 40 个 lendingToken 代币。

测试偿还贷款

该函数测试用户能否正确偿还贷款。

  • 用户 1 存入 50 个抵押代币并申请了 40 个 lendingToken 代币的贷款。

  • 用户 1 授权借贷合约使用 40 个 lendingToken 代币并偿还贷款。

  • 测试验证贷款偿还后用户 1 的 lendingToken 余额为零。

测试提取抵押品

该函数测试用户能否在偿还贷款后提取其抵押品。

  • 用户 1 存入 50 个抵押代币,申请 40 个 lendingToken 代币的贷款,并偿还贷款。

  • 贷款偿还后,用户 1 提取其抵押品。

  • 测试验证:- 用户 1 收回其 50 个抵押代币。

  • 用户 1 的余额为 100(因为其最初铸造了 100 个代币)。

  • 贷款偿还后用户 1 的 lendingToken 余额为零。

运行测试

forge test

在 Linea 上部署和验证

将必要的变量添加到 .env 文件中,.env 文件应位于根目录。

LINEA_RPC_URL=https://linea-mainnet.infura.io/v3/ // Infura RPC endpoint for connecting to Linea 
PRIVATE_KEY=************************************************* // Wallet private key for signing transactions LINEA_API_KEY=************************* // API key for accessing Linea services via Infura
复制

将以下详细信息添加到 foundry.toml

[etherscan]
linea = { key = "${LINEA_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
复制

创建 Makefile 并添加以下详细信息

deploy:
    forge create src/Lending.sol: LendingBorrowing  --rpc-url $(LINEA_RPC_URL) --private-key $(PRIVATE_KEY)

verify:; forge verify-contract --rpc-url $(LINEA_RPC_URL) --chain linea <contract address> src/Lending.sol:LendingBorrowing --constructor-args arg1 arg2
复制

部署

make deploy

验证

make verify

总结

构建一个简单的借贷智能合约为理解 DeFi 和区块链开发奠定了基础。本指南涵盖了以下内容:

  • 存入抵押品 – 用户锁定资产作为贷款担保。

  • 申请贷款 – 借款人根据其抵押品获取资金。

  • 偿还贷款 – 用户归还借入资金以清偿债务。

  • 提取抵押品 – 用户在全额还款后取回抵押品。

通过整合这些要素,我们创建了一个功能完善的自动化借贷系统。随着 DeFi 的持续发展,这些概念将成为构建更高级金融应用的基础模块。

如需更多实践教程和区块链开发深度解析,请访问 MetaMask Developer YouTube 频道,并在 Twitter 上关注我们,获取最新动态。

AI 翻译。可能包含错误。请务必核实信息。

评价此翻译

本文作者:

  • Kingsley Okonkwo

    Kingsley 是一位经认证的以太坊区块链开发者,现任 Consensys 技术写作工程师,专注于开发者工具领域。加入 Consensys 之前,Kingsley 曾在 Gigster 和 Braintrust 等多个平台担任自由职业后端开发者。他拥有 5 年软件开发经验,熟练掌握 JavaScript(Node.js 和 React)、GoLang、SQL 及 Docker 等技术工具。Kingsley 擅长撰写分步教程和操作指南,善用最新 Web3 技术帮助开发者构建更优质的 dapp。他目前定居迪拜,业余时间喜欢打篮球和踢足球。

    阅读所有文章