スマートコントラクトを使った入金システムについて全力で理解してみた

これは ビットバンク株式会社 Advent Calendar 2020 の 22 日目の記事です。

はじめに

ビットバンクでエンジニアをしているadrenaline0206です。
先日、弊社ジョナサン・アンダーウッドがEthereumのスマートコントラクトを使った入金システムを構築しました。個人的にスマートコントラクトに興味があったので、この仕組みについて理解しようとしましたが、かなり苦戦しました。理由は必要な前提知識が多岐に渡るからです。スマートコントラクトの実用的な使用例について興味のある方も多いのではないかと思い、今回この様な題材を取り上げました。

Exchange Deposit Contractとは

現行のEthereumの入金システムは、1つのシードから複数のアドレスを作成する階層的決定性ウォレット(通称HDウォレット)を採用しています。Exchange Deposit Contractはスマートコントラクトからの入金に対応出来る入金システムになります。

Exchange Deposit Contractのソースコードはこちら

入金システムを変えた理由

入金に対する管理の軽減

従来の入金システムではアカウントから出金する際に残高照会・オフライン署名などの作業が必要でした。スマートコントラクトを使った入金システムにすることで、これらが自動化されるので管理が軽減されます。

スマートコントラクトからの入金に対応できる

現行の入金システムは単純入金(EOAからEOA)にしか対応していませんが、入金システムを変えることでスマートコントラクトからの入金にも対応できるようになります。

ERC20(※1)トークンなどのプロトコルに対応できる

ユーザーの入金アドレスがコントラクトだった場合に、後で簡単にERC20トークンなどのプロトコルに対応することができます。

※1 ERC20はEthereumのブロックチェーン上で発行されるトークンの規格の一つです

Exchange Deposit Contractの全体図

Exchange Deposit Contractは主に4つのコントラクトから構成されています。

  1. Exchange Deposit Contract
    • 入金システムのメインロジックとなるコントラクト
  2. Extra Implementation Contract
    • ロジックを追加する際に使用するコントラクト
  3. Forwarding Contract
    • 各ユーザー毎に割り当てられる入金受付用のコントラクト
  4. Proxy Factory Contract
    • Forwarding Contractを作成するコントラクト

cb79bc67-e5d2-46ab-af5f-c5cd5577cc2b-1

Exchange Deposit Contractを理解するポイント

いくつかのポイントに分けてExchange Deposit Contractを説明します。

  • 必要な前提知識
  • DELEGATECALL
  • Proxy Factory Contract
  • Forwarding Contract
  • Exchange Deposit Contract
  • Forwarding Contractへ入金

必要な前提知識

Exchange Deposit Contractを理解する上で必要となる、いくつかの前提知識をご紹介します。

アカウントの種類

Ethereumには2種類のアカウントがあります。

  • 外部所有アカウント(EOA: Externally owned accounts)
    • 秘密鍵によってコントロールされ、イーサリアムの残高を所有し、ETH転送の為のトランザクションを送信することが出来ます
  • コントラクトアカウント
    • イーサリアムの残高・コントラクトコードを所有し、他のコントラクトからのトランザクションまたはメッセージによって動作します
      cc2544ed-a7ad-425d-8f58-9cead59f20b3-1

fallback関数

solidityにはfallback関数と呼ばれる関数があります。

コントラクトAからコントラクトBに対して関数Dを呼び出した場合、関数Dがないのでエラーになります。
コントラクトAからコントラクトCに対して関数Dを呼び出した場合、関数Dは存在しませんがその場合に呼ばれるのがfallback関数です。

03f466e5-d80f-4970-934c-34431495a7e2-1

solidity assembly

solidityにはコード内でアセンブリ言語を使用することが出来る、インラインアセンブリがあります。ストレージ(※2)にデータを書き込む際に、インラインアセンブリでデータサイズを意識しながら書き込むことで、ガスを節約することが可能になります。しかし多用すると可読性が低下し、バグを生み出す可能性が高まりますので使用する際には注意が必要になります。

assembly {
    let result := add(x, y)
    mstore(0x0, result)
    return(0x0, 32)
}

※2 ブロックチェーンに永続的に格納される領域

Proxy Pattern

コントラクトコードは本番のブロックチェーンに一度デプロイされると、後から変更することが出来ません。そこでデプロイされたコントラクトコードを変更するのではなく、コントラクトコードをアップデートする方法としてProxy Patternを用います。これはストレージとして使用するプロキシコントラクトとロジックを定義するロジックコントラクトを分け、プロキシコントラクトからDELEGATECALLでロジックコントラクトの呼び出し先を変える方法です。

Forwarding Contractはプロキシコントラクトになります。またExchange Deposit Contractはfallback関数の実行時にプロキシコントラクトです。

95c379ab-9320-40c1-9d19-496442067e98-1

Method ID

solidityではコントラクトから他のコントラクトの関数を実行する際に、Method IDをFunction Selectorに渡して実行します。関数名と引数の型の文字列をkeccak256でハッシュ化し、頭の4byteを取ったものがMethod IDになります。

bytes4(keccak256("setNum(uint256)") = 0xcd16ecbf

calldata

EVM(Ethereum Virtual Machine)でコードを実行する際にstack、memory、storage、calldata、returndataの5つのデータ領域を使います。その中の1つcalldataはcallまたはdelegatecallで別のコントラクトを呼び出した時に使用するデータ領域です。

例えばHogeコントラクトのfuga関数について、fuga関数を引数x=64、y=trueで呼び出した場合のcalldataは以下の通りです。

Hogeコントラクト

contract Hoge {
  function fuga(uint32 x, bool y) returns (bool r) { ... }
}

Method ID(4bytes)

0xe7fac2e0

x=64(32bytes)

0x0000000000000000000000000000000000000000000000000000000000000040

y=true(32bytes)

0x0000000000000000000000000000000000000000000000000000000000000001

calldataはMethod IDxyを合わせた68bytesのデータになります。

0xe7fac2e000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001

DELEGATECALL

Exchange Deposit Contractでは、コントラクトから別のコントラクトの関数を呼び出す際にCALLとDELEGATECALLと言う関数を使用しています。

CALLとDELEGATECALLの違い

以下のコントラクトを用いてCALLとDELEGATECALLの違いを説明します。このコントラクトはEOAアカウントからC1コントラクトを呼び出し、C1コントラクトの2つの関数を実行します。その際にC1コントラクトからC2コントラクトのsetNum関数をcallまたはdelegatecallで呼び出しています。

contract C1 {
    uint256 public num;
    address public sender;

    function callSetNum(address c2, uint256 _num) public {
        (bool success, ) =
            c2.call(
                abi.encodeWithSelector(
                    bytes4(keccak256('setNum(uint256)')),
                    _num
                )
            );
        require(success);
    }

    function delegatecallSetNum(address c2, uint256 _num) public {
        (bool success, ) =
            c2.delegatecall(
                abi.encodeWithSelector(
                    bytes4(keccak256('setNum(uint256)')),
                    _num
                )
            );
        require(success);
    }
}

contract C2 {
    uint256 public num;
    address public sender;

    function setNum(uint256 _num) public {
        num = _num;
        sender = msg.sender;
    }
}

callでsetNum関数を呼び出した場合、senderはC1のコントラクトアドレスになります。C2が参照するストレージはC2なので、C2のnumにsetNum関数で指定した_numが入ります。
delegatecallでsetNum関数を呼び出した場合、senderは呼び出し元のEOAアドレスになります。C2が参照するストレージはC1なので、C1のnumにsetNum関数で指定した_numが入ります。

注目して頂きたいのは、setNum関数のロジックはC2コントラクトにありますが、delegatecallを使うことで呼び出し元(EOA)から見た場合に、あたかもC1コントラクトで全ての処理行っているかのように動作します。

呼び出し方 コンテキスト num sender
call C2 C2のnumにcallSetNum関数の引数で指定した_numが入る C1のコントラクトアドレス
delegatecall C1 C1のnumにdelegatecallSetNum関数の引数で指定した_numが入る 呼び出し元のEOAアドレス

DELEGATECALLの使用例

例えばテスト用フォルダにあるSample Logic Contractで定義されているgatherHalfErc20を呼び出す際についてです。この場合UserからgatherHalfErc20を呼び出しており、calldata != nullなのでDELEGATECALLでExchange Deposit Contractに対してgatherHalfErc20を呼びます。Exchange Deposit ContractにはgatherHalfErc20がないのでfallback関数が実行されます。fallback関数の中にはSample Logic Contractに対してDELEGATECALLでgatherHalfErc20を呼び出すロジックが定義されています。

46b8b1f7-e6b3-42df-8284-d570f09b74dd-1

なぜForwarding Contractにこの様なロジックを定義しないかと言うと、Forwarding Contractはユーザー毎にデプロイされるのでその都度、ロジックの分のガスコストがかかります。そこで上記ではForwarding ContractとExchange Deposit ContractをプロキシとしてdelegatecallでSample Logic Contractから呼び出します。すると、ユーザーから見た場合にあたかもForwarding Contractで全ても処理をしている様に振る舞い、かつExchange Deposit Contractは1度しかデプロイされないので、デプロイコストを下げることが出来ます。

Exchange Deposit Contractについて

Exchange Deposit Contractには5つのsolidityファイルがあります。入金システムに直接関わるのは①と②で③〜⑤はテストの為のコントラクトになります。本記事では①と②のコードについて説明します。

①ExchangeDeposit
メインのコントラクトになります。このコントラクトは、はじめに1度だけデプロイされます。

②ProxyFactory
顧客ごとにForwarding Contractを作成するコントラクトになります。顧客はForwarding Contractのコントラクトアドレスに対して入金を行います。

③【テスト用】SimpleCoin
ERC20を模擬したコントラクトになります。このコントラクトはERC20のテストする際に使用します。

④【テスト用】SimpleBadCoin
ERC20を模擬したコントラクトになります。SimpleCoinの悪い事例をテストする際に使用します。

⑤【テスト用】SampleLogic
プロキシコントラクトから新しいロジックのコントラクトを指定した際にどの様に機能するのか模擬したコントラクトです。テストする際に使用します。

Proxy Factory Contract

contract ProxyFactory {
    bytes
        private constant INIT_CODE = hex'604080600a3d393df3fe7300000000000000000000000000000000000000003d366025573d3d3d3d34865af16031565b363d3d373d3d363d855af45b3d82803e603c573d81fd5b3d81f3';

    address payable private immutable mainAddress;

    constructor(address payable addr) public {
        require(addr != address(0), '0x0 is an invalid address');
        mainAddress = addr;
    }

    function deployNewInstance(bytes32 salt) external returns (address dst) {
        bytes memory initCodeMem = INIT_CODE;
        address payable addrStack = mainAddress;
        assembly {
        
            let pos := add(initCodeMem, 0x20)
            let first32 := mload(pos)
            let addrBytesShifted := shl(8, addrStack)
            
            mstore(pos, or(first32, addrBytesShifted))

            dst := create2(
                0,
                pos,
                74,
                salt
            )
            
            if eq(dst, 0) {
                revert(0, 0)
            }
        }
    }
}

Forwarding ContractをバイトコードでINIT_CODEに書き込んでいます。

bytes private constant INIT_CODE = hex'604080600a3d393df3fe7300000000000000000000000000000000000000003d366025573d3d3d3d34865af16031565b363d3d373d3d363d855af45b3d82803e603c573d81fd5b3d81f3';

具体的には下記のバイトコードです。

0x604080600a3d393df3fe7300000000000000000000000000000000000000003d366025573d3d3d3d34865af16031565b363d3d373d3d363d855af45b3d82803e603c573d81fd5b3d81f3

bytes memoryでメモリにバイトコードを書き込んでいます。initCodeMemにはINIT_CODEのバイト長が含まれます。

bytes memory initCodeMem = INIT_CODE;

let pos := add(initCodeMem, 0x20)はForwarding Contractのバイトコードの先頭を指しています。

let pos := add(initCodeMem, 0x20)
let first32 := mload(pos)

first32にはForwarding Contractの先頭の32byteが格納されます。

604080600a3d393df3fe7300000000000000000000000000000000000000003d

ここでaddrStack(アドレス)を8bit左にシフトします。

let addrBytesShifted := shl(8, addrStack)
シフト前:
000000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

シフト後:
0000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA00

シフトしたアドレスとメモリから読み込んだForwarding Contractの先頭の32byteをorします。

mstore(pos, or(first32, addrBytesShifted))

FF60408060...を、00AAを上書きすることで、バイトコードを整形しています。

60 40 80 60 0a 3d 39 3d f3 fe 73 0000000000000000000000000000000000000000 3d
or
00 00 00 00 00 00 00 00 00 00 00 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 00

整形した結果は以下の通りになります。

60 40 80 60 0a 3d 39 3d f3 fe 73 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 3d

Forwarding Contract

Proxy Factory ContractのdeployNewInstanceに定義されている下記のバイトコードがForwarding Contractになります。

60 40 80 60 0a 3d 39 3d f3 fe 73 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA 3d 36 60 25 57 3d 3d 3d 3d 34 86 5a f1 60 31 56 5b 36 3d 3d 37 3d 3d 36 3d 85 5a f4 5b 3d 82 80 3e 60 3c 57 3d 81 fd 5b 3d 81 f3

Forwarding Contractがどの様な処理を行っているのか説明したいと思います。その際にPOS(ポジション)、OPCODE(オペコード)、OPCODE TEXT(オペコードの内容)、STACK(スタック)の状態を表した図を使用します(※3)。

※3 OPCODEについては Ethereum Virtual Machine Opcodesをご参照して下さい

Forwarding Contractのデプロイコード

この部分は実行コードをデプロイする為のデプロイコードになります。

60 40 80 60 0a 3d 39 3d f3 fe 
  • POS 00〜06

メモリの0x0領域にPOSの0a〜3F(実行コード)の0x40(64byte)をコピーしています。

  • POS 09

OPCODEのfeINVALIDを表しデプロイコードと実行コードの境目を意味します。

POS OPCODE OPCODE TEXT STACK
00 60 40 PUSH1 0x40 0x40
02 80 DUP 1 0x40 0x40
03 60 0a PUSH1 0x0a 0x0a 0x40 0x40
05 3d RETURNDATASIZE 0x0 0x0a 0x40 0x40
06 39 CODECOPY 0x40
07 3d RETURNDATASIZE 0x0 0x40
08 f3 RETURN
09 fe INVALID

Forwarding Contractの実行コード

この部分ではcalldataの長さが0かどうかで分岐を行っています。

73 AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA AA 3d 36 60 25 57
  • POS 00〜19

calldataの長さが0だった場合はジャンプせずに引き続きPOS 1Aのバイトコードを実行します。calldataの長さが0以外の場合はPOS 25にジャンプします。

POS OPCODE OPECODE TEXT STACK
00 73 ... PUSH20 ... {ADDR}
15 3d RETURNDATASIZE 0x0 {ADDR}
16 36 CALLDATASIZE CDS 0x0 {ADDR}
17 6025 PUSH1 0x25 0x25 CDS 0x0 {ADDR}
19 57 JUMPI 0x0 {ADDR}

calldataの長さ = 0の場合の処理

calldataの長さ = 0(外部呼び出しがない、つまり単純送金を意味しています)の場合にcallを実行します。

3d 3d 3d 3d 34 86 5a f1 60 31 56
  • POS 1A〜21

CALLを実行する際には、いくつかの引数を指定する必要があります。{RES}はCALLが成功したかをブール値で返します。

call(g, a, v, in, insize, out, outsize)

g: 送金する際に使用するガス
a: 送金先のアドレス
v: 送金するetherの量(単位はwei)
in: callするデータが保持されるメモリの位置
insize: データのサイズ
out: 戻り値のデータを保持するメモリの位置
outsize: データサイズ

  • POS 22〜24

必ずPOS 31にジャンプします。

POS OPCODE OPECODE TEXT STACK
1A 3d RETURNDATASIZE 0x0 0x0 {ADDR}
1B 3d RETURNDATASIZE 0x0 0x0 0x0 {ADDR}
1C 3d RETURNDATASIZE 0x0 0x0 0x0 0x0 {ADDR}
1D 3d RETURNDATASIZE 0x0 0x0 0x0 0x0 0x0 {ADDR}
1E 34 CALLVALUE VALUE 0x0 0x0 0x0 0x0 0x0 {ADDR}
1F 86 DUP7 {ADDR} VALUE 0x0 0x0 0x0 0x0 0x0 {ADDR}
20 5a GAS GAS {ADDR} VALUE 0x0 0x0 0x0 0x0 0x0 {ADDR}
21 f1 CALL {RES} 0x0 {ADDR}
22 6031 PUSH1 0x31 0x31{RES} 0x0 {ADDR}
24 56 JUMP {RES} 0x0 {ADDR}

calldataの長さ != 0 の場合の処理

calldataの長さ != 0の場合にdelegatecallを実行します。

5b 36 3d 3d 37 3d 3d 36 3d 85 5a f4
  • POS 25〜30

DELEGATECALLは基本的に引数などCALLと同じですが、呼び出し元とcalldataは保持します。

delegatecall(g, a, in, insize, out, outsize)
POS OPCODE OPECODE TEXT STACK
25 5b JUMPDEST 0x0 {ADDR}
26 36 CALLDATASIZE CDS 0x0 {ADDR}
27 3d RETURNDATASIZE 0x0 CDS 0x0 {ADDR}
28 3d RETURNDATASIZE 0x0 0x0 CDS 0x0 {ADDR}
29 37 CALLDATACOPY 0x0 {ADDR}
2A 3d RETURNDATASIZE 0x0 0x0 {ADDR}
2B 3d RETURNDATASIZE 0x0 0x0 0x0 {ADDR}
2C 36 CALLDATASIZE CDS 0x0 0x0 0x0 {ADDR}
2D 3d RETURNDATASIZE 0x0 CDS 0x0 0x0 0x0 {ADDR}
2E 85 DUP6 {ADDR} 0x0 CDS 0x0 0x0 0x0 {ADDR}
2F 5a GAS GAS {ADDR} 0x0 CDS 0x0 0x0 0x0 {ADDR}
30 f4 DELEGATECALL {RES} 0x0 {ADDR}

CALLもしくはDELEGATECALLが成功か失敗した場合の分岐

この部分では前記でCALLもしくはDELEGATECALLを実行した結果に応じた処理を行っています。

5b3d82803e603c573d81fd5b3d81f3
  • POS 31〜38
    RES(実行した結果)が0(失敗)の場合はそのままPOS 39以降の処理を行い、RESが1(成功)の場合はPOS 3Cにジャンプします。

  • POS 39〜3B
    状態を元に戻し(リバートして)0x0からRDSまでのデータを返します。

  • POS 3C〜3F
    0x0からRDSまでのデータを返します。

POS OPCODE OPECODE TEXT STACK
31 5b JUMPDEST {RES} 0x0 {ADDR}
32 3d RETURNDATASIZE RDS {RES} 0x0 {ADDR}
33 82 DUP3 0x0 RDS {RES} 0x0 {ADDR}
34 80 DUP1 0x0 0x0 RDS {RES} 0x0 {ADDR}
35 3e RETURNDATACOPY {RES} 0x0 {ADDR}
36 60 3c PUSH1 0x3c 0x3c RDS {RES} 0x0 {ADDR}
38 57 JUMPI 0x0 {ADDR}
39 3d RETURNDATASIZE RDS 0x0 {ADDR}
3A 81 DUP2 0x0 RDS 0x0 {ADDR}
3B fd REVERT 0x0 {ADDR}
3C 5b JUMPDEST 0x0 {ADDR}
3D 3d RETURNDATASIZE RDS 0x0 {ADDR}
3E 81 DUP2 0x0 RDS 0x0 {ADDR}
3F f3 RETURN 0x0 {ADDR}

ExchangeDeposit Contract

ライブラリのインポート

スマートコントラクトのライブラリであるOpenZeppelinを使用します。OpenZeppelinはコントラクトでよく使われる処理をセキュアに行う目的で使用します。

import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/SafeERC20.sol';
import '@openzeppelin/contracts/utils/Address.sol';

ライブラリを使用する際はusing...for...の様な書き方をします。SafeERC20はERC20インターフェースの基本実装で、例えばsafeTransfer関数はERC20のトークンを安全に転送することができます。IERC20はERC20を実装する際に準拠する必要があるインターフェイスです。Addressはaddressタイプに関連する関数を扱うもので、例えばisContract関数はコントラクトアカウントかどうかをbooleanで返します。

using SafeERC20 for IERC20;
using Address for address payable;

変数

コールドウォレットのアドレスになります。payable修飾子をつけることで、このコントラクトアドレスに対して送金することが出来る様になります。

address payable public coldAddress;

ユーザーが入金する際の最低入金金額です。1ETH=1e18(10の18乗)なので1e16は0.01ETHと言う意味です。

uint256 public minimumInput = 1e16;

プロキシコントラクトがDELEGATECALLで呼び出す、ロジックコントラクトのアドレスです。

address payable public implementation;

ExchangeDepositコントラクトをコントロールする権限を持ったアドレスで、コントラクトの強制停止、ロジックの転送の無効化、コールドアドレスやimplementationアドレスの変更などをすることが可能です。
immutableキーワードは読み取り専用の値で、ストレージではなくコントラクト本体に直接値を格納するものになります。

address payable public immutable adminAddress;

ExchangeDeposit Contractのインスタンスアドレスです。このアドレスがプロキシアドレスかExchangeDepositアドレスかを識別するために使用します。

address payable private immutable thisAddress;

コンストラクタ

coldAddrとadminAddrが0アドレスでないか (無効なアドレスでないか)チェックしています。

require(coldAddr != address(0), '0x0 is an invalid address');
require(adminAddr != address(0), '0x0 is an invalid address');

thisAddressには、このコントラクトのアドレスを設定しています。

coldAddress = coldAddr;
adminAddress = adminAddr;
thisAddress = address(this);

イベント

イベントはトランザクションのログ記録する為に使用します。DepositイベントはForwardingコントラクトから送金された入金のログを記録します。引数には資金の送り元のアドレスと送った金額を指定します。

event Deposit(address indexed receiver, uint256 amount);

修飾子

修飾子は関数を実行する前に特定の処理を実行したい場合に指定します。この修飾子をつけた関数を実行するコントラクトが、以下の条件だった場合に実行されます。

adminAddressである場合に内部のコードを実行します。

modifier onlyAdmin {
    require(msg.sender == adminAddress, 'Unauthorized caller');
    _;
}

強制停止によりコールドアドレスが0アドレスになっていない場合に内部のコードを実行します。

modifier onlyAlive {
    require(
        getExchangeDepositor().coldAddress() != address(0),
        'I am dead :-('
    );
    _;
}

ExchangeDepositがcallで呼び出された場合に内部のコードを実行します。

modifier onlyExchangeDepositor {
    require(isExchangeDepositor(), 'Calling Wrong Contract');
    _;
}

関数

isExchangeDepositor関数

コードの文脈がExchangeDepositなのかProxyなのかチェックしブール値で返します。

function isExchangeDepositor() internal view returns (bool) {
    return thisAddress == address(this);
}

getExchangeDepositor関数

コードの文脈がExchangeDepositの場合ExchangeDepositコントラクトのインスタンスを返し、そうでない場合はExchangeDeposit関数を使い引数にこのコントラクトのアドレスを指定しExchangeDepositコントラクトのインスタンスを返します。

function getExchangeDepositor() internal view returns (ExchangeDeposit) {
    return isExchangeDepositor() ? this : ExchangeDeposit(thisAddress);
}

getImplAddress関数

implementationアドレスを取得する為の関数です。コードの文脈がExchangeDepositの場合はimplementation(ロジックコントラクトのインスタンスアドレス)を返し、そうでない場合はExchangeDeposit関数でインスタンス化した後にimplementation()でインスタンスアドレスを返します。

function getImplAddress() internal view returns (address payable) {
    return
        isExchangeDepositor()
            ? implementation
            : ExchangeDeposit(thisAddress).implementation();
}

getSendAddress関数

gatherEthやgatherErc20の関数のsendToアドレスを取得するための関数です。通常はコールドウォレットのアドレスですが、ExchangeDepositコントラクトが強制終了している場合はadminAddressになります。

function getSendAddress() internal view returns (address payable) {
    ExchangeDeposit exDepositor = getExchangeDepositor();

    address payable coldAddr = exDepositor.coldAddress();
    address payable toAddr = coldAddr == address(0)
        ? exDepositor.adminAddress()
        : coldAddr;
    return toAddr;
}

gatherErc20関数

ERC20のトークンをコールドウォレット(強制終了している場合はadminAddress)に転送します。

function gatherErc20(IERC20 instance) external {
    uint256 forwarderBalance = instance.balanceOf(address(this));
    if (forwarderBalance == 0) {
        return;
    }
    instance.safeTransfer(getSendAddress(), forwarderBalance);
}

gatherEth関数

他のコントラクトがselfdestruct(※4)によってETHを付与された場合にreceive関数では対応できません。その場合にETHをコールドウォレット(強制終了している場合はadminAddress)に転送する為の関数です。

function gatherEth() external {
    uint256 balance = address(this).balance;
    if (balance == 0) {
        return;
    }
    (bool result, ) = getSendAddress().call{ value: balance }('');
    require(result, 'Could not gather ETH');
}

※4 コントラクトを削除する関数

changeColdAddress関数
コールドアドレスを新しいアドレスに変更することが出来ます。その際に条件として以下である必要があります。
①実行する文脈がExchangeDeposit
②強制終了されていない
③adminAddressである

function changeColdAddress(address payable newAddress)
    external
    onlyExchangeDepositor
    onlyAlive
    onlyAdmin
{
    require(newAddress != address(0), '0x0 is an invalid address');
    coldAddress = newAddress;
}

changeImplAddress関数

implementationアドレスを新しいアドレスに変更することが出来ます。条件はchangeColdAddress関数と同様です。変更するアドレスは0アドレス(ない状態に戻せる様にする為)もしくはコントラクトアドレスである必要があります。

function changeImplAddress(address payable newAddress)
    external
    onlyExchangeDepositor
    onlyAlive
    onlyAdmin
{
    require(
        newAddress == address(0) || newAddress.isContract(),
        'implementation must be contract'
    );
    implementation = newAddress;
}

changeMinInput関数

最低入金額を変更することが出来ます。条件はchangeColdAddress関数と同様です。

function changeMinInput(uint256 newMinInput)
    external
    onlyExchangeDepositor
    onlyAlive
    onlyAdmin
{
    minimumInput = newMinInput;
}

kill関数

コールドアドレスを0アドレスにして、コントラクトを強制終了します。

function kill() external onlyExchangeDepositor onlyAlive onlyAdmin {
    coldAddress = address(0);
}

receiveとfallback

receive関数

receive関数はfallback関数に似ていますが、calldataがない取引にてETHが送金された場合に呼び出されます。receive関数がない場合はfallback関数が呼ばれます。

receive() external payable {
    require(coldAddress != address(0), 'I am dead :-(');
    require(msg.value >= minimumInput, 'Amount too small');
    (bool success, ) = coldAddress.call{ value: msg.value }('');
    require(success, 'Forwarding funds failed');
    emit Deposit(msg.sender, msg.value);
}

コールドアドレスがkillされていないか、最低入金金額を下回っていないかをチェックします。

require(coldAddress != address(0), 'I am dead :-(');
require(msg.value >= minimumInput, 'Amount too small');

コールドアドレスにETHを転送します。emiteventで記録したトランザクションのログを出力する際に使用します。資金の送り元のアドレスとその金額を指定して出力しています。

(bool success, ) = coldAddress.call{ value: msg.value }('');
require(success, 'Forwarding funds failed');
emit Deposit(msg.sender, msg.value);

fallback関数

ExchangeDepositコントラクトにない関数を呼ばれた場合に、このfallback関数が実行されます。

fallback() external payable onlyAlive {
    address payable toAddr = getImplAddress();
    require(toAddr != address(0), 'Fallback contract not set');
    (bool success, ) = toAddr.delegatecall(msg.data);
    require(success, 'Fallback contract failed');
}

ImplAddress(ロジックコントラクトのアドレス)を取得しそのアドレスに対してdelegatecallでmsg.dataをcalldataとして呼び出します。

address payable toAddr = getImplAddress();
(bool success, ) = toAddr.delegatecall(msg.data);

Forwarding Contractへ入金

それでは入金の流れを確認する為、テストネットワーク上でユーザーの入金アドレスにETHを入金をしてみたいと思います。

環境

MetaMask 8.1.9
solidity version 0.6.11
Remix
Etherscan

送金の流れ

①MetaMaskとRemixの連携

RemixのENVIRONMENTInjected Web3に変更します。MetaMaskにログインしRopstenテストネットを選択して連携を行います。

7d6d8ac3-4518-4ca9-b157-9d37c0555d05-1920x986r

②MetaMaskの設定

MetaMaskでAdminAccount, ColdAccount, TransferAccountを作成します。AdminAccountとTransferAccountにFaucetからETHを入金しておきます。

c43b72e8-ccc7-425b-b0ae-d1e7f2604165-1137x1920r

③Remixの設定

ExchangeDepositContractとProxyFactoryContractを1つのファイルにまとめてコンパイルします。
1611fa86-45e5-4000-8ba2-2a361bacf159-1920x942r

④ExchangeDepositContractをデプロイ

  • AdminAccountでExchangeDepositContractをデプロイします。ContractをExchangeDepositに設定して下さい
  • 引数にcold address(ColdAccountのアドレス)admin address(AdminAccountのアドレス)を指定してtransactを押下します
  • MetaMaskのウィンドウが開きますので確認ボタンを押します
    00a866f6-9040-4e6e-8d70-47e9827d15eb-1920x1049r

⑤ProxyFactoryContractをデプロイ

  • AdminAccountでProxyFactoryContractをデプロイします。ContractをProxyFactoryContractに設定して下さい
  • 引数に④でデプロイしたExchangeDepositContractのコントラクトアドレスを指定してtransactを押下します
  • MetaMaskのウィンドウが開きますので確認ボタンを押します
    5e5bce6a-5432-4a40-bc3e-9e027af8cb97-1920x942r

⑥deployNewInstanceの作成

  • ⑤でデプロイしたProxyFactoryContractのdeployNewInstance関数を実行します
  • 引数に32byteのsaltを指定します(16進数32byteであればなんでもOK)
例:
0x0000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA00

90bf09d2-c4a7-4250-ba25-4a70d113ad3c-1920x946r

⑦deployNewInstanceのアドレスを確認

deployNewInstanceの詳細を確認する為にEtherscanのURLをクリックします
d974d592-c870-4e0b-a9b6-4e1b6e52c001-1920x946r

赤で囲った部分がdeployNewInstanceのアドレスになります
9c144b30-746c-4235-86d8-aeaf427875e5-1920x800r

deployNewInstanceのコントラクトアドレスを調べるとForwarding ContractのバイトコードとExchangeDepositContractのコントラクトアドレスを確認することが出来ます。
a68cc36e-d71e-4363-abc3-bfa348c5c71b-1920x947r

⑧入金用のアドレスへ送金

MetaMaskを使って実際にユーザ毎に割り当てられる入金用のアドレス(deployNewInstanceのコントラクトアドレス)に対して、ユーザーのアカウント(TransferAccount)から0.3ETH送金してみます。
bc916ee3-991f-49be-a8f8-9c8817da03ec-1135x1920r

⑨入金の確認

ユーザーのアカウント(Transfer Address)から0.3ETHをユーザーの入金用アドレス(deployNewInstanceAddress)に送金した金額がコールドアドレス(ColdAddress)に届いているのが確認出来ました。
c837955d-e192-4bad-bac5-5f7ec70c1b02-1920x894r

まとめ

全てのコードには触れませんでしたが、理解する上で必要最低限の部分は網羅出来たかと思います。この記事で興味を持たれた方は、是非今回触れていない部分もご覧になって下さい。特にテストコードを見ると、更に深く理解出来るかと思います。Solidityは非常に取っ付きやすい言語なので、自分でコードに手を加えながら実行してみるのも良いかと思います。

参考文献

Author image
About adrenaline
expand_less