MakerDAOのコントラクトを参考に、ERC20トークンをデポジットして、それを担保に新しいERC20トークンを発行する簡易版のミニチュアMakerコントラクトのコードを書いてみました。Remixで試したので動くことは検証済みです。学習の記録として共有&解説します。
参考
・MakerDocs
・Makerdao/dss(github)
##MakerDAOとは
MakerDAOはステーブルコインであるDAIを発行するDeFi(分散型金融)スマートコントラクトの一つです。例えばコントラクトに担保としてETHをデポジットすると、ローンとしてステーブルコインであるDAIを受け取ることができます。
最近のMakerDAOはETHだけでなくBATやKNC、USDC、WBTCなど、様々なErc20トークンを担保にDAIを発行できるMulti-Collateral-Daiモデルに移行しています。ただ特にUSDCやWBTCなど、カウンターパーティーリスクのあるトークンが担保資産として使われていることに、個人的に疑問を持っています。
あと、最近Ethereum上にERC20トークン型のBTCを流入させる動きが活発化しているので、どうせならトラストレスモデルなERC20 BTCだけを担保にした、DAIとは方向性が少し異なるステーブルコイン作れないかなーと思っています。
##どんなコードを書くのか
今回は超基本の基本として、
1-ERC20トークンのデポジット
2-ERC20トークンの引き出し
3-ステーブルコインとなる新しいERC20トークンの発行
4-ステーブルコインとなる新しいERC20トークンの返済
以上、4つの基本的なfunctionを実行できるコントラクトを書きました。
参考にしたソースコードは、Makerのgithubのdssディレクトリ内にある
の3つです。
vat.solは、MakerDAOでいう所のVault(旧CDP)に辺り、ユーザーの担保資産を管理するコントラクトです。dai.solはステーブルコインDAIのトークンコントラクトです。最後のjoin.solはユーザーインターフェースで、ユーザーの呼び出しに対して、vat.solとdai.solを呼び出す役割を持ちます。
そして僕が作ったコントラクトが以下4つ。
・openvault.sol(join.solに該当)
・vault.sol(vat.solに該当)
・stbtc.sol(dai.solに該当)
・renbtc.sol
stbtcはStableBitcoinの略です。w 最後のrenbtc.solは担保となるERC20トークンのトークンコントラクトです。RenBTCというERC20型のビットコインで、BTCと価格がペッグしています。
RenBTCはもう既にEthereumチェーン上に1万以上流通してるトークンです。今回用意したコードはRenBTCと名付けただけの架空のトークンですが、このなんちゃってRenBTCを担保資産にStable Bitcoin(stBTC)を発行していきたいと思います。
コントラクトの関係を表すイメージ図はこんな感じです。ユーザーはOpenVaultとしかやり取りしません。OpenVaultが司令塔となって、左二つのトークンの送金を指示したり、Vaultコントラクト内の担保資産と債務(ステーブルコイン)の帳簿を更新したりする形です。
##コード
以下はMakerのコードをだいぶ参考(コピペ)にしているので、MakerDAOがコードベースでどのように作られているかを理解するのに役立ちます。ただし簡易化のためにいくつかの関数やrequire構文など取り除いていたりしています。
さて、まずはopenvault.solからです。
###openvault.sol
pragma solidity >=0.5.12;
//intefaceでvault.sol, stbtc.sol, renbtc.sol内のfunctionを使えるようにする。
interface Vault {
function deposit(address, uint) external;
function withdraw(address, uint) external;
function mint(address, uint) external;
function burn(address, uint) external;
}
interface StableBitcoin {
function stBTCmint(address, uint) external;
function stBTCburn(address, uint) external;
function stBTCtransferFrom(address, address, uint) external returns (bool);
function getstBTCBalanceOf(address) view external returns(uint);
}
interface RenBTC {
function rBTCtransferFrom(address, address, uint) external returns (bool);
function getrBTCBalanceOf(address) view external returns(uint);
}
contract OpenValut {
//Vault, StableBitcoin, RenBTCの3つのコントラクトをインスタンス化
Vault public vault;
StableBitcoin public stbtc;
RenBTC public rbtc;
//OpenVault内で関数を呼び出せるようにする
constructor(address _vault, address _stbtc, address _rbtc) public {
vault = Vault(_vault);
stbtc = StableBitcoin(_stbtc);
rbtc = RenBTC(_rbtc);
}
//renBTCのbalanceチェック
function rBTCBalanceOf() view external returns(uint) {
return rbtc.getrBTCBalanceOf(msg.sender);
}
//stBTCのbalanceチェック
function stBTCBalanceOf() view external returns(uint) {
return stbtc.getstBTCBalanceOf(msg.sender);
}
//担保をデポジットする
function depositCol(uint amount) external {
//vault.depositでVaultコントラクトのdeposit関数を実行
vault.deposit(msg.sender, amount);
//RenBTCコントラクトのtransferFrom関数を実行
rbtc.rBTCtransferFrom(msg.sender, address(vault), amount);
}
//担保を引き出す
function withdrawCol(uint amount) external {
vault.withdraw(msg.sender, amount);
rbtc.rBTCtransferFrom(address(vault), msg.sender, amount);
}
//stBTCを借りる
function borrowstBTC(uint amount) external{
//vault.mintでVaultコントラクト内のmint関数を実行
vault.mint(msg.sender, amount);
//Stable Bitcoinコントラクト内のmint関数を実行
stbtc.stBTCmint(msg.sender, amount);
}
//stBTCを返済する
function paybackstBTC(uint amount) external {
vault.burn(msg.sender, amount);
stbtc.stBTCburn(msg.sender, amount);
}
}
openvault.solはあくまでインターフェイスなので、このコントラクト内で何かしらのデータが動くことはありません。以下で紹介するvault.solやstBTC, renBTCなどのコントラクトに値を渡して、関数の実行を呼び出すことしかしません。
次にvault.solです。
###vault.sol
pragma solidity ^0.5.12;
contract Vault {
//--- Math ---
function add(uint x, uint y) internal pure returns (uint z) {
require((z = x + y) >= x);
}
function sub(uint x, uint y) internal pure returns (uint z) {
require((z = x - y) <= x);
}
//Vault。colは担保資産。loanは発行されたステーブルコイン。
struct Val {
uint col;
uint loan;
}
//Vaultをアドレスと紐付けてvalsマッピングに格納。
mapping(address => Val) public vals;
//担保資産をデポジット。ユーザーのVault内のcolにamountを追加する。
function deposit(address user, uint amount) external {
Val memory val = vals[user];
val.col = add(val.col, amount);
vals[user] = val;
}
//担保資産を引き出す。ユーザーのVault内のcolからamountを引く。
function withdraw(address user, uint amount) external {
Val memory val = vals[user];
val.col = sub(val.col, amount);
vals[user] = val;
//担保比率200%を下回る担保資産は引き出せないようrequire設定。
require((vals[user].loan * stBTCprice) <= ((vals[user].col * rBTCprice) / 2),
"You can't withdraw collateral unless the collateral ratio is more than 2 times");
}
//ステーブルコイン借入(発行)。Vault内のloanにamountを追加。
function mint(address user, uint amount) external {
Val memory val = vals[user];
val.loan = add(val.loan, amount);
vals[user] = val;
//担保資産の1/2以上のloanは発行できないようrequire設定。
require((vals[user].loan * stBTCprice) <= ((vals[user].col * rBTCprice) / 2),
"You can't mint the loan which is more than 1/2 of collateral");
vals[user].ratio = ((vals[user].loan * stBTCprice) / (vals[user].col * rBTCprice) * 100);
}
//ステーブルコイン返済(焼却)。Vault内のloanからamountを引く。
function burn(address user, uint amount) external {
Val memory val = vals[user];
val.loan = sub(val.loan, amount);
vals[user] = val;
vals[user].ratio = ((vals[user].loan * stBTCprice) / (vals[user].col * rBTCprice) * 100);
}
}
vault.sol(vat.sol)はMakerDAOの最も根幹的なコントラクトです。公式ドキュメントみてわかる通り、全ての中心にvat.sol(真ん中の円錐)がいることが分かります。
以上のコードは元のvat.solを超超シンプルにした貧弱verだといえます。ステーブルコインの価値を保証するためのパラメータである担保比率は2倍に設定しているので、withdrawとmintの場面ではrequireで過剰なステーブルコイン発行と、過剰な担保資産の引き出しを制御しています。つまり、担保がないとお金(ステーブルコイン)が借りれない状況を作っています。
以下はstbtc.solとrenbtc.solです。これらのコードは、openzeppelinなどのよくあるERC20トークンコントラクトのコードと大差ありません。加えて、名前や関数名などが違うだけで、2つのコードには大した違いはないです。
###stbtc.sol
pragma solidity 0.5.12;
contract StableBitcoin {
string public constant name = "Stable Bitcoin";
string public constant symbol = "stBTC";
string public constant version = "1";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
mapping (address => uint) public nonces;
//--- Math ---
function add(uint x, uint y) internal pure returns (uint z) {
require((z = x + y) >= x);
}
function sub(uint x, uint y) internal pure returns (uint z) {
require((z = x - y) <= x);
}
function getstBTCBalanceOf(address owner) view external returns(uint) {
return balanceOf[owner];
}
function stBTCtransferFrom(address src, address dst, uint wad)
external returns (bool) {
balanceOf[src] = sub(balanceOf[src], wad);
balanceOf[dst] = add(balanceOf[dst], wad);
return true;
}
function stBTCmint(address usr, uint wad) external {
balanceOf[usr] = add(balanceOf[usr], wad);
totalSupply = add(totalSupply, wad);
}
function stBTCburn(address usr, uint wad) external {
balanceOf[usr] = sub(balanceOf[usr], wad);
totalSupply = sub(totalSupply, wad);
}
function stBTCapprove(address usr, uint wad) external returns (bool) {
allowance[msg.sender][usr] = wad;
return true;
}
}
###renbtc.sol
pragma solidity 0.5.12;
contract RenBTC {
string public constant name = "RenBTC";
string public constant symbol = "rBTC";
string public constant version = "1";
uint8 public constant decimals = 18;
uint256 public totalSupply;
mapping (address => uint) public balanceOf;
mapping (address => mapping (address => uint)) public allowance;
mapping (address => uint) public nonces;
//--- Math ---
function add(uint x, uint y) internal pure returns (uint z) {
require((z = x + y) >= x);
}
function sub(uint x, uint y) internal pure returns (uint z) {
require((z = x - y) <= x);
}
function getrBTCBalanceOf(address owner) view external returns(uint) {
return balanceOf[owner];
}
function rBTCtransferFrom(address src, address dst, uint wad)
external returns (bool) {
balanceOf[src] = sub(balanceOf[src], wad);
balanceOf[dst] = add(balanceOf[dst], wad);
return true;
}
function rBTCmint(address usr, uint wad) external {
balanceOf[usr] = add(balanceOf[usr], wad);
totalSupply = add(totalSupply, wad);
}
function rBTCburn(address usr, uint wad) external {
require(balanceOf[usr] >= wad, "RenBTC/insufficient-balance");
if (usr != msg.sender && allowance[usr][msg.sender] != uint(-1)) {
require(allowance[usr][msg.sender] >= wad, "RenBTC/insufficient-allowance");
allowance[usr][msg.sender] = sub(allowance[usr][msg.sender], wad);
}
balanceOf[usr] = sub(balanceOf[usr], wad);
totalSupply = sub(totalSupply, wad);
}
function rBTCapprove(address usr, uint wad) external returns (bool) {
allowance[msg.sender][usr] = wad;
return true;
}
}
以上です。Makerの実際のディレクトリを見ると、これに加えて精算プロセスやVaultのクローズ、価格安定化メカニズム、オラクルなど他にも様々なコントラクトがあるのですが、その辺りはまた勉強して、次回以降書いていきます。
###追記
清算プロセスの実行には担保比率が必要。vault.solのValストラクト内にratioという変数を追加し、以下の一文をwithdraw, mint, burn関数内に差し込むと、Vaultから資産を出し入れする中で自動で担保比率が変わる。
val.ratio = (val.col * rBTCprice) * 100 / (val.loan * stBTCprice);
オラクルが未実装なので、rBTCpriceもstBTCpriceもどちらも1。担保比率(ratio)が300%なら債務に対して3倍の担保があることを意味します。