Ethereum Storage 进阶
0x01 例题1:复杂数据结构 storage 位置计算
pragma solidity >=0.4.0 <0.7.0;
contract C {
struct s { uint a; uint b; }
uint x;
mapping(uint => mapping(uint => s)) data;
}
计算 data[4][9].b 的 slot 位置。
- 要计算 data[4][9].b的位置,因为 data[4][9] 是一个 struct s 类型的数据结构,所以我们先要计算这个 struct 的位置。也就是计算 data[4][9] 的位置。
- 因为 data[4][9] 是一个映射,所以要计算值 data[4][9] 的位置,得先计算键 data[4] 的位置。
- 要计算 data[4] 的位置,根据 Ethereum Storage 一文中提到的数组的存储规律,此刻就可以套用公式了。
- contract C 中的布局如下图,要注意 uint 类型变量 x 会默认初始化为 0,占 1 个 slot。mapping 也会默认初始化为0,占 1 个 slot(后面的键值对会继续按照上文提到的规律实例化占 slot)。但是 struct 不赋值不会默认初始化,也就是说结构体不赋值则不会占 slot,所以内存中没有结构体 s 的位置。这段红字说的规律我在下一个小节会证明。
-----------------------------------------------------
| x(32) | <- slot 0
-----------------------------------------------------
| occupied by data but unused (32) | <- slot 1
-----------------------------------------------------
5. 所以 mapping data 位于slot 1(该变量本来的位置),求 data[4] 所在的slot位置,则公式为:keccak256(bytes32(4),bytes32(1))
6. 找到了 key data[4] 的位置,要找值 data[4][9] 的位置,因为这个变量本来位于 slot keccak256(bytes32(4),bytes32(1)),所以求 data[4][9] 的位置,则公式为 keccak256(bytes32(9),keccak256(bytes32(4),bytes32(1)))
注: keccak 函数的返回值为 bytes32
7. 最后求 data[4][9].b 的位置。因为结构体其实就是变量的组合,struct s 结构体的布局应该是如下这样的:
-----------------------------------------------------
| a(32) | <- slot 0
-----------------------------------------------------
| b(32) | <- slot 1
-----------------------------------------------------
8. data[4][9] 这个结构体的位置在 keccak256(bytes32(9), keccak256(bytes32(4), bytes32(1))) 。所以 data[4][9].b 的位置实际上应该是这个结构体的位置往下移动1slot也就是 keccak256(bytes32(9), keccak256(bytes32(4), bytes32(1)))+1。注:在 solidity 中直接把前一部分转 uint 然后加1最后输出的值是十进制的,再转为 bytes32 则输出16进制的。
验证一下计算结果,如下图所示是正确的:


0x02 一些规律证明
- uint 类型变量 x 在不赋值的情况下会默认初始化为 0,占一个 slot(uint = uint256)
不给 uint 变量赋值,在 remix 上面查看变量看上去没有占用 slot:

看上去是如果赋值之后就会占用:

但其实如下图,uint 变量 a 虽然没被赋值,但其实占了 slot 0,缺省值为0;uint 变量 b 顺次排到 slot 1,值为 10。


对于 mapping 类型也是同理:


但对于 struct 对象则不是这样,没有实例化的话不占 slot:

实例化之后,就算不赋值,也占拆分出的变量大小的slot:




对比一下赋值之后:

再对比一下 mapping 赋值之后:可以看到总会预留一个 slot 给 mapping,但是 mapping 键值对的存储却是根据公式另外算的。

再来看一种情况:Uninitialized Storage Pointer 会覆盖变量 b 的值。

注:贴出来上面用汇编语句取 storage slot 的一段代码:
pragma solidity ^0.4.24;
contract test {
struct s{
uint x;
uint y;
}
uint b = 10;
constructor() public{
s s1;
s1.x = 0xb;
s1.y = 0xc;
}
function get_value_from_slot(uint s) public view returns(bytes32 _value){
assembly{
let _v := sload(s)
_value := _v
}
}
}
0x03 例题2
pragma solidity ^0.4.24;
contract Bank {
event SendEther(address addr);
event SendFlag(address addr);
address public owner;
uint randomNumber = 0;
constructor() public {
owner = msg.sender;
}
struct SafeBox {
bool done;
function(uint, bytes12) internal callback;
bytes12 hash;
uint value;
}
SafeBox[] safeboxes;
struct FailedAttempt {
uint idx;
uint time;
bytes12 triedPass;
address origin;
}
mapping(address => FailedAttempt[]) failedLogs;
modifier onlyPass(uint idx, bytes12 pass) {
if (bytes12(sha3(pass)) != safeboxes[idx].hash) {
FailedAttempt info;
info.idx = idx;
info.time = now;
info.triedPass = pass;
info.origin = tx.origin;
failedLogs[msg.sender].push(info);
}
else {
_;
}
}
function deposit(bytes12 hash) payable public returns(uint) {
SafeBox box;
box.done = false;
box.hash = hash;
box.value = msg.value;
if (msg.sender == owner) {
box.callback = sendFlag;
}
else {
require(msg.value >= 1 ether);
box.value -= 0.01 ether;
box.callback = sendEther;
}
safeboxes.push(box);
return safeboxes.length-1;
}
function withdraw(uint idx, bytes12 pass) public payable {
SafeBox box = safeboxes[idx];
require(!box.done);
box.callback(idx, pass);
box.done = true;
}
function sendEther(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
msg.sender.transfer(safeboxes[idx].value);
emit SendEther(msg.sender);
}
function sendFlag(uint idx, bytes12 pass) internal onlyPass(idx, pass) {
require(msg.value >= 100000000 ether);
emit SendFlag(msg.sender);
selfdestruct(owner);
}
}
- 求 failedLogs[msg.sender][0] 中的 tx.origin | triedPass 的 slot 位置。
- 求 safeboxes 数组中第一个元素所在的 slot 的位置。
failedLogs 是 mapping 加上数组的形式:mapping(address => FailedAttempt[]) failedLogs
onlyPass 中的 FailedAttempt 的布局如下:
-----------------------------------------------------
| idx (32) |
-----------------------------------------------------
| time (32) |
-----------------------------------------------------
| tx.origin (20) | triedPass (12) |
-----------------------------------------------------
- 首先求 failedLogs[msg.sender] 的位置,这是一个 mapping。因为合约刚创建的时候的 slot 的布局如下:
-----------------------------------------------------
| unused (12) | owner (20) | <- slot 0
-----------------------------------------------------
| randomNumber (32) | <- slot 1
-----------------------------------------------------
| safeboxes.length (32) | <- slot 2
-----------------------------------------------------
| occupied by failedLogs but unused (32) | <- slot 3
-----------------------------------------------------
- 所以 failedLogs 应该位于 slot 3,然后 key = msg.sender,所以根据公式这个mapping 位于keccak256(bytes32(msg.sender),bytes32(3));
- 计算完了 mapping,然后再计算数组的第一个元素也是位于 keccak256(bytes32(msg.sender),bytes32(3));
- 然后再计算 tx.origin | triedPass 的位置,每一个数组元素是一个 FailedAttempt 类型的结构体,所以位置为 keccak256(bytes32(msg.sender),bytes32(3)) +2。
计算 safeboxes 数组中第一个元素所在的 slot 的位置,本来 deposit 中的 SafeBox 的布局如下:
-----------------------------------------------------
| unused (11) | hash (12) | callback (8) | done (1) |
-----------------------------------------------------
| value (32) |
-----------------------------------------------------
但只是让我们计算第一个元素所在的 slot 位置,而不是元素中的具体结构体变量。所以根据数组的 slot 位置公式计算即可:
base = keccak256(bytes32(2))
这个2是 safeboxes 数组长度位于的 slot 位置,如下图为 slot2:
-----------------------------------------------------
| unused (12) | owner (20) | <- slot 0
-----------------------------------------------------
| randomNumber (32) | <- slot 1
-----------------------------------------------------
| safeboxes.length (32) | <- slot 2
-----------------------------------------------------
| occupied by failedLogs but unused (32) | <- slot 3
-----------------------------------------------------
0x04 总结
复杂数据结构对象的 slot 位置是区块链 ctf 里面的常考题,还需要多加练习。
感谢 @r4v3n @Rivaill 一直以来的不吝赐教。
您好,我有个关于CS的VNC无法连接的问题想请教您。但是没有看到您有联系方式可以交流的