Build EVM State Variable Reader

Posted on Tue, Oct 10, 2023 dev blockchain

Cause

在进行合约检测时,有时候需要获取合约的状态变量的值,去判断是不是恶意代码,举例:比如在某些外部调用合约的 interface 或着 Library 中,项目方会传入动态变化的 address 去获取外部合约的能力,有的时候这个外部地址是没有恶意的,属于项目方白名单中,但是有些是具有恶意的,需要识别出这类代码。

Fundamentals

状态变量因为需要维护在整个区块链网络中的唯一性,所以必须在设计的时候考虑不能重复的特征,所以 EVM 选择了使用 slot 的方式来存储状态变量,并且使用映射的方式,将一个 32 字节的 key 映射到一个 32 字节的 value 上,所以会存在(2^256)-1 个 key,这个数字不会发生重复。并且在初始化的时候,全部为 0。

Rule

EVM 使用了 Slot Packing 的方案来规定数据的排序顺序,比如有三个变量uint128uint128uint256,则最好使用当前的顺序来声明变量,而不是uint128uint256uint128,因为前者只占两个 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 个字节,可以确定的他们插槽位置:

Fixed-size Array

固定大小的数据也是比较好处理的,只需要按照基础规则进行排序即可,比如:

contract StorageTest {
	uint256 a;
	uint256[2] b;
	address c;
}

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

在调研时,分别研究了 slither 以及 hardhat,最终选择 slither 解决变量插槽排序的问题,对于复杂结构(如:struct)则进行手动编码的方式进行识别判断。

**`// 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 提供并发能力,然而生产速度远远大于消费速度,即使通过队列异步处理,高峰期仍然存在队列堆积的问题。

Reference