ブラウニー “Brownie” でDeFiのテストが簡単にできる!!

Yuya Sugano
16 min readJan 20, 2021

--

スマートコントラクトの開発フレームワークはHardhatやTruffleが有名ですが、Brownieを使えばPythonでスマートコントラクトの開発がすすめられます。このフレームワークはPython使いにはよいですね。DeFiの開発では既存のプロトコルとのインタラクションをテストしたいことが多いと思いますが、BrownieであればメインネットのフォークとローカルのSolidityのスマートコントラクトをローカルで効率的に開発・テストができます。先日のStableCreditの脆弱性の再現のリポジトリでこのフレームワークの存在に気がつきました。Pythonに慣れていなくてもこのフレームワークは非常に使いやすいと思います。今回はBrownieをインストールした後にDeFiプロトコルとフラッシュローンのテストまでやってみました

Image by Karolina Grabowska from Pixabay

目次です。

  • メインネットのテストどうする問題
  • Brownieを使ってみましょう
  • フラッシュローンのテストをする

メインネットのテストどうする問題

DeFiのmoney-legosはオープンソースのプロトコル同士が関連して動作するため実装するスマートコントラクト単体だけでは全量のテストが行えません。STUDYDEFIの記事ではDedgeというアプリケーションを開発するにあたり、Aave/Uniswap/Compoundなどの各プロトコルと連携するスマートコントラクトをテストする上でこの問題に遭遇したと書いてあります。彼らは他のDeFiプロトコルを開発しているスタートアップへインタビューを行って2つの主要な方法を確認したようです。[1]

  1. ローカルにモックコントラクトをデプロイする
  2. 実際のプロトコルをローカルにデプロイする

モックコントラクトは関数等でダミーデータを返すように定義しておく方法だと思います。この方法は作成するモックコントラクトの量が少なければ問題ないでしょう。ただ多くのプロトコルや関数のダミーを作成するとなると骨が折れそうです。また現実的に存在する脆弱性やバグを突くような使用方法を発見することができないのであまりお勧めの方法ではありません。なぜなら関数のリターンを与える変数によって操作することや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
ganache-cli -f -i 1
$ node test.js
ETH balance: 1.5546242061324925

あとはフォークしたメインネット上で truffle migratetruffle 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]

https://eth-brownie.readthedocs.io/en/latest/index.html
  • 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 Ethereum
SUCCESS: 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 など色々と設定できるのですが特に compilernetwork が重要です。まず 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 accounts operations
brownie smart contract interaction

Brownieはテストの中でのお役立ちライブラリとしては pytest.mark.parametrizeです。テスト関数へ渡したパラメータそれぞれをイテレートしてテストを実行してくれます。StableCreditの脆弱性のテストケースを実際に見て頂くとpytest.mark.parametrizeの具体的な利用法が分かりやすいです。以下に似たようなテストを書いてみました。

このテストでは鯨のアドレスを使いUniswapV2で各種トークンとUNIトークンのスワップ価格や流動性を確認しています。 test_defi というテスト関数が各トークンと鯨のアドレス分だけイテレートされ、計8回のテストが行われます。ものすごく簡単にテストが書けてしまっています。※すいません、このテストassert文を使っていないですね

brownie test test/test_defi.py

フラッシュローンのテストをする

仮にいま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
brownie test tests/test_flashloan_failure.py

テストをパスするように書き換えたコードです。テスト用アカウントからWETHへデポジットして受け取ったWETHをFlashloanのスマートコントラクトへ送ります。フラッシュローンを実行したあとのコントラクトのWETHバランスがテスト用アカウントから受け取ったWETHより2 wei小さければフラッシュローン手数料をちゃんと払えていることが証明できます。このテストはパスします。

$ brownie test tests/test_flashloan.py

BrownieでDeFiのプロトコルやフラッシュローンのテストがお手軽にできました。これまでメインネットでの挙動を確認するのは手間でした。が、この方法であればInfuraのエンドポイントからメインネットをフォークし、ローカルでコンパイルしたスマートコントラクトをテストケース上で走らせることができます。デバッグも非常にやりやすいのでこのツールを強くお勧めします。

https://github.com/yuyasugano/brownie-test

まとめ

  • DeFiのプロジェクトのテストではモックコントラクトをデプロイする、オープンソースのプロトコルをすべてデプロイするなどの方法が採用されてきた
  • ganache-cliのフォークオプションでメインネットを簡単にフォークできるがテストにはひと手間かかる
  • BrownieはPythonベースのスマートコントラクト統合開発ツールである
  • Brownieはテストにpytestフレームワークを採用しており数多くの便利な機能をデフォルトで内包している

--

--

Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。