#はじめに
ERC20とETHのためのオープンソースな銀行とレンディングプラットフォームを作成する。著者は、Merunas Grincalaitis merunasgrincalaitis@gmail.com氏である。世界初の仮想通貨銀行がスイスで営業を始めています。 ドイツの銀行、2020年から仮想通貨の販売・カストディが可能に:報道などがある。新しい技術にキャッチアップする。
###コードの場所
https://github.com/PacktPublishing/Mastering-Ethereum/tree/master/Chapter13/bank-dapp-master
###簡単なフロー
###機能
・ETHを銀行に預ける。
・任意のERC20トークンを担保にETHによる貸付を行う。
・プラットフォームに預けられたERC20トークンの価格を得るためにOraclizeを利用する。
・価格が40%を下回った場合にオーナーがオペレーター(オープンローンをクローズできる)を追加できるようにするホワイトリスト機能。
・ローンが貸し出された時点でのトークンの価格を現在の価格と比較して確認する監視機能。
・トークンが貸し出しに利用されている場合、貯金しているETHの量に基づいて預金者に利子を払う機能。
・預金者の現在の残高を表示する機能。
##関数フロー
#コード
##イベント
貸し出し時に発火される
event CreatedLoan(uint256 indexed id, address indexed token, uint256 indexed borrowedEth, address receiver);
トークン価格を更新した時に発火される
event UpdatedLoanTokenPrice(uint256 indexed id, int256 indexed tokenPrice);
##構造体
###Loan
貸し出しの基本構成です。id、借りている人のアドレス、検索ID、ステイクしているERC20トークンのコントラクトアドレス、ステイクしているERC20トークンの量、預けた時のERC20トークンの価格、現在のトークンの価格、貸し出したETHの量、貸し出しを行った時のunix time stamp、返済を行った時のunix time stamp、pending(検討中)・started(貸し出し中)・expired(返済済)の状態などから構成される。
struct Loan {
uint256 id;
address receiver;
bytes32 queryId;
address stakedToken;
uint256 stakedTokenAmount;
int256 initialTokenPrice;
int256 currentTokenPrice;
uint256 borrowedEth;
uint256 createdAt;
uint256 expirationDate;
bool isOpen;
string state; // It can be 'pending', 'started', 'expired', or 'paid'
}
###Hold
預金者の基本情報です。id、預金者のアドレス、日付、預金量から構成される。
struct Hold {
uint256 id;
address holder;
uint256 date;
uint256 quantity;
}
##mapping
ユーザー(預金者)アドレスの配列と預金(ETH)を紐付ける。
// User address => eth holding
mapping(address => Hold[]) public holdings;
ユーザー(預金者)アドレスと現在、特定のユーザーに貸し出しているETHの量を紐付ける。
// User address => amount of ETH currently lend for a particular user
mapping(address => uint256) public lendEth;
Oraclizeによる検索IDと構造体Loanを紐付ける。
// Query id by oraclize => Loan
mapping(bytes32 => Loan) public queryLoan;
Oraclizeによる検索IDと担保を確認するため現在の価格を更新する貸付の紐付け
// Query id => The loan to update the current price to check for collaterals
mapping(bytes32 => Loan) public queryUpdateLoanPrice;
IDと構造体Loanの紐付け
// Id => Loan
mapping(uint256 => Loan) public loanById;
ユーザーアドレス(利用者)とその利用者の貸付の紐付け
// User address => loans by that user
mapping(address => Loan[]) public userLoans;
##動的配列
現在ある貸し付けの基本情報を格納する配列
Loan[] public loans;
返済された貸付の基本情報を格納する配列
Loan[] public closedLoans;
##アドレス
預金者のアドレスを格納する
address[] public holders;
オペレイターのアドレスを格納する
address[] public operators;
仮想通貨銀行のオーナーのアドレス。コントラクトのデプロイ時に決まる。
address public owner;
###uint256
uint256 public lastId;
uint256 public earnings;
##関数
###addFunds(預金機能)
ETHをコントラクトに預けます。
/// @notice To add ETH funds to the bank, those funds may be used for loans and if so, the holder won't be able to extract those funds in exchange for a 5% total payment of their funds when the loan is closed
function addFunds() public payable {
require(msg.value > 0, 'You must send more than zero ether');
if(!checkExistingHolder()) {
holders.push(msg.sender);
}
if(holdingEth[msg.sender].holder != address(0)) {
Hold memory hold = holdingEth[msg.sender];
hold.investmentDates.push(now);
hold.investmentQuantities.push(msg.value);
holdingEth[msg.sender] = hold;
} else {
Hold memory hold = Hold(msg.sender, new uint256[](now), new uint256[](msg.value), new uint256[](now));
holdingEth[msg.sender] = hold;
}
}
###loan(レンディング機能)
ERC20トークンを担保として預けることでETHを借入ることができます。引数としてERC20コントラクトアドレスと借入るETHの量を渡します。またコントラクトがOraclizeを利用して担保のERC20トークンの価格を問い合わせるために、0.01ETHを関数実行時にコントラクトに送金する必要があります。この時作成されるLoanはpending(検討中)です。実際にOraclizeから担保となるトークン価格が返され、担保の総額が借入より多い場合に、pendingがstarted(貸し出し中)となります。
/// @notice To get a loan for ETH in exchange for the any compatible token note that you need to send a small quantity of ETH to process this transaction at least 0.01 ETH so that the oracle can pay for the cost of requesting the token value
/// @param _receivedToken The token that this contract will hold until the loan is payed
/// @param _quantityToBorrow The quantity of ETH that you want to receive as the loan
function loan(address _receivedToken, uint256 _quantityToBorrow) public payable {
require(_quantityToBorrow > 0, 'You must borrow more than zero ETH');
require(address(this).balance >= _quantityToBorrow, 'There are not enough ETH funds to lend you right now in this contract');
require(msg.value >= 10 finney, 'You must pay at least 0.01 ETH to run this function so that it can read the current token price');
string memory symbol = IERC20(_receivedToken).symbol();
// Request the price in ETH of the token to receive the loan
bytes32 queryId = oraclize_query(oraclize_query("URL", strConcat("json(https://api.bittrex.com/api/v1.1/public/getticker?market=ETH-", symbol, ").result.Bid"));)
Loan memory l = Loan(lastId, msg.sender, queryId, _receivedToken, 0, 0, _quantityToBorrow, now, 0, false, 'pending');
queryLoan[queryId] = l;
loanById[lastId] = l;
lastId++;
}
###__callback
oraclizeによって呼び出され、ローンの担保であるERC20トークンの価格を取得する。その後updataLoanPriceとsetLoan関数をそれぞれ呼び出す。
/// @notice The function that gets called by oraclize to get the price of the symbol to stake for the loan
/// @param _queryId The unique query id generated when the oraclize event started
/// @param _result The received token price with decimals as a string
/// @param _proof The unique proof to confirm that this function has been called by a valid smart contract
function __callback(
bytes32 _queryId,
string memory _result,
bytes memory _proof
) public {
require(msg.sender == oraclize_cbAddress(), 'The callback function can only be executed by oraclize');
Loan memory l = queryUpdateLoanPrice[_queryId];
Loan memory l = queryLoan[_queryId];
if(l.receiver != address(0)) {
updateLoanPrice(_result, _queryId);
} else if(l.receiver != address(0)) {
setLoan(_result, _queryId);
}
}
###updataLoanPrice
__callbackから呼び出される。ローンで使用されるトークンの価格を更新して、イベントUpdatedLoanTokenPriceを発火する。
/// @notice To update the price of a token used in a loan to close it if the value drops too low
function updateLoanPrice(string _result, bytes32 _queryId) internal {
Loan memory l = queryLoan[_queryId];
int256 tokenPrice = parseInt(_result);
l.currentTokenPrice = tokenPrice;
loanById[l.id] = l;
for(uint256 i = 0; i < userLoans[l.receiver].length; i++) {
if(userLoans[l.receiver][i].id == l.id) {
userLoans[l.receiver][i] = l;
break;
}
}
emit UpdatedLoanTokenPrice(l.id, tokenPrice);
}
###setLoan
__callbackから呼び出される。トークン価格が渡されるので、担保額を計算して貸付量より多い場合、実際に貸付が行われる。
function setLoan(string _result, bytes32 _queryId) internal {
Loan memory l = queryLoan[_queryId];
int256 tokenPrice = parseInt(_result);
uint256 amountToStake = l.stakedTokenAmount * tokenPrice * 0.5; // Multiply it by 0.5 to divide it by 2 so that the user sends double the quantity to borrow worth of tokens
require(tokenPrice > 0, 'The token price must be larger than absolute zero');
require(amountToStake >= l.borrowedEth, 'The quantity of tokens to stake must be larger than or equal twice the amount of ETH to borrow');
IERC20(l.stakedToken).transferFrom(l.receiver, address(this), l.stakedTokenAmount);
l.receiver.transfer(l.borrowedEth);
l.initialTokenPrice = tokenPrice;
l.currentTokenPrice = tokenPrice;
l.expirationDate = now + 6 months;
l.isOpen = true;
l.state = 'started';
loanById[l.id] = l;
queryLoan[_queryId] = l;
userLoans[l.receiver].push(l);
loans.push(l);
emit CreatedLoan(l.id, l.stakedToken, l.borrowedEth, l.receiver);
}
###payLoan(ローンの返済)
ローンの返済を行う。返済時は貸付の1.05倍のETHを送金する必要がある。(5%の利子を払う)担保のERC20トークンを返金する。支払われた利子はこのサービスの収益になる。
/// @notice To pay a given loan with the 5% fee of the lend ETH
/// @param _loanId The loan id to pay
function payLoan(uint256 _loanId) public payable {
Loan memory l = loanById[_loanId];
uint256 priceWithFivePercentFee = l.borrowedEth + (l.borrowedEth * 0.05);
require(l.isOpen, 'The loan must be open to be payable');
require(msg.value >= priceWithFivePercentFee, 'You must pay the ETH borrowed by the loan plus the five percent fee not less');
// If he paid more than he borrowed, return him the difference without the fee tho
if(msg.value > priceWithFivePercentFee) {
l.receiver.transfer(msg.value - priceWithFivePercentFee);
}
// Send him his tokens back
IERC20(l.stakedToken).transfer(l.stakedTokenAmount);
earnings += l.borrowedEth * 0.05;
l.isOpen = false;
l.state = 'paid';
queryLoan[l.queryId] = l;
loanById[l.id] = l;
closedLoans.push(l);
// Update the loan from the array of user loans with the paid status
for(uint256 i = 0; i < userLoans[l.receiver].length; i++) {
if(userLoans[l.receiver][i].id == l.id) {
userLoans[l.receiver][i] = l;
}
}
}
###payHolder(利息の受け取り)
現在の収益の5%を預金者に支払う。預金者はこの関数を実行して収益の一部をETHで受け取れる。
/// @notice To pay a holder depending on time holding up to 5% per year of the current dynamic earnings
function payHolder() public {
require(holdingEth[msg.sender].holder != address(0), 'You must hold more than zero ether to earn a profit');
int256 totalEarnings = checkEarnings();
// Update the state of the holdings before sending the ether to avoid reentrancy
for(uint256 i = 0; i < holdings[msg.sender].length; i++) {
holdings[msg.sender][i].date = now;
}
msg.sender.transfer(totalEarnings);
}
###extractFunds(預金引き出し)
ユーザー(預金者)が銀行で保有しているETHを全額引き出す。
/// @notice To extract the funds that a user may be holding in the bank
function extractFunds() public {
uint256 totalFunds;
for(uint256 i = 0; i < holdings[msg.sender].length; i++) {
totalFunds += holdings[msg.sender][i].quantity;
holdings[msg.sender][i].quantity = 0;
}
msg.sender.transfer(totalFunds);
}
###modifyOperator
オペレイターを追加するか削除する。
/// @notice To add an operator Ethereum address or to remove one based on the _type value
/// @param _type If it's an 'add' or 'remove' operation
/// @param _user The address of the operator
function modifyOperator(bytes32 _type, address _user) public onlyOwner {
bool operatorExists = false;
for(uint256 i = 0; i < operators.length; i++) {
if(_type == 'add' && operators[i] == _user) {
operatorExists = true;
break;
} else if(_type == 'remove' && operators[i] == _user) {
address lastOperator = operators[operators.length - 1];
operators[i] = lastOperator;
operators--;
break;
}
}
if(_type == 'add' && !operatorExists) {
operators.push(_user);
}
}
###closeLoan(貸し出しの強制終了)
担保の総額が貸し出しているETHよりも少ない場合に、その貸し出しを強制終了するこの場合、貸した銀行に元本は帰ってくるように設計できるのか?
function closeLoan(uint256 _loanId) public onlyOwnerOrOperator {
Loan memory l = loanById[_loanId];
require(l.receiver != address(0), 'The selected loan is invalid');
int256 percentageDrop =
// If the price of the token used in the loan dropped below or equal 40% the initial value, close it
if(l.currentTokenPrice <= l.initialTokenPrice * 0.6) {
} else if() {
// Or if the time to pay the loan has expired it, close it
}
l.isOpen = false;
l.state = 'expired';
queryLoan[l.queryId] = l;
loanById[l.id] = l;
closedLoans.push(l);
// Update the loan from the array of user loans with the paid status
for(uint256 i = 0; i < userLoans[l.receiver].length; i++) {
if(userLoans[l.receiver][i].id == l.id) {
userLoans[l.receiver][i] = l;
}
}
}
###moditorLoan(担保の監視)
ローンに使用されるトークンの価格を比較して、必要なときにそれらの担保総額の低下を検出できるようにする。
/// @notice To compare the price of the token used for the loan so that we can detect drops in value for selling those tokens when needed
function monitorLoan(uint256 _loanId) public payable {
Loan memory l = loanById[_loanId];
require(l.receiver != address(0), 'The loan id must be an existing loan');
string memory symbol = IERC20(l.stakedToken).symbol();
// Request the price in ETH of the token to receive the loan
bytes32 queryId = oraclize_query(oraclize_query("URL", strConcat("json(https://api.bittrex.com/api/v1.1/public/getticker?market=ETH-", symbol, ").result.Bid"));)
queryUpdateLoanPrice[queryId] = l;
}
###checkEarnings(銀行の収益の確認)
ETH預金により生じた金利収入を確認する。
利子=収益の5%預金期間 365日預金額
/// @notice To check how much ether you've earned
/// @return int256 The number of ETH
function checkEarnings() public view returns(int256) {
int256 quantityOfEarnings;
for(uint256 i = 0; i < holdings[msg.sender].length; i++) {
int256 percentageOfHoldings = holdingEth[msg.sender].quantity * 100 / address(this).balance;
uint256 daysPassed = now - holdings[msg.sender][i].date;
int256 thisEarnings = earnings * 0.05 * daysPassed / 365 days * percentageOfHoldings;
/* 365 days = earnings * 0.05 * percentage of holdings
timeSinceLastExit days = x earnings */
quantityOfEarnings += thisEarnings;
}
return quantityOfEarnings;
}
###checkExistingHolder
ユーザーが既に預金者のリストに追加されているかどうかを確認する
/// @notice To check if a user is already added to the list of holders
function checkExistingHolder() public view returns(bool) {
for(uint256 i = 0; i < holders.length; i++) {
if(holders[i] == msg.sender) {
return true;
}
}
return false;
}
###checkExistingOperator
ユーザーがオペレイターのリストに既に追加されているかどうかを確認する
/// @notice To check if a user is already added to the list of operators
function checkExistingOperator(address _operator) public view returns(bool) {
for(uint256 i = 0; i < operators.length; i++) {
if(operators[i] == _operator) {
return true;
}
}
return false;
}
}
#まとめ
レンディングサービスがやっていることとほとんど同じでした。ただし当局から銀行業の免許を取得して行うという点では、STO同様、今後コンプライアンスに基づいた金融業として、ニーズがあるのかも?