はじめに

初めまして。 CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です! スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、エスクロー・スマートコントラクトによるERC20トークンの払い戻しを可能にする規格であるERC5528についてまとめていきます!

エスクローサービスとは、安全な取引をする第三者の仲介サービスのことです。 第三者がある当事者から金銭を受け取り、一定の条件が満たされた後に別の当事者に支払うという契約を実行します。 従来の金融では、第三者は銀行などの信頼できる個人や組織である必要があったが、DeFiではスマートコントラクトが担うことができます。 詳しくは以下の記事を参考にしてください。

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

概要

この規格はEIP20の拡張で以下のエスクローサービスのフローを定義します。

  1. 売り手がトークンを発行。
  2. 売り手は、コントラクトアドレス、ロック期間、為替レート、追加のエスクロー成功条件などの詳細な情報とともにエスクローのスマートコントラクトを作成。
  3. 売り手は、自身トークンをエスクローコントラクトに資金供給。
  4. 買い手は、エスクローコントラクトで事前に定義された自身のトークンを資金供給。
  5. エスクローの状況が成功と判定されたとき、売り手は買い手のトークンを引き出し、買い手は為替レートに基づいて売り手のトークンを引き出す。
  6. エスクロー処理が失敗した場合やエスクロー処理の途中である場合、買い手は供給したトークンを引き出す(または払い戻す)。

動機

暗号通貨の匿名性により一度支払われた資金を自動的に回収する手段はありません。

伝統的な金融では、信頼できるエスクローサービスがこの問題を解決します。 しかし、分散型暗号通貨の世界では、第三者の仲裁者なしにエスクローサービスを実装することが可能です。 この規格は、エスクローが完了しない場合にトークンが元のウォレットに送り返される機能を持つエスクローサービスとしてのスマートコントラクトのインターフェイスを定義します。

「そもそもインターフェースって何?」という方は以下の記事を参考にしてください!

仕様

エスクローには2種類のコントラクトがあります。

支払可能なコントラクト

売り手と買い手はエスクローコントラクトを使用してトークンを資金供給します。 このコントラクトはEIP20インターフェイスを上書きする必要があります。

エスクローコントラクト

エスクローポリシーを定義し、一定期間支払可能なコントラクトのトークンを保持します。 この契約はEIP20インターフェイスを上書きする必要はありません。

メソッド

constructor

コントラクトのデプロイ時に、エスクローポリシーの詳細を設定します。

以下のポリシーを定義する

  • 売り手のトークンのコントラクトアドレス
  • 買い手のトークンのコントラクトアドレス

以下のポリシーを定義できる

  • エスクロー期間。
  • 投資家の最大(または最小)数。
  • 資金供給するトークンの最大(または最小)数。
  • 売り手/買い手トークンの為替レート。
  • ユーザーのKYC確認

escrowFund

_value量のトークンを_toのアドレスに送ります。

エスクローコントラクトの場合

  • _toはユーザーのアドレスでなければならない。
  • msg.senderは支払可能なコントラクトのアドレスでないといけない。
  • ポリシーの検証を確認する必要がある。

支払可能なコントラクトの場合

  • _toアドレスはエスクローコントラクトのアドレスでなければならない。
  • エスクローコントラクトインターフェイスの同じ関数を呼び出す必要がある。
  • _toは、エスクローコントラクト内のユーザーアドレスを認識するために、msg.senderでないといけない。
javascript
function escrowFund(address _to, uint256 _value) public returns (bool)

escrowRefund

_value量のトークンを_fromアドレスから払い戻します。

エスクローコントラクトの場合

  • _fromはユーザーのアドレスでなければならない。
  • msg.senderは支払可能なコントラクトのアドレスでなければならない。
  • ポリシーの検証を確認する必要がある。

支払可能なコントラクトの場合

  • _fromアドレスはエスクローコントラクトのアドレスでなければならない。
  • エスクローコントラクトインターフェイスの同じ関数を呼び出す必要があります。
  • _fromは、エスクローコントラクト内のユーザーアドレスを認識するために、msg.senderでなければならない。
javascript
function escrowRefund(address _from, uint256 _value) public returns (bool)

escrowWithdraw

エスクローコントラクトから資金を引き出します。

エスクローコントラクトの場合

  • エスクロープロセスが完了したことを確認する必要がある。
  • 売り手と買い手の残高のトークンをmsg.senderの売り手と買い手のコントラクトウォレットに送信する。
  • 支払可能なコントラクトの場合オプション機能になる。
javascript
function escrowWithdraw() public returns (bool)

インターフェースの参考コード

javascript
pragma solidity ^0.4.20;

interface IERC5528 {

    function escrowFund(address _to, uint256 _value) public returns (bool);

    function escrowRefund(address _from, uint256 _value) public returns (bool);

    function escrowWithdraw() public returns (bool);

}

contract PayableContract is IERC5528, IERC20 {
    /*
      General ERC20 implementations
    */

    function _transfer(address from, address to, uint256 amount) internal {
        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
        _balances[from] = fromBalance - amount;
        _balances[to] += amount;
    }

    function transfer(address to, uint256 amount) public returns (bool) {
        address owner = msg.sender;
        _transfer(owner, to, amount);
        return true;
    }

    function escrowFund(address _to, uint256 _value) public returns (bool){
        bool res = IERC5528(to).escrowFund(msg.sender, amount);
        require(res, "Fund Failed");
        _transfer(msg.sender, to, amount);
        return true;
    }

    function escrowRefund(address _from, uint256 _value) public returns (bool){
        bool res = IERC5528(_from).escrowRefund(msg.sender, _value);
        require(res, "Refund Failed");
        _transfer(_from, msg.sender, _value);
        return true;
    }
}

contract EscrowContract is IERC5528 {

    enum State { Inited, Running, Success, Closed }
    struct BalanceData {
        address addr;
        uint256 amount;
    }

    address _addrSeller;
    address _addrBuyer;
    BalanceData _fundSeller;
    BalanceData _fundBuyer;
    EscrowStatus _status;

    constructor(address sellerContract, address buyerContract){
        _addrSeller = sellerContract;
        _addrBuyer = buyerContract;
        _status = State.Inited;
    }

    function escrowFund(address _to, uint256 _value) public returns (bool){
        if(msg.sender == _addrSeller){
            require(_status.state == State.Running, "must be running state");
            _fundSeller.addr = _to;
            _fundSeller.amount = _value;
            _status = State.Success;
        }else if(msg.sender == _addrBuyer){
            require(_status.state == State.Inited, "must be init state");
            _fundBuyer.addr = _to;
            _fundBuyer.amount = _value;
            _status = State.Running;
        }else{
            require(false, "Invalid to address");
        }
        return true;
    }

    function escrowRefund(address _from, uint256 amount) public returns (bool){
        require(_status.state == State.Running, "refund is only available on running state");
        require(msg.sender == _addrBuyer, "invalid caller for refund");
        require(_fundBuyer.addr == _from, "only buyer can refund");
        require(_fundBuyer.amount >= amount, "buyer fund is not enough to refund");
        _fundBuyer.amount = _fundBuyer.amount - amount
        return true;
    }

    function escrowWithdraw() public returns (bool){
        require(_status.state == State.Success, "withdraw is only available on success state");
        uint256 common = MIN(_fundBuyer.amount, _fundSeller.amount);

        if(common > 0){
            _fundBuyer.amount = _fundBuyer.amount - common;
            _fundSeller.amount = _fundSeller.amount - common;

            // Exchange
            IERC5528(_addrSeller).transfer(_fundBuyer.addr, common);
            IERC5528(_addrBuyer).transfer(_fundSeller.addr, common);

            // send back the remaining balances
            if(_fundBuyer.amount > 0){
                IERC5528(_addrBuyer).transfer(_fundBuyer.addr, _fundBuyer.amount);
            }
            if(_fundSeller.amount > 0){
                IERC5528(_addrSeller).transfer(_fundSeller.addr, _fundSeller.amount);
            }
        }

        _status = State.Closed;
    }

}

補足

3つの関数(escrowFund、escrowRefund、escrowWithdraw)はEIP20のtransfer関数に基づいています。

escrowFundはトークンをエスクローコントラクトに送ります。 エスクローコントラクトはエスクロー処理状態をコントラクト内に保持するか、ポリシーが満たされない場合はトークンの預け入れを拒否することができます。

escrowRefundは、エスクロー処理の途中またはエスクロー処理が失敗したときに呼び出すことができます。

escrowWithdrawは、ユーザー(売り手と買い手)がエスクローアカウントからトークンを送信することを可能にします。 エスクロープロセスが完了すると、売り手は買い手のトークンを取得し、買い手は売り手のトークンを取得することができます。

後方互換性

このEIPを実装した支払可能なコントラクトは、EIP20の仕様と完全に後方互換性があります。

テストケース

truffleによる単体テストの例。

このテストケースでは、売り手/買い手のトークンを交換するための以下の条件を示しています。

  • 為替レートは1対1。
  • 買い手の数が2に達すると、エスクロー処理は終了(成功)。
  • それ以外の場合(まだ成功条件を満たしていない場合)、買い手は供給したトークンを払い戻す(または引き出す)。

最後に

今回は、「エスクロー・スマートコントラクトによるERC20トークンの払い戻しを可能にする規格であるERC5528」についてまとめてきました!

いかがだったでしょうか? 実装については今後追記していきます。

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

採用強化中!

CryptoGamesでは一緒に働く仲間を大募集中です。

この記事で書いた自分の経験からもわかるように、裁量権を持って働くことができて一気に成長できる環境です。 「ブロックチェーンやWeb3、NFTに興味がある」、「スマートコントラクトの開発に携わりたい」など、少しでも興味を持っている方はまずはお話ししましょう!