区块链安全

Ethereum Storage

C 语言编程中有全局变量的概念,也就是区别于函数中定义的局部变量的概念,在 Solidity 中这种变量被称为「状态变量」。状态变量被存储于 Storage 中,这种变量数据会上链,而函数中的局部变量不会上链。Storage 存储结构是在合约创建的时候就确定好的,它取决于合约所声明状态变量,但是内容可以通过 Transaction 改变。

对于状态变量,根据类型分为两大类:

  • 定长类型(除 mapping 和动态数组之外的所有类型)
  • 变长类型(mapping 和动态数组)

定长类型的存储

  • 逐次存入

logs:

[{"address":"0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B","data":"0x0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4","topics":["0x61c7110fd97ef0e1e5305db590ba4d321e46968ec0809ac0edc9eb90b7bf0199"],"rawVMRespon[
	{
		"from": "0xD7ACd2a9FD159E69Bb102A1ca21C9a3e3A5F771B",
		"topic": "0x61c7110fd97ef0e1e5305db590ba4d321e46968ec0809ac0edc9eb90b7bf0199",
		"event": "_event1",
		"args": {
			"0": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
			"_addr": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
		}
	}
]

可以很明显的看到:

  • slot 0 -> 6(a)
  • slot 3 -> asdwq(c)
  • slot 4 -> msg.sender(q)
  • slot 1 -> 0x58(b[0])
  • slot 2 -> 0x63(b[1])

所以对于这种定长类型的值,就是按照顺序逐次排入 slot。

  • 拼接

可以看出因为 uint 8占1字节,uint16占2字节,uint32占4字节,加起来一共7字节,小于1slot==32字节,所以被存入了同一个slot0里面。

而且可以看到,从左到右顺次存入,06(一字节两个hex),000f,00000010,右侧的空位补0。

变长类型的存储

  • 映射

可以看到:

  • 非状态变量 a 没有存到 storage 里面。

可以看到 key 为一个 hash,值为 mapping 的 value。其实 key 这里的 hash 是针对 mapping 的 key 的一个计算结果。如果mapping _map位于slot x(该变量本来的位置),求_map[y]所在的slot位置,则公式为:

keccak256(bytes32(y),bytes32(x))

注意:当传入多个参数到keccak256方法时,首先会将这些参数进行连接,然后在进行hash运算。因为是将mapping的slot值与key值同时进行的hash运算,所以不同mapping之间是不会存在冲突的。
所以,把参数按序传入keccak256方法就行了。

参考:

写了一个小合约来计算试试。对于 z[234],本来应该位于 slot 0,所以 y = 234,x = 0;

计算结果是一致的。同理,对于 x[237],y=237,x = 1。计算结果也完全正确。

所以上面就是 mapping 映射在 storage 里面的存储规律。

  • 动态数组

可以看到,slot 0 处存的是长度(元素个数)。
slot[hash] 处存的是从左到右依次写入了三个值。

这个 hash 的计算公式为:

keccak256(bytes32(position))

其中的 position 就是存放数组长度的位置,此处即0,验证如下:

如果元素超过了一个 slot,可以看到后面的 key 是递增的。

  • 结构体

可以很好的反推:

  • slot key0 = keccak256(bytes32(233),bytes32(0)),value0 = 123
  • slot key1 = keccak256(bytes32(233),bytes32(0))+1,value1 = 456
  • slot key2 = keccak256(bytes32(233),bytes32(0))+2,value2 = 789
  • 字节数组和字符串

如果 bytes 和 string 的数据很短,那么它们的长度也会和数据一起存储到同一个插槽。具体地说:如果数据长度小于等于 31 字节, 则它存储在高位字节(左对齐),最低位字节存储 length * 2。如果数据长度超出 31 字节,则在主插槽存储 length * 2 + 1, 数据照常存储在 keccak256(slot) 中。

可以看到,对于 string a,因为长度等于6<31字节(在 storage 中会转为 ascii 码,刚好两个 hex 为1字节)。所以 slot 0 的值为 左边是字符串的值,右边是长度*2=c。

对于 bytes s,因为长度等于 91>31 字节,所以在本来应该存储的 slot 1中存放了 length*2+1=0xb7。然后才开始存放数据,因为 91 – 32*2 < 32,所以需要三个slot。第一个 slot 的 key = keccak256(bytes32(1)),如下图计算的,因为 1 是存放 bytes 变量长度的位置。然后往后逐次递增 slot 的 key 值存入 bytes 的数据。

在这篇文章:以太坊智能合约 OPCODE 逆向之理论基础篇 – 全局变量的储存模型 中提到了 flag 的问题:

他这个 +”00″ 或者 |1 的实际效果和  length*2 或者  length*2+1 是一致的。既然 flag 是为了区分长度是否 >31,那么直接判断是奇数或是偶数就可以,偶数表示 0-31,奇数表示长度 >31。

参考文档: