
selfdestruct 等低级指令的风险Solidity 使用 32 字节为一个存储槽(storage slot)。在继承或代理合约模式下,如果新旧合约的状态变量定义不一致,就可能发生槽冲突,导致关键数据被覆盖。
// V1
contract LogicV1 {
uint256 public value; // slot 0
}
// V2 (错误升级)
contract LogicV2 {
address public owner; // slot 0 (与 V1 的 value 冲突)
}在升级后,owner 会直接读取到旧的 value,导致 权限错乱。
防御手段: 使用
storage gap预留存储空间: uint25650 private __gap;遵循 OpenZeppelin 的升级合约工具(@openzeppelin/contracts-upgradeable)。
ABI 负责定义函数签名到 函数选择器(4 字节) 的映射。
攻击者可能利用选择器碰撞,让不同函数共享同一个选择器,从而调用到意料之外的逻辑。
contract Victim {
function transfer(address to, uint256 amount) public {}
function f123456789() public {}
}不同函数签名哈希后的前 4 字节可能相同,导致 ABI 解码错误。
虽然概率极低(约 1/ 2^32 ),但已被多次利用于攻击 ABI 解析库。
防御手段:
selfdestruct 的风险selfdestruct(address) 指令会销毁合约,并强制向指定地址转账余额。
虽然 EIP-6049 已提出废弃 selfdestruct,但目前仍存在隐患:
selfdestruct 强行转账到任意合约,即使目标合约没写 receive()。selfdestruct,可能彻底失效。contract Victim {
uint256 public balance;
function deposit() external payable {
balance += msg.value;
}
}即使 Victim 没有 receive(),攻击者仍可通过 selfdestruct 注入 ETH,导致 balance 与 address(this).balance 不一致,引发资金错账。
contract Attacker {
function attack(address payable target) external payable {
selfdestruct(target);
}
}SlotLogicV1.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract LogicV1 {
uint256 public value; // slot 0
function setValue(uint256 v) external {
value = v;
}
}SlotLogicV2.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract LogicV2 {
address public owner; // slot 0
function setOwner(address o) external {
owner = o;
}
}test/SlotCollision.t.sol// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/SlotLogicV1.sol";
import "../src/SlotLogicV2.sol";
contract SlotCollisionTest is Test {
LogicV1 v1;
LogicV2 v2;
function setUp() public {
v1 = new LogicV1();
v2 = LogicV2(address(v1)); // 模拟升级代理
}
function testCollision() public {
v1.setValue(123);
emit log_named_uint("Stored in V1.value", v1.value());
// 直接读 slot 0 的原始值
bytes32 raw = vm.load(address(v1), bytes32(uint256(0)));
emit log_named_bytes32("Raw slot0 data", raw);
// 解释为 address
address fakeOwner = address(uint160(uint256(raw)));
emit log_named_address("Interpreted as V2.owner", fakeOwner);
}
}执行测试:
➜ counter git:(main) ✗ forge test --match-path test/SlotCollision.t.sol -vvv
[⠊] Compiling...
[⠢] Compiling 1 files with Solc 0.8.29
[⠆] Solc 0.8.29 finished in 1.13s
Compiler run successful!
Ran 1 test for test/SlotCollision.t.sol:SlotCollisionTest
[PASS] testCollision() (gas: 39104)
Logs:
Stored in V1.value: 123
Raw slot0 data: 0x000000000000000000000000000000000000000000000000000000000000007b
Interpreted as V2.owner: 0x000000000000000000000000000000000000007B
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.49ms (1.14ms CPU time)// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Victim {
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
contract Attacker {
function attack(address payable target) external payable {
selfdestruct(target);
}
}test/Selfdestruct.t.sol// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/Victim.sol";
import "../src/Attacker.sol";
contract SelfdestructTest is Test {
Victim victim;
Attacker attacker;
function setUp() public {
victim = new Victim();
attacker = new Attacker();
}
function testForcedETH() public {
emit log_named_uint("Victim balance before", victim.getBalance());
attacker.attack{value: 1 ether}(payable(address(victim)));
emit log_named_uint("Victim balance after", victim.getBalance());
}
}执行测试:
➜ counter git:(main) ✗ forge test --match-path test/Selfdestruct.t.sol -vvv
[⠊] Compiling...
[⠢] Compiling 2 files with Solc 0.8.29
[⠆] Solc 0.8.29 finished in 1.08s
Compiler run successful with warnings:
Warning (5159): "selfdestruct" has been deprecated. Note that, starting from the Cancun hard fork, the underlying opcode no longer deletes the code and data associated with an account and only transfers its Ether to the beneficiary, unless executed in the same transaction in which the contract was created (see EIP-6780). Any use in newly deployed contracts is strongly discouraged even if the new behavior is taken into account. Future changes to the EVM might further reduce the functionality of the opcode.
--> src/VictimAttacker.sol:12:9:
|
12 | selfdestruct(target);
| ^^^^^^^^^^^^
Ran 1 test for test/Selfdestruct.t.sol:SelfdestructTest
[PASS] testForcedETH() (gas: 28393)
Logs:
Victim balance before: 0
Victim balance after: 1000000000000000000
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 6.99ms (646.87µs CPU time)
Ran 1 test suite in 345.96ms (6.99ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)输出显示 Victim 收到强制 ETH,哪怕它没有 receive()。
本课揭示了 Solidity 与 EVM 的底层隐患:
开发者必须:
selfdestruct原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。