LoginSignup
3
0

More than 3 years have passed since last update.

MakerDAOのコードを基に、ERC20トークンを担保にERC20トークンを発行するコントラクト書いてみた。

Last updated at Posted at 2020-08-19

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ディレクトリ内にある

vat.sol
join.sol
dai.sol

の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コントラクト内の担保資産と債務(ステーブルコイン)の帳簿を更新したりする形です。

Screenshot 2020-08-19 at 6.21.48 PM.png

コード

以下は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(真ん中の円錐)がいることが分かります。

MCD System 2.0.png

以上のコードは元の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倍の担保があることを意味します。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0