MakerDAOのコードを理解しよう その1

DeFiだ!デファイだ!と盛り上がってますが、仕組みの説明はされても肝心の中身のコードがどう動いているのかきちんと理解できていないので、ミドルウェアプロトコルの設計をやっている自分のために解説記事を書くことにしました。

とりあえず、第一弾としてDeFi代表格のMakerDAOをみていきます。いくつのパートに分かれるかまだまだ検討ついてません。

本記事ではMakerDAO,DAIの仕組みについて詳しくは解説しない予定です。仕組みを知りたい方は、d10n labへ入って良質な最新情報を手軽に仕入れるか、頑張って膨大な情報の中から体系的な知識を自力で身につけてください。まずここから仕組みを確認しよう!(平野さん紹介料ください笑)

まずDAIを発行するためのCDPのコントラクトを見ていきます。
コードの中身はこちら。今回参照するコードは、https://cdp.makerdao.com でDAIを発行した際のトランザクションからEtherscanを辿りました。

どこから説明するべきかも検討ついていないので、とりあえず上から何があって、どのコントラクト、関数と繋がっているか、それぞれの関数が何を意図しているかを把握していきます。コードの解説自体初めてなので、わかりにくいところもあるかと思いますが、ご容赦くだされ。一通り読んだ後に各トークンの動きなど含めて図にまとめ直そうと思います。

contract DSMath

openzeppelin-solidityでいうSafeMathの部分です。コントラクト内で四則演算を行う際の、オーバーフロー、アンダーフローを防ぐものになってます。関数についての説明は省きます。よくわからなかったらクリプトゾンビをやり直しましょう!

contract TubInterface,TokenInterface,PepInterface,OctInterface

interfaceとして別の既存のコントラクトから関数を呼ぶ準備をしています。どの関数を呼び出すかを宣言しているだけなので、ここには関数の詳細は載っていません。後で、それぞれの参照先をみて行きます。

contract SaiProxy

DAI発行のためのコントラクトなのですが、DAIのDev版であるSaiの名前がついています。中身が同じだから同じ名前なのかな?
関数の説明をして行きます。

function open(address tub_) public returns (bytes32) {
return TubInterface(tub_).open();
}

まずopen関数です。DAIを発行するための借用書であるcupを作成します。cupがよくわからない場合はなまはげ君のsaiの解説が超おすすめです。簡単に言うと、それぞれ個別のCDPのことをcupと呼びます。
参照先のコントラクトアドレスをtub_ で指定してTubInterfaceコントラクトのopen関数を呼び実行しています。
こうやってInterfaceからのアドレス参照先を変更できる設計にすることでコントラクトの実質的なアップデートも可能にしてるんですね。めちゃ勉強になる。

このSaiProxy コントラクトで扱う関数はほとんどTubInterfaceを使っているので、TubInterfaceの参照元のコントラクトSaiTubに飛んで中身を見に行きます。

ちなみに今見たSaiProxyコントラクトのETH,DAI,WETH,PETHのトランザクションは以下のようになっています。

このコントラクトに入るETHの履歴
コントラクトから出るPETH,DAI,WETHの履歴

CDPの利用者はETHをこのコントラクトに投げ、DAIを受け取り、その他のPETHとWETHはMaker 1というアドレスに送られているのがわかります。よく見ると上と下で取引しているアドレスが対応しているはずです。

このMaker 1というアドレスが、今から見るSaiTubコントラクトのことです。つまり、SaiTubはPETHとWETHを扱っているコントラクトとなります。

contract SaiTub

Etherscanでコントラクトのトランザクションを確認して、指定されてるTubInterfaceのアドレスを探してきました。
このSaiTubというコントラクトです。

下の方に、Interfaceで宣言していた関数達、open, join, exit, free…が確認できます。SaiProxyはこいつらを呼んで使おうとしているわけです。

先ほどのSaiProxyから呼び出しているSaiTubのopen関数が以下です。

function open() public note returns (bytes32 cup) {
require(!off);
cupi = add(cupi, 1);
cup = bytes32(cupi);
cups[cup].lad = msg.sender;
LogNewCup(msg.sender, cup);
}

offがfalseの場合のみ、この関数を実行できるようになっています。open関数以外の関数も、ほとんどがrequire(!off)がついています。
offがtrueになる場合は、cage関数を実行する場合です。このcage関数とは以下のように、Liquidation penaltyであるaxeを書き換え、SaiTub内にあるWETHをSaiTapコントラクト(Liquidator)に送信する内容であるため、担保にしているWETHがロスカットになってしまう時に実行される関数であることがわかります。

function cage(uint fit_, uint jam) public note auth {
require(!off && fit_ != 0);
off = true;
axe = RAY;
gap = WAD;
fit = fit_; // ref per skr
require(gem.transfer(tap, jam));
}

open関数では、このcupがロスカット状態でないことを確認し、登録しているcup数を一つ増やし、cup作成者のアドレスを書き込み、Eventlogを残していますね。(正直今までのところでスッと読めたのはここだけ。。)

まとめ

今回はここまでとします。どういった形でコントラクトが継承され別のところから呼び出されているのかを確認しながら、MakerDAOの仕組みとトークンの動きも把握するとなるとなかなか大変な作業でした。
とりあえず、一番最初の関数の呼び出しと中身の見方がわかったので、次のパートからは、今回をベースに関数の動きを一気に見ていくことができると思います。

今週末から福岡のchaintopeさんでBBQしたり実家帰ったり、来週はつくばから東京へ引っ越しがあったりでずっとバタバタしてますが、やっとコード見る心の余裕ができて楽しくなってきたので、最低週一で更新していきます。
今立ち上げてるプロジェクトの設計・開発を一緒にやっているmktiaの方からもミドルウェアプロトコルに関するコードリサーチが上がる予定なので、そちらもよろしくお願いします〜

参考文献の上二つも、ほぼ同世代の大学生が書いてるので、刺激を受けながら参考にさせていただきました。なまはげ君、草太君ありがとう!!

参考文献