Cause
在进行合约检测时,有时候需要获取合约的状态变量的值,去判断是不是恶意代码,举例:比如在某些外部调用合约的 interface 或着 Library 中,项目方会传入动态变化的 address 去获取外部合约的能力,有的时候这个外部地址是没有恶意的,属于项目方白名单中,但是有些是具有恶意的,需要识别出这类代码。
Fundamentals
状态变量因为需要维护在整个区块链网络中的唯一性,所以必须在设计的时候考虑不能重复的特征,所以 EVM 选择了使用 slot 的方式来存储状态变量,并且使用映射的方式,将一个 32 字节的 key 映射到一个 32 字节的 value 上,所以会存在(2^256)-1 个 key,这个数字不会发生重复。并且在初始化的时候,全部为 0。
Rule
- 每一个 slot 的大小为 32 字节
- 如果一个 slot 的剩余空间不足以存储一个新的值类型,那么他会被存储到下一个 slot
- struct 和 array 总是会开启一个新的 slot
EVM 使用了 Slot Packing 的方案来规定数据的排序顺序,比如有三个变量uint128,uint128,uint256,则最好使用当前的顺序来声明变量,而不是uint128,uint256,uint128,因为前者只占两个 slot,而后者将占用三个。这块也是优化 gas 的一个小技巧。
Kind
Simple Variable
我们把可以直接根据 slot 获取到对应 value 的统称为 Simple Variable,比如下面的示例:
contract StorageTest {
bool a;
uint8 b;
address c;
address d;
}这类的数据类型是比较好处理的,因为只需要关注数据类型本身的大小然后利用 32 字节的大小结合 Slot Packing 方案进行顺序排序即可。 比如上面的例子中,bool 类型占 1 个字节,uint8 占 1 个字节,address 占 20 个字节,可以确定的他们插槽位置:
- a 被存储到 slot 0 中,占用 1 个字节
- b 被存储到 slot 0 中,占用 1 个字节
- c 被存储到 slot 0 中,因为 slot 目前只剩 30 个字节,而 c 占用 20 个字节,所以仍然可以被存储到 slot 0 中,此时 slot 0 还剩余 32-(1+1+20)=10 个字节
- d 被存储到 slot 1,因为此时 slot0 中只剩余 10 个字节,而 d 占用 20 个字节
Fixed-size Array
固定大小的数据也是比较好处理的,只需要按照基础规则进行排序即可,比如:
contract StorageTest {
uint256 a;
uint256[2] b;
address c;
}- a 被存储到 slot 0 中
- b 被存储到 slot 1 和 2 中
- c 被存储到 slot 3 中
Dynamic Array
动态数组是不好处理的,因为长度的不确定,EVM 并不知道在编译期间分配多大的 slot 去存储,所以 EVM 选择一个特定的 slot(根据某种规则计算得出)去专门给他们存储他们的值,其规则如下:
function getArrayLocation(uint slot, uint index,uint elementSize) public returns (uint256) {
return uint256(keccak256(abi.encodePacked(slot))) + (index * elementSize);
}以下是使用 java 语言编写的获取动态数组的值部分代码:
public BigInteger getDynamicArrayLocation(BigInteger slot, BigInteger elementSize) {
//uint256 location = uint(keccak256(abi.encodePacked(slot))) + (index * elementSize);
byte[] bytesPadded = Numeric.toBytesPadded(slot, 32);
BigInteger bigIntSlot = Numeric.toBigInt(bytesPadded);
String s = DefaultFunctionEncoder.encodeConstructor(Collections.singletonList(new Uint256(bigIntSlot)));
String sha = Hash.sha3(s);
BigInteger keccakHashBigInt = Numeric.toBigInt(sha);
return keccakHashBigInt.add(elementSize);
}
int typeSize = StateVariableTypeUtil.getTypeSize(variableOrder.getType());
int i = 64 / (typeSize * 2);
int slotIndex = index / i;
int valueIndex = index % i;
BigInteger dynamicArrayLocation = storageVariableTypeHandle.getDynamicArrayLocation(
variableOrder.getSlot(),
new BigInteger(String.valueOf(slotIndex))
);Mapping
Mapping 是同样的,因为数据结构的特殊性,其长度也是不可知的,EVM 采用哈希函数确定某一个 key 对应的 value 值。计算方法如下:
function mapLocation(uint256 slot, uint256 key) public pure returns (uint256) {
return uint256(keccak256(key, slot));
}Others
在 evm 中,bytes 和 string 也被当作 Arrays 来处理,其排序规则仍然遵守上述规则
Solutions
- 以下是一个简单的示例:ReadStateVariable.sol
**`// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;
contract StorageTest {
bool a;
uint8 b;
address c;
struct Entry {
uint256 id;
uint256 value;
}
Entry d;
}`**
**`./slither ReadStateVariable.sol --print variable-order`**
**`+---------------+-------------------+------+--------+
| Name | Type | Slot | Offset |
+---------------+-------------------+------+--------+
| StorageTest.a | bool | 0 | 0 |
| StorageTest.b | uint8 | 0 | 1 |
| StorageTest.c | address | 0 | 2 |
| StorageTest.d | StorageTest.Entry | 1 | 0 |
+---------------+-------------------+------+--------+`**以下是获取一个 struct 里面的数据值的部分代码:
public List<StructEntry> extractStructEntry(String source, String structName) {
List<StructEntry> structEntries = new ArrayList<>();
String regex = "struct\\s+" + structName + "\\s*\\{([^}]*)\\}";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(source);
if (matcher.find()) {
String structDefinition = matcher.group(1).trim();
String[] fields = structDefinition.split(";");
for (String field : fields) {
String[] parts = field.trim().split("\\s+");
if (parts.length >= 2) {
String fieldName = parts[1];
String fieldType = parts[0];
StructEntry build = StructEntry.builder().entryName(fieldName).entryType(fieldType).build();
structEntries.add(build);
}
}
}
return structEntries;
}`**
**`List<StructValueObj> objects = storageVariableTypeHandle.getStructValueObj(chainId,
contractAddress,
variableOrder.getSlot(),
blockNumber,
structEntries
);`**
**`public List<StructValueObj> getStructValueObj(String chainId,
String contractAddress,
BigInteger slot,
BigInteger blockNumber,
List<StructEntry> structEntries) {
BigInteger preByteSize = BigInteger.ZERO;
List<StructValueObj> objects = new ArrayList<>();
// offset 累计值,只有在同一个Slot时才需要累计offset,如果是新的slot,则累计offset=0
int cumulativeOffset = 0;
// 判断是否是新的(下一个)slot
//Boolean isNewSlot = false;
// 累计的字节大小,目的是为了解决同一个slot存储多个entry
BigInteger cumulativeByteSize = BigInteger.ZERO;
for (int i = 0; i < structEntries.size(); i++) {
StructEntry structEntry = structEntries.get(i);
int nextTypeSize = 0;
if (i + 1 < structEntries.size()) {
StructEntry NextStructEntry = structEntries.get(i + 1);
String nextEntryType = NextStructEntry.getEntryType();
nextTypeSize = getTypeSize(nextEntryType);
}
structEntry.setSlot(slot);
if (structEntry.getEntryType().contains("[") || structEntry.getEntryType().contains("mapping")) {
StructValueObj structValueObj = StructValueObj.builder()
.name(structEntry.getEntryName())
.type(structEntry.getEntryType())
.value(null)
.build();
//objects.add(structValueObj);
log.warn("unSupport type:{}", structValueObj);
cumulativeByteSize = BigInteger.ZERO;
cumulativeOffset = 0;
slot = structEntry.getSlot().add(BigInteger.ONE);
continue;
}
String storageAt = contractStorageVarManager.getStorageAt(chainId, contractAddress, structEntry.getSlot(), blockNumber);
VariableOrder build = VariableOrder.builder().chainId(chainId).contractAddress(contractAddress)
.type(structEntry.getEntryType())
.name(structEntry.getEntryName())
.slot(structEntry.getSlot())
.offset(cumulativeOffset).build();
VariableOrder variableOrder = parseHexResult(storageAt, build);
StructValueObj structValueObj = StructValueObj.builder()
.name(structEntry.getEntryName())
.type(structEntry.getEntryType())
.value(variableOrder.getValueExtend()).build();
objects.add(structValueObj);
int typeSize = getTypeSize(structEntry.getEntryType());
cumulativeByteSize = cumulativeByteSize.add(BigInteger.valueOf(typeSize));
BigInteger divide = cumulativeByteSize.divide(BigInteger.valueOf(32));
BigInteger mod = cumulativeByteSize.mod(BigInteger.valueOf(32));
if (divide.compareTo(BigInteger.ONE) > 0) {
slot = structEntry.getSlot().add(BigInteger.ONE);
cumulativeByteSize = BigInteger.ZERO;
} else {
if (preByteSize.add(cumulativeByteSize).compareTo(BigInteger.valueOf(32)) >= 0) {
slot = structEntry.getSlot().add(BigInteger.ONE);
cumulativeByteSize = BigInteger.ZERO;
} else {
if (cumulativeByteSize.add(BigInteger.valueOf(nextTypeSize)).compareTo(BigInteger.valueOf(32)) >= 0) {
slot = structEntry.getSlot().add(BigInteger.ONE);
cumulativeByteSize = BigInteger.ZERO;
} else {
slot = structEntry.getSlot();
}
}
}
preByteSize = cumulativeByteSize;
if (!slot.equals(structEntry.getSlot())) {
cumulativeOffset = 0;
} else {
cumulativeOffset += mod.intValue();
}
}
return objects;
}Legacy issues
slither(solc) 需要全局管理,目前通过多 Pod 提供并发能力,然而生产速度远远大于消费速度,即使通过队列异步处理,高峰期仍然存在队列堆积的问题。