分散型金融(DeFi)は、仲介者を排除し、スマートコントラクトを使ってトランザクションを自動処理することで、貸し借りの仕組みを根本から変えました。かつては銀行や書類手続きが必要だったことが、今ではすべてオンチェーンで数回のクリックで完結します。
Lineaは、ゼロ知識ロールアップ(zk-rollups)を使ってトランザクションをバッチ処理・圧縮することで、Ethereumメインネットと比べてスケーラビリティを向上させ、ガスコストを削減したDeFiアプリケーションの構築を開発者が行えるよう支援します。
このガイドでは、Linea上でシンプルな貸し借りスマートコントラクトを構築・デプロイする手順を説明します。担保の管理、ローンの発行、返済ルールの設定など、DeFiレンディングシステムに不可欠な機能を学べます。
最終的には、動作するスマートコントラクトと、DeFiレンディングの仕組みへの深い理解が得られます。ブロックチェーン開発が初めての方も、スキルを磨きたい方も、このステップバイステップガイドでLineaでの開発を始めるためのツールが手に入ります。
前提条件
Solidityの基本的な理解が必要です。
NodejsとFoundryがPCにインストールされている必要があります。
コントラクトの概要
このコントラクトで実現できること:
担保の預け入れ:ユーザーはコントラクトに担保を預け入れることができ、詳細はスマートコントラクトに安全に保存されます。
借り入れ:ユーザーは預け入れた担保を元にローンを組むことができます。
返済:ユーザーはローンを返済して担保をアンロックできます。
担保の引き出し:ユーザーはアクティブなローンによってロックされていない担保を引き出せます。
プロジェクトのセットアップ
このプロジェクトには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;
上記のスニペットは3つの重要な状態変数を定義しており、それぞれがレンディングプラットフォームの機能を有効化・保護する特定の役割を担っています。以下に各変数の説明を示します:
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の3つの引数を受け取ります。
_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コントラクトが担保トークン、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でフォローして最新情報をお届けします。