返回 SC 笔记
SC Day 15

Solidity - 继承(is) + 接口(interface) + 抽象合约(abstract) + super

### 1. Solidity 继承基础

2026-04-15
第一阶段:基础构建
solidityinheritanceinterfaceabstractsuperdiamond-problem

日期: 2026-04-15 方向: Solidity 阶段: 第一阶段:基础构建 标签: #solidity #inheritance #interface #abstract #super #diamond-problem


今日目标

类型内容
学习掌握 Solidity 的继承体系、C3 线性化、接口与抽象合约的设计模式
实操实现 BaseToken → BurnableToken → MyToken 继承链
产出完整的多层继承代码 + 钻石问题分析 + 设计模式总结

核心概念

1. Solidity 继承基础

Solidity 支持多重继承,使用 is 关键字。子合约继承父合约的所有 publicinternal 状态变量与函数。

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

/// @notice 最基础的所有权管理
contract Ownable {
    address public owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
    }

    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Zero address");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

/// @notice 可暂停功能
contract Pausable is Ownable {
    bool public paused;

    event Paused(address account);
    event Unpaused(address account);

    modifier whenNotPaused() {
        require(!paused, "Contract is paused");
        _;
    }

    modifier whenPaused() {
        require(paused, "Contract is not paused");
        _;
    }

    function pause() public onlyOwner whenNotPaused {
        paused = true;
        emit Paused(msg.sender);
    }

    function unpause() public onlyOwner whenPaused {
        paused = false;
        emit Unpaused(msg.sender);
    }
}

2. virtual 和 override 关键字

virtual 标记函数可以被重写override 标记函数正在重写父合约的函数

contract Base {
    // virtual: 允许子合约重写此函数
    function greet() public pure virtual returns (string memory) {
        return "Hello from Base";
    }

    // 没有 virtual 的函数不能被重写
    function version() public pure returns (uint256) {
        return 1;
    }
}

contract Child is Base {
    // override: 重写父合约的 virtual 函数
    function greet() public pure override returns (string memory) {
        return "Hello from Child";
    }

    // 编译错误!version() 不是 virtual,不能 override
    // function version() public pure override returns (uint256) { return 2; }
}

// 如果子合约也想让孙合约重写,需要同时标记 virtual 和 override
contract GrandChild is Child {
    // 需要 Child.greet() 也标记为 virtual
    // function greet() public pure override returns (string memory) {
    //     return "Hello from GrandChild";
    // }
}

3. 接口 (interface)

接口定义了合约必须实现的函数签名,但不能包含任何实现

// ====== 接口规则 ======
// 1. 所有函数必须是 external
// 2. 不能有构造函数
// 3. 不能有状态变量
// 4. 不能有函数实现
// 5. 可以继承其他接口
// 6. 可以定义 event、struct、enum、error

interface IERC20 {
    // 事件(可以在接口中定义)
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    // 自定义错误(Solidity 0.8.4+)
    error InsufficientBalance(uint256 required, uint256 available);

    // 函数签名(必须是 external)
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

// 接口继承接口
interface IERC20Metadata is IERC20 {
    function name() external view returns (string memory);
    function symbol() external view returns (string memory);
    function decimals() external view returns (uint8);
}

4. 抽象合约 (abstract)

抽象合约介于接口和完整合约之间:可以有部分实现,也可以有未实现的函数。

/// @notice 抽象合约:提供部分实现,留下关键函数给子合约
abstract contract AbstractToken {
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;

    mapping(address => uint256) internal _balances;

    // 构造函数(抽象合约可以有)
    constructor(string memory _name, string memory _symbol, uint8 _decimals) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
    }

    // 已实现的函数
    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }

    // 未实现的函数(必须由子合约实现)
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual;

    // 提供基础转账逻辑,但调用了未实现的钩子
    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "Transfer from zero");
        require(to != address(0), "Transfer to zero");
        require(_balances[from] >= amount, "Insufficient balance");

        _beforeTokenTransfer(from, to, amount);  // 钩子:子合约可在此加逻辑

        unchecked {
            _balances[from] -= amount;
            _balances[to] += amount;
        }
    }
}

接口 vs 抽象合约 对比

特性interfaceabstract contract
函数实现不允许可以有部分实现
状态变量不允许允许
构造函数不允许允许
函数可见性只能 external任意(public/internal/...)
继承只能继承 interface可以继承任何合约
用途定义标准/接口规范提供共享逻辑

5. super 关键字和 C3 线性化

当多重继承时,super 不是简单地调用 "直接父合约",而是遵循C3 线性化顺序。

// ====== C3 线性化示例 ======

contract A {
    event Log(string message);

    function foo() public virtual {
        emit Log("A.foo");
    }
}

contract B is A {
    function foo() public virtual override {
        emit Log("B.foo");
        super.foo(); // 调用 A.foo
    }
}

contract C is A {
    function foo() public virtual override {
        emit Log("C.foo");
        super.foo(); // 调用 A.foo
    }
}

// D 继承 B 和 C,两者都继承 A(钻石问题)
contract D is B, C {
    function foo() public override(B, C) {
        emit Log("D.foo");
        super.foo(); // 这里 super 是谁?
    }
}

// D 的 C3 线性化顺序: D → C → B → A
// 调用 D.foo() 的输出顺序:
// 1. "D.foo"
// 2. super.foo() → C.foo() → "C.foo"
// 3. super.foo() → B.foo() → "B.foo"  (不是 A!)
// 4. super.foo() → A.foo() → "A.foo"
// 每个合约的 foo() 只被调用一次!

C3 线性化规则

规则:
1. 子合约在父合约之前
2. 父合约按 is 声明的逆序排列
3. 每个合约只出现一次

示例:contract D is B, C
线性化:D → C → B → A

示例:contract E is C, B
线性化:E → B → C → A(注意顺序变了!)

关键原则:is 列表中越靠后的合约越"基础"
推荐写法:从最基础到最派生
contract MyToken is Ownable, Pausable, ERC20 { ... }

6. 构造函数参数传递

contract Base1 {
    uint256 public x;
    constructor(uint256 _x) {
        x = _x;
    }
}

contract Base2 {
    uint256 public y;
    constructor(uint256 _y) {
        y = _y;
    }
}

// 方法1:在继承列表中传参(编译时固定)
contract Child1 is Base1(10), Base2(20) {
    // x = 10, y = 20
}

// 方法2:在子合约构造函数中传参(运行时动态)
contract Child2 is Base1, Base2 {
    constructor(uint256 _x, uint256 _y) Base1(_x) Base2(_y) {
        // x = _x, y = _y
    }
}

// 混合使用
contract Child3 is Base1(100), Base2 {
    constructor(uint256 _y) Base2(_y) {
        // x = 100, y = _y
    }
}

代码实战:完整的代币继承链

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

// ========== 接口层 ==========

interface IERC20 {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);

    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

// ========== 基础代币合约 ==========

contract BaseToken is IERC20 {
    string public name;
    string public symbol;
    uint8 public decimals;
    uint256 public totalSupply;

    mapping(address => uint256) internal _balances;
    mapping(address => mapping(address => uint256)) internal _allowances;

    constructor(string memory _name, string memory _symbol, uint8 _decimals) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
    }

    function balanceOf(address account) public view returns (uint256) {
        return _balances[account];
    }

    function allowance(address owner_, address spender) public view returns (uint256) {
        return _allowances[owner_][spender];
    }

    function transfer(address to, uint256 amount) public virtual returns (bool) {
        _transfer(msg.sender, to, amount);
        return true;
    }

    function approve(address spender, uint256 amount) public virtual returns (bool) {
        _approve(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address from, address to, uint256 amount) public virtual returns (bool) {
        _spendAllowance(from, msg.sender, amount);
        _transfer(from, to, amount);
        return true;
    }

    // ====== 内部函数(可被子合约重写)======

    function _transfer(address from, address to, uint256 amount) internal virtual {
        require(from != address(0), "Transfer from zero");
        require(to != address(0), "Transfer to zero");

        _beforeTokenTransfer(from, to, amount);

        uint256 fromBalance = _balances[from];
        require(fromBalance >= amount, "Insufficient balance");

        unchecked {
            _balances[from] = fromBalance - amount;
            _balances[to] += amount;
        }

        emit Transfer(from, to, amount);
    }

    function _mint(address to, uint256 amount) internal virtual {
        require(to != address(0), "Mint to zero");

        _beforeTokenTransfer(address(0), to, amount);

        totalSupply += amount;
        _balances[to] += amount;

        emit Transfer(address(0), to, amount);
    }

    function _approve(address owner_, address spender, uint256 amount) internal virtual {
        require(owner_ != address(0), "Approve from zero");
        require(spender != address(0), "Approve to zero");

        _allowances[owner_][spender] = amount;
        emit Approval(owner_, spender, amount);
    }

    function _spendAllowance(address owner_, address spender, uint256 amount) internal virtual {
        uint256 currentAllowance = _allowances[owner_][spender];
        if (currentAllowance != type(uint256).max) {
            require(currentAllowance >= amount, "Insufficient allowance");
            unchecked {
                _allowances[owner_][spender] = currentAllowance - amount;
            }
        }
    }

    /// @notice 钩子函数:子合约可在转账前插入逻辑
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual {
        // 默认为空,子合约重写
    }
}

// ========== 可销毁扩展 ==========

contract BurnableToken is BaseToken {
    event Burn(address indexed burner, uint256 amount);

    constructor(
        string memory _name,
        string memory _symbol,
        uint8 _decimals
    ) BaseToken(_name, _symbol, _decimals) {}

    /// @notice 销毁自己的代币
    function burn(uint256 amount) public virtual {
        _burn(msg.sender, amount);
    }

    /// @notice 销毁已授权给你的代币
    function burnFrom(address account, uint256 amount) public virtual {
        _spendAllowance(account, msg.sender, amount);
        _burn(account, amount);
    }

    function _burn(address account, uint256 amount) internal virtual {
        require(account != address(0), "Burn from zero");

        _beforeTokenTransfer(account, address(0), amount);

        uint256 accountBalance = _balances[account];
        require(accountBalance >= amount, "Burn exceeds balance");

        unchecked {
            _balances[account] = accountBalance - amount;
            totalSupply -= amount;
        }

        emit Transfer(account, address(0), amount);
        emit Burn(account, amount);
    }
}

// ========== 可暂停扩展 ==========

abstract contract PausableToken is BaseToken {
    bool public paused;
    address public admin;

    event Paused(address account);
    event Unpaused(address account);

    modifier onlyAdmin() {
        require(msg.sender == admin, "Not admin");
        _;
    }

    function pause() external onlyAdmin {
        paused = true;
        emit Paused(msg.sender);
    }

    function unpause() external onlyAdmin {
        paused = false;
        emit Unpaused(msg.sender);
    }

    /// @notice 重写钩子:暂停时禁止转账
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
        require(!paused, "Token transfers paused");
        super._beforeTokenTransfer(from, to, amount); // 调用父合约的钩子
    }
}

// ========== 带上限的铸造扩展 ==========

abstract contract CappedToken is BaseToken {
    uint256 public immutable cap;

    constructor(uint256 _cap) {
        require(_cap > 0, "Cap must be > 0");
        cap = _cap;
    }

    function _mint(address to, uint256 amount) internal virtual override {
        require(totalSupply + amount <= cap, "Cap exceeded");
        super._mint(to, amount);
    }
}

// ========== 最终合约:组合所有扩展 ==========

contract MyToken is BurnableToken, PausableToken, CappedToken {
    constructor()
        BurnableToken("MyToken", "MTK", 18)
        CappedToken(1_000_000 * 10**18) // 100万上限
    {
        admin = msg.sender;
        _mint(msg.sender, 100_000 * 10**18); // 初始铸造10万
    }

    /// @notice 管理员铸造新代币
    function mint(address to, uint256 amount) external onlyAdmin {
        _mint(to, amount);
    }

    // 必须显式 override 两个父合约的同名函数
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal override(BaseToken, PausableToken) {
        // 调用 super 会按 C3 线性化顺序调用所有父合约
        super._beforeTokenTransfer(from, to, amount);
    }

    // CappedToken 重写了 _mint,需要通过 C3 线性化解决
    function _mint(address to, uint256 amount) internal override(BaseToken, CappedToken) {
        super._mint(to, amount); // 先调用 CappedToken._mint(检查上限),再调用 BaseToken._mint
    }
}

关键要点总结

要点说明
is 实现继承子合约获得父合约的状态和函数
virtual标记函数可被子合约重写
override标记函数正在重写父合约函数
C3 线性化确定多重继承时 super 的调用顺序
interface纯粹的接口定义,无实现
abstract部分实现,留钩子给子合约
super按 C3 线性化顺序调用父合约
钩子模式_beforeTokenTransfer 等钩子让扩展更灵活

常见误区

误区 1:is 顺序不影响行为

// 这两个的 C3 线性化不同!
contract X is A, B { }  // X → B → A
contract Y is B, A { }  // Y → A → B

// 原则:is 列表中最右边的最"基础"
// 推荐从基础到派生排列

误区 2:忘记在 override 列表中包含所有父合约

// 如果 A 和 B 都有 foo(),子合约必须显式 override 两者
contract C is A, B {
    // 编译错误!
    // function foo() public override { }

    // 正确
    function foo() public override(A, B) { }
}

误区 3:认为 super 只调用直接父合约

super.foo() 不是调用 "直接父合约的 foo()",而是调用 C3 线性化中的下一个合约的 foo()。在钻石继承中,这确保每个合约的函数只被调用一次。

误区 4:abstract 合约可以被部署

抽象合约不能直接部署。必须由一个非抽象的子合约实现所有未实现的函数后才能部署。


面试关联

Q: Solidity 的多重继承如何解决钻石问题?

30 秒回答:Solidity 使用 C3 线性化算法将继承图展平为一条线性顺序。super 调用按这个顺序依次执行,确保每个合约的函数只被调用一次。开发者需要在 is 声明中正确排列顺序(从基础到派生),并在 override 中列出所有被覆盖的父合约。

Q: interface 和 abstract contract 什么时候用哪个?

  • interface:定义标准规范(如 IERC20),让不同实现互相兼容
  • abstract contract:提供共享的基础逻辑(如 BaseToken),减少重复代码
  • 实践建议:先定义 interface 作为标准,再用 abstract contract 提供公共实现,最终合约组合两者

Q: 钩子模式 (_beforeTokenTransfer) 的设计意图是什么?

钩子模式允许子合约在不重写核心逻辑的情况下插入自定义行为。例如:暂停检查、黑名单检查、快照记录等。这是模板方法设计模式在智能合约中的应用,既保持了核心逻辑的完整性,又提供了扩展点。


参考资源

资源说明
Solidity 继承文档官方文档
C3 线性化算法理解 MRO 原理
OpenZeppelin ERC20继承设计的最佳实践
Solidity Patterns - Template Method智能合约设计模式