去中心化金融(DeFi)透過移除中間人並使用智能合約自動處理交易,重塑了人們借貸資金的方式。過去需要銀行和繁瑣文件才能完成的事,現在只需幾次點擊即可在鏈上完成。
Linea 透過使用零知識卷積(zk-rollups)對交易進行批次處理與壓縮,協助開發者建構 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 上追蹤我們,掌握最新動態。