これは ビットバンク株式会社 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つのコントラクトから構成されています。
- Exchange Deposit Contract
- 入金システムのメインロジックとなるコントラクト
- Extra Implementation Contract
- ロジックを追加する際に使用するコントラクト
- Forwarding Contract
- 各ユーザー毎に割り当てられる入金受付用のコントラクト
- Proxy Factory Contract
- Forwarding Contractを作成するコントラクト
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転送の為のトランザクションを送信することが出来ます
- コントラクトアカウント
- イーサリアムの残高・コントラクトコードを所有し、他のコントラクトからのトランザクションまたはメッセージによって動作します
- イーサリアムの残高・コントラクトコードを所有し、他のコントラクトからのトランザクションまたはメッセージによって動作します
fallback関数
solidityにはfallback関数と呼ばれる関数があります。
コントラクトAからコントラクトBに対して関数Dを呼び出した場合、関数Dがないのでエラーになります。
コントラクトAからコントラクトCに対して関数Dを呼び出した場合、関数Dは存在しませんがその場合に呼ばれるのがfallback関数です。
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関数の実行時にプロキシコントラクトです。
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 ID
とx
とy
を合わせた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を呼び出すロジックが定義されています。
なぜ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))
FF
に60408060...
を、00
にAA
を上書きすることで、バイトコードを整形しています。
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のfe
はINVALID
を表しデプロイコードと実行コードの境目を意味します。
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を転送します。emit
はevent
で記録したトランザクションのログを出力する際に使用します。資金の送り元のアドレスとその金額を指定して出力しています。
(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のENVIRONMENT
をInjected Web3
に変更します。MetaMaskにログインしRopstenテストネット
を選択して連携を行います。
②MetaMaskの設定
MetaMaskでAdminAccount, ColdAccount, TransferAccountを作成します。AdminAccountとTransferAccountにFaucetからETHを入金しておきます。
③Remixの設定
ExchangeDepositContractとProxyFactoryContractを1つのファイルにまとめてコンパイルします。
④ExchangeDepositContractをデプロイ
- AdminAccountでExchangeDepositContractをデプロイします。ContractをExchangeDepositに設定して下さい
- 引数に
cold address(ColdAccountのアドレス)
とadmin address(AdminAccountのアドレス)
を指定してtransact
を押下します - MetaMaskのウィンドウが開きますので
確認
ボタンを押します
⑤ProxyFactoryContractをデプロイ
- AdminAccountでProxyFactoryContractをデプロイします。ContractをProxyFactoryContractに設定して下さい
- 引数に④でデプロイしたExchangeDepositContractのコントラクトアドレスを指定して
transact
を押下します - MetaMaskのウィンドウが開きますので
確認
ボタンを押します
⑥deployNewInstanceの作成
- ⑤でデプロイしたProxyFactoryContractのdeployNewInstance関数を実行します
- 引数に32byteのsaltを指定します(16進数32byteであればなんでもOK)
例:
0x0000000000000000000000AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA00
⑦deployNewInstanceのアドレスを確認
deployNewInstanceの詳細を確認する為にEtherscanのURLをクリックします
赤で囲った部分がdeployNewInstanceのアドレスになります
deployNewInstanceのコントラクトアドレスを調べるとForwarding ContractのバイトコードとExchangeDepositContractのコントラクトアドレスを確認することが出来ます。
⑧入金用のアドレスへ送金
MetaMaskを使って実際にユーザ毎に割り当てられる入金用のアドレス(deployNewInstanceのコントラクトアドレス)に対して、ユーザーのアカウント(TransferAccount)から0.3ETH送金してみます。
⑨入金の確認
ユーザーのアカウント(Transfer Address)から0.3ETHをユーザーの入金用アドレス(deployNewInstanceAddress)に送金した金額がコールドアドレス(ColdAddress)に届いているのが確認出来ました。
まとめ
全てのコードには触れませんでしたが、理解する上で必要最低限の部分は網羅出来たかと思います。この記事で興味を持たれた方は、是非今回触れていない部分もご覧になって下さい。特にテストコードを見ると、更に深く理解出来るかと思います。Solidityは非常に取っ付きやすい言語なので、自分でコードに手を加えながら実行してみるのも良いかと思います。