首页
学习
活动
专区
圈层
工具
发布
49 篇文章
1
《纸上谈兵·solidity》第 0 课:搭建 Solidity 开发环境(三种方式)
2
《纸上谈兵·solidity》第 1 课:部署你的第一个 Solidity 合约
3
《纸上谈兵·solidity》第 2 课:调用、修改、读取,Solidity 合约不是 REST API
4
《纸上谈兵·solidity》第 3 课:事件(Event)机制与链上日志——不是 print,是广播!
5
《纸上谈兵·solidity》第 4 课:Solidity 合约中的错误处理机制(`require`、`revert`、`assert`)和自定义错误
6
《纸上谈兵·solidity》第 5 课:依赖与外部调用 —— 合约交互的风险与防护
7
《纸上谈兵·solidity》第 6 课:Solidity 数据存储布局 —— memory、storage、calldata 傻傻分不清?
8
《纸上谈兵·solidity》第 7 课:Solidity 函数可见性和修饰器 —— public 和 private 不只是权限标签
9
《纸上谈兵·solidity》第 8 课:Solidity 中的继承与接口 —— 模块化不是“复制粘贴”的借口
10
《纸上谈兵·solidity》第 9 课:Solidity 事件与日志机制 —— 合约世界的“printf”工具
11
《纸上谈兵·solidity》第 10 课:Solidity `fallback` / `receive` 函数 —— 合约如何收 ETH 和响应未知调用?
12
《纸上谈兵·solidity》第 11 课:Solidity 错误处理与异常机制 —— 让合约优雅地失败
13
《纸上谈兵·solidity》第 12 课:Solidity 函数选择器与 ABI 编码原理
14
《纸上谈兵·solidity》第 13 课:Solidity 低级调用 call/delegatecall/staticcall —— 直接和 EVM“对话”
15
《纸上谈兵·solidity》第 14 课:Solidity 中的可升级合约模式 —— 从代理合约到透明代理、UUPS 与安全陷阱
16
《纸上谈兵·solidity》第 15 课:Solidity 库与可重用代码
17
《纸上谈兵·solidity》第 16 课:Pull over Push 支付模式与 Check-Effects-Interactions 原则
18
《纸上谈兵·solidity》第 17 课:合约设计模式实战(二)—— Access Control 与权限管理
19
《纸上谈兵·solidity》第 18 课:合约设计模式实战(三)—— 代理 + 插件化架构(Diamond Standard / EIP-2535)
20
《纸上谈兵·solidity》第 19 课:安全专题(一)—— 常见攻击手法与防御
21
《纸上谈兵·solidity》第 20 课:Solidity 安全专题(二)—— 编译器特性与低级漏洞
22
《纸上谈兵·solidity》第 21 课:Gas 优化与成本分析 —— 写出便宜的智能合约
23
《纸上谈兵·solidity》第 22 课:代币合约(ERC20)从零实现与扩展
24
《纸上谈兵·solidity》第 23 课:NFT 合约(ERC721 / ERC1155)实战
25
《纸上谈兵·solidity》第 24 课:去中心化众筹合约(Crowdfunding)实战
26
《纸上谈兵·solidity》第 25 课:简化版的去中心化交易所(DEX)简化版
27
《纸上谈兵·solidity》第 26 课:借贷合约简化实现
28
《纸上谈兵·solidity》第 27 课:DAO 治理合约(去中心化自治组织)
29
《纸上谈兵·solidity》第 28 课:智能合约安全审计案例复盘 -- The DAO Hack(2016)
30
《纸上谈兵·solidity》第 29 课:智能合约安全审计案例复盘 -- Parity Wallet Hack(2017)
31
《纸上谈兵·solidity》第 30 课:智能合约安全审计案例复盘 -- Nomad Bridge(2022)
32
《纸上谈兵·solidity》第 31 课:多签钱包在跨链桥中的应用 —— Nomad 事件复盘
33
《纸上谈兵·solidity》第 32 课:DeFi 基础合约
34
《纸上谈兵·solidity》第 33 课:多签钱包(Multisig Wallet)-- 合约设计与实现
35
《纸上谈兵·solidity》第 34 课:多签钱包(Multisig Wallet)-- 上线
36
《纸上谈兵·solidity》第 35 课:去中心化交易所(DEX)实战 — 合约设计
37
《纸上谈兵·solidity》第 36 课:去中心化交易所(DEX)实战 — 上线
38
《纸上谈兵·solidity》第 37 课:DeFi 实战 -- 资金池与利率模型
39
《纸上谈兵·solidity》第 38 课:DeFi 实战(2) -- 清算机制与价格预言机
40
《纸上谈兵·solidity》第 39 课:DeFi 实战(3) -- 利息累积与 aToken 设计
41
《纸上谈兵·solidity》第 40 课:DeFi 实战(4) -- 风险控制与防护
42
《纸上谈兵·solidity》第 41 课:DeFi 实战(5) -- 协议费与治理
43
《纸上谈兵·solidity》第 42 课:DeFi 实战(6) -- 跨资产借贷与多市场支持
44
《纸上谈兵·solidity》第 43 课:DeFi 实战(7) -- 清算机制进阶(多资产抵押清算路径、拍卖机制)
45
《纸上谈兵·solidity》第 44 课:DeFi 实战(8) -- 利率曲线与资金池优化(动态利用率模型)
46
《纸上谈兵·solidity》第 45 课:DeFi 实战(9) -- 利息累积与结算机制(可复利)
47
《纸上谈兵·solidity》第 46 课:DeFi 实战(10) -- 跨链借贷与流动性桥接
48
《纸上谈兵·solidity》第 47 课:DeFi 实战(11) -- 治理代币 & 激励机制(Tokenomics & Governance)
49
《纸上谈兵·solidity》第 48 课:DeFi 实战(12) -- 前端 DApp 集成与用户交互(React + ethers.js 实战)

《纸上谈兵·solidity》第 20 课:Solidity 安全专题(二)—— 编译器特性与低级漏洞

课程目标

  • 理解 Solidity 编译器的存储布局机制
  • 学会识别 存储槽冲突、ABI 混淆攻击
  • 掌握 selfdestruct 等低级指令的风险
  • 通过 Foundry 测试模拟攻击与验证

1、存储槽冲突(Storage Slot Collision)

Solidity 使用 32 字节为一个存储槽(storage slot)。在继承或代理合约模式下,如果新旧合约的状态变量定义不一致,就可能发生槽冲突,导致关键数据被覆盖。

示例:代理升级导致的槽冲突

代码语言:txt
复制
// 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)。


2、ABI 混淆攻击

ABI 负责定义函数签名到 函数选择器(4 字节) 的映射。

攻击者可能利用选择器碰撞,让不同函数共享同一个选择器,从而调用到意料之外的逻辑。

示例:选择器碰撞

代码语言:txt
复制
contract Victim {
    function transfer(address to, uint256 amount) public {}
    function f123456789() public {}
}

不同函数签名哈希后的前 4 字节可能相同,导致 ABI 解码错误。

虽然概率极低(约 1/ 2^32 ),但已被多次利用于攻击 ABI 解析库。

防御手段

  • 使用最新 Solidity 编译器,避免 ABI 自动推导漏洞
  • 避免函数名过长或构造极端签名
  • 使用工具检测潜在冲突(如 Slither、Surya)

3、selfdestruct 的风险

selfdestruct(address) 指令会销毁合约,并强制向指定地址转账余额。

虽然 EIP-6049 已提出废弃 selfdestruct,但目前仍存在隐患:

  1. 强制转账:攻击者可以部署一个带余额的合约,并 selfdestruct 强行转账到任意合约,即使目标合约没写 receive()
  2. 代理合约被摧毁:如果逻辑合约或代理被不慎写入 selfdestruct,可能彻底失效。

示例:强制转账绕过逻辑

代码语言:txt
复制
contract Victim {
    uint256 public balance;

    function deposit() external payable {
        balance += msg.value;
    }
}

即使 Victim 没有 receive(),攻击者仍可通过 selfdestruct 注入 ETH,导致 balanceaddress(this).balance 不一致,引发资金错账。

代码语言:txt
复制
contract Attacker {
    function attack(address payable target) external payable {
        selfdestruct(target);
    }
}

4、Foundry 实战测试

测试 1:存储槽冲突

SlotLogicV1.sol

代码语言:txt
复制
// 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:

代码语言:txt
复制
// 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

代码语言:txt
复制
// 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);
    }
}

执行测试:

代码语言:bash
复制
➜  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)

测试 2:强制转账

代码语言:txt
复制
// 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

代码语言:txt
复制
// 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());
    }
}

执行测试:

代码语言:bash
复制
➜  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()


5、总结

本课揭示了 Solidity 与 EVM 的底层隐患

  1. 存储槽冲突 —— 升级合约的最大坑
  2. ABI 混淆 —— 极端但可能的攻击面
  3. selfdestruct —— 强制转账与合约摧毁

开发者必须:

  • 使用官方工具(OpenZeppelin Upgrades、Slither)检查
  • 谨慎对待 ABI 与存储布局
  • 避免在合约中随意调用 selfdestruct
下一篇
举报
领券