ブラウニー “Brownie” でDeFiのテストが簡単にできる!!
スマートコントラクトの開発フレームワークはHardhatやTruffleが有名ですが、Brownieを使えばPythonでスマートコントラクトの開発がすすめられます。このフレームワークはPython使いにはよいですね。DeFiの開発では既存のプロトコルとのインタラクションをテストしたいことが多いと思いますが、BrownieであればメインネットのフォークとローカルのSolidityのスマートコントラクトをローカルで効率的に開発・テストができます。先日のStableCreditの脆弱性の再現のリポジトリでこのフレームワークの存在に気がつきました。Pythonに慣れていなくてもこのフレームワークは非常に使いやすいと思います。今回はBrownieをインストールした後にDeFiプロトコルとフラッシュローンのテストまでやってみました。
目次です。
- メインネットのテストどうする問題
- Brownieを使ってみましょう
- フラッシュローンのテストをする
- ローカルにモックコントラクトをデプロイする
- 実際のプロトコルをローカルにデプロイする
モックコントラクトは関数等でダミーデータを返すように定義しておく方法だと思います。この方法は作成するモックコントラクトの量が少なければ問題ないでしょう。ただ多くのプロトコルや関数のダミーを作成するとなると骨が折れそうです。また現実的に存在する脆弱性やバグを突くような使用方法を発見することができないのであまりお勧めの方法ではありません。なぜなら関数のリターンを与える変数によって操作することやReentrancyを仕組むことができないと思われるので。
次に実際のオープンソースプロトコルをgithubから持ってきてローカルに全てデプロイしていまう方法が書かれています。この方法もお勧めされていません。理由としてはローカルチェーンにデプロイされた時点でメインネットのようなステートがないため、現実的にありうるウォレット、ユーザやプールのバランスを設定しなければならないからです。ブートストラップの問題ということですね。そのためのこの方法を選んでいる開発者はほとんどいないとのこと。
海外の開発者にはメインネットでテストするのが一番であるという認識があるようです。RinkebyやKovanなどがテストネットとして存在していますが、実際問題としてメインネットと同じスマートコントラクトが用意されていないケースが非常に多い。メインネットには流動性があり、フラッシュローンやレンディングもメインネットを使ってテストしたほうが確実です。中には本当にメインネットへコントラクトをデプロイして、『User at your own risk』でユーザに使わせてしまっているケースもあります(yearn.finance系のプロジェクトはこれ)。この記事ではメインネットをローカルにフォークすればテストしたいステートが得られるとして、ganache-cliを使ったメインネットのフォーク方法が提案されています。
メインネットをフォークしてローカルチェーンでウォレットの残高を確認するスクリプトを走らせてみましょう。フォークの仕方は簡単で -f
オプションを使うだけです。 -i
のオプションはネットワーク番号で数字の1がメインネットです。これだけでローカルにメインネットのブロックチェーンがフォークされます。デフォルトのURIである127.0.0.1:8545で接続可能です。実際にメインネットで使っているウォレットの残高をNode.jsのスクリプトで確認すると正しい値が返ってきました。ただフォークがあまり安定せずたまにこのエラーが発生しました。
$ ganache-cli -f https://mainnet.infura.io/v3/<your end point> -i 1
$ node test.js
ETH balance: 1.5546242061324925
あとはフォークしたメインネット上で truffle migrate
や truffle console
でスマートコントラクトをデプロイおよび操作できます。また以下のサイトで紹介されているようにメインネットをフォークする際にブロック番号を指定することが可能です。過去のある時点でのステートを引っ張ってこれるので既に修復された脆弱性やハックを再現してみるなんてこともできると思います。※128ブロック以上過去のブロック高だとエラーが出るかもしれません [2]
$ ganache-cli -f https://USERNAME:PASSWORD@RPC_ENDPOINT@BLOCK_NUMBER
Brownieを使ってみましょう
BrownieはHardhatやTruffleとは違いPythonベースのフレームワークです。Truffleはあまりテストの機能が充実していないような気がします。Hardhatはテストスピードも早くおそらくJavascriptではHardhatが今後メインのフレームワークになっていくでしょう。反対にBrownieは異色でPythonベースです、多機能でテストやデプロイもやりやすそうな印象です。スマートコントラクトの言語としてはSolidityとVyperもサポートしています。Brownie自体はスマートコントラクトの統合開発フレームワークですが、テストだけ見ても様々な機能が搭載されています。[4]
- pytestフレームワークの採用、テストカバレッジの評価が可能
- 様々なフィクスチャやマーカーのサポート、コントラクト取り込み
- hypothesisフレームワークによるプロパティを変化させるテスト
- Stateful testingを利用した複雑なコントラクトの連携テスト
- Brownie Package Mangerを利用した既存プロトコルのインストール
最後のBrownie Package Managerは非常に強力なツールです。例えばCompound V2をインストールしたければ以下のワンライナーでパッケージをインストールできます。gistは実際にプロジェクト内でパッケージを呼び出す方法です。 brownie pm
はgithubで公開されているプロジェクトであれば読み込めるので ethpm
で管理されているパッケージである必要がありません。個人のプロジェクトであっても brownie pm
コマンドでgithubからプロジェクトへ取り込めます。
$ brownie pm install ethpm://defi.snakecharmers.eth:1/compound@1.1.0
Brownieのインストールからはじめます。インストールはpipから行うことができます。以下はpyenv環境下でのインストール結果です。eth-brownie 1.12.4
としてインストールされました。コマンドは brownie
です。
$ pyenv versions
system
3.6.0
* 3.7.3 (set by /home/ether/.pyenv/version)$ npm list -global --depth 0 | grep ganache
ganache-cli@6.8.2$ pip list | grep brownie
eth-brownie 1.12.4$ brownie --version
Brownie v1.12.4 - Python development framework for Ethereum
brownie init
で生成されるフォルダや構造の説明からデプロイの方法、テストのやり方までここで明瞭に説明されています。SimpleContract.solをデプロイしてその関数呼び出しをコンソールから操作してみました。[5]
まずは brownie init
でプロジェクトの開始です。プロジェクトの開始後に brownie-config.yaml
という設定ファイルを準備する必要があります。このファイルが超重要なので次に取り上げます。サンプルとして作成してみた brownie-config.yaml
ファイルを貼付けました。
$ brownie init
Brownie v1.12.4 - Python development framework for EthereumSUCCESS: A new Brownie project has been initialized at /brownie-test
:~/brownie-test$ ls -al
total 40
drwxrwxr-x 8 user user 4096 Jan 11 15:36 .
drwxrwxr-x 59 user user 4096 Jan 11 15:36 ..
drwxrwxr-x 5 user user 4096 Jan 11 15:36 build
drwxrwxr-x 2 user user 4096 Jan 11 15:36 contracts
-rw-rw-r-- 1 user user 63 Jan 11 15:36 .gitattributes
-rw-rw-r-- 1 user user 50 Jan 11 15:36 .gitignore
drwxrwxr-x 2 user user 4096 Jan 11 15:36 interfaces
drwxrwxr-x 2 user user 4096 Jan 11 15:36 reports
drwxrwxr-x 2 user user 4096 Jan 11 15:36 scripts
drwxrwxr-x 2 user user 4096 Jan 11 15:36 tests
project_structure
networks
compiler
console
reports
など色々と設定できるのですが特に compiler
と network
が重要です。まず compiler
ではsolcのバージョンが指定できます。基本的にここで指定されるバージョンですべてのスマートコントラクトがコンパイルされます。この値がnull値の場合にはスマートコントラクトファイルのpragma指定からコンパイラバージョンを読み取ります。compiler設定 > pragma指定です。
network
では gasLimit や gasPrice などネットワークに関する各種パラメータが設定可能です。 cmd_settings
はganache-cliへ渡すコマンドを指示できるオプションですが、forkを渡すことで対象のhttpエンドポイントからチェーンをフォークできます。ここにInfuraのメインネットのエンドポイントを指定するとBrownieコンソールやテストでそのままメインネットのステートが利用できます。
brownie compile
でローカルのスマートコントラクトをコンパイルしてから brownie console --development
で開発用のコンソールを立ち上げてみましょう(上記サイトのSimpleContract.solをコンパイルした状態)。立ち上がるインタラクティブシェルはpythonがベースになっています。pythonを使ったことがある人は非常に直感的に操作が可能になってます。
またコンソール上に文字の色付きが指定でき補完機能もデフォルトである状態です。引数等についていちいちドキュメントを調べる必要がありません。コンパイルしたスマートコントラクトのデプロイやabiのアクセスもインスタントに提供されています。出力の形式も分かりやすくganache-cli + Javascriptよりも個人的には使いやすい印象です。
Brownieはテストの中でのお役立ちライブラリとしては pytest.mark.parametrizeです。テスト関数へ渡したパラメータそれぞれをイテレートしてテストを実行してくれます。StableCreditの脆弱性のテストケースを実際に見て頂くとpytest.mark.parametrizeの具体的な利用法が分かりやすいです。以下に似たようなテストを書いてみました。
このテストでは鯨のアドレスを使いUniswapV2で各種トークンとUNIトークンのスワップ価格や流動性を確認しています。 test_defi
というテスト関数が各トークンと鯨のアドレス分だけイテレートされ、計8回のテストが行われます。ものすごく簡単にテストが書けてしまっています。※すいません、このテストassert文を使っていないですね
フラッシュローンのテストをする
仮にいまdYdXのフラッシュローンを行うスマートコントラクトをテストしたいとしましょう。Truffleであればメインネットをローカルにフォークして、スマートコントラクトをデプロイ、テストを走らせるといった手順かと思います。Brownieであればこれらの作業を効率的に行うことができます。Flashloanコントラクトと簡単なテストコードを作成してみました。
dYdXのフラッシュローンコントラクトの作成にはmoney-legosのライブラリを使います。サンプルコードのままフラッシュローンのコントラクトの中では何もせず、テストコードからフラッシュローンを実行します。
$ npm init -y
$ npm install @studydefi/money-legos
$ brownie compile
100 WETHをdYdXのソロからフラッシュローンで引き出し、そのまま何もせず返すテストです。一見よさそうなのですがこのテストは失敗します。コンソールを見るとトランザクションがリバートして『Not enough funds to repay dydx loan!』というエラーメッセージが表示されています。dYdXのフラッシュローンは借りた原資と同じトークンで2 weiの手数料がかかります。この場合には2 wei以上のWETHがデプロイしたFlashloanコントラクト上になければ処理が失敗してしまいます。
$ brownie test tests/test_flashloan_failure.py
テストをパスするように書き換えたコードです。テスト用アカウントからWETHへデポジットして受け取ったWETHをFlashloanのスマートコントラクトへ送ります。フラッシュローンを実行したあとのコントラクトのWETHバランスがテスト用アカウントから受け取ったWETHより2 wei小さければフラッシュローン手数料をちゃんと払えていることが証明できます。このテストはパスします。
$ brownie test tests/test_flashloan.py
BrownieでDeFiのプロトコルやフラッシュローンのテストがお手軽にできました。これまでメインネットでの挙動を確認するのは手間でした。が、この方法であればInfuraのエンドポイントからメインネットをフォークし、ローカルでコンパイルしたスマートコントラクトをテストケース上で走らせることができます。デバッグも非常にやりやすいのでこのツールを強くお勧めします。
まとめ
- DeFiのプロジェクトのテストではモックコントラクトをデプロイする、オープンソースのプロトコルをすべてデプロイするなどの方法が採用されてきた
- ganache-cliのフォークオプションでメインネットを簡単にフォークできるがテストにはひと手間かかる
- BrownieはPythonベースのスマートコントラクト統合開発ツールである
- Brownieはテストにpytestフレームワークを採用しており数多くの便利な機能をデフォルトで内包している