[Solidity]構造体を削除しても内部のmappingは消えない

# はじめに

以下のような構造体があった場合に、構造体のmappingを削除する方法と各方法での違いを調べました。

```javascript
struct Person {
   uint256 age;
   mapping(uint256 => uint256) numMap;
}

mapping(uint256 => Person) public persons;
```
---
# コード
```javascript
pragma solidity >=0.8.2 <0.9.0;

contract Storage {

    struct Person {
        uint256 age;
        mapping(uint256 => uint256) numMap;
    }

    mapping(uint256 => Person) public persons;

    function add(uint256 p_key, uint256 age, uint256 key, uint256 value) public {
        persons[p_key].age = age;
        persons[p_key].numMap[key] = value;
    }

  　// deleteを使ってPerson自体を削除
    function removeByDelete(uint256 p_key) public {
        delete persons[p_key];
    }

    // Personの中身を初期化。ただし、mappingはdeleteで削除
    function removeByInitDelete(uint256 p_key, uint256 key) public {
        persons[p_key].age = 0;
        delete persons[p_key].numMap[key];
    }

    // Personの中身を全て初期化
    function removeByInit(uint256 p_key, uint256 key) public {
        persons[p_key].age = 0;
        persons[p_key].numMap[key] = 0;
    }

    function viewMap(uint256 p_key, uint256 key) public view returns (uint256){
        return persons[p_key].numMap[key];
    }
}
```

---

## deleteを使う場合
deleteを使って構造体をmappingから削除します。

| order | function | result |Execution Cost | Transaction Cost |
| ---- | ---- | ---- | ---- |---- |
| 1 | viewMap(1, 1) | 0 | - |- |
| 2 | add(1,1,1,1) | - | 45468 | 67092
| 3 | removeByDelete(1) | - | 5500 | 21904
| 4 | viewMap(1, 1) | 1 | - | -
| 5 | add(1,1,1,1) | - | 25568 | 47192


4番目に注目していただくと、removeしているのにも関わらずnumMapが初期化されていないのが分かると思います。

また、2,5番目に着目していただくと、addした時のガス代が変わっていますね。これはまた後で議論したいと思います。

---
## numMapに直接deleteを使う場合
| order | function | result |Execution Cost | Transaction Cost |
| ---- | ---- | ---- | ---- |---- |
| 1 | viewMap(1, 1) | 0 | - |- |
| 2 | add(1,1,1,1) | - | 45468 | 67092
| 3 | removeByInitDelete(1,1) | - | 10859 | 25763
| 4 | viewMap(1, 1) | 0 | - | -
| 5 | add(1,1,1,1) | - | 45468 | 67092

numMapをdeleteする方法だと、numMapはちゃんと初期化されていますね。
また、2,5番目に着目していただくと、addした時のガス代が同じです。

---
## numMapを初期値で初期化する場合
| order | function | result |Execution Cost | Transaction Cost |
| ---- | ---- | ---- | ---- |---- |
| 1 | viewMap(1, 1) | 0 | - |- |
| 2 | add(1,1,1,1) | - | 45468 | 67092
| 3 | removeByInit(1,1) | - | 10810 | 25724
| 4 | viewMap(1, 1) | 0 | - | -
| 5 | add(1,1,1,1) | - | 45468 | 67092



numMapに初期値を代入する方法だと、若干ですがガス代が安いのが分かります。
今回はmapping(uint256 => uint256)のケースしか試していませんが、もしかしたら直接初期値を入れた方が安いかもしれません。
あとは、同じでaddした時のガス代も同じですね。

---
# なぜdeleteしてもデータが残っているのか

deleteが何をしているかというと、結局のところデフォルト値を入れているだけです。
`uint`なら0を、`bool`なら`false`です。
しかし、mappingにはデフォルト値が存在しません。そのためmappingは無視してデフォルト値を入れています。

---
### opcode
実際にopcode上どうなっているのか確認してみましょう。

以下のようなシンプルなコントラクトを考えます。
```javascript
pragma solidity >=0.8.2 <0.9.0;

contract Storage {

    struct Person {
        uint256 age;
        mapping(uint256 => uint256) numMap;
    }

    mapping(uint256 => Person)  persons;

    function add() public {
        persons[1].age = 1;
        persons[1].numMap[1] = 1;
        delete persons[1];
    }  
}
```

---

これをコンパイルしたopcodeが以下です。

```
PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xE JUMPI PUSH0 DUP1 REVERT JUMPDEST POP PUSH1 0xC7 DUP1 PUSH1 0x1A PUSH0 CODECOPY PUSH0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xE JUMPI PUSH0 DUP1 REVERT JUMPDEST POP PUSH1 0x4 CALLDATASIZE LT PUSH1 0x26 JUMPI PUSH0 CALLDATALOAD PUSH1 0xE0 SHR DUP1 PUSH4 0x4F2BE91F EQ PUSH1 0x2A JUMPI JUMPDEST PUSH0 DUP1 REVERT JUMPDEST PUSH1 0x30 PUSH1 0x32 JUMP JUMPDEST STOP JUMPDEST PUSH1 0x1 PUSH0 DUP1 PUSH1 0x1 DUP2 MSTORE PUSH1 0x20 ADD SWAP1 DUP2 MSTORE PUSH1 0x20 ADD PUSH0 KECCAK256 PUSH0 ADD DUP2 SWAP1 SSTORE POP PUSH1 0x1 PUSH0 DUP1 PUSH1 0x1 DUP2 MSTORE PUSH1 0x20 ADD SWAP1 DUP2 MSTORE PUSH1 0x20 ADD PUSH0 KECCAK256 PUSH1 0x1 ADD PUSH0 PUSH1 0x1 DUP2 MSTORE PUSH1 0x20 ADD SWAP1 DUP2 MSTORE PUSH1 0x20 ADD PUSH0 KECCAK256 DUP2 SWAP1 SSTORE POP PUSH0 DUP1 PUSH1 0x1 DUP2 MSTORE PUSH1 0x20 ADD SWAP1 DUP2 MSTORE PUSH1 0x20 ADD PUSH0 KECCAK256 PUSH0 DUP1 DUP3 ADD PUSH0 SWAP1 SSTORE POP POP JUMP INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 ISZERO OR PUSH25 0x10037E66E7746ECAD1C5401907581BB61AEB76D020B241ECF6 0x22 0x26 0x2C 0xE2 PUSH5 0x736F6C6343 STOP ADDMOD BYTE STOP CALLER

```

---

正直これだけ見てもよく分からないと思いますが、ポイントはSSTOREというopcodeです。
ストレージに値を入れたり、変更したりするためにSSTOREというopcodeが使われるのですが、この中で3回しか使われていません。

つまり、
- `persons[1].age = 1;`
- `persons[1].numMap[1] = 1;`
- `delete persons[1];`

これで合計3回ストレージへのアクセスがあります。
さらにopcodeを調べると、`delete persons[1];`では`persons[1].age = 0;`に対応する処理をしており、`persons[1].numMap`には触れていません。

この部分の詳細については割愛しますが、EVMのStorageはKey-Valueの形で値が保存されています。`delete persons[1];`においては、`persons[1].age`に対応するKeyにValueとして0を保存しているため、`persons[1].age = 0;`に対応する処理をしていると分かります。

---
### なぜMappingはdeleteされないのか
何かのドキュメントがあるわけではないのでこの辺は推測ですが、そもそもEVM上ではMappingのKeyを走査する方法がありません。つまり、あるKeyを指定したときにそのValueは取得できますが、どのKeyに値が入っているかを知ることはできません。もしこれを実現したかったら自分でどのkeyに値が入っているかを残しておく必要があります。

そのため構造体の中のMappingは初期化されずノータッチでデータが残るような仕様になっていると考えられます。

---
### なぜaddした時のgas代が変わるのか
Storageに値を入れた時に、それが0なのか、もしくはすでに値が入っているかでガス代が変わるためです。
上述の通り、deleteした場合にはnumMapに値が残っているためSStoreに必要なガス代が変わったためと考えられます。

[Yello Paper](https://ethereum.github.io/yellowpaper/paper.pdf)より、ガス代は以下のようになります。
- 値が0から0でない数値を書き込む時: 20000
- non-zero値が入っている状態からnon-zero値が入っている状態、もしくは0を書き込む時: 2900

これをみると、初期化するときはあえてuint256のmax値を入れておくとかするとガス代安くなりますね。（バグの温床なので非推奨ですが）

---
# まとめ
これmappingのmappingでも同じかと思った人も多いかと思いますが、上記の通りそもそもmapping全体をdeleteする方法がないので、コンパイルエラーになります。

だったらこれもコンパイルエラーにした方が良い気がするのですが、構造体で色々なプロパティがある中でmappingがあるだけでdeleteできないとそれはそれで面倒なのかもしれません。