フラミナル

考え方や調べたことを書き殴ります。IT技術系記事多め

Solidity で無限ループするコントラクトを作ったらどうなる?

f:id:lirlia:20220212125721p:plain

物は試しでやってみた。

最初

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Loop {
  mapping (address=>uint) Num;

  function updateNum() public {
    Num[msg.sender] = block.timestamp;
    updateNum();
  }
}

Error: non-payable method cannot override valueで怒られてしまいました。

payable にする

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Loop2 {
  mapping (address=>uint) Num;

  function updateNum() public payable {
    Num[msg.sender] = block.timestamp;
    updateNum();
  }
}
Error: sender doesn't have enough funds to send tx. The upfront cost is: 123000000000000000000 and the sender's account only has: 100000000000000000000

GAS fee を 0 にする

GASを0にしてみました。 するとスタックオーバーフローが発生しましたね。処理のネスト数が限界を超えたのでしょうか。

Error: VM Exception while processing transaction: stack overflow

インクリメントにしてみる

今度は時刻ではなくインクリメントしていこうと思います。またget関数も追加しました。

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Loop3 {
  mapping (address=>uint) Num;

  function updateNum() public payable {
    Num[msg.sender] = Num[msg.sender] + 1;
    updateNum();
  }

  function getNum() public view returns(uint){
    return Num[msg.sender];
  }
}

再び実行しましたが変わらずError: VM Exception while processing transaction: stack overflow のままでした。(そりゃそうだ)

ここでgetNum関数を実行してみましたがトランザクションに保存されているデータは0のままでした。

f:id:lirlia:20220212125800p:plain

当たり前ですが、処理に失敗するとトランザクションにデータは書き込まれないようです。(トランザクション自体は生成されていました)

1024で条件付してみる

スタックサイズが1024らしいのでこれで試しました。

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Loop4 {
  mapping (address=>uint) Num;

  function updateNum() public payable {
    require(Num[msg.sender] < 1023);
    Num[msg.sender] = Num[msg.sender] + 1;
    updateNum();
  }

  function getNum() public view returns(uint){
    return Num[msg.sender];
  }
}

だめでした。あとなんか Ganache が落ちたのでこのタイミングでチェーンをリセットしました。

面倒なのでネスト回数を指定できるようにした

reset するための関数を足した。 成功したらデータ消したいので追加。

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

contract Loop7 {
  mapping (address=>uint) Num;

  function updateNum(uint _count) public payable {
    if (Num[msg.sender] < _count) {
      Num[msg.sender] = Num[msg.sender] + 1;
      updateNum(_count);
    }
  }

  function resetNum() public {
    Num[msg.sender] = 0;
  }

  function getNum() public view returns(uint){
    return Num[msg.sender];
  }
}

その結果かんたんに試せるようになったのでどのぐらいネストできるのかを色々試してみました。 - 200回->OK - 500回->OK - 800回->NG - 700回->NG - 600回->NG - 550回->NG - 525回->NG - 513回->NG - 506回->OK - 510回->NG - 508回->NG - 507回->OK

結果507回ネストしても大丈夫ということがわかりました。

結論(答え出ていない)

  • スタックサイズが1024なのでその制限にひっかかったっぽい
  • 507 x 2 = 1014 なので リターンアドレスとローカル変数?かなんかで2個づつスタックがつまれてる
  • あと9?10?がなんなのかはよくわからない。
  • 通常は gas 代で処理止まるので基本は起きないはず