remix编译运行solidity3

calldata相关的问题

这两天学习了一些小合约,尝试了calldata,但还没有用过storage和memory。在deployed contracts中,callbyinterface的下方有calldata,点击copy calldata to clipboard,就得到了0x469883d10000000000000000000000003328358128832a260c76a4141e19e2a943cd4b6d。但我不知道这个calldata是什么意思

下面我给你一个专业、体系化、逐字段拆解的解释,让你完全理解:

  • 什么是 calldata
  • 为什么 Remix 会生成这串十六进制
  • 这串 calldata 每一部分代表什么
  • 什么是“4 字节函数选择器 + ABI 编码参数”

你未来做 Auditor 必须吃透这一点。

一、你看到的这串是什么?

你复制到的这段十六进制:

1
0x469883d10000000000000000000000003328358128832a260c76a4141e19e2a943cd4b6d

这就是:

传入给函数 callByInterface(address) 的 ABI-encoded calldata(函数选择器 + 参数编码)

也就是说:
这是 EVM 在进行一次跨合约调用时“实际发送给合约的原始字节消息”。

Solidity(以及 Remix)把它用十六进制(hex)表示出来。

二、为什么会有这段 calldata?

当你按下 “callByInterface” 按钮时,Remix 必须以 “低级格式” 把真正的数据发送给合约。

你看到的就是:

接口函数调用的底层表现

这就是“你使用 interface 调用,但底层仍然是以 calldata 字节形式发送 CALL”。
这正是为什么 interface 是“高层调用”,但本质仍是低级 CALL。

三、我们来逐字段拆解你的 calldata

你这段 calldata:

1
2
3
4
0x
469883d1
0000000000000000000000000000000000000000000000000000000000000000
3328358128832a260c76a4141e19e2a943cd4b6d

我们拆成两部分:

第一部分(前 4 字节):函数选择器(function selector)

1
469883d1

这是:

1
keccak256("callByInterface(address)")[0:4]

EVM 用这 4 个字节决定“我要调用哪个函数”。

你可以自己验证:

如果你在 Remix console 输入:

1
web3.utils.keccak256("callByInterface(address)")

会得到:

1
0x469883d143c9c2ab...

前 4 字节就是:

1
0x469883d1

第二部分(32 字节):参数 address 的 ABI 编码

Solidity ABI 规定:

  • 所有参数都必须占 32 字节(256 bit)
  • address 20 字节 → 左侧补 12 字节 0

你传入的地址是:

1
0x3328358128832a260c76a4141e19e2a943cd4b6d

ABI 编码为:

1
0000000000000000000000003328358128832a260c76a4141e19e2a943cd4b6d

组合起来就是完整的 calldata:

1
2
3
0x
469883d1 ← 函数选择器
0000000000000000000000003328358128832a260c76a4141e19e2a943cd4b6d ← 参数

你现在完全能看懂了。

四、为什么 storage / memory / calldata 重要?

这里你第一次看到“真实的 calldata”。

在 Solidity 中:

| 名称 | 存储位置 | 生命周期 | 用途 |
| storage | 永久存储(链上) | 交易之间持久存在 | 状态变量 |
| memory | 临时(函数执行期间) | 调用结束后释放 | 局部变量、数组 |
| calldata | 外部调用输入数据 | 只读、不可修改 | 函数参数(external) |

你看到的十六进制就是:

callByInterface(address) 的 calldata 区域内容

这些数据从外部发送到合约,被 EVM 加载到 calldata。

五、你现在应该掌握的核心概念(Auditor 必备)

你在学习合约早期阶段,看见 calldata 十六进制是非常好的信号,说明你开始深入 EVM 层。

请记住这几个重点:

  1. 所有跨合约调用最终都以 calldata 字节方式在 EVM 中执行
  2. 函数选择器是 keccak256 的前 4 字节
  3. 所有参数都编码成 32 字节(address 也要 padding)
  4. interface 调用只是语法糖,底层仍是这种字节消息
  5. call 调用需要手动写 abi.encode → 就是生成这样的字节

当你看到 calldata,你就能“读懂”调用发生了什么,这就是未来审计的核心能力之一。

mapping相关的问题

下面我给你一份专业、清晰、完全从 Solidity 设计哲学 + 业务场景 + 安全需求出发的解释,让你彻底理解:

  • 为什么 Solidity 需要 mapping
  • 它为什么类似 Python 字典但又完全不同
  • 为什么智能合约大量使用“单映射”和“双重映射”
  • 它的底层结构是什么

这部分是你成为 Junior Auditor / Developer 的基础核心能力。

一、mapping 为什么看起来像 Python 字典?

从表面上看:

1
mapping(address => uint) balances;

就像 Python:

1
balances = { "0xabc": 1000 }

你看到的“像字典”是正常反应,因为两者都表达:

用某个 key 查找某个 value 的数据结构。

但是重点来了:

mapping 的行为、限制、底层结构与 Python 字典完全不同。

mapping 不是字典,而是 区块链上为“定址查找”专门设计的哈希结构

二、为什么 Solidity 需要 mapping?(核心原因)

因为链上有几个独特需求:

1)区块链无法遍历状态 —— mapping 完美符合这一点

链上状态必须:

  • 明确定址(通过 key 直接找到数据)
  • 无法遍历(增加安全性和可扩展性)
  • 永远以 O(1) 查找

mapping 恰好满足:

1
balances[user]O(1)

没有数组那种越界风险,也不会出现 Python 字典那种 rehash 行为。

mapping = 区块链上最安全、可预测、可扩展的键值结构。

2)mapping 不可遍历(by design),非常重要

新手会觉得这是缺点:

“为什么 mapping 不能遍历?好奇怪。”

但这是区块链的必然设计:

  • 遍历 = 线性扫描 = 随着用户数量增长,gas 会爆炸
  • 链上合约必须保证 gas 可控
  • 所以 EVM 禁止“自动列出所有 key”

这就是为什么:

mapping 是无边界结构,理论上可以放无限用户,而 gas 始终稳定。

3)mapping 更安全

如果链上允许地址遍历,会导致严重隐私泄漏与攻击可能:

  • 所有用户地址
  • 所有用户余额
  • 所有用户状态
  • 全部可被恶意代码扫描

mapping 不可遍历 → 避免这种风险。

4)mapping 是 EVM 原生数据结构(gas 最低)

mapping 的存储模型是:

1
slot = keccak256( concat(key, mapping_slot) )

这是 EVM 原生支持的模式,读取和写入都极快。

相比之下:

  • array[0], array[1] 需要边界检查
  • struct 需要固定 slot 排布
  • dictionary(Python)有大量 runtime 逻辑

mapping 是 EVM 为智能合约量身定做的键值结构。

三、为什么要用 mapping 来做“一重映射”和“双重映射”?

初学者一般在 NFT、Token、权限管理时会看到这种写法:

1
2
mapping(address => uint) balances;
mapping(address => mapping(address => bool)) isApproved;

你可能会想:

“为什么这么复杂?真的需要吗?”

答案是:
链上业务天然需要映射关系。

我们看常见场景。

1)Token 余额(ERC20)

1
mapping(address => uint256) balances;

每个用户都有余额,这就是典型的“一对一关系”:

1
用户 → 余额

2)授权关系(ERC20 allowance)

1
mapping(address => mapping(address => uint256)) allowance;

意思是:

1
owner → spender → 可花费金额

这是“双重映射”。

为什么不是数组?
因为:

  • 每个用户可能授权给多个 DEX、钱包
  • 关系不可能预先固定
  • 也无法遍历所有用户
  • 必须 O(1) 查找

ERC20、ERC721、ERC1155 都依赖双重映射。

3)关注关系 / 好友关系(你做过的小合约)

1
mapping(address => mapping(address => bool)) isFollowing;

表示:

1
A 是否关注 B

社交、GameFi、DAO 里非常常见。

4)访问权限(AccessControl)

1
2
mapping(address => bool) isAdmin;
mapping(bytes32 => mapping(address => bool)) roles;

安全控制必须做到:

  • O(1) 判断权限
  • 不可被遍历攻击
  • 不可被猜测结构

mapping 是最安全方法。

四、mapping 与 Python 字典的本质区别(审计特别重要)

| 项目 | Python dict | Solidity mapping |
| 存储位置 | 内存 | 链上永久存储 |
| 是否可遍历 | 是 | 否 |
| 是否能删除 key | 可以删除 | delete 不会删除 key,只置 0 |
| 是否有长度 | 有 len() | 无 length |
| 是否可判断 key 是否存在 | 可以 | 无法判断(默认值 = 0) |
| hash 是否可见 | 不可见 | slot = keccak(key, slot) |
| gas 是否增长 | 不相关 | 必须可预期稳定 |

结论:

mapping 不是“字典”,而是“不可遍历的链上键值数据库”。

五、你未来会遇到三类常见 mapping 模式(学习重点)

1)简单映射
1
mapping(address => uint) balance;
2)双重映射
1
mapping(address => mapping(address => bool)) approval;
3)结构体映射
1
2
3
4
5
6
struct User {
uint age;
address ref;
}

mapping(address => User) users;

这是最常见的 Web3 数据模式。

六、你什么时候需要 array?什么时候 mapping?

| 数据结构 | 用途 |
| array | 有顺序、要遍历、高维度数据结构 |
| mapping | 用户数量不定、无法遍历、安全要求高、O(1) 查找 |

例子:

  • NFT 全部 tokenId:array
  • 用户余额:mapping
  • DAO 成员列表:array + mapping(组合)
  • 谁授权了谁:mapping

mapping 是链上业务的核心。

七、总结(面试级别回答)

你可以这样总结:

Solidity 的 mapping 不是 Python 字典,而是链上为不可遍历、O(1) 查找设计的特殊键值存储结构。它无法遍历、没有长度、所有 key 都有默认值,因此非常安全、节省 gas,适合表示用户余额、授权、权限、关注关系等不可预知规模的数据。双重映射允许表达 owner→spender、user→user 的复杂关系,是智能合约中最常见的数据结构之一。

如果你愿意,我可以继续教你:

  • mapping 在 EVM 的底层存储结构(slot 计算)
  • 写一个双映射的练习合约
  • 写一个 mapping + array 的组合模式(最常见)
  • 编写一个访问控制系统(AccessControl)

你想继续哪个方向?

白名单钱包

whitelistWallet.sol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SimpleWallet {

// 1. 储存每个用户余额
mapping(address => uint) public balances;

// 2. 白名单
mapping(address => bool) public whitelist;

// ========== Events ==========
event Whitelisted(address indexed user);
event Removed(address indexed user);
event Deposited(address indexed user, uint amount);
event Transferred(address indexed from, address indexed to, uint amount);

// ========== Modifier ==========
modifier onlyWhitelist() {
require(whitelist[msg.sender], "Not in whitelist");
_;
}

// ========== 公共函数 ==========

// 存钱
function deposit() external payable {
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}

// 加白名单
function addWhitelist(address user) external {
whitelist[user] = true;
emit Whitelisted(user);
}

// 移除白名单
function removeWhitelist(address user) external {
whitelist[user] = false;
emit Removed(user);
}

// 转账(仅白名单用户能操作)
function transferTo(address to, uint amount) external onlyWhitelist {
require(balances[msg.sender] >= amount, "Insufficient balance");

balances[msg.sender] -= amount;
balances[to] += amount;

emit Transferred(msg.sender, to, amount);
}
}

remix中测试

第一步,点copy account,复制eth地址0x617F2E2fD72FD9D5503197092aC168c91465E7f2

第二步,把eth地址 0x617F2E2fD72FD9D5503197092aC168c91465E7f2 添加到白名单,如下图所示

第三步,试下transfer to功能

上图的意思,就是往这个地址转账

第四步,balance 查看这个地址有多少存款

第五步,试下非白名单地址

1
call to SimpleWallet.balances errored: Error encoding arguments: TypeError: invalid address (argument="address", value="0xa39cd51110598e2277f8122940cdde41cff06b061de260d47d79753c0b64ea9c", code=INVALID_ARGUMENT, version=6.14.0) (argument="", value="0xa39cd51110598e2277f8122940cdde41cff06b061de260d47d79753c0b64ea9c", code=INVALID_ARGUMENT, version=6.14.0)

为什么 Solidity 里叫 modifier(修饰器)?它到底类似什么?

因为你会 Python / JavaScript,所以我用它们的语法帮你建立“类比概念”,让你 100% 理解 modifier 的作用与设计哲学。

一句话本质总结(非常准确)

Solidity 的 modifier 就相当于:“在函数执行前后,自动插入一段通用逻辑的语法糖”。

它不是函数、不是结构体、不是类,而是:
一个可以包裹函数的“逻辑包装器(wrapper)”。

你可以把它理解为下面三者的混合体:

  • Python 的装饰器(@decorator)
  • Java 的 AOP(面向切面编程)
  • Web 中间件(middleware)

但又不像它们那么自由,范围更窄,只用于函数前置/后置检查。

二、你理解不了,是因为 JS / Python 没有完全等价的词

Solidity 的 modifier 是 Solidity 语言特有的语法结构,其他语言确实没有完全对应的。

为了帮助你理解,我用三层比喻:

三、比喻 1:modifier = “门卫”

你进公司需要刷卡。

函数执行之前,要先判断“你有没有权限”。

例如:

1
2
3
4
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}

这是门卫逻辑。
_ 表示:检查通过后,允许你进入(继续执行函数主体)。

四、比喻 2:modifier = “函数入口处插入一段代码”

有这样一个函数:

1
2
3
function withdraw() external {
// 业务逻辑
}

你想让所有 sensitive 函数都“先检查权限”,那你得写:

1
require(msg.sender == owner);

每个地方都写一遍,可读性非常差。

Solidity 让你把通用逻辑封装成 modifier:

1
2
3
4
modifier onlyOwner() {
require(msg.sender == owner);
_;
}

然后你可以这样用:

1
2
3
function withdraw() external onlyOwner {
// 业务逻辑
}

你看到的 onlyOwner 就像对函数加了一个“前置检查”。

五、比喻 3:modifier = Python 装饰器 @decorator

你可能见过:

1
2
3
4
5
6
7
8
9
10
def check_login(func):
def wrapper(*args, **kwargs):
if not user_logged_in:
raise Exception("not logged in")
return func(*args, **kwargs)
return wrapper

@check_login
def buy():
...

这里的 @check_login 就是在“修饰”函数。

Solidity 的 modifier 作用完全一样,只是写法不同。

六、比喻 4:modifier = Web 中间件(Middleware)

比如 Express.js:

1
2
3
app.get('/admin', authMiddleware, (req, res) => {
res.send("hello");
});

这里 authMiddleware 会在请求进入路由之前执行身份验证。

Solidity 的 modifier 作用一样:

1
2
3
function adminTask() external onlyOwner {
...
}

onlyOwner 就像请求进入 adminTask 前必须经过一层“检查”。

七、为什么叫 modifier?

因为它字面含义就是 “修改函数的执行方式”。

  • 它不执行真正的业务逻辑
  • 它只在函数执行前(或后)插入一些检查
  • 它修饰了这个函数的行为

所以叫 修饰器(modifier)

八、你要理解的关键点(专业解释)

modifier 的结构:

1
2
3
4
5
modifier XXX {
// 前置逻辑
_;
// 后置逻辑(可选)
}

_ 表示:
把“函数主体”替换到这里。

也就是:

  • _ 之前的代码 = 函数前置逻辑
  • _ 之后的代码 = 函数后置逻辑

绝大多数 modifier 只有前置逻辑(例如权限检查)。

九、最经典例子(你今天写的白名单)

你写了:

1
2
3
4
5
6
7
8
modifier onlyWhitelist() {
require(whitelist[msg.sender], "Not in whitelist");
_;
}

function transferTo(...) external onlyWhitelist {
...
}

翻译成自然语言:

“在执行 transferTo 之前,先做一个白名单检查。如果不在白名单,直接 revert;如果在白名单,继续执行。”

这完全符合“门卫”、“装饰器”、“中间件”的概念。

十、总结一句你能记住的话

modifier 本质上是一个“函数前置/后置检查模板”,它会把自己的代码插入到目标函数中,用来写权限控制、输入验证、状态检查等逻辑,是 Solidity 独有的轻量化 AOP 机制。

如果你愿意,我可以继续帮你:

A)写一个可视化示例:把 modifier 展开成编译后的完整函数
B)写 “多个 modifier 组合” 的例子
C)告诉你 modifier 使用不当会产生哪些安全漏洞
D)让你练习自己写几个小 modifier(非常有用)

你想继续哪一个?