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

#はじめに
ERC20とETHのためのオープンソースな銀行とレンディングプラットフォームを作成する。著者は、Merunas Grincalaitis <merunasgrincalaitis@gmail.com>氏である。[世界初の仮想通貨銀行がスイスで営業を始めています。](https://www.swissinfo.ch/jpn/business/seba_%E4%B8%96%E7%95%8C%E5%88%9D%E3%81%AE%E4%BB%AE%E6%83%B3%E9%80%9A%E8%B2%A8%E9%8A%80%E8%A1%8C%E3%81%8C%E3%82%B9%E3%82%A4%E3%82%B9%E3%81%A7%E5%96%B6%E6%A5%AD%E9%96%8B%E5%A7%8B/45370740) [ドイツの銀行、2020年から仮想通貨の販売・カストディが可能に：報道](https://www.coindeskjapan.com/29500/)などがある。新しい技術にキャッチアップする。

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

###簡単なフロー

![スライド1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294497/97c448a7-fcb6-5264-1d0c-616e99500e08.jpeg)

###機能

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

##関数フロー

###関数フロー（LenderとBank間）
![スライド2.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294497/7557bf24-d6a0-8a23-35cd-6e9dc3ae716a.jpeg)



###関数フロー（BorrowerとBank間）
![スライド3.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/294497/11fdcddd-c454-0f38-4f45-3339ba97cfd0.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同様、今後コンプライアンスに基づいた金融業として、ニーズがあるのかも?