オーバーフローは、スマートコントラクトならではの問題ではありませんが意外と見落としがちです。
オーバーフローが発生すると、データの不整合が発生するので注意が必要です。
ソースコード
オーバーフローを確認するためのサンプルソースは以下の通りです。
[ソースコード]
1 | pragma solidity ^0.4.11; |
マーケットプレイスを想定したコントラクトで、在庫数の追加を行う関数のみを実装しています。
次回は、このスマートコントラクトを使ってオーバーフローになる場合の動作を確認します。
オーバーフローは、スマートコントラクトならではの問題ではありませんが意外と見落としがちです。
オーバーフローが発生すると、データの不整合が発生するので注意が必要です。
オーバーフローを確認するためのサンプルソースは以下の通りです。
[ソースコード]
1 | pragma solidity ^0.4.11; |
マーケットプレイスを想定したコントラクトで、在庫数の追加を行う関数のみを実装しています。
次回は、このスマートコントラクトを使ってオーバーフローになる場合の動作を確認します。
前回作成した重要な情報を扱うスマートコントラクトの動作確認を行います。
今回の動作確認では、アカウントの役割は重要ではないのですが、一応次のようにしておきます。
まずはデプロイを行います。デプロイアカウントはアカウント3です。
デプロイするのはSecretで、コンストラクタパラメータにはtestを設定します。
[デプロイ]
デプロイ後のコントラクトの状態を確認します。
[コントラクト状態]
さすがにこの画面からはsecretにどんな文字が設定されているかは確認できません。
(privateで宣言しているため)
コントラクトを生成したときのハッシュを確認します。
[トランザクションの確認]
このトランザクション・ハッシュを引数にして、トランザクションのinputというフィールドを確認します。
gethコンソールから次のコマンドを実行します。
[トランザクションのinputフィールド確認]
1 | > eth.getTransaction('0xd34e1aa86318225a66a77399f95a835b250ac75139c1c01eb2a80d3636238939').input |
前半部分はコンパイルされたコントラクトのコードですが、後半部分のゼロに囲まれた474657374に注目してみます。
実は、この部分の最初の4はバイト数を表し、その直後の74657374(4バイト)はコントラクトで設定された文字列を表しています。
試しにこのデータをアスキーに変換してみます。
[アスキー変換]
1 | > web3.toAscii("0x74657374") |
なんとprivateで設定した文字列”test”が簡単に見れてしまいました。
setSecret関数をコールして、文字列をkoushinに変更します。
[文字列の確認]
再度、トランザクションのハッシュから、inputフィールドを確認します。
[トランザクションのinputフィールド確認]
1 | > eth.getTransaction('0x1bf2c055abc6e5d0f861c64c52c7a84419c4b80a41edbf954bce7c78b4147392').input |
また後半部分のゼロに囲まれた76b6f757368696eに注目してみます。
最初の7はバイト数を表し、6b6f757368696e(7バイト)は更新した文字列を表しています。
このデータをアスキーに変換してみます。
[アスキー変換]
1 | > web3.toAscii("0x6b6f757368696e") |
更新した文字列を確認することができました。
privateな変数を宣言しても、トランザクションの中身を確認すれば簡単にそのデータを確認できるということが分かりました。
Ethereumではトランザクションが暗号化されないため、中身を参照すればどんなデータか確認することができるのです。
個人情報のような重要な情報をステートに設定する場合は、暗号化するなどの処理を行うようにしましょう。
スマートコントラクトは、ステートに情報を保持し他の誰かと情報を連携することができます。
ただこのステートに保持する情報の扱いには気を付ける必要があります。
privateなステート(secret)に文字を格納するだけのサンプルソースを準備します。
[ソースコード]
1 | pragma solidity ^0.4.11; |
このコントラクトを生成する場合に、秘密の文字列を設定します。(6-8行目)
またこの秘密の文字列はsetSecret関数(11-13行目)を使って更新することはできますが、その文字列を参照する関数はなく、privateで宣言している(3行目)ためその内容を確認することはできない・・・はずです。
本当にprivateで宣言していれば、その情報を参照することはできないのでしょうか。
次回は、このスマートコントラクトの動作確認を行います。
前回作成した抽選を行うスマートコントラクトの動作確認を行います。
アカウントの役割は次の通りです。
まずはデプロイを行います。デプロイアカウントはMAIN ACCOUNTです。
デプロイするのはLotteryです。
[デプロイ]
デプロイ後のコントラクトの状態を確認します。
[コントラクト状態]
主催者(Owner)がMain accountで、応募者の数(Num applicants)が0であることが確認できます。
アカウント1,アカウント2、アカウント3から抽選会に応募します。
各アカウントからEnter関数を指定しトランザクションを発行します。
[アカウント1から抽選会に応募]
[アカウント2から抽選会に応募]
[アカウント3から抽選会に応募]
応募したあとのコントラクトの状態を確認します。
応募者(Applicants)は入力欄の右端にある小さい三角ボタンをクリックすると、インデックスを変えて応募者を確認することができます。
[応募1の確認]
[応募2の確認]
[応募3の確認]
応募者の人数(Num applicants)が3で、各応募者(Applicants)がアカウント1,アカウント2、アカウント3であることが確認できます。
抽選の開催者(Main Account)から、Hold関数をコールして当選者を決定します。
[開催者が当選者を決める]
コントラクトの状態を参照し、当選者を確認します。
[当選者の確認]
当選者(Winner address)が、アカウント2であることが確認できます。
また、当選者を決めるパラメータとなったTimestampは1627990495であることが分かります。
抽選を行った時のトランザクションを確認します。
[当選者を決めた際のトランザクション]
このトランザクションは15909番目のブロックに取り込まれています。
このブロックのタイムスタンプをgethコンソールから確認します。
1 | > eth.getBlock(15909).timestamp |
15909番目のブロックのタイムスタンプは1627990495となっています。
この値は、コントラクトで抽選を行った時のタイムスタンプと同じです。
つまり、スマートコントラクトで参照しているblock.timestampはトランザクションが発行されたときのタイムスタンプではなく、ブロックに取り込まれた時のタイムスタンプだということになります。
ブロックのタイムスタンプはマイナーがブロックを生成する際に設定するもので、どんな値が設定されるのかはマイナー次第となります。
抽選会にマイナーが応募していたとすると、マイナーは抽選時のトランザクションを検知し自分自身が当選者となるようなタイムスタンプを設定し、ブロックを作成するということが可能です。
今回のサンプルソースは、抽選結果がタイムスタンプにより決定されるといった仕様のため、タイムスタンプを操作され当選者を自由に決められてしまうという脆弱性があったといことになります。
タイムスタンプを操作されると困るような場合は、タイムスタンプだけに依存した仕様にしないように気を付けましょう。
ブロックのタイムスタンプに依存した処理(Timestamp Dependence)を行うと脆弱性となる可能性があります。
抽選を行うスマートコントラクトを作成し、この問題を検証してみます。
抽選を行うサンプルソースは次の通りです。
[ソースコード]
1 | pragma solidity ^0.4.11; |
処理のポイントを解説します。
非常にシンプルな処理ですが、当選者がタイムスタンプに依存していることが脆弱性となりえます。
次回は、このスマートコントラクトの動作確認を行います。
今回は、トランザクション発行を検知して、そのトランザクションより優先して処理を実行してみます。
アカウントの役割は次の通りです。
現状のコントラクトの状態を確認します。
[コントラクトの状態]
価格は20 etherであることが確認できます。
トランザクションを検知して、priceを自動で20 etherから25 etherに更新する処理を設定します。
コマンドの全体は下記の通りで、gethコンソール上で実行します。
[コマンド]
1 | var filter = web3.eth.filter('pending'); |
上記コマンドの説明は以下の通りです。
アカウント1から購入トランザクション(buy関数)を発行します。
[コマンド]
1 | > personal.unlockAccount(eth.accounts[0]) |
14行目以降が、イベントが検知され自動で実行された処理のログになります。
発行されたトランザクションの内容を確認します。
[トランザクションの確認]
アカウント1としてはpriceが20 etherで購入(buy)したはずですが、イベント検知処理により価格更新のトランザクションが優先的に実行されたために25 etherでの購入することになっていることが確認できます。
今回のサンプルでは攻撃者がスマートコントラクトのオーナーという前提で行いましたが、逆のパターンもありえます。
自分が作成したスマートコントラクトに対して、TODを突いた攻撃を受ける可能性もあります。
また、作成したスマートコントラクトがTODの影響を受けた場合に問題になるかどうかは、攻撃意思がなくても発生する可能性があります。
スマートコントラクトを実装する際にはTODの影響を受けるかどうかを必ず確認するようにしましょう。
前回は、Transaction-Ordering Dependence問題を検証するためのコントラクトをデプロイし、geth上で操作できるようにコントラクトの変数宣言を行いました。
今回はこのスマートコントラクトの動作確認を行います。
アカウントの役割は次の通りです。
動作確認は2つのトランザクを間髪入れずに連続で行う必要があるため、gethコンソール上から行います。
最初に買い手からbuy関数を呼び出し、2つ目でオーナーからupdate関数をコールしpriceを更新します。
buy関数のgasPriceを高く設定しているのがポイントとなります。
送金を行う前に各アカウントのロックを解除しています。
[コマンド]
1 | > personal.unlockAccount(eth.accounts[0]) |
実行結果を確認します。
[結果]
先にbuyを発行しているため、updatePriceでpriceが15 etherに更新される前のpriceで購入できているのは当たり前のように思えます。
しかし実際には発行順とは違う順番でトランザクションが実行されてしまうことがあります。
買い手はbuyトランザクションを発行する時に参照していたpriceではなくupdatePrice後のpriceで購入してしまうケースがあります。
コマンド発行順は同じですが、updatePrice関数のgasPriceを高く設定して実行してみます。
[コマンド]
> personal.unlockAccount(eth.accounts[0])
Unlock account 0xec3b01f36b44182746ca109230567c4915512e35
Passphrase:
true
> personal.unlockAccount(eth.accounts[1])
Unlock account 0x63f25b9bbd974fdfa07477cb99c60d2073cfe560
Passphrase:
true
> mpt.buy.sendTransaction(10, {from:eth.accounts[1], gas:5000000, gasPrice:50000000000, value:web3.toWei(500, "wei")});
"0x75840755d8756c7918b925329b83ddba5d62c43caeb7c80f9144b15c5594c173"
> mpt.updatePrice.sendTransaction(20, {from:eth.accounts[0], gas:5000000, gasPrice:90000000000});
"0x4583af1ec7929ba834d493482e57f0474349dba2d09cea8d8c9376d16409708f"```
<br>
実行結果を確認します。
[結果]
<img src="/img/geth/tod3b.png" alt="" />
買い手がbuyトランザクションを発行する時に参照していたpriceは<b>15 ehter</b>でしたが、<b>20 ehter</b>での購入となってしまいました。
<br>
1回目と結果が変わったのは意図的にこの順番になるようにオーナーが<b>トランザクションの順番をコントロールした</b>からです。
Ethereumではマイニングする際に、<b>gasPriceが高いトランザクションを優先して実行</b>するようになっています。
そのため、後者のトランザクションのgasPriceを前者のgasPriceより高く設定すれば、トランザクションの発行順によらず後者が優先されてしまいます。
次回は、トランザクションのイベントを検知しこの操作を自動化してみます。
前回は、Transaction-Ordering Dependence問題を検証するためのサンプルコードを準備しました。
今回はそのスマートコントラクトをデプロイし、gethコンソール上でそのコントラクトを操作できるように準備します。
アカウントの役割は次の通りです。
まずはデプロイを行います。デプロイアカウントはMAIN ACCOUNTです。
デプロイするのはMarket Place TODです。
[デプロイ]
デプロイ後のコントラクトの状態を確認します。
[コントラクト状態]
コントラクトのアドレスとインターフェースは、geth上のコントラクト定義の際に必要になりますので、コピーしておいて下さい。
geth上でコントラクトを操作できるようにmptという変数名で定義します。
コントラクトを定義する際は次のようなコマンドを実行します。
[コントラクトの定義方法]
1 | var mpt = eth.contract(インターフェース).at('アドレス') |
インターフェースとアドレスには、Mist Walletから取得したものを設定します。
[コマンド]
1 | var mpt = eth.contract([ { "constant": true, "inputs": [], "name": "stockQuantity", "outputs": [ { "name": "", "type": "uint256", "value": "100" } ], "payable": false, "type": "function" }, { "constant": false, "inputs": [ { "name": "_price", "type": "uint256" } ], "name": "updatePrice", "outputs": [], "payable": false, "type": "function" }, { "constant": true, "inputs": [], "name": "owner", "outputs": [ { "name": "", "type": "address", "value": "0xec3b01f36b44182746ca109230567c4915512e35" } ], "payable": false, "type": "function" }, { "constant": true, "inputs": [], "name": "price", "outputs": [ { "name": "", "type": "uint256", "value": "10" } ], "payable": false, "type": "function" }, { "constant": false, "inputs": [ { "name": "_quantity", "type": "uint256" } ], "name": "buy", "outputs": [], "payable": true, "type": "function" }, { "inputs": [], "payable": false, "type": "constructor" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "_price", "type": "uint256" } ], "name": "UpdatePrice", "type": "event" }, { "anonymous": false, "inputs": [ { "indexed": false, "name": "_price", "type": "uint256" }, { "indexed": false, "name": "_quantity", "type": "uint256" }, { "indexed": false, "name": "_value", "type": "uint256" }, { "indexed": false, "name": "_change", "type": "uint256" } ], "name": "Buy", "type": "event" } ]).at('0x350aC614C66c28dF49B9791A59c2b388b3253F18') |
定義したコントラクトの内容はgeth上で確認できます。
[定義したコントラクトの内容確認]
1 | > mpt |
以上で、geth上でコントラクトの操作を行う準備ができました。
次回は、このスマートコントラクトの動作確認を行います。
トランザクションがブロックに取り込まれる順番はマイナーに依存するため、意図した順番でトランザクションが実行されないことがあります。
この問題はTransaction-Ordering Dependenceと呼ばれています。
サンプルコードを作成し、この問題を検証していきます、
Transaction-Ordering Dependence問題を確認するための、サンプルコードは下記の通りです。
マーケットプレイスを表現したスマートコントラクトになります。
[ソースコード]
1 | pragma solidity ^0.4.11; |
このコードのポイントは次の通りです。
次回は、このスマートコントラクトをデプロイし、geth上で動作確認できるようにコントラクト変数を定義します。
今回は、Reentrancy問題を改善したスマートコントラクトに対して攻撃を行います。
アカウントの役割は次の通りです。
まずはデプロイを行います。デプロイアカウントはMAIN ACCOUNTです。
デプロイするのは、前回改善したVictim Balanceコントラクトになります。
[Victim Balanceコントラクトのデプロイ]
デプロイ後のコントラクトの状態を確認します。
[攻撃される側のコントラクト状態]
デプロイ直後なので、コントラクトの残高が0 ehterであることが確認できます。
次は攻撃する側のコントラクトのデプロイを行います。デプロイアカウントはACCOUNT2です。
デプロイするのは、Evil Receiverコントラクトになります。
targetには、攻撃される側のコントラクト(改善したVictim Balance)のアドレスを設定します。
[Evil Receiverコントラクトのデプロイ]
デプロイ後のコントラクトの状態を確認します。
[攻撃する側のコントラクト状態]
ターゲットのコントラクトがVictim Balanceとなっていることが確認できます。
ACCOUNT1から、攻撃される側のコントラクト(Victim Balance)に20 ether送金します。
Add To Balance関数を使用します。
[送金]
送金後のコントラクトの状態を確認します。
[攻撃される側のコントラクト状態]
残高が20 etherになっていることが確認できます。
ACCOUNT2から、攻撃する側のコントラクト(Evil Receiver)に10 etherを送金します。
Add Balance関数を使用します。
[送金]
送金後のコントラクトの状態を確認します。
[攻撃する側のコントラクト状態]
残高が10 etherになっていることが確認できます。
攻撃する側のコントラクト(Evil Receiver)から攻撃される側のコントラクト(Victim Balance)に10 etherを送金します。
Send Eth To Target関数を使用します。
[送金]
攻撃される側のコントラクトの状態を確認します。
[攻撃される側のコントラクト状態]
10 ether追加されて、残高が30 etherになりました。
攻撃する側のコントラクトの状態を確認します。
[攻撃する側のコントラクト状態]
10 ether送金したので、残高が0 etherになりました
攻撃する側のコントラクト(Evil Receiver)から攻撃される側のコントラクト(Victim Balance)に返金要求を行います。Withdraw関数を使用します。
10 ether送金したので、返金額は10 etherとなるはずです。
[返金]
攻撃された側のコントラクト状態を確認します。
[攻撃される側のコントラクト状態]
改善前は、20 ether送金されてしまい10 etherしか残っていませんでしたが、今回は問題なく10 etherが返金され残高が20 etherとなっています。
攻撃した側のコントラクト状態も確認します。
[攻撃する側のコントラクト状態]
前回は攻撃の結果20 ether引き出せましたが、今回は本来の10 ehterの返金となり攻撃に失敗しています。
以上で、Reentrancy問題のあったらスマートコントラクトの改善を確認することができました。