返回 SC 笔记
SC Day 62

复习 - Week 10总结 (Ethernaut + Sui开发)

### 1. Week 10 学习路径回顾

2026-06-23
第三阶段:安全审计
复习EthernautSui开发安全攻防总结

日期: 2026-06-23 方向: Solidity / Move/Sui 阶段: 第三阶段:安全审计 标签: #复习 #Ethernaut #Sui开发 #安全 #攻防总结


今日目标

  1. 回顾 Week 10 所有 Ethernaut 挑战和安全漏洞模式
  2. 梳理 Sui 开发完整工作流和核心概念
  3. 构建"攻击模式 → 防御策略"映射表
  4. 深度对比 Move/Sui 为什么比 EVM 更安全

核心概念

1. Week 10 学习路径回顾

Week 10 学习轨迹:
├── Day 55: Ethernaut #1 Fallback + #2 Fallout
│   → receive/fallback 函数风险, 构造函数命名错误
├── Day 56: Ethernaut #3 Coin Flip + #4 Telephone
│   → 链上随机数不安全, tx.origin vs msg.sender
├── Day 57: Ethernaut #5 Token + #6 Delegation
│   → 整数溢出, delegatecall 存储冲突
├── Day 58: Ethernaut #7 Force + #8 Vault
│   → selfdestruct 强制转账, 链上数据不是隐私的
├── Day 59: Ethernaut #9 King + #10 Reentrancy
│   → 拒绝服务 (DoS), 经典重入攻击
├── Day 60: Sui 开发基础 (Counter/NFT/Coin)
│   → Sui 对象模型, 所有权, Move 基础
└── Day 61: Sui Kiosk + NFT 市场
    → TransferPolicy, 版税强制执行

2. Ethernaut 攻击模式总结

2.1 访问控制类漏洞

Challenge漏洞攻击原理防御
#1 Fallbackreceive() 可改变 owner发送 ETH 就能接管合约不要在 fallback 中做权限变更
#2 Fallout构造函数拼写错误任何人可调用"构造函数"使用 constructor 关键字
#4 Telephonetx.origin 做认证通过中间合约绕过永远用 msg.sender

2.2 数据安全类漏洞

Challenge漏洞攻击原理防御
#8 Vaultprivate 变量可读直接读取 storage slot敏感数据不要存链上
#3 Coin Flip链上"随机"可预测用同一区块数据计算使用 Chainlink VRF

2.3 底层调用类漏洞

Challenge漏洞攻击原理防御
#6 Delegationdelegatecall 存储冲突改写了调用者的 storage确保 storage layout 一致
#7 Forceselfdestruct 强制转 ETH绕过所有检查发送 ETH不要依赖 address(this).balance 做逻辑

2.4 状态操纵类漏洞

Challenge漏洞攻击原理防御
#5 Token整数下溢balances[msg.sender] - _value 下溢使用 Solidity ≥0.8 或 SafeMath
#10 Reentrancy先转账后更新状态在回调中递归调用CEI 模式 + ReentrancyGuard
#9 King无法接收 ETH 的合约回退函数 revert 造成 DoS使用 Pull 模式

3. 安全编码核心原则

安全编码金字塔:

                    ┌───────────┐
                    │  形式化   │  Move Prover, Certora
                    │   验证    │
                ┌───┴───────────┴───┐
                │   自动化工具扫描   │  Slither, Mythril, Aderyn
            ┌───┴───────────────────┴───┐
            │     手动审计 + 同行评审     │  代码审查, 攻击思维
        ┌───┴───────────────────────────┴───┐
        │        安全编码模式和最佳实践       │  CEI, ReentrancyGuard
    ┌───┴───────────────────────────────────┴───┐
    │          语言和框架层面的安全保障            │  Move 类型系统, Solidity 0.8+
    └───────────────────────────────────────────┘

4. Sui 开发知识体系

Sui 开发知识树:
├── 对象模型
│   ├── Owned Objects (单所有者)
│   ├── Shared Objects (共享, 需共识)
│   ├── Immutable Objects (不可变)
│   └── Wrapped Objects (嵌套在其他对象中)
│
├── 能力系统 (Abilities)
│   ├── key    → 可作为对象(有 id 字段)
│   ├── store  → 可存储在其他对象中
│   ├── copy   → 可复制
│   └── drop   → 可丢弃(离开作用域时自动销毁)
│
├── 核心模式
│   ├── One-Time Witness (OTW) → 模块初始化
│   ├── Witness Pattern → 类型级授权
│   ├── Hot Potato → 强制完成操作序列
│   ├── Capability Pattern → 基于对象的权限
│   └── Transfer Policy → 强制规则执行
│
├── 开发工作流
│   ├── sui move new → 创建项目
│   ├── sui move build → 编译
│   ├── sui move test → 测试
│   ├── sui client publish → 发布
│   └── sui client call → 调用函数
│
└── 框架模块
    ├── sui::object → 对象创建和管理
    ├── sui::transfer → 转移和共享
    ├── sui::coin → 自定义代币
    ├── sui::kiosk → 去中心化商务
    ├── sui::transfer_policy → 转移规则
    ├── sui::display → 链上显示标准
    └── sui::event → 事件发射

代码实战

实战 1: 完整攻击→防御代码对照

重入攻击(最经典的例子)

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// ===== 漏洞合约 =====
contract VulnerableBank {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    // 漏洞: 先转账,后更新状态
    function withdraw() external {
        uint256 balance = balances[msg.sender];
        require(balance > 0, "No balance");

        // 危险! 外部调用在状态更新之前
        (bool success, ) = msg.sender.call{value: balance}("");
        require(success, "Transfer failed");

        // 这行在重入攻击中太晚了
        balances[msg.sender] = 0;
    }
}

// ===== 攻击合约 =====
contract ReentrancyAttacker {
    VulnerableBank public target;

    constructor(address _target) {
        target = VulnerableBank(_target);
    }

    function attack() external payable {
        target.deposit{value: msg.value}();
        target.withdraw();
    }

    // 每次收到 ETH 时递归调用 withdraw
    receive() external payable {
        if (address(target).balance >= target.balances(address(this))) {
            target.withdraw();
        }
    }
}

// ===== 修复后的合约 =====
contract SecureBank {
    mapping(address => uint256) public balances;
    bool private _locked;

    modifier nonReentrant() {
        require(!_locked, "Reentrant call");
        _locked = true;
        _;
        _locked = false;
    }

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    // 修复 1: CEI 模式 (Checks-Effects-Interactions)
    // 修复 2: nonReentrant 修饰符
    function withdraw() external nonReentrant {
        uint256 balance = balances[msg.sender];
        require(balance > 0, "No balance");   // Checks

        balances[msg.sender] = 0;              // Effects (先更新状态!)

        (bool success, ) = msg.sender.call{value: balance}("");  // Interactions
        require(success, "Transfer failed");
    }
}

tx.origin 攻击

// ===== 漏洞合约 =====
contract VulnerableWallet {
    address public owner;

    constructor() { owner = msg.sender; }

    // 漏洞: 使用 tx.origin 做认证
    function transfer(address to, uint256 amount) external {
        require(tx.origin == owner, "Not owner");
        payable(to).transfer(amount);
    }
}

// ===== 攻击合约 =====
contract TxOriginAttacker {
    VulnerableWallet public target;
    address public attacker;

    constructor(address _target) {
        target = VulnerableWallet(_target);
        attacker = msg.sender;
    }

    // 诱骗 owner 调用这个函数
    function claimReward() external {
        // tx.origin 是 owner,但 msg.sender 是本合约
        target.transfer(attacker, address(target).balance);
    }
}

// ===== 修复 =====
contract SecureWallet {
    address public owner;

    constructor() { owner = msg.sender; }

    // 修复: 使用 msg.sender
    function transfer(address to, uint256 amount) external {
        require(msg.sender == owner, "Not owner");
        payable(to).transfer(amount);
    }
}

实战 2: Sui 核心模式速查代码

module review::patterns_cheatsheet {
    use std::string::String;
    use sui::event;

    // ===== 模式 1: Capability Pattern(能力模式)=====
    // 用途: 基于对象的权限控制

    public struct AdminCap has key, store {
        id: UID,
    }

    public struct Config has key {
        id: UID,
        fee_bps: u64,
    }

    // 只有持有 AdminCap 的人才能调用
    public fun update_fee(
        _admin: &AdminCap,
        config: &mut Config,
        new_fee: u64,
    ) {
        config.fee_bps = new_fee;
    }

    // ===== 模式 2: One-Time Witness (OTW) =====
    // 用途: 保证模块初始化只执行一次

    public struct MY_MODULE has drop {}

    fun init(otw: MY_MODULE, ctx: &mut TxContext) {
        // otw 只在模块发布时创建一次
        // 用于创建 Publisher, Coin, Display 等唯一对象
        let admin = AdminCap { id: object::new(ctx) };
        transfer::transfer(admin, ctx.sender());
    }

    // ===== 模式 3: Hot Potato =====
    // 用途: 强制调用者完成一系列操作

    /// 没有 drop 能力! 必须被消耗
    public struct FlashLoanReceipt {
        amount: u64,
        fee: u64,
    }

    // 借款 - 返回 receipt(没有 drop,必须归还)
    public fun flash_borrow(amount: u64): (u64, FlashLoanReceipt) {
        let receipt = FlashLoanReceipt {
            amount,
            fee: amount / 100,  // 1% 手续费
        };
        (amount, receipt)
    }

    // 还款 - 消耗 receipt
    public fun flash_repay(receipt: FlashLoanReceipt, repaid: u64) {
        let FlashLoanReceipt { amount, fee } = receipt;
        assert!(repaid >= amount + fee, 0);
        // receipt 被解构,不再存在
    }

    // ===== 模式 4: Witness Pattern =====
    // 用途: 类型级别的授权证明

    /// witness 必须由特定模块创建
    public struct CreatePoolWitness has drop {}

    public fun create_pool(_witness: CreatePoolWitness) {
        // 只有能创建 CreatePoolWitness 的模块才能调用
    }

    // ===== 模式 5: Object Wrapping =====
    // 用途: 动态组合对象

    public struct Sword has key, store {
        id: UID,
        damage: u64,
    }

    public struct Hero has key {
        id: UID,
        name: String,
        weapon: Option<Sword>,  // 包装其他对象
    }

    public fun equip(hero: &mut Hero, sword: Sword) {
        option::fill(&mut hero.weapon, sword);
    }

    public fun unequip(hero: &mut Hero): Sword {
        option::extract(&mut hero.weapon)
    }
}

实战 3: 安全对比——同一逻辑在 Solidity vs Move 的实现

// ===== Solidity: 代币转账 =====
// 需要手动防止溢出和重入

contract SimpleToken {
    mapping(address => uint256) balances;

    // 隐患: 如果 Solidity < 0.8,可能溢出
    // 隐患: 外部调用可能导致重入
    function transfer(address to, uint256 amount) external {
        require(balances[msg.sender] >= amount, "Insufficient");
        balances[msg.sender] -= amount;
        balances[to] += amount;
    }
}
// ===== Move: 代币转账 =====
// 类型系统自动保证安全

module safe_token::token {
    use sui::coin::{Self, Coin, TreasuryCap};
    use sui::sui::SUI;

    // Coin<T> 是一个资源类型
    // - 不能复制 (no copy) → 不能凭空创造代币
    // - 不能丢弃 (no drop) → 不能销毁代币(除非显式调用 burn)
    // - 转移时从 A 移走 → 不存在"双花"

    // Move 版本的"转账"就是 transfer::public_transfer
    // 根本不需要余额映射 (mapping)
    // Coin 对象自身代表所有权

    public fun send_payment(
        coin: Coin<SUI>,    // 调用者必须拥有这个 Coin
        recipient: address,
    ) {
        // coin 从调用者手中转移给 recipient
        // 无法重入,无法溢出,无法双花
        transfer::public_transfer(coin, recipient);
    }

    // 拆分代币
    public fun split_and_send(
        coin: &mut Coin<SUI>,
        amount: u64,
        recipient: address,
        ctx: &mut TxContext,
    ) {
        let payment = coin::split(coin, amount, ctx);
        transfer::public_transfer(payment, recipient);
    }
}

关键要点总结

攻击模式 → 防御策略 完整映射表

#攻击模式漏洞根因防御策略Move/Sui是否免疫
1重入攻击外部调用在状态更新前CEI + ReentrancyGuard
2整数溢出/下溢无边界检查Solidity 0.8+ / SafeMath
3tx.origin 钓鱼tx.origin 做认证使用 msg.senderN/A
4delegatecall 注入存储布局冲突审慎使用 proxy
5selfdestruct 强制发 ETH依赖余额做逻辑不依赖 address.balance
6链上随机数预测使用区块数据做随机Chainlink VRF需VRF
7访问控制缺失缺少权限检查OpenZeppelin Ownable/AccessControlCapability
8私有数据泄露链上数据公开不存敏感数据在链上同样公开
9DoS via revert依赖外部调用成功Pull over Push部分免疫
10前端/构造函数攻击拼写错误/疏忽使用 constructor 关键字init()自动

Move/Sui 为什么更安全?

Move 安全优势层次:

1. 资源类型系统(最核心)
   - 资源不能复制 → 无双花
   - 资源不能丢弃 → 无意外丢失
   - 所有权是强制的 → 无未授权访问

2. 无动态调度
   - 没有 delegatecall → 无存储冲突攻击
   - 没有回调函数 → 无重入攻击
   - 调用目标在编译时确定 → 无代理合约风险

3. 对象模型
   - 全局存储不可任意访问 → 无 storage slot 读取
   - 对象访问需要所有权证明 → 减少越权操作
   - 共享对象需要显式标记 → 并发控制更明确

4. 模块系统
   - 结构体字段默认私有 → 封装性更强
   - 类型只能在定义模块中创建 → 无伪造
   - init() 函数自动调用 → 无构造函数攻击

5. 形式化验证
   - Move Prover 内置支持 → 数学证明正确性
   - Specification Language → 声明不变量

但 Move/Sui 不能防止的问题

Move 无法解决的安全问题:
├── 业务逻辑错误(如价格计算错误)
├── 预言机操纵(外部数据源问题)
├── 治理攻击(社会工程学)
├── 前端攻击(钓鱼网站)
├── 密钥管理不当(私钥泄露)
├── 经济模型设计缺陷(Death Spiral)
└── 共享对象竞态条件(需要仔细设计)

常见误区

误区 1: "Move 没有重入,所以 DeFi 一定安全"

纠正: Move 确实消除了重入攻击,但 DeFi 安全远不止重入。闪电贷攻击、预言机操纵、价格滑点等问题与语言无关,需要在业务逻辑层面防护。Sui 上的 DeFi 协议同样可能因为数学错误或经济模型缺陷而被攻击。

误区 2: "学了 Ethernaut 就能做审计了"

纠正: Ethernaut 是入门级安全训练,覆盖的是最基础的漏洞模式。真实审计需要理解复杂的 DeFi 组合性、闪电贷交互、MEV、跨链消息等高级话题。Ethernaut 是必要的第一步,但远远不够。

误区 3: "Sui 不需要安全审计"

纠正: 虽然 Move 消除了一大类漏洞,但 Sui 上的合约仍然需要审计。常见的 Sui 安全问题包括:

  • 共享对象的并发访问逻辑错误
  • 对象所有权转移逻辑错误
  • 数学计算精度问题(整数除法截断)
  • 权限管理不当(Capability 泄露或丢失)

误区 4: "Solidity 不安全,应该全面转向 Move"

纠正: Solidity 生态系统经过多年发展,已经有了成熟的安全工具链(Slither/Mythril/Foundry fuzz)、审计方法论和丰富的安全库(OpenZeppelin)。选择语言时安全性是一个因素,但生态成熟度、开发者可用性、链的选择等也很重要。


面试关联

Q1: 如果你在面试中被问到"Solidity 和 Move 的安全性对比",如何系统性回答?

回答框架:

Move 在语言层面提供了更强的安全保证:资源线性类型消除了双花和意外销毁,没有动态调度消除了重入和代理攻击,模块系统提供了更好的封装。

但安全不仅仅是语言问题。Solidity 生态有更成熟的工具链(Slither 静态分析、Foundry fuzz testing、Certora 形式化验证)和审计生态。此外,业务逻辑层面的漏洞(预言机操纵、经济模型缺陷、治理攻击)与语言无关。

从 PM 视角看,选择哪条链/语言需要综合考虑:安全性、开发效率、生态成熟度、用户基数和商业需求。最佳实践是:无论使用什么语言,都需要完善的审计流程。

Q2: 总结 Week 10 学到的最重要的三个安全教训?

回答思路:

  1. "不要信任外部输入" —— tx.origin、链上随机数、未验证的回调
  2. "状态更新必须在外部调用之前" —— CEI 模式是黄金法则
  3. "链上没有秘密" —— private 变量、合约逻辑都是公开的

参考资源

  1. Ethernaut 全部关卡
  2. Sui Move 官方文档
  3. Move Book
  4. SWC Registry - 智能合约漏洞分类
  5. Damn Vulnerable DeFi
  6. Sui 安全最佳实践
  7. Trail of Bits - 智能合约安全审计指南