智能合约变量储存机制详解
字数 1864 2025-08-22 12:22:54

智能合约变量存储机制详解

0x01 前言

以太坊智能合约的状态数据在链上永久存储,了解其存储机制对于智能合约开发和安全性分析至关重要。本文将详细解析以太坊虚拟机(EVM)中智能合约变量的存储方式。

0x02 存储基础

  • 以太坊使用一个巨大的数组作为存储空间,数组长度为2^256
  • 每个数组元素(称为"插槽"或"slot")可存储32字节(256位)数据
  • 存储是稀疏的,未使用的插槽不会占用实际空间
  • 存储是持久的,与内存(memory)和调用栈(stack)不同

0x03 变量类型分类

Solidity变量分为两大类:

值类型(Value Type)

  1. 布尔型(bool): 2bit(0/1)
  2. 整型(int/uint):
    • int8/uint8表示8位有符号/无符号整数
    • int/uint默认是int256/uint256
  3. 定长浮点型(fixed/ufixed): Solidity尚未完全支持
  4. 地址类型(address): 160bits
    • 成员变量:
      • .balance (uint256)
      • .transfer() (uint256)
  5. 定长字节数组(byte[1]/bytes[1]): 定义时指定长度

引用类型(Reference Type)

  1. 不定长字节数组(bytes[], string, uint[]等)
  2. 结构体(struct)
  3. 映射(mapping)

0x04 简单变量存储分析

存储优化原则

如果连续声明的变量总长度不超过256bits,它们会共享同一个存储插槽

示例合约:

pragma solidity ^0.4.25;
contract TEST {
    bool a = false; 
    bool b = true;
    int16 c = 32767;
    uint16 d = 0x32;
    byte e = 10;
    bytes1 f = 11;
    bytes2 g = 22;
    uint h = 0x1; // uint是uint256的简称
    address i = 0xbc6581e11c216B17aDf5192E209a7F95a49e6837;
}

存储布局:

slot0: 0x0000000000000000000000000000000000000000000000160b0a00327fff0100
  - a (false): 0x00
  - b (true): 0x01
  - c (32767): 0x7fff
  - d (0x32): 0x0032
  - e (10): 0x0a
  - f (11): 0x0b
  - g (22): 0x0016

slot1: 0x0000000000000000000000000000000000000000000000000000000000000001
  - h (0x1)

slot2: 0x000000000000000000000000bc6581e11c216b17adf5192e209a7f95a49e6837
  - i (address)

关键点:

  • 存储顺序从右向左(小端序)
  • byte和bytes1长度相同(8bits)
  • 变量尽可能打包到同一个插槽中

0x05 数组类型存储

定长数组

pragma solidity ^0.4.25;
contract TEST {
    bytes8[5] a = [byte(0x6a), 0x68, 0x79, 0x75];
    bool b = true;
}

存储特点:

  • 实际使用的元素数量决定存储空间
  • 未初始化的元素不占用存储

变长数组

pragma solidity ^0.4.25;
contract TEST {
    uint[] a = [0x77, 0x88, 0x99];
    function add() {
        a.push(0x66);
    }
}

存储布局:

slot0: 0x0000000000000000000000000000000000000000000000000000000000000003
  - 数组长度(3)

slotx: 0x0000000000000000000000000000000000000000000000000000000000000077
  - a[0]

slot(x+1): 0x0000000000000000000000000000000000000000000000000000000000000088
  - a[1]

slot(x+2): 0x0000000000000000000000000000000000000000000000000000000000000099
  - a[2]

存储地址计算

x = keccak256(slot)
其中slot是数组长度存储的位置(本例中为0)

Python计算示例:

import sha3
import binascii

def byte32(i):
    return binascii.unhexlify('%064x' % i)

a = sha3.keccak_256(byte32(0)).hexdigest()
print(a) # 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563

调用add()函数后:

  1. 数组长度变为4
  2. 在a[2]后面的插槽(x+3)写入0x66

0x06 字符串类型

短字符串(<=31字节)

pragma solidity ^0.4.25;
contract TEST {
    string a = 'whoami';
}

存储:

  • 字符串内容直接存储在插槽中
  • 长度编码在最高位字节(0xc表示长度12,每个字母2个十六进制位)

中文字符串

pragma solidity ^0.4.25;
contract TEST {
    string a = '先知社区';
}

存储:

  • 每个汉字占6个十六进制位
  • 长度0x18(24)

长字符串(>32字节)

pragma solidity ^0.4.25;
contract TEST {
    string a = "Genius only means hard-working all one's life.";
}

存储方式与变长数组类似:

  1. 插槽存储字符串长度
  2. 内容存储在keccak256(slot)计算得到的地址开始的位置

0x07 结构体类型

pragma solidity ^0.4.25;
contract TEST {
    struct test {
        bool a;
        uint8 b;
        uint c;
        string d;
    }
    test student = test(true, 0x01, 0xff, 'abcd');
}

存储布局:

  • a, b: slot0 (打包存储)
  • c: slot1
  • d: slot2 (如果超过32字节,则使用keccak256(2)计算存储位置)

结构体数组

pragma solidity ^0.4.25;
contract TEST {
    struct test {
        bool a;
        uint8 b;
        uint c;
        string d;
    }
    test[] student;
    function add() {
        student.push(test(true, 0x01, 0xff, 'abcd'));
    }
}

存储方式与变长数组类似,但以结构体长度为一个存储周期。

0x08 映射类型

简单映射

pragma solidity ^0.4.25;
contract TEST {
    mapping(address => uint) balance;
    function add() {
        balance[0xbc6581e11c216B17aDf5192E209a7F95a49e6837] = 0x01;
    }
}

存储位置计算:

x = keccak256(key + slot)
其中:
- key是映射的键(本例为地址)
- slot是映射变量定义的插槽位置(本例为0)

Python计算示例:

import sha3
import binascii

def byte32(i):
    return binascii.unhexlify('%064x' % i)

key = 0xbc6581e11c216B17aDf5192E209a7F95a49e6837
b = byte32(key) + byte32(0)
a = sha3.keccak_256(b).hexdigest()
print(a) # 21d25f73dd60df1532a052f5f1044cb0f7986a3f609d8674628447c29af248fb

复杂映射

pragma solidity ^0.4.25;
contract TEST {
    mapping(uint8 => string) balance;
    function add() {
        balance[0xb] = "Genius only means hard-working all one's life.";
    }
}

存储位置计算:

  1. 键值对位置:keccak256(key + slot)
  2. 长字符串数据位置:keccak256(keccak256(key + slot))

0x09 综合练习

pragma solidity >0.5.0;
contract StorageExample6 {
    uint256 a = 11;
    uint8 b = 12;
    uint128 c = 13;
    bool d = true;
    uint128 e = 14;
    uint256[] public array = [401,402,403,405,406];
    address owner;
    mapping(address => UserInfo) public users;
    string str = "name value";
    
    struct UserInfo {
        string name;
        uint8 age;
        uint8 weight;
        uint256[] orders;
        uint64[3] lastLogins;
    }
    
    constructor() public {
        owner = msg.sender;
        addUser(owner, "admin", 17, 120);
    }
    
    function addUser(address user, string memory name, uint8 age, uint8 weight) public {
        require(age > 0 && age < 100, "bad age");
        uint256[] memory orders;
        uint64[3] memory logins;
        users[user] = UserInfo({
            name: name,
            age: age,
            weight: weight,
            orders: orders,
            lastLogins: logins
        });
    }
    
    function addLog(address user, uint64 id1, uint64 id2, uint64 id3) public {
        UserInfo storage u = users[user];
        assert(u.age > 0);
        u.lastLogins[0] = id1;
        u.lastLogins[1] = id2;
        u.lastLogins[2] = id3;
    }
    
    function addOrder(address user, uint256 orderID) public {
        UserInfo storage u = users[user];
        assert(u.age > 0);
        u.orders.push(orderID);
    }
    
    function getLogins(address user) public view returns(uint64, uint64, uint64) {
        UserInfo storage u = users[user];
        return (u.lastLogins[0], u.lastLogins[1], u.lastLogins[2]);
    }
    
    function getOrders(address user) public view returns(uint256[] memory) {
        UserInfo storage u = users[user];
        return u.orders;
    }
}

存储布局分析:

  1. 简单变量:
    • slot0: a, b, c, d, e (打包存储)
  2. 数组:
    • array长度存储在固定位置
    • 数组元素存储在计算得到的位置
  3. 映射:
    • 用户信息存储在keccak256(key + slot)位置
    • 用户订单数组有独立的存储计算方式

0x10 实际应用与安全

读取私有变量

虽然Solidity中标记为private的变量不能被其他合约访问,但可以通过直接读取存储插槽来获取:

web3.eth.getStorageAt(address, position [, defaultBlock] [, callback])

参数说明:

  • address: 要读取的合约地址
  • position: 存储插槽编号
  • defaultBlock: 可选,指定区块
  • callback: 可选回调函数

示例1:读取私有变量

pragma solidity ^0.6.0;
contract Vault {
    bool public locked;
    bytes32 private password;
    
    constructor(bytes32 _password) public {
        locked = true;
        password = _password;
    }
    
    function unlock(bytes32 _password) public {
        if (password == _password) {
            locked = false;
        }
    }
}

攻击方法:

  1. 读取slot1获取password:
    web3.eth.getStorageAt(contract.address, 1)
    
  2. 使用获取的password调用unlock函数

示例2:时间相关PIN

pragma solidity 0.4.24;
import "../CtfFramework.sol";

contract Lockbox1 is CtfFramework {
    uint256 private pin;
    
    constructor(address _ctfLauncher, address _player) public payable
        CtfFramework(_ctfLauncher, _player) 
    {
        pin = now % 10000;
    }
    
    function unlock(uint256 _pin) external ctf {
        require(pin == _pin, "Incorrect PIN");
        msg.sender.transfer(address(this).balance);
    }
}

攻击方法:

  1. 读取slot0获取pin值
  2. 使用获取的pin调用unlock函数

总结

  1. 以太坊存储是键值对数据库,每个合约有2^256个32字节的插槽
  2. 存储优化原则:连续变量尽可能打包到同一插槽
  3. 不同类型变量有特定的存储布局规则:
    • 简单变量:直接存储或打包存储
    • 数组:长度存储在固定位置,元素存储在计算位置
    • 映射:使用keccak256(key + slot)计算存储位置
    • 结构体:成员变量依次存储,可能打包
  4. 私有变量仅限制合约访问,存储数据仍可公开读取
  5. 理解存储机制有助于合约优化和安全审计
智能合约变量存储机制详解 0x01 前言 以太坊智能合约的状态数据在链上永久存储,了解其存储机制对于智能合约开发和安全性分析至关重要。本文将详细解析以太坊虚拟机(EVM)中智能合约变量的存储方式。 0x02 存储基础 以太坊使用一个巨大的数组作为存储空间,数组长度为2^256 每个数组元素(称为"插槽"或"slot")可存储32字节(256位)数据 存储是稀疏的,未使用的插槽不会占用实际空间 存储是持久的,与内存(memory)和调用栈(stack)不同 0x03 变量类型分类 Solidity变量分为两大类: 值类型(Value Type) 布尔型(bool): 2bit(0/1) 整型(int/uint): int8/uint8表示8位有符号/无符号整数 int/uint默认是int256/uint256 定长浮点型(fixed/ufixed): Solidity尚未完全支持 地址类型(address): 160bits 成员变量: .balance (uint256) .transfer() (uint256) 定长字节数组(byte[ 1]/bytes[ 1 ]): 定义时指定长度 引用类型(Reference Type) 不定长字节数组(bytes[], string, uint[ ]等) 结构体(struct) 映射(mapping) 0x04 简单变量存储分析 存储优化原则 如果连续声明的变量总长度不超过256bits,它们会共享同一个存储插槽 示例合约: 存储布局: 关键点: 存储顺序从右向左(小端序) byte和bytes1长度相同(8bits) 变量尽可能打包到同一个插槽中 0x05 数组类型存储 定长数组 存储特点: 实际使用的元素数量决定存储空间 未初始化的元素不占用存储 变长数组 存储布局: 存储地址计算 : Python计算示例: 调用add()函数后: 数组长度变为4 在a[ 2 ]后面的插槽(x+3)写入0x66 0x06 字符串类型 短字符串( <=31字节) 存储: 字符串内容直接存储在插槽中 长度编码在最高位字节(0xc表示长度12,每个字母2个十六进制位) 中文字符串 存储: 每个汉字占6个十六进制位 长度0x18(24) 长字符串(>32字节) 存储方式与变长数组类似: 插槽存储字符串长度 内容存储在keccak256(slot)计算得到的地址开始的位置 0x07 结构体类型 存储布局: a, b: slot0 (打包存储) c: slot1 d: slot2 (如果超过32字节,则使用keccak256(2)计算存储位置) 结构体数组 存储方式与变长数组类似,但以结构体长度为一个存储周期。 0x08 映射类型 简单映射 存储位置计算: Python计算示例: 复杂映射 存储位置计算: 键值对位置: keccak256(key + slot) 长字符串数据位置: keccak256(keccak256(key + slot)) 0x09 综合练习 存储布局分析: 简单变量: slot0: a, b, c, d, e (打包存储) 数组: array长度存储在固定位置 数组元素存储在计算得到的位置 映射: 用户信息存储在keccak256(key + slot)位置 用户订单数组有独立的存储计算方式 0x10 实际应用与安全 读取私有变量 虽然Solidity中标记为private的变量不能被其他合约访问,但可以通过直接读取存储插槽来获取: 参数说明: address: 要读取的合约地址 position: 存储插槽编号 defaultBlock: 可选,指定区块 callback: 可选回调函数 示例1:读取私有变量 攻击方法: 读取slot1获取password: 使用获取的password调用unlock函数 示例2:时间相关PIN 攻击方法: 读取slot0获取pin值 使用获取的pin调用unlock函数 总结 以太坊存储是键值对数据库,每个合约有2^256个32字节的插槽 存储优化原则:连续变量尽可能打包到同一插槽 不同类型变量有特定的存储布局规则: 简单变量:直接存储或打包存储 数组:长度存储在固定位置,元素存储在计算位置 映射:使用keccak256(key + slot)计算存储位置 结构体:成员变量依次存储,可能打包 私有变量仅限制合约访问,存储数据仍可公开读取 理解存储机制有助于合约优化和安全审计