首页
学习
活动
专区
圈层
工具
发布
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》第 7 课:Solidity 函数可见性和修饰器 —— public 和 private 不只是权限标签

在 Solidity 中,函数的可见性不仅决定了“谁可以调用”,更深层地影响到合约之间的交互方式、函数的 ABI 暴露、安全性设计和 gas 成本。本课还将介绍如何使用函数修饰器(modifier)实现访问控制与逻辑封装。


一、函数可见性的四种类型

Solidity 中的函数(和状态变量)支持 4 种可见性:

可见性

外部调用

合约内部调用

派生合约调用

ABI 导出

典型用途

public

用户或其他合约调用

external

❌(需 this.f()

节省 gas 的入口函数

internal

内部逻辑、继承使用

private

完全私有逻辑

示例代码:

代码语言:txt
复制
contract Visibility {
    // 可被任何人调用
    function publicFn() public pure returns (string memory) {
        return "public";
    }

    // 只能从外部调用:Visibility(address).externalFn()
    function externalFn() external pure returns (string memory) {
        return "external";
    }

    // 合约内 & 子合约可调用
    function internalFn() internal pure returns (string memory) {
        return "internal";
    }

    // 仅当前合约内部可调用
    function privateFn() private pure returns (string memory) {
        return "private";
    }
}

继承中的可见性

子合约可以:

  • 继承并 override publicinternal 函数
  • ❌ 无法 override 或访问 private 函数
代码语言:txt
复制
contract Base {
    function visible() internal virtual {}
    function secret() private {}
}

contract Child is Base {
    function useVisible() public {
        visible(); // ✅
        // secret(); // ❌ compile error
    }
}

external 是不是更安全?

不是。external 函数依旧公开访问,只是:

  • gas 成本更低(尤其是动态数组传参)
  • 不能合约内部直接调用(除非使用 this. 前缀)
代码语言:txt
复制
function update(uint[] calldata data) external {
    // ...
}

二、修饰器(modifier)的作用

Modifier 是 Solidity 的语法糖,允许在函数执行前或后添加逻辑 —— 非常适合权限控制、状态检查、reentrancy 防御等场景

常见用途:

  1. 权限控制(如 onlyOwner)
  2. 重入保护
  3. 状态锁定
  4. 函数执行顺序约束

示例:访问控制修饰器

代码语言:txt
复制
modifier onlyOwner() {
    require(msg.sender == owner, "Not owner");
    _;
}

function sensitiveAction() public onlyOwner {
    // 只有 owner 才能执行
}
  • _; 表示“继续执行被修饰的函数”
  • modifier 中的逻辑优先于主函数体执行。
  • modifier 的执行是链式调用(按声明顺序,即从左往右执行),且可以在同一函数上附加多个 modifiers。

三、Foundry 示例

我们来编写一个带有函数可见性示例的测试用例:

合约:Visibility.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Visibility {
    address public owner = msg.sender;

    function publicFunc() public pure returns (string memory) {
        return "public";
    }

    function externalFunc() external pure returns (string memory) {
        return "external";
    }

    function internalFunc() internal pure returns (string memory) {
        return "internal";
    }

    function privateFunc() private pure returns (string memory) {
        return "private";
    }

    function callInternal() public pure returns (string memory) {
        return internalFunc();
    }

    function callPrivate() public pure returns (string memory) {
        return privateFunc();
    }
}

测试:test/Visibility.t.sol

代码语言:txt
复制
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/Visibility.sol";

contract VisibilityTest is Test {
    Visibility vis;

    function setUp() public {
        vis = new Visibility();
    }

    function testPublic() public {
        assertEq(vis.publicFunc(), "public");
    }

    function testExternal() public {
        // 只能外部调用
        string memory val = Visibility(address(vis)).externalFunc();
        assertEq(val, "external");
    }

    function testInternalAccess() public {
        // 内部函数间接调用
        assertEq(vis.callInternal(), "internal");
    }

    function testPrivateAccess() public {
        assertEq(vis.callPrivate(), "private");
    }
}

执行测试命令:

代码语言:bash
复制
➜  counter git:(main) ✗ forge test --match-path test/Visibility.t.sol -vvv   
[⠊] Compiling...
[⠒] Compiling 3 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 544.36ms
Compiler run successful!

Ran 4 tests for test/Visibility.t.sol:VisibilityTest
[PASS] testExternal() (gas: 10240)
[PASS] testInternalAccess() (gas: 10329)
[PASS] testPrivateAccess() (gas: 10285)
[PASS] testPublic() (gas: 10293)
Suite result: ok. 4 passed; 0 failed; 0 skipped; finished in 5.25ms (4.99ms CPU time)

Ran 1 test suite in 189.08ms (5.25ms CPU time): 4 tests passed, 0 failed, 0 skipped (4 total tests)

四、modifier 示例

合约:ModifierVault.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract ModifierVault {
    address public owner;
    uint public balance;

    constructor() {
        owner = msg.sender;
    }

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

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

    function withdraw() public onlyOwner {
        payable(msg.sender).transfer(balance);
        balance = 0;
    }
}

测试:test/ModifierVault.t.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/ModifierVault.sol";

contract ModifierTest is Test {
    ModifierVault vault;
    address alice = address(0x1);
    address bob = address(0x2);

    // 添加 receive 用于接收 ETH
    receive() external payable {}

    function setUp() public {
        vault = new ModifierVault();
        vm.deal(alice, 1 ether);
    }

    function testOnlyOwnerCanWithdraw() public {
        // alice 向 vault 存款
        vm.prank(alice);
        vault.deposit{value: 1 ether}();

        // 使用合约拥有者 address(this) 执行提款
        vault.withdraw();

        // 验证余额清零
        assertEq(vault.balance(), 0);
    }

    function test_RevertWhen_NonOwnerWithdraws() public {
        vm.prank(bob);
        vm.expectRevert("Not owner"); // 或自定义错误选择器
        vault.withdraw();
    }
}

执行测试命令:

代码语言:bash
复制
➜  counter git:(main) ✗ forge test --match-path test/ModifierVault.t.sol -vvv
[⠊] Compiling...
[⠒] Compiling 1 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 558.83ms
Compiler run successful!

Ran 2 tests for test/ModifierVault.t.sol:ModifierTest
[PASS] testOnlyOwnerCanWithdraw() (gas: 36840)
[PASS] test_RevertWhen_NonOwnerWithdraws() (gas: 13505)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.30ms (1.59ms CPU time)

Ran 1 test suite in 190.26ms (5.30ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

五、pureview 函数修饰符 —— 状态访问语义的标识

在 Solidity 中,函数除了权限可见性(public / private 等),还可以声明它们对状态变量的访问行为。这就是 pureview 修饰符的用途:

修饰符

能否读取状态变量

能否修改状态变量

能否发送交易

示例用途

pure

数学运算,纯逻辑函数

view

查询、只读状态

无修饰符

默认允许任何操作

pure 修饰符:完全不接触状态的函数

pure 修饰符用于标记那些 不读取也不修改任何合约状态(包括 msg.sender, block.timestamp 等) 的函数。

代码语言:solidity
复制
function add(uint a, uint b) public pure returns (uint) {
    return a + b;
}

特性

  • 编译器静态检查,不允许访问任何状态变量或全局变量
  • 理论上可以在链下完全复现,无需部署或调用链上数据

view 修饰符:只读函数

view 表示该函数 读取状态变量,但不允许写入(修改)状态

代码语言:txt
复制
uint public totalSupply = 100;

function getSupply() public view returns (uint) {
    return totalSupply;
}

特性

  • 常用于 getter 函数
  • 只能读取状态,不能写入
  • 使用 view 函数是读取链上状态数据的标准方法

编译器强制规则举例

代码语言:txt
复制
uint public counter = 0;

function get() public pure returns (uint) {
    return counter; // ❌ 编译器报错:pure 函数不能读取状态变量
}

function read() public view returns (uint) {
    return counter; // ✅ 允许读取
}

function write() public {
    counter += 1; // ✅ 可读可写
}

测试示例(Foundry)

合约:src/MathLib.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract MathLib {
    uint public base = 10;

    function addPure(uint a, uint b) public pure returns (uint) {
        return a + b;
    }

    function addView(uint x) public view returns (uint) {
        return base + x;
    }
}

测试代码:test/MathLib.t.sol

代码语言:txt
复制
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "forge-std/Test.sol";
import "../src/MathLib.sol";

contract MathTest is Test {
    MathLib math;

    function setUp() public {
        math = new MathLib();
    }

    function testPureAdd() public view {
        assertEq(math.addPure(2, 3), 5);
    }

    function testViewAdd() public view {
        assertEq(math.addView(5), 15);
    }
}

执行测试

代码语言:bash
复制
➜  counter git:(main) ✗ forge test --match-path test/MathLib.t.sol -vvv      
[⠊] Compiling...
[⠒] Compiling 2 files with Solc 0.8.30
[⠑] Solc 0.8.30 finished in 534.57ms
Compiler run successful!

Ran 2 tests for test/MathLib.t.sol:MathTest
[PASS] testPureAdd() (gas: 9940)
[PASS] testViewAdd() (gas: 11665)
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 5.33ms (3.01ms CPU time)

Ran 1 test suite in 189.73ms (5.33ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)

六、函数可见性的设计建议

场景

建议使用

原因

用户或外部合约调用的入口函数

external

节省 gas,明确只供外部访问

需要外部也可内部复用的函数

public

可在合约内部和外部同时调用

仅合约内部或子合约调用的辅助函数

internal

控制作用域,避免被误用或暴露接口

私有状态修改/校验/哈希计算等内部逻辑

private

强限制,仅当前合约可访问,增强封装性

不访问任何状态或区块变量,仅进行计算

pure

完全独立,节省 gas,适用于纯逻辑计算

读取状态变量或全局上下文(如 block)

view

只读合约状态,不能修改,适用于查询或只读函数场景


七、课后练习

  1. 编写一个 Bank 合约,要求只有 owner 能调用 withdraw()
  2. Bank 合约中添加一个 internal 函数来计算利息。
  3. 使用 Foundry 为这两个函数编写测试用例,确保权限控制生效。

下一课预告

第 8 课:Solidity 中的继承与接口 —— 模块化不是“复制粘贴”的借口

在下一课中,我们将学习 Solidity 中的合约继承、接口、抽象合约等代码复用机制,掌握智能合约的模块化和解耦技巧。

下一篇
举报
领券