去中心化金融(DeFi)通过消除中间人并使用智能合约自动处理交易,重塑了人们借贷资金的方式。过去需要银行和繁琐文件才能完成的事情,如今只需几次点击即可在链上实现。
Linea 通过使用零知识 rollup(zk-rollup)对交易进行批处理和压缩,帮助开发者构建 DeFi 应用程序,与以太坊主网相比,具有更好的可扩展性和更低的 gas 费用。
在本指南中,我们将逐步介绍如何在 Linea 上构建和部署一个简单的借贷智能合约。你将学习如何管理抵押品、发放贷款以及设置还款规则——这些都是 DeFi 借贷系统的核心功能。
学完本指南后,你将拥有一个可运行的智能合约,并对 DeFi 借贷的底层运作原理有深入的理解。无论你是区块链开发新手,还是希望提升技能的开发者,本分步指南都将为你提供在 Linea 上开始构建所需的工具。
前提条件
你应具备 Solidity 的基础知识。
你的电脑上应已安装 Nodejs 和 Foundry。
合约概述
本合约支持以下功能:
抵押品存入:用户可以向合约存入抵押品,相关信息安全存储在智能合约中。
借款:用户可以凭借已存入的抵押品申请贷款。
还款:用户可以偿还贷款以解锁其抵押品。
抵押品提取:用户可以提取未因活跃贷款而被锁定的抵押品。
项目设置
本项目将使用 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借贷智能合约完整代码
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;
uint256 collateral;
bool isActive;
}
IERC20 public immutable collateralToken;
IERC20 public immutable lendingToken;
uint256 public collateralFactor;
mapping(address => uint256) public collateralBalances;
mapping(address => Loan) public 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;
uint256 collateral;
bool isActive;
}
Loan 结构体作为存储用户个人贷款详情的蓝图。结构体中的每个字段代表贷款状态和条件的关键信息。以下是各字段的说明:
Amount(金额):存储用户已借入的代币数量。该值用于追踪用户的欠款金额、确定还款义务,并验证贷款条件。
Collateral(抵押品):存储用户为担保贷款而存入的抵押品价值。它保护出借方的资金,并根据抵押因子帮助确定最大可借金额。
IsActive(是否活跃):表示贷款当前是否处于活跃状态。该字段用于追踪贷款状态,并防止用户在现有贷款未全额还清前申请新贷款。
状态变量
IERC20 public immutable collateralToken;
IERC20 public immutable lendingToken;
uint256 public collateralFactor;
上述代码片段定义了三个关键状态变量,每个变量在借贷平台的功能实现和安全保障中发挥特定作用。以下是这些变量的说明:
CollateralToken(抵押代币):代表用户作为贷款抵押品存入的代币。例如,若使用 USDC 作为抵押品,该变量存储 USDC 合约的区块链地址。
LendingToken(借贷代币):该变量代表平台向借款人出借的代币。若 USDT 为借贷代币,则存储 USDT 合约的区块链地址。
CollateralFactor(抵押因子):定义用户可借入的抵押品价值百分比。以整数表示,其中 100 代表抵押品价值的 100%。例如,若 collateralFactor 为 50,用户最多可借入其抵押品价值的 50%。这确保贷款处于超额抵押状态,以维护借贷系统的安全性。若用户存入价值 $1,000 的抵押品且 collateralFactor 为 50%,则其最大可借金额为 $500。
映射
mapping(address => uint256) public collateralBalances;
mapping(address => Loan) public 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 文件,并将以下代码添加到该文件中。
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 文件,并将以下代码添加到该文件中。
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/
PRIVATE_KEY=*************************************************
将以下详细信息添加到 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 上关注我们,获取最新动态。