remix编译运行solidity4

昨日学习回顾

昨天我学习了错误处理,包括require,revert和assert。
assert只有确保代码正确的时候才使用,如果报错了说明代码有bug。
assert不用于检查用户的输入是否正确。
require是使用最多的错误处理,检查满足条件才继续执行。
revert推荐在复杂逻辑中使用,但我还是不太明确复杂逻辑和简单逻辑中间的区别。

然后我学习了跨合约调用,这是solidity非常重要的一个概念。
interface调用是高级语法糖,看起来逻辑更加clear,推荐使用interface调用
而call属于底层的安全调用。 除了call调用应该还有其他不安全的跨合约调用,审计的时候会遇到。但我学习solidity的第四天,
还不知道有哪些不安全的跨合约调用。

非常好的学习总结!你理解得很到位。我来帮你澄清 revert 的使用场景,并补充那些”不安全”的跨合约调用。

revert vs require 的明确区别

简单逻辑 vs 复杂逻辑

require (简单逻辑)

1
2
3
4
5
6
// 简单的条件检查
function buyToken(uint amount) external payable {
require(amount > 0, "Amount must be positive");
require(msg.value >= amount * tokenPrice, "Insufficient payment");
// ... 执行逻辑
}

revert (复杂逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 复杂的条件检查 - 使用 revert
function complexOperation(uint a, uint b) external {
bool condition1 = (a > b);
bool condition2 = (a + b < 1000);
bool condition3 = (a * b % 2 == 0);

if (!(condition1 && condition2 && condition3)) {
revert("Complex conditions not satisfied");
}
// ... 执行逻辑
}

// 或者在复杂计算中
function calculateReward(address user) external {
uint reward = 0;

// 复杂的奖励计算逻辑
for (uint i = 0; i < userHistory[user].length; i++) {
reward += userHistory[user][i] * rewardRate;
if (reward > MAX_REWARD) {
revert("Reward exceeds maximum limit");
}
}
}

不安全的跨合约调用(审计重点!)

1. 直接调用(最危险!)

1
2
3
4
5
6
7
8
// 危险!如果 AnotherContract 有恶意代码或漏洞,会影响到当前合约
contract VulnerableContract {
AnotherContract public other;

function unsafeCall() external {
other.someFunction(); // 直接调用
}
}

2. 未检查返回值的底层调用

1
2
3
4
5
6
7
contract VulnerableContract {
function unsafeTransfer(address to, uint amount) external {
// 危险!没有检查 call 是否成功
(bool success, ) = to.call{value: amount}("");
// 缺少 require(success) 检查!
}
}

3. 重入攻击漏洞

1
2
3
4
5
6
7
8
9
10
11
contract VulnerableBank {
mapping(address => uint) public balances;

// 危险!存在重入攻击风险
function withdraw() external {
uint amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // 太晚了!攻击者可以重入
}
}

安全的跨合约调用模式

1. 使用 Interface(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
interface IERC20 {
function transfer(address to, uint amount) external returns (bool);
}

contract SafeContract {
IERC20 public token;

function safeTransfer(address to, uint amount) external {
bool success = token.transfer(to, amount);
require(success, "Transfer failed");
}
}

2. 安全的底层调用

1
2
3
4
5
6
7
8
9
10
11
12
contract SafeContract {
function safeTransfer(address to, uint amount) external {
(bool success, bytes memory data) = to.call{value: amount}("");
require(success, "Transfer failed");
}

function safeCall(address contractAddr, bytes memory data) external {
(bool success, bytes memory returnData) = contractAddr.call(data);
require(success, "Call failed");
// 还可以解析 returnData
}
}

3. 防重入模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract SafeBank {
mapping(address => uint) public balances;
bool private locked;

modifier noReentrant() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}

function safeWithdraw() external noReentrant {
uint amount = balances[msg.sender];
balances[msg.sender] = 0; // 先更新状态!
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
}
}

审计检查清单

当你审计合约时,重点关注:

  1. ✅ 所有外部调用都检查返回值
  2. ✅ 使用防重入保护
  3. ✅ 优先使用 Interface 而非底层 call
  4. ❌ 避免直接合约调用(AnotherContract().function())
  5. ❌ 避免未检查的 delegatecall

你现在对 revert 的理解如何?还有什么具体的使用场景想要讨论吗?

python里有try except,solidity里有require和revert,是不是有些类似呢?

很好的观察!确实有些相似之处,但也有很多重要的区别。让我来详细对比一下:

相似之处:错误处理的基本模式

Python 的 try-except

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def transfer_funds(amount):
try:
if amount <= 0:
raise ValueError("Amount must be positive")

if balance < amount:
raise InsufficientFunds("Not enough balance")

# 正常执行逻辑
process_transfer(amount)

except ValueError as e:
print(f"输入错误: {e}")
return False
except InsufficientFunds as e:
print(f"资金错误: {e}")
return False
except Exception as e:
print(f"未知错误: {e}")
return False

return True

Solidity 的 require/revert

1
2
3
4
5
6
7
8
9
function transferFunds(uint amount) external returns (bool) {
// 类似 Python 中的条件检查 + raise
require(amount > 0, "Amount must be positive");
require(balance >= amount, "Not enough balance");

// 正常执行逻辑
processTransfer(amount);
return true;
}

关键区别

1. 执行模型不同

1
2
3
4
5
6
// Solidity: 全部回滚
function solidityExample() external {
stateVariable = 100; // 会被回滚
require(false, "Error occurred");
// 整个交易回滚,包括 stateVariable = 100
}
1
2
3
4
5
6
7
8
# Python: 局部处理
def python_example():
state_variable = 100 # 这个修改会保留
try:
raise Exception("Error occurred")
except:
pass # 错误被捕获,继续执行
print(state_variable) # 输出: 100

2. 错误恢复机制

1
2
3
4
5
6
7
8
9
10
// Solidity: 状态完全回滚 +  gas 费扣除
contract Bank {
uint public totalSupply;

function transferWithRevert() external {
totalSupply = 1000; // 会被回滚
revert("Something went wrong");
// 用户支付 gas,状态完全回滚
}
}
1
2
3
4
5
6
7
8
9
10
11
12
# Python: 局部恢复,程序继续
class Bank:
def __init__(self):
self.total_supply = 0

def transfer_with_except(self):
self.total_supply = 1000 # 这个修改会保留
try:
raise Exception("Something went wrong")
except Exception as e:
print(f"Error handled: {e}")
print(f"Total supply: {self.total_supply}") # 输出: 1000

3. 错误信息处理

1
2
3
4
5
6
// Solidity: 错误信息在链上可查,但消耗 gas
function detailedRevert(uint x) external {
if (x > 100) {
revert("Value too large: provided value exceeds maximum limit");
}
}
1
2
3
4
# Python: 错误信息丰富,不额外消耗资源
def detailed_except(x):
if x > 100:
raise ValueError(f"Value too large: {x} exceeds maximum limit of 100")

Solidity 特有的模式

1. 自定义错误(节省 gas)

1
2
3
4
5
6
7
8
9
10
11
12
// 定义自定义错误
error InsufficientBalance(address user, uint available, uint required);

function transfer(uint amount) external {
if (balance[msg.sender] < amount) {
revert InsufficientBalance({
user: msg.sender,
available: balance[msg.sender],
required: amount
});
}
}

2. 复杂条件中的 revert

1
2
3
4
5
6
7
8
9
function complexValidation(User memory user) external {
bool isValid = (user.age >= 18) &&
(user.balance > 0) &&
(bytes(user.name).length > 0);

if (!isValid) {
revert("User validation failed");
}
}

实际开发中的对应关系

Python 模式 Solidity 对应 说明
try-except require/revert 基本错误处理
raise Exception("msg") revert("msg") 主动抛出错误
if condition: raise require(condition, "msg") 条件检查
多个 except 多个 require 多重条件检查
finally 无直接对应 Solidity 无 finally

总结

是的,你的直觉是对的! require/revert 确实类似于 Python 中的条件检查 + raise,但:

  1. 执行结果不同:Solidity 是整个交易回滚,Python 是局部异常处理
  2. 成本模型不同:Solidity 错误会消耗 gas,Python 不会
  3. 恢复机制不同:Solidity 是状态机回滚,Python 是程序流控制

这种理解对你学习 Solidity 很有帮助,但要记住区块链的”全有或全无”特性!

昨日学习回顾2

在remixai assistant那里,我看到了一个提问:”storage,memory,calldata的区别是什么“,看起来这个问题是solidity非常关键且重要的问题。
我的回答如下,不一定准确。storage在链上永久存储,gas最贵。 另外两个gas便宜,memory是临时存储,可读可写, calldata只可读。
推荐使用calldata。
然后在deployed contracts中,callbyinterface的下方有calldata,点击copy calldata to clipboard,就得到了一串十六进制的字符串。
它是是“4 字节函数选择器 + ABI 编码参数”,我不知道abi是什么? 以及calldata和call调用有区别吗,还是说其实是一样的概念?

你的理解基本正确!我来详细解释这些概念,特别是ABI和calldata的关系。

你的理解总结

完全正确:

  • storage: 链上永久存储,gas最贵
  • memory: 临时存储,函数内可读写
  • calldata: 只读,最便宜,推荐用于外部函数参数

ABI 是什么?

ABI (Application Binary Interface) 就像合约的”使用说明书”,它定义了:

  • 如何调用合约函数
  • 函数参数如何编码
  • 返回值如何解码

实际例子

1
2
3
4
5
6
// 合约代码
contract MyContract {
function transfer(address to, uint256 amount) external {
// 转账逻辑
}
}

对应的ABI:

1
2
3
4
5
6
7
8
9
{
"name": "transfer",
"type": "function",
"inputs": [
{"name": "to", "type": "address"},
{"name": "amount", "type": "uint256"}
],
"outputs": []
}

Calldata 详解

1. 函数参数位置

1
2
3
4
5
6
7
function example(
uint256 a, // 默认是 calldata (external函数)
string memory b, // 显式指定 memory
bytes calldata c // 显式指定 calldata
) external {
// a 和 c 在 calldata 中,b 在 memory 中
}

2. 为什么推荐 calldata

1
2
3
4
5
6
7
8
9
10
11
// 好:使用 calldata,节省 gas
function processData(bytes calldata data) external {
// 直接读取 calldata,不复制到 memory
require(data.length > 0);
}

// 不好:使用 memory,消耗更多 gas
function processData(bytes memory data) external {
// 数据被复制到 memory,消耗 gas
require(data.length > 0);
}

Calldata 字符串解析

你复制的 0x... 字符串就是 ABI 编码的调用数据

解码示例

假设函数调用:transfer(0x1234..., 1000)

编码过程:

  1. 函数选择器 = keccak256("transfer(address,uint256)") 的前4字节
  2. 参数编码 = 地址 + 数值的ABI编码
  3. 最终calldata = 选择器 + 参数编码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 实际例子
// 函数: transfer(address,uint256)
// 参数: to=0x5B38Da6a701c568545dCfcB03FcB875f56beddC4, amount=1000

// 计算函数选择器
selector = bytes4(keccak256("transfer(address,uint256)")) // 0xa9059cbb

// ABI编码参数
params = abi.encode(
0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,
1000
)

// 最终calldata
calldata = selector + params
// 结果: 0xa9059cbb0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc400000000000000000000000000000000000000000000000000000000000003e8

Calldata vs Call

这是两个相关但不同的概念:

Calldata (数据区域)

1
2
3
4
5
// calldata 是存储函数调用数据的内存区域
function readCalldata(bytes calldata data) external {
// 这里的数据就在 calldata 区域
uint length = data.length;
}

Call (操作)

1
2
3
4
5
6
7
8
9
10
// call 是进行外部调用的底层操作
function makeCall(address target) external {
// 使用 call 进行底层调用
(bool success, ) = target.call{value: 1 ether}("");
require(success);

// 带 calldata 的 call
bytes memory data = abi.encodeWithSignature("transfer(address,uint256)", msg.sender, 100);
(success, ) = target.call(data);
}

实际开发中的使用

1. 函数参数(推荐 calldata)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ✅ 好:外部函数使用 calldata
function processUserData(
string calldata name,
uint256[] calldata ids,
bytes calldata signature
) external {
// 直接读取,不复制
}

// ❌ 不好:不必要地使用 memory
function processUserData(
string memory name, // 浪费 gas
uint256[] memory ids, // 浪费 gas
bytes memory signature // 浪费 gas
) external {
// 数据被复制到 memory
}

2. 返回值(必须 memory)

1
2
3
4
function getData() external view returns (string memory, uint256[] memory) {
// 返回值必须用 memory
return ("hello", new uint256[](3));
}

3. 内部处理(需要 memory)

1
2
3
4
5
6
7
function processInternally(bytes calldata input) external {
// 如果需要修改数据,必须复制到 memory
bytes memory mutableData = input;
mutableData[0] = 0x01; // 可以修改

// input[0] = 0x01; // ❌ 错误!calldata 不可写
}

总结

  • ABI: 合约的调用规范,定义如何编码/解码数据
  • Calldata: 存储外部调用数据的只读区域
  • Call: 进行外部调用的操作
  • 十六进制字符串: ABI编码的实际调用数据

你现在对这几个概念的理解如何?还有什么具体的使用场景想要了解吗?

昨日学习回顾3

然后我学习了mapping键值对结构,虽然有点像python的字典,但是不同的概念。mapping键值对结构不支持遍历,安全性好。

最后我学习了modifier(修饰器),它类似于门卫,在检查通过后才允许执行函数。 但我不理解它和require的区别? require似乎也要在函数开始前检查是否符合,否则要revert。

很好的观察!modifier 和 require 确实在功能上有重叠,但它们的定位和使用场景完全不同。让我用具体的例子来解释:

基础区别

require (条件检查)

1
2
3
4
5
6
7
8
function withdraw(uint amount) external {
require(amount > 0, "Amount must be positive");
require(balance[msg.sender] >= amount, "Insufficient balance");

// 业务逻辑
balance[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}

modifier (访问控制)

1
2
3
4
5
6
7
8
9
10
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_; // 这里插入原函数体
}

function withdraw(uint amount) external onlyOwner {
// 业务逻辑
balance[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
}

核心区别:代码复用与组合

1. modifier 实现复杂访问控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 多个条件组合的 modifier
modifier validWithdrawal(uint amount) {
require(amount > 0, "Amount must be positive");
require(balance[msg.sender] >= amount, "Insufficient balance");
require(!isFrozen[msg.sender], "Account frozen");
require(block.timestamp >= lastWithdrawTime[msg.sender] + 1 days, "Too frequent");
_;
}

// 使用 - 一行搞定所有检查!
function withdraw(uint amount) external validWithdrawal(amount) {
balance[msg.sender] -= amount;
lastWithdrawTime[msg.sender] = block.timestamp;
payable(msg.sender).transfer(amount);
}

function transfer(address to, uint amount) external validWithdrawal(amount) {
// 同样的检查逻辑,无需重复编写
balance[msg.sender] -= amount;
balance[to] += amount;
}

2. modifier 支持参数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 带参数的 modifier
modifier minimumAmount(uint minAmount) {
require(msg.value >= minAmount, "Amount too small");
_;
}

modifier duringTimeRange(uint start, uint end) {
require(block.timestamp >= start, "Too early");
require(block.timestamp <= end, "Too late");
_;
}

// 组合使用多个 modifier
function participate()
external
payable
minimumAmount(0.1 ether)
duringTimeRange(startTime, endTime)
{
participants[msg.sender] = true;
}

3. modifier 可以修改执行流程

1
2
3
4
5
6
7
8
9
10
11
12
// modifier 可以在函数前后插入逻辑
modifier withFee() {
uint fee = msg.value / 10; // 10% 手续费
feeCollector.transfer(fee);
_; // 执行原函数
emit FeeCharged(msg.sender, fee); // 函数执行后触发事件
}

function premiumService() external payable withFee {
// 用户支付的钱已经自动扣除了手续费
premiumUsers[msg.sender] = true;
}

实际项目中的最佳实践

场景1:权限管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
contract Bank {
address public owner;
mapping(address => bool) public managers;

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

modifier onlyManager() {
require(managers[msg.sender], "Only manager");
_;
}

// 清晰的分层权限
function setManager(address manager) external onlyOwner {
managers[manager] = true;
}

function freezeAccount(address user) external onlyManager {
frozen[user] = true;
}
}

场景2:重入攻击防护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract SecureContract {
bool private locked;

modifier noReentrant() {
require(!locked, "No reentrancy");
locked = true;
_;
locked = false;
}

function withdraw() external noReentrant {
// 自动防重入保护
payable(msg.sender).transfer(address(this).balance);
}
}

场景3:复杂业务规则

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
contract Auction {
modifier onlyDuringAuction() {
require(block.timestamp >= auctionStart, "Auction not started");
require(block.timestamp <= auctionEnd, "Auction ended");
_;
}

modifier onlyHighestBidder() {
require(msg.sender == highestBidder, "Not highest bidder");
_;
}

modifier bidIncrease() {
require(msg.value > highestBid * 105 / 100, "Bid too low"); // 至少提高5%
_;
}

// 清晰的业务逻辑
function placeBid()
external
payable
onlyDuringAuction
bidIncrease
{
// 所有条件自动检查
refundPreviousBidder();
highestBidder = msg.sender;
highestBid = msg.value;
}
}

什么时候用 require?什么时候用 modifier?

使用 require 的情况:

1
2
3
4
5
6
7
8
9
10
// 简单的、一次性的条件检查
function calculate(uint a, uint b) external pure returns (uint) {
require(b != 0, "Division by zero"); // 简单的数学检查
return a / b;
}

// 特定业务的独特检查
function customOperation() external {
require(specialCondition, "Special condition not met"); // 不会被复用的检查
}

使用 modifier 的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 会被多个函数复用的检查逻辑
modifier onlyActiveUser() {
require(users[msg.sender].active, "User not active");
require(users[msg.sender].kycCompleted, "KYC not completed");
_;
}

// 涉及状态修改的预处理/后处理
modifier withLogging() {
emit FunctionCalled(msg.sender, msg.sig);
_;
emit FunctionCompleted(msg.sender, msg.sig);
}

// 权限管理和访问控制
modifier onlyRole(bytes32 role) {
require(hasRole(role, msg.sender), "Access denied");
_;
}

总结

  • require: 简单的条件检查,适合单一场景
  • modifier: 复杂的、可复用的访问控制和业务流程组合

简单比喻:

  • require 像是门口的”请勿吸烟”标志
  • modifier 像是需要刷卡+人脸识别+安全检查的”安全通道”

现在你对 modifier 和 require 的区别理解如何?在实际编码中你会如何选择使用?

深入理解 storage / memory / calldata

StorageVsMemory.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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

/**
* @title Storage vs Memory vs Calldata 教学对比实验
* @author Yuki 学习版
* @notice 在 Remix 部署后,你可以用不同函数调用对比结果
*/
contract StorageVsMemory {
struct User {
string name;
uint age;
}

User public user = User("Alice", 18);

// 修改 storage 变量(会永久改变状态)
function changeStorage() external {
User storage u = user; // 指向同一个 storage 数据
u.age = 99; // 修改将直接写入区块链
}

// 修改 memory 变量(仅函数内部有效,不会保存)
function changeMemory() external view returns (User memory) {
User memory u = user; // 创建一个副本(复制 user 的数据)
u.age = 200; // 修改副本,不影响原始 user
return u; // 返回副本结果
}

// 用 calldata 调用(只读,不可修改)
function showCalldata(User calldata u)
external
pure
returns (string memory, uint)
{
// u.age = 1; ❌ 报错:calldata 是只读的
return (u.name, u.age);
}

// 辅助函数:查看链上 user 数据
function getUser() external view returns (string memory, uint) {
return (user.name, user.age);
}
}

🧪 Remix 实验步骤(强烈建议你自己动手试)

第一步:部署合约

点击 Deploy。

初始状态:

getUser() → (“Alice”, 18)

第二步:调用 changeMemory()
返回值 → (“Alice”, 200)

再调用:

getUser() → (“Alice”, 18)

🟡 结论:
memory 创建了副本,只在函数里改动,不影响链上数据。

第三步:调用 changeStorage()
调用成功。

再查看:

getUser() → (“Alice”, 99)

🟢 结论:
storage 修改了链上数据,永久保存。

第四步:测试 showCalldata

参数输入:

[“Bob”, 30]

返回结果:

(“Bob”, 30)

尝试在函数中修改 u.age 会报错(calldata 是只读的)。

测试

点击changeMemory,返回[call]from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4to: StorageVsMemory.changeMemory()data: 0x92e…32b11 ,decoded_output等于{
“0”: “tuple(string,uint256): Alice,200”
}

✅ 太好了!你观察到的这一行结果,正是 Solidity 在 Remix 中返回的 memory 副本结果
我们来详细拆解一下它的意义,让你彻底理解背后的逻辑。


🧩 你看到的内容解析

1
2
3
4
5
6
7
8
[call]
from: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
to: StorageVsMemory.changeMemory()
data: 0x92e...32b11
decoded_output:
{
"0": "tuple(string,uint256): Alice,200"
}

逐项解释如下👇

字段 含义
from 你的账户地址(msg.sender)
to 调用的合约函数 StorageVsMemory.changeMemory()
data 编译后的函数选择器 + ABI 编码参数(十六进制形式)
decoded_output Remix 自动解码的返回值结果

🧠 decoded_output 详解

1
"tuple(string,uint256): Alice,200"

这一句表示:

  • 函数返回了一个 结构体(tuple)

  • 结构体的类型为 (string,uint256)

  • 对应值是:

    • name = "Alice"
    • age = 200

也就是说:

你调用 changeMemory(),函数复制了 user(Alice,18)到 memory 中;
然后把副本的 age 改成了 200;
最后返回这个修改后的 副本

但这只是返回值,不会影响区块链上真正的存储内容


🧪 验证这个结论

立刻再调用:

1
getUser()

你会看到:

1
("Alice", 18)

说明:

链上的原始数据 (storage) 并没有被修改。
memory 只是函数执行时的一份临时副本。


🔬 延伸理解:为什么要区分 storage / memory?

场景 推荐使用 原因
改状态变量(如余额、所有权) storage 要永久保存
函数内计算或复制数据 memory 快、便宜
只读外部参数(如外部调用传入的大数组) calldata 最省 gas,防止误修改

💡 总结一句话:

storage 是链上房子(改了永久变),
memory 是临时拍的照片(改了不影响原图),
calldata 是别人发来的快照(你连编辑都不行)。


是否想继续做第二个实验:
👉「数组版对比:在 storage / memory 中修改 uint[] 数组」
你可以清楚看到引用型变量的差异(尤其是 push、pop 操作的行为)。

问题

问题一,这个代码指定的编译器是 pragma solidity ^0.8.24;
但我编译选择的编译器是0.8.31,于是编译遇到了一些bug

1
2
3
4
5
6
7
call to StorageVsMemory.changeMemory errored: Error occurred: invalid opcode.

invalid opcode

The execution might have thrown OR the EVM version used by the selected environment is not compatible with the compiler EVM version.

If the transaction failed for not having enough gas, try increasing the gas limit gently.

不同的编译器有什么影响?在真实工作的时候应该选哪个编译器?

非常好的问题 👏👏
这其实是 Solidity 开发里一个非常重要、且经常让初学者困惑的点。
你看到的错误:

1
2
invalid opcode
The execution might have thrown OR the EVM version used by the selected environment is not compatible with the compiler EVM version.

不是你的代码写错,而是——编译器版本与执行环境不匹配导致的。
下面我一步步帮你理清楚。


🧩 一、pragma 指令是什么意思?

1
pragma solidity ^0.8.24;

这一句的意思是:

“本合约可以在 Solidity 0.8.24 及以上版本(但小于 0.9.0)编译。”

⚠️ 也就是说:

  • ✅ 允许 0.8.24, 0.8.25, 0.8.30, 0.8.31
  • ❌ 不允许 0.9.x 或更低的 0.8.0

所以你的 0.8.31 是合法的。
但为什么还是报错?问题不在 pragma,而在执行环境的 EVM 配置


⚙️ 二、编译器版本与 EVM 版本的关系

Solidity 是高级语言,编译后会生成 EVM 字节码(Ethereum Virtual Machine code)。
不同 Solidity 版本编译出的指令集略有不同。

例如:

  • Solidity 0.8.20 引入了新的 PUSH0 指令(EIP-3855)。
    👉 某些旧的 EVM 环境(如 Remix VM old 或 Ganache old)不支持 PUSH0

  • 这时运行时就会出现:

    1
    invalid opcode

    因为旧的虚拟机根本不认识 PUSH0 这个新指令。


🧪 三、解决方案

✅ 方案 1:升级 EVM 环境(推荐)

在 Remix 右上角:

“Environment” → 选择 Remix VM (Shanghai) 或更高版本。

确保支持最新 EVM(建议至少 Shanghai 或 Cancun)。


✅ 方案 2:降低编译器版本

如果你的 EVM 环境太旧(例如一些私链、测试框架),
可以改成:

1
pragma solidity ^0.8.19;

然后在 Remix 选择 Compiler 0.8.19
这时编译器会生成旧格式的字节码,旧 EVM 也能跑。


✅ 方案 3:生产环境最佳实践

场景 建议做法
学习 / 测试 用最新稳定版(例如 0.8.31)+ Remix Shanghai
公司项目 / 审计项目 锁定具体版本(如 pragma solidity 0.8.21;),不要用 ^,避免升级破坏
开源项目 在 README 中注明编译器版本(如:“Tested with Solidity 0.8.24”)

💡 原因:不同版本在 gas、opcode、内置函数(例如 abi.encodeCall)上会有差异,
在审计和部署时保持一致的编译器是非常重要的。


🧠 四、工作中选编译器的标准答案

场景 编译器版本选择
新项目 最新稳定版(≥0.8.20),支持 PUSH0,节省 gas
审计旧合约 使用原项目指定版本(完全一致)
部署到主网 锁定版本号(如 pragma solidity 0.8.21;
教学 / 实验 最新版(如 0.8.31)即可,但 EVM 环境要匹配 Shanghai 或更高

✅ 总结一句话:

“invalid opcode” 通常不是代码错,而是 编译器版本 > EVM 环境版本 导致的兼容性问题。
在真实工作中,应 锁定特定 Solidity 版本,并确保 EVM 支持相同指令集


如果你想,我可以帮你写一段短代码:
👉 在不同编译器版本下展示 “PUSH0” 的差异(即为什么 0.8.20+ 编译出的字节码在旧 EVM 报 invalid opcode)。
要我展示一下这个对比实验吗?