はじめに
ブロックチェーン技術の発展により、従来の金融サービスを分散化・自動化する「DeFi(分散型金融)」が注目を集めています。その中でも、個人間の融資を仲介する「ソーシャルレンディング」をブロックチェーン上で実現する取り組みが増えています。
本記事では、Ethereum上で動作する分散型ソーシャルレンディングのスマートコントラクトを実装し、その仕組みと技術的なポイントを解説します。このコントラクトでは、担保付きローンの作成、資金提供、返済、デフォルト処理などの機能を提供します。
コントラクトの概要
今回実装するSocialLendingWithCollateral
コントラクトは、以下の主要機能を持ちます:
- 担保付きローンのリクエスト:借り手がERC20トークンを担保として預け、ETHでのローンをリクエスト
- ローンへの資金提供:貸し手が借り手のリクエストに対してETHで資金提供
- ローンの返済:借り手が期限内に元本と利息を返済
- デフォルト処理:返済期限を過ぎた場合の担保の清算
また、以下のセキュリティ機能も実装しています:
- リエントランシー攻撃の防止
- 緊急停止機能
- Chainlinkオラクルによる担保価値の評価
- アクセス制御
Github
技術スタック
- Solidity: ^0.8.0
- OpenZeppelin: コントラクトのセキュリティと標準実装
- Chainlink: 価格フィードオラクル
- Hardhat: 開発・テスト環境
コントラクトの実装
1. 基本構造とインポート
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// OpenZeppelinのライブラリをインポート
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
// Chainlinkのオラクルインターフェースをインポート
import "./interfaces/AggregatorV3Interface.sol";
// カスタムエラー定義
error InvalidAmount();
error InvalidInterestRate();
// ... その他のエラー定義 ...
contract SocialLendingWithCollateral is ReentrancyGuard, Ownable, Pausable {
using SafeERC20 for IERC20;
// ... コントラクト本体 ...
}
OpenZeppelinのライブラリを活用して、セキュリティと標準実装を取り入れています。また、Solidity 0.8.0以降で導入されたカスタムエラーを使用してガス効率を向上させています。
2. 状態変数と構造体
// 定数
uint256 public constant BASIS_POINTS = 10000;
uint256 public constant MAX_INTEREST_RATE = 2000; // 20%
uint256 public constant MAX_PLATFORM_FEE = 500; // 5%
uint256 public constant SECONDS_PER_YEAR = 365 days;
uint256 public constant PRICE_FEED_TIMEOUT = 1 hours; // 価格フィードの有効期限
// ローンの状態を表す列挙型
enum LoanState { Requested, Funded, Repaid, Defaulted, Cancelled }
// ローンの構造体
struct Loan {
address payable borrower;
address payable lender;
uint256 principalAmount; // 元本
uint256 interestRate; // 利率(ベーシスポイント、例: 500で5%)
uint256 repaymentAmount; // 返済総額
uint256 duration; // 期間(秒)
uint256 startTime; // 開始時刻
LoanState state; // ローンの状態
address collateralToken; // 担保トークンのアドレス
uint256 collateralAmount;// 担保数量
uint256 remainingRepaymentAmount; // 残りの返済額
}
// ローンIDからローン情報へのマッピング
mapping(uint256 => Loan) public loans;
uint256 public loanCount;
// ユーザーのアクティブローン数追跡
mapping(address => uint256) public borrowerActiveLoans;
mapping(address => uint256) public lenderActiveLoans;
// 許可された担保トークンリスト
mapping(address => bool) public allowedCollateralTokens;
// 担保トークンのデシマル
mapping(address => uint8) public collateralTokenDecimals;
// オラクルのマッピング(担保トークンアドレス => プライスフィードアドレス)
mapping(address => address) public priceFeeds;
// プラットフォーム手数料(ベーシスポイント)
uint256 public platformFee = 100; // 1%
address public feeRecipient;
// ローン・トゥ・バリュー(LTV)比率(例: 5000で50%)
uint256 public ltvRatio = 5000;
ローン情報を管理するための構造体と、各種マッピングを定義しています。特に注目すべき点は:
- ベーシスポイント(1万分率)を使用して、パーセンテージを整数で表現
- ローンの状態を列挙型で管理
- 担保トークンとプライスフィードの関連付け
3. イベント定義
// イベントの定義
event LoanRequested(
uint256 indexed loanId,
address indexed borrower,
uint256 amount,
uint256 interestRate,
uint256 duration,
address collateralToken,
uint256 collateralAmount
);
event LoanFunded(uint256 indexed loanId, address indexed lender);
event LoanPartiallyRepaid(uint256 indexed loanId, address indexed borrower, uint256 amountRepaid, uint256 remainingAmount);
event LoanRepaid(uint256 indexed loanId, address indexed borrower, address indexed lender, uint256 repaymentAmount);
event DefaultDeclared(uint256 indexed loanId, address indexed lender);
event LoanCancelled(uint256 indexed loanId, address indexed borrower);
// ... その他のイベント ...
ブロックチェーン上でのイベント追跡とフロントエンド連携のために、各種アクションに対応するイベントを定義しています。
4. 修飾子とアクセス制御
// 修飾子: 借り手のみ
modifier onlyBorrower(uint256 loanId) {
if (msg.sender != loans[loanId].borrower) revert Unauthorized();
_;
}
// 修飾子: 貸し手のみ
modifier onlyLender(uint256 loanId) {
if (msg.sender != loans[loanId].lender) revert Unauthorized();
_;
}
// 修飾子: 有効なローンIDかチェック
modifier validLoanId(uint256 loanId) {
if (loanId >= loanCount) revert InvalidLoanId();
_;
}
// 緊急停止機能
function pause() external onlyOwner {
_pause();
}
function unpause() external onlyOwner {
_unpause();
}
アクセス制御と入力検証のための修飾子を定義しています。また、緊急時にコントラクトの機能を一時停止できる機能も実装しています。
5. 設定関数
// 担保トークンの許可状態を設定する関数
function setCollateralTokenStatus(address token, bool allowed) external onlyOwner {
if (token == address(0)) revert InvalidAddress();
allowedCollateralTokens[token] = allowed;
emit CollateralTokenStatusUpdated(token, allowed);
}
// 担保トークンのデシマルを設定する関数
function setCollateralTokenDecimals(address token, uint8 decimals) external onlyOwner {
if (token == address(0)) revert InvalidAddress();
collateralTokenDecimals[token] = decimals;
emit CollateralTokenDecimalsUpdated(token, decimals);
}
// 担保トークンに対するプライスフィードを設定する関数(管理者用)
function setPriceFeed(address token, address priceFeed) external onlyOwner {
if (token == address(0) || priceFeed == address(0)) revert InvalidAddress();
priceFeeds[token] = priceFeed;
emit PriceFeedUpdated(token, priceFeed);
}
// ... その他の設定関数 ...
コントラクト管理者が各種パラメータを設定するための関数を提供しています。
6. ローンリクエスト機能
// 借り手がローンをリクエストする関数
function requestLoan(
uint256 amount,
uint256 interestRate,
uint256 duration,
address collateralToken,
uint256 collateralAmount
) external nonReentrant whenNotPaused {
if (amount == 0) revert InvalidAmount();
if (interestRate == 0 || interestRate > MAX_INTEREST_RATE) revert InvalidInterestRate();
if (duration == 0) revert InvalidDuration();
if (collateralAmount == 0) revert InvalidCollateral();
if (!allowedCollateralTokens[collateralToken]) revert TokenNotAllowed();
// 担保価値の評価
uint256 collateralValueInETH = getCollateralValueInETH(collateralToken, collateralAmount);
uint256 requiredCollateralValueInETH = amount * ltvRatio / BASIS_POINTS;
if (collateralValueInETH < requiredCollateralValueInETH) revert InsufficientCollateralValue();
// 担保のデポジット
IERC20(collateralToken).safeTransferFrom(msg.sender, address(this), collateralAmount);
uint256 loanId = loanCount++;
// 単利による返済総額の計算(オーバーフロー防止のため計算順序を変更)
uint256 interestAmount = amount * interestRate * duration / SECONDS_PER_YEAR / BASIS_POINTS;
uint256 repaymentAmount = amount + interestAmount;
loans[loanId] = Loan({
borrower: payable(msg.sender),
lender: payable(address(0)),
principalAmount: amount,
interestRate: interestRate,
repaymentAmount: repaymentAmount,
duration: duration,
startTime: 0,
state: LoanState.Requested,
collateralToken: collateralToken,
collateralAmount: collateralAmount,
remainingRepaymentAmount: repaymentAmount
});
borrowerActiveLoans[msg.sender]++;
emit LoanRequested(loanId, msg.sender, amount, interestRate, duration, collateralToken, collateralAmount);
}
借り手がローンをリクエストする機能です。主なポイント:
- 入力パラメータの検証
- 担保価値の評価(Chainlinkオラクル使用)
- 担保のデポジット
- 返済額の計算(単利)
- ローン情報の保存
7. 資金提供機能
// 貸し手がローンを資金提供する関数
function fundLoan(uint256 loanId) external payable nonReentrant validLoanId(loanId) whenNotPaused {
Loan storage loan = loans[loanId];
if (loan.state != LoanState.Requested) revert InvalidLoanState();
if (msg.value != loan.principalAmount) revert IncorrectFundingAmount();
if (loan.borrower == msg.sender) revert SelfFunding();
// 状態変更を先に行う(再入攻撃対策)
loan.lender = payable(msg.sender);
loan.startTime = block.timestamp;
loan.state = LoanState.Funded;
lenderActiveLoans[msg.sender]++;
// プラットフォーム手数料の計算と送金
uint256 feeAmount = msg.value * platformFee / BASIS_POINTS;
uint256 amountToBorrower = msg.value - feeAmount;
// 手数料送金
(bool feeSuccess, ) = feeRecipient.call{value: feeAmount}("");
if (!feeSuccess) revert TransferFailed();
// 借り手への送金
(bool success, ) = loan.borrower.call{value: amountToBorrower}("");
if (!success) revert TransferFailed();
emit LoanFunded(loanId, msg.sender);
}
貸し手がローンに資金を提供する機能です。主なポイント:
- 状態変更を先に行い、再入攻撃を防止
- プラットフォーム手数料の計算と送金
- 借り手への資金送金
8. 返済機能
// 借り手が返済する関数(部分返済をサポート)
function repayLoan(uint256 loanId) external payable nonReentrant onlyBorrower(loanId) validLoanId(loanId) whenNotPaused {
Loan storage loan = loans[loanId];
if (loan.state != LoanState.Funded) revert InvalidLoanState();
if (msg.value == 0) revert InvalidAmount();
uint256 amount = msg.value;
uint256 excessAmount = 0;
// 状態変更を先に行う(再入攻撃対策)
if (amount >= loan.remainingRepaymentAmount) {
// 全額返済
excessAmount = amount - loan.remainingRepaymentAmount;
amount = loan.remainingRepaymentAmount;
loan.remainingRepaymentAmount = 0;
loan.state = LoanState.Repaid;
borrowerActiveLoans[loan.borrower]--;
lenderActiveLoans[loan.lender]--;
// 担保の返却
IERC20(loan.collateralToken).safeTransfer(loan.borrower, loan.collateralAmount);
emit LoanRepaid(loanId, loan.borrower, loan.lender, amount);
} else {
// 部分返済
loan.remainingRepaymentAmount -= amount;
emit LoanPartiallyRepaid(loanId, loan.borrower, amount, loan.remainingRepaymentAmount);
}
// 貸し手への送金
(bool success, ) = loan.lender.call{value: amount}("");
if (!success) revert TransferFailed();
// 余剰分を借り手に返却
if (excessAmount > 0) {
(bool excessSuccess, ) = loan.borrower.call{value: excessAmount}("");
if (!excessSuccess) revert TransferFailed();
}
}
借り手がローンを返済する機能です。主なポイント:
- 部分返済と全額返済の両方をサポート
- 全額返済時は担保を返却
- 過払い分は借り手に返却
- 状態変更を先に行い、再入攻撃を防止
9. デフォルト処理
// 貸し手がデフォルトを宣言して担保を取得する関数
function declareDefault(uint256 loanId) external nonReentrant onlyLender(loanId) validLoanId(loanId) whenNotPaused {
Loan storage loan = loans[loanId];
if (loan.state != LoanState.Funded) revert InvalidLoanState();
if (block.timestamp <= loan.startTime + loan.duration) revert LoanNotExpired();
if (loan.remainingRepaymentAmount == 0) revert LoanAlreadyRepaid();
// 状態変更を先に行う(再入攻撃対策)
loan.state = LoanState.Defaulted;
borrowerActiveLoans[loan.borrower]--;
lenderActiveLoans[loan.lender]--;
// 担保の貸し手への移転
IERC20(loan.collateralToken).safeTransfer(loan.lender, loan.collateralAmount);
emit DefaultDeclared(loanId, loan.lender);
}
// 自動的にデフォルトを検出して担保を清算する関数(誰でも呼び出し可能)
function checkAndDeclareDefault(uint256 loanId) external nonReentrant validLoanId(loanId) whenNotPaused {
// ... 同様の処理 ...
}
返済期限を過ぎたローンのデフォルト処理を行う機能です。主なポイント:
- 貸し手専用の
declareDefault
と誰でも呼び出せるcheckAndDeclareDefault
の2つの関数を提供 - 期限切れの確認
- 担保の貸し手への移転
10. 担保価値評価
// 担保のETHにおける価値を取得する関数
function getCollateralValueInETH(address collateralToken, uint256 collateralAmount) public view returns (uint256) {
address priceFeedAddress = priceFeeds[collateralToken];
if (priceFeedAddress == address(0)) revert PriceFeedNotAvailable();
AggregatorV3Interface priceFeed = AggregatorV3Interface(priceFeedAddress);
// 価格データの取得と検証
(
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = priceFeed.latestRoundData();
// 価格データの検証
if (price <= 0) revert InvalidPriceData();
if (updatedAt < block.timestamp - PRICE_FEED_TIMEOUT) revert StaleData();
if (answeredInRound < roundId) revert StaleData();
uint8 decimals = priceFeed.decimals();
// 担保トークンのデシマルを考慮した計算
uint8 tokenDecimals = collateralTokenDecimals[collateralToken];
uint256 normalizedAmount = collateralAmount;
if (tokenDecimals > 0) {
normalizedAmount = collateralAmount * 10**18 / 10**uint256(tokenDecimals);
}
uint256 collateralValueInETH = normalizedAmount * uint256(price) / 10**uint256(decimals);
return collateralValueInETH;
}
Chainlinkオラクルを使用して担保トークンのETH価値を評価する機能です。主なポイント:
- プライスフィードの存在確認
- 価格データの鮮度と有効性の検証
- トークンのデシマルを考慮した正規化
- 価格計算
テストの実装
コントラクトの動作を検証するために、Hardhatを使用した包括的なテストを実装しました。
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("SocialLendingWithCollateral", function () {
let socialLending;
let owner, feeRecipient, borrower, lender, mockToken, mockPriceFeed;
beforeEach(async function () {
// テスト環境のセットアップ
[owner, feeRecipient, borrower, lender, ...others] = await ethers.getSigners();
// モックトークンとプライスフィードのデプロイ
const MockERC20Factory = await ethers.getContractFactory("MockERC20");
mockToken = await MockERC20Factory.deploy("Mock Token", "MOCK", 18);
const MockPriceFeedFactory = await ethers.getContractFactory("MockPriceFeed");
mockPriceFeed = await MockPriceFeedFactory.deploy();
// メインコントラクトのデプロイ
const SocialLendingFactory = await ethers.getContractFactory("SocialLendingWithCollateral");
socialLending = await SocialLendingFactory.deploy(feeRecipient.address);
// 初期設定
await socialLending.setCollateralTokenStatus(mockToken.getAddress(), true);
await socialLending.setCollateralTokenDecimals(mockToken.getAddress(), 18);
await socialLending.setPriceFeed(mockToken.getAddress(), mockPriceFeed.getAddress());
await mockToken.mint(borrower.address, ethers.parseEther("1000"));
await mockToken.connect(borrower).approve(socialLending.getAddress(), ethers.parseEther("1000"));
});
// 各種テストケース
describe("ローンリクエスト", function () {
it("有効なローンリクエストが作成できること", async function () {
// テストコード
});
it("担保価値が不足している場合はリクエストが失敗すること", async function () {
// テストコード
});
});
// その他のテストケース
});
テストでは以下の項目を検証しています:
- 基本設定(コンストラクタ、トークン設定など)
- ローンリクエスト機能
- 資金提供機能
- 返済機能(部分返済と全額返済)
- デフォルト処理
セキュリティ上の考慮点
このコントラクトでは、以下のセキュリティ対策を実装しています:
-
リエントランシー攻撃対策:
- OpenZeppelinの
ReentrancyGuard
の使用 - 状態変更を外部呼び出しの前に行う「Checks-Effects-Interactions」パターンの採用
- OpenZeppelinの
-
オーバーフロー/アンダーフロー対策:
- Solidity 0.8.0以降の自動チェック機能の活用
- 計算順序の最適化
-
価格操作対策:
- Chainlinkオラクルの使用
- 価格データの鮮度と有効性の検証
-
アクセス制御:
- 適切な修飾子による関数アクセスの制限
- OpenZeppelinの
Ownable
の使用
-
緊急停止機能:
- OpenZeppelinの
Pausable
の使用 - 緊急時にコントラクトの機能を一時停止可能
- OpenZeppelinの
改善点と今後の展望
このコントラクトには、さらに以下の改善点が考えられます:
-
担保価値の変動対応:
- 担保価値が大幅に下落した場合の自動清算機能
- 追加担保の要求機能
-
複数通貨のサポート:
- ETH以外の通貨でのローン提供
- 複数の担保トークンの組み合わせ
-
ガバナンス機能:
- パラメータ変更の投票システム
- 手数料分配の自動化
-
流動性プール:
- 個別の貸し手ではなく、流動性プールからの資金提供
- 自動金利調整メカニズム
-
信用スコアシステム:
- オンチェーンでの借り手の信用履歴の構築
- 信用スコアに基づく金利調整
まとめ
本記事では、Ethereum上で動作する分散型ソーシャルレンディングのスマートコントラクトの実装について解説しました。このコントラクトは、担保付きローンの作成、資金提供、返済、デフォルト処理などの基本機能を提供し、セキュリティと使いやすさを重視して設計されています。
DeFiの発展とともに、このような分散型金融サービスはますます重要になっていくでしょう。本実装が、ブロックチェーン上での金融サービス開発の参考になれば幸いです。