智能合约变量储存机制详解
字数 1864 2025-08-22 12:22:54
智能合约变量存储机制详解
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,它们会共享同一个存储插槽
示例合约:
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()函数后:
- 数组长度变为4
- 在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.";
}
存储方式与变长数组类似:
- 插槽存储字符串长度
- 内容存储在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.";
}
}
存储位置计算:
- 键值对位置:
keccak256(key + slot) - 长字符串数据位置:
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;
}
}
存储布局分析:
- 简单变量:
- slot0: a, b, c, d, e (打包存储)
- 数组:
- array长度存储在固定位置
- 数组元素存储在计算得到的位置
- 映射:
- 用户信息存储在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;
}
}
}
攻击方法:
- 读取slot1获取password:
web3.eth.getStorageAt(contract.address, 1) - 使用获取的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);
}
}
攻击方法:
- 读取slot0获取pin值
- 使用获取的pin调用unlock函数
总结
- 以太坊存储是键值对数据库,每个合约有2^256个32字节的插槽
- 存储优化原则:连续变量尽可能打包到同一插槽
- 不同类型变量有特定的存储布局规则:
- 简单变量:直接存储或打包存储
- 数组:长度存储在固定位置,元素存储在计算位置
- 映射:使用keccak256(key + slot)计算存储位置
- 结构体:成员变量依次存储,可能打包
- 私有变量仅限制合约访问,存储数据仍可公开读取
- 理解存储机制有助于合约优化和安全审计