昨日学习回顾 昨天我学习了错误处理,包括require,revert和assert。 assert只有确保代码正确的时候才使用,如果报错了说明代码有bug。 assert不用于检查用户的输入是否正确。 require是使用最多的错误处理,检查满足条件才继续执行。 revert推荐在复杂逻辑中使用,但我还是不太明确复杂逻辑和简单逻辑中间的区别。
然后我学习了跨合约调用,这是solidity非常重要的一个概念。 interface调用是高级语法糖,看起来逻辑更加clear,推荐使用interface调用 而call属于底层的安全调用。 除了call调用应该还有其他不安全的跨合约调用,审计的时候会遇到。但我学习solidity的第四天, 还不知道有哪些不安全的跨合约调用。
非常好的学习总结!你理解得很到位。我来帮你澄清 revert 的使用场景,并补充那些”不安全”的跨合约调用。
revert vs require 的明确区别简单逻辑 vs 复杂逻辑 require (简单逻辑)
// 简单的条件检查 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. 直接调用(最危险!) // 危险!如果 AnotherContract 有恶意代码或漏洞,会影响到当前合约 contract VulnerableContract { AnotherContract public other; function unsafeCall() external { other.someFunction(); // 直接调用 } }
2. 未检查返回值的底层调用 contract VulnerableContract { function unsafeTransfer(address to, uint amount) external { // 危险!没有检查 call 是否成功 (bool success, ) = to.call{value: amount}(""); // 缺少 require(success) 检查! } }
3. 重入攻击漏洞 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(推荐) 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. 安全的底层调用 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); } }
审计检查清单 当你审计合约时,重点关注:
✅ 所有外部调用都检查返回值
✅ 使用防重入保护
✅ 优先使用 Interface 而非底层 call
❌ 避免直接合约调用(AnotherContract().function())
❌ 避免未检查的 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 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. 执行模型不同 // Solidity: 全部回滚 function solidityExample() external { stateVariable = 100; // 会被回滚 require(false, "Error occurred"); // 整个交易回滚,包括 stateVariable = 100 }
def python_example (): state_variable = 100 try : raise Exception("Error occurred" ) except : pass print (state_variable)
2. 错误恢复机制 // Solidity: 状态完全回滚 + gas 费扣除 contract Bank { uint public totalSupply; function transferWithRevert() external { totalSupply = 1000; // 会被回滚 revert("Something went wrong"); // 用户支付 gas,状态完全回滚 } }
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} " )
3. 错误信息处理 // Solidity: 错误信息在链上可查,但消耗 gas function detailedRevert(uint x) external { if (x > 100) { revert("Value too large: provided value exceeds maximum limit"); } }
def detailed_except (x ): if x > 100 : raise ValueError(f"Value too large: {x} exceeds maximum limit of 100" )
Solidity 特有的模式 1. 自定义错误(节省 gas) // 定义自定义错误 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 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,但:
执行结果不同 :Solidity 是整个交易回滚,Python 是局部异常处理
成本模型不同 :Solidity 错误会消耗 gas,Python 不会
恢复机制不同 :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) 就像合约的”使用说明书”,它定义了:
如何调用合约函数
函数参数如何编码
返回值如何解码
实际例子 // 合约代码 contract MyContract { function transfer(address to, uint256 amount) external { // 转账逻辑 } }
对应的ABI:
{ "name" : "transfer" , "type" : "function" , "inputs" : [ {"name" : "to" , "type" : "address" }, {"name" : "amount" , "type" : "uint256" } ], "outputs" : [] }
Calldata 详解 1. 函数参数位置 function example( uint256 a, // 默认是 calldata (external函数) string memory b, // 显式指定 memory bytes calldata c // 显式指定 calldata ) external { // a 和 c 在 calldata 中,b 在 memory 中 }
2. 为什么推荐 calldata // 好:使用 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)
编码过程:
函数选择器 = keccak256("transfer(address,uint256)") 的前4字节
参数编码 = 地址 + 数值的ABI编码
最终calldata = 选择器 + 参数编码
selector = bytes4(keccak256("transfer(address,uint256)" )) params = abi.encode( 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 , 1000 ) calldata = selector + params
Calldata vs Call 这是两个相关但不同的概念:
Calldata (数据区域) // calldata 是存储函数调用数据的内存区域 function readCalldata(bytes calldata data) external { // 这里的数据就在 calldata 区域 uint length = data.length; }
Call (操作) // 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) function getData() external view returns (string memory, uint256[] memory) { // 返回值必须用 memory return ("hello", new uint256[](3)); }
3. 内部处理(需要 memory) 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 (条件检查) 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 (访问控制) 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 可以修改执行流程 // 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:重入攻击防护 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 的情况: // 简单的、一次性的条件检查 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 ; 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 副本结果 。 我们来详细拆解一下它的意义,让你彻底理解背后的逻辑。
🧩 你看到的内容解析 [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 详解 "tuple(string,uint256): Alice,200"
这一句表示:
也就是说:
你调用 changeMemory(),函数复制了 user(Alice,18)到 memory 中; 然后把副本的 age 改成了 200; 最后返回这个修改后的 副本 。
但这只是返回值,不会影响区块链上真正的存储内容 。
🧪 验证这个结论 立刻再调用:
你会看到:
说明:
链上的原始数据 (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
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 开发里一个非常重要、且经常让初学者困惑 的点。 你看到的错误:
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 指令是什么意思?
这一句的意思是:
“本合约可以在 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 版本编译出的指令集略有不同。
例如:
🧪 三、解决方案 ✅ 方案 1:升级 EVM 环境(推荐) 在 Remix 右上角:
“Environment” → 选择 Remix VM (Shanghai) 或更高版本。
确保支持最新 EVM(建议至少 Shanghai 或 Cancun)。
✅ 方案 2:降低编译器版本 如果你的 EVM 环境太旧(例如一些私链、测试框架), 可以改成:
然后在 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)。 要我展示一下这个对比实验吗?