1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[イーサリアム]スマートコントラクトを使った仮想通貨銀行の作り方

Last updated at Posted at 2019-12-22

#はじめに
ERC20とETHのためのオープンソースな銀行とレンディングプラットフォームを作成する。著者は、Merunas Grincalaitis merunasgrincalaitis@gmail.com氏である。世界初の仮想通貨銀行がスイスで営業を始めています。 ドイツの銀行、2020年から仮想通貨の販売・カストディが可能に:報道などがある。新しい技術にキャッチアップする。

###コードの場所
https://github.com/PacktPublishing/Mastering-Ethereum/tree/master/Chapter13/bank-dapp-master

###簡単なフロー

スライド1.jpeg

###機能

・ETHを銀行に預ける。
・任意のERC20トークンを担保にETHによる貸付を行う。
・プラットフォームに預けられたERC20トークンの価格を得るためにOraclizeを利用する。
・価格が40%を下回った場合にオーナーがオペレーター(オープンローンをクローズできる)を追加できるようにするホワイトリスト機能。
・ローンが貸し出された時点でのトークンの価格を現在の価格と比較して確認する監視機能。
・トークンが貸し出しに利用されている場合、貯金しているETHの量に基づいて預金者に利子を払う機能。
・預金者の現在の残高を表示する機能。

##関数フロー

###関数フロー(LenderとBank間)
スライド2.jpeg

###関数フロー(BorrowerとBank間)
スライド3.jpeg

#コード

##イベント

貸し出し時に発火される

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同様、今後コンプライアンスに基づいた金融業として、ニーズがあるのかも?

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?