CompoundでLending Dappを作ってみる
- 投稿び
はじめに#
初めてAirBnBのアイデアを聞いたとき、興奮したのを覚えています。使っていないスペースを旅行者に貸すことで、自分も幾らか儲けることができる。win-winで、かつ資源を有効利用する素晴らしい仕組みだと思いました。
いま同じような興奮を暗号通貨の領域で感じています。Compoundを使えば、自分が使っていない暗号通貨をそれを有効利用できる誰かに貸して、金利を稼ぐことができます。しかもDaiなどのステーブルコインを使えば、金利は年10%までにもなります。Compoundの仕組み上ほとんど貸しリスクがないことを考えると、定期預金のように使われるようになるでしょう。0.01%ほどしか既存の銀行では金利を稼げないことを考えると、約1000倍です。
この記事では、Compoundを使ってSaiを貸し、金利を稼いだ上で引き出すアプリを作ります。先にコードを読んでしまいたい方はこちらのリンクから見れます。
コードを書く前にまずCompoundの簡単な仕組みを説明します。また、Etherscanでスマートコントラクトとやりとりしてみることで、アプリの中で呼ぶコントラクトの関数を理解します。
注: MakerDAOが複数担保型Daiに移行して、今までの単一担保型DaiをSaiと呼び、複数担保型DaiをDaiと呼ぶと発表しました。よって、この記事でも単一担保型DaiをSaiと呼びます。
Compoundの概要#
CompoundはEthereum上の暗号通貨の貸借のためのプロトコルです。非中心的で、誰も支配しない自由参加型のプロトコルであるため、誰でも参加することができます。
金利を稼げる市場をEthとEthereum上のトークンのために作っており、現在7つの市場があります。
この一つ一つの市場ごとにスマートコントラクトがあります。貸し手の人はスマートコントラクトに資産を供給して、金利を得ることができます。逆に、借り手の人はコントラクトに貯蓄されている資産を、担保をロックすることで借り、返済する際に金利を支払います。
貸し手も借り手も金利を指定する必要がなく、金利は市場原理によって決まります。借りる需要が小さければ金利は低くなり、逆に需要が大きければ金利は高くなります。これについては後で詳解します。
さらに、返済締め切りがないモデルのため、締め切りを決める必要がありません。ただ不履行のリスクを軽減するため、借り手は担保をロックしなければならず、担保の価格が一定額を下回った場合には、精算されます。
貸し手と借り手をマッチングし、金利や担保や期間などを決めなければいけないこれまでのモデルに比べると、効率的なのがわかると思います。貯蓄の仕組みを使うことでマッチングのコストを下げ、また、自動的な金利計算式や、あらかじめ決められた担保、そして返済締め切りの撤廃で交渉のコストを下げています。このおかげでユーザーは簡単に速く貸し借りをすることが可能です。
均衡利率モデル#
利率は、それぞれの市場の資産の流動性によって、ブロック毎に動的に変動します。流動性が低いと、利率は上がります。逆に流動性が高いと、利率は低くなります。
Compoundのダッシュボードを見ると、金利(APR = Annual Percentage Rates)が変動しているのがわかります。流動性はUtilizationという指標で確認することができます(以下はSaiの例)。これは、資産のプールから何パーセントが貸し出されているかという指標です。Utilizationが90%であれば、10%のSaiが残っていることになります。よって、Utilizationが高ければAPRが高くなります。逆にUtilizationが低ければAPRが低くなります。
cToken#
貸し手が金利を得られるという話をしましたが、具体的にはどうやって得るのでしょうか?実際に誰かがSaiをCompoundのコントラクトに送ったトランズアクションを見てみます。7600Saiをコントラクトに送っているのがわかると思いますが、それだけではなく、約361,487のcSaiを代わりに受け取っています。 *USDの値はEtherscanがCompoundとは違う算出方法で出しているため、間違っているようです。無視してください。
cSaiは、コントラクトに提供したSaiと金利の残高を記録しているERC20トークンです。つまり、cSaiをただ持っているだけで、金利を稼ぐことができます。
cSaiの価値は、毎ブロックごとに金利が足され、上がります。Saiを引き出して金利を稼ぎたい場合は、cSaiをコントラクトに送り返すことで、最初にロックしたSaiに加えて金利を得ることができます(cSaiはこの時点で焼却されます)。
このcSaiはcTokenと一般的に呼ばれるものの一つで、それぞれの市場にこのcTokenが存在します。EthならばcEth、BatならばcBatといった具合です。
このcTokenのERC20のメソッドとその他の拡張メソッドを使うことで、資産を貸したり、引き出したり、借りたり、返済したりすることができます。
Etherscanでスマートコントラクトとやりとりしてみる#
次に、Etherscanを使ってスマートコントラクトとやりとりすることで、後で作るアプリの機能をなぞってみます。
MetaMaskでトランズアクションを送るので、MainnetのEthとSaiが必要になります。EthはあるけどSaiはないという方は、Uniswapで交換することができます。
Saiを貸し、金利を稼いだのちに引き出すのには3つのステップがあります。
- Saiトークンコントラクトのapprove関数を呼び、cSaiコントラクトにSaiを送る許可を与える
- cSaiコントラクトのmint関数を呼び、Saiを提供して、cSaiを代わりに得る
- cSaiコントラクトのredeemUnderlying関数を呼び、cSaiとSaiを交換する
1. approve関数#
EtherscanのSaiトークンコントラクトのページに行って、Connect to Web3をいうリンクをクリックし、MetaMaskにログインしてください。これでMetaMaskを使ってトランズアクションが送れるようになります。
Saiを貸す前に、cSaiコントラクトがSaiを送れるようにする必要があります。そのためにapprove
関数を呼びます。Write Contractというタブに行き、下にスクロールすると、approve
関数があります。ここに送るのを許可したいSaiの量を入れます。
Solidityは固定小数点で、Saiトークンコントラクトは小数点を18
に設定しているので、0を18個足す必要があることに注意してください。
1Saiを送ることを許可したければ、以下のようになります。
Copied!1000000000000000000 // 1 * 10 ** 18
Writeボタンをクリックし、トランズアクションを送ります。トランズアクションがマインされるのを待ちます。遅い場合はガスを積んでみてください。
トランズアクションがうまくいれば、cSaiコントラクトは1Saiを送る許可を与えられています。
2. mint関数#
次にcSaiコントラクトのページに行きます。再度Connect to Web3のリンクをクリックし、MetaMaskにログインします。
mint関数でSaiをcSaiコントラクトに提供することで、発行されたcSaiを得ることができます。
Copied!function mint(uint mintAmount) returns (uint)
mint関数に貸し出したいSaiの量を指定します。
Writeボタンをクリックして、トランズアクションを送ります。
1Saiがコントラクトに送られ、cSaiを代わりに受け取っているのがEtherscanで見れると思います。
3. redeemUnderlying関数#
Saiを引き出すのには大きく2つの方法があります。引き出すSaiの量を指定するか、Saiと交換するcSaiの量を指定するかです。
redeemUnderlying関数はSaiの量を引数にとり、redeem関数はcSaiの量を引数に取ります。ここではアプリをシンプルにするためにredeemUnderlying関数を使います。
Copied!function redeemUnderlying(uint redeemAmount) returns (uint)
引き出したいSaiの量を指定します。
Writeボタンを押してトランズアクションを送ります。
cSaiを返却し、代わりにSaiを受け取っていることがわかります。
また、自分のアカウントにまだcSaiが残っていることがわかると思います。これがcDaiを持っている間に貯まった金利です。
セットアップ#
ではここからアプリを作っていきます。Saiを貸して、金利を稼いだ上で引き出すJavaScriptのアプリです。こちらのレポジトリでコードを確認できます。
プライベートキーとアドレスの取得
vanity-ethからプライベートキーとアドレスを取ってきてください。Generateボタンを押すと一番下に生成されたプライベートキーとアドレスが出てきます。安全ではないので、テスト目的以外で使わないようにしましょう。
EthとSaiを取得したアドレスに送る
上で取得したアドレスにEthとSaiを送ります。Ethは0.01あればガス代を払えます。Saiは1だけ使います。
*本当はテストネットを使いたいのですが、トークンを入手するのが難しいためやむなくメインネットを使っています。
Infuraのエンドポイントを取得
EthereumのネットワークとやりとりするためのプロバイダーとしてInfuraを使います。Infuraに登録して新しいプロジェクトを作ってからMainnetのエンドポイントを取得してください。URLは以下のようなフォーマットになります。
Copied!https://mainnet.infura.io/v3/[PROJECT_ID]
Npmのプロジェクトを作る
Copied!mkdir eht-compound-demo cd eht-compound-demo/ npm init --yes
*npmとnodejsが入っていない方はこちらからダウンロードできます。
gitをinitializeする
Copied!git init
web3のパッケージをインストールする
Copied!npm i —-save [email protected]
web3
はEthereumのJavaScript APIと呼ばれています。テストネットを含めたEthereumのネットワークへのインターフェースです。JavaScriptのアプリからEthereumのノードとやりとりしたり、ブロックチェーン上にデプロイされているスマートコントラクトとやりとりすることもできます。
ethereumjs-txパッケージをインストールする
Copied!npm i --save [email protected]
ethereumjs-txは、トランズアクションオブジェクトを生成したり、サインするのに使います。
package.jsonにスクリプトを追加
Copied!"scripts": { “approve-sai-main": "node --experimental-modules ./approveSaiMain.mjs", “supply-sai-main": "node --experimental-modules ./supplySaiMain.mjs", “withdraw-sai-main": "node --experimental-modules ./withdrawSaiMain.mjs" },
mjsは、ES6の文法で書くための拡張子です。
saiTokenContract.mjsとcSaiContract.mjsを作成
こことここからファイルをコピーしてきてください。
これらのファイルでは、SaiトークンコントラクトとcSaiトークンコントラクトのabiとアドレスを変数宣言し、エクスポートしています。
Saiを貸し出す機能を実装する#
ここからはコードを書いていきます。まずはSaiを貸し出す機能を実装します。
この機能を実装するためにファイルを2つ作ります。
approveSaiMain.mjs
supplySaiMain.mjs
1. approveSaiMain.mjs#
ファイル作成
approveSaiMain.mjsというファイルを作ります。
パッケージのインポート
Copied!import Web3 from "web3"; import EthTx from "ethereumjs-tx";
cSaiContractとsaiTokenContractのインポート
Copied!import cSaiContract from './cSaiContract'; import saiTokenContract from ‘./saiTokenContract';
Web3のセットアップ
Infuraをweb3のプロバイダーとして設定します
Copied!const web3 = new Web3( new Web3.providers.HttpProvider("https://mainnet.infura.io/v3/[YOUR_PROJECT_ID]" ) );
アドレスとプライベートキーの定数宣言
Copied!const addressFrom = “[YOUR_ADDRESS]"; const privKey = “[YOUR_PRIVATE_KEY]";
Saiトークンコントラクトのインスタンスを作成
web3のContract
というメソッドにSaiトークンコントラクトのabiとアドレスを渡すことでSaiトークンコントラクトのインスタンスを作成します。
Copied!const saiTokenContractInstance = new web3.eth.Contract( JSON.parse(saiTokenContract.saiTokenContractAbi), saiTokenContract.saiTokenContractAddress );
これでSaiトークンコントラクトのメソッドを呼べるようになります。
approve関数に渡す定数の宣言
Copied!const ADDRESS_SPENDER = cSaiContract.cSaiContractAddress; const TOKENS = web3.utils.toHex(-1);
先ほどは1 * 10 ** 18
の値を第二引数に渡しましたが、実際にアプリを作る際は-1
を渡すのが常套手段です。ユーザーからすると毎回approve
関数を呼びたくはないので、最大値を渡して、それ以降はapprove
関数を呼ばなくてもSaiを貸し出せるようにします。なぜ最大値にするために-1という数値を渡すのかを理解したい方は、英語ですがこちらのリンクを参考にしてください。
上の定数に入れた値をapprove関数に渡してabiをエンコード
Copied!const approveEncodedABI = saiTokenContractInstance.methods .approve(ADDRESS_SPENDER, TOKENS) .encodeABI();
sendSignedTx関数の作成
トランズアクションオブジェクトにサイン/シリアライズなどをして、それをEthereumネットワークにブロードキャストする関数を作成。
Copied!function sendSignedTx(transactionObject, cb) { let transaction = new EthTx(transactionObject); const privateKey = new Buffer.from(privKey, "hex"); transaction.sign(privateKey); const serializedEthTx = transaction.serialize().toString("hex"); web3.eth.sendSignedTransaction(`0x${serializedEthTx}`, cb); }
トランズアクションオブジェクトをsendSignedTx関数に渡して実行
Copied!web3.eth.getTransactionCount(addressFrom).then(transactionNonce => { const transactionObject = { chainId: 1, nonce: web3.utils.toHex(transactionNonce), gasLimit: web3.utils.toHex(100000), gasPrice: web3.utils.toHex(5000000000), to: saiTokenContract.saiTokenContractAddress, from: addressFrom, data: approveEncodedABI }; sendSignedTx(transactionObject, function(error, result){ if(error) return console.log("error ===>", error); console.log("sent ===>", result); }) } );
gasLimit
は100000
に設定します。gasPrice
は執筆時点では高めに設定していますが、EthGasStationで最適な額を調べて変更してみてください。EthGasStationでは単位がgwei
なので、Ethereum unit converterを使ってwei
に変更してください。
ファイルを走らせる
Copied!npm run approve-sai-main
_0xa18d71767ba95b95831fbf3a441494165002bc5f4104afdb0406511ebce7cbe3_
のようなトランズアクションハッシュが返ってくるので、それをEtherscanで検索して、トランズアクションの詳細をみてみてください(以下は例)。仮にトランズアクションが通らない場合は、ガスを積んで、トランズアクションを再度送ってみてください。
“Input Data”のところでapprove関数が呼ばれているのがわかります。
2. supplySaiMain.mjs#
次はsupplySaiMain.mjsというファイルを作り、mint
関数を呼びます。ほとんどのコードが被っているため、その箇所は割愛します。最終的なコードは、レポジトリで確認してください。
mint
関数に、貸し出したいSaiの値を渡してabiエンコードします。
Copied!const MINT_AMOUNT = web3.utils.toHex(1 * 10 ** 18); const mintEncodedABI = cSaiContractInstance.methods .mint(MINT_AMOUNT) .encodeABI();
このAbiをトランズアクションオブジェクトのdata
プロパティに渡します。gasLimit
は200000
に設定しています。
Copied!web3.eth.getTransactionCount(addressFrom).then(transactionNonce => { const transactionObject = { chainId: 1, nonce: web3.utils.toHex(transactionNonce), gasLimit: web3.utils.toHex(200000), gasPrice: web3.utils.toHex(5000000000), to: cSaiContract.cSaiContractAddress, from: addressFrom, data: mintEncodedABI }; sendSignedTx(transactionObject, function(error, result){ if(error) return console.log("error ===>", error); console.log("sent ===>", result); }) } );
ファイルを走らせます。
Copied!npm run supply-sai-main
1Saiが自分のアカウントからcSaiコントラクトに送られ、代わりにcSaiを受け取っていることがわかります。
Saiを引き出す機能の実装#
最後にSaiを引き出す機能の実装です。今回も被っている箇所は割愛します。最終的なコードはここからみられます。withdrawSaiMain.mjs
というファイルを作り、redeemUnderlying
関数を呼びます。
redeemUnderlying
関数に、引き出したいSaiの値を渡してabiエンコードします。
Copied!const REDEEM_AMOUNT = web3.utils.toHex(1 * 10 ** 18); const redeemUnderlyingEncodedABI = cSaiContractInstance.methods .redeemUnderlying(REDEEM_AMOUNT) .encodeABI();
このAbiをトランズアクションオブジェクトのdata
プロパティに渡します。gasLimit
は400000
に設定しています。
Copied!web3.eth.getTransactionCount(addressFrom).then(transactionNonce => { const transactionObject = { chainId: 1, nonce: web3.utils.toHex(transactionNonce), gasLimit: web3.utils.toHex(400000), gasPrice: web3.utils.toHex(5000000000), to: cSaiContract.cSaiContractAddress, from: addressFrom, data: redeemUnderlyingEncodedABI }; sendSignedTx(transactionObject, function(error, result){ if(error) return console.log("error ===>", error); console.log("sent ===>", result); }) } );
ファイルを走らせます。
Copied!npm run withdraw-sai-main
cSaiがcSaiコントラクトに戻り、Saiが引き出されているのがわかります。
また、自分のアカウントのcSaiのバランスを見ると、まだ少しだけ残っているのがわかります。これが稼いだ金利です。
まとめ#
貸借のための分散型プロトコルであるCompoundを使えば、誰でも自由にEthereum上の特定の暗号通貨を貸し借りできます。また、
マッチングのコストや交渉のコストを減らしたことで、既存のクリプトの貸借の仕組みに比べ速く簡単に取引をすることが可能です。
また開発者からも視点から見ても、スマーとコントラクトの関数を3つ呼ぶだけで、Saiを貸して金利を得た上で引き出すアプリを簡単に作れることがわかったと思います。ぜひこのアプリをdappやwalletに統合してみてください。
この記事を通じて、クリプトのシェアリングエコノミーの可能性を感じることができたでしょうか?最後まで読んでくれてありがとうございます!