欢迎继续踏上Solidity大神之路!前四章中我们深入探讨了Solidity的基础与进阶知识。本章将聚焦于更深层次的主题,包括函数签名、低级调用、unchecked关键字、存储原理以及Solidity汇编。这些内容将帮助你更全面地理解智能合约的底层机制,并为编写高效、安全的代码奠定基础
欢迎继续踏上 Solidity 大神之路!在前四章中,我们深入探讨了 Solidity 的基础与进阶知识。本章将聚焦于更深层次的主题,包括函数签名、低级调用、unchecked 关键字、存储原理以及 Solidity 汇编。这些内容将帮助你更全面地理解智能合约的底层机制,并为编写高效、安全的代码奠定基础。
函数签名是 Solidity 中用于标识函数的独特字符串,由函数名及其参数类型组成。它在智能合约交互中至关重要,尤其是在 ABI 编码和低级调用中。
什么是函数 selector?\
函数的 selector 是函数签名的 Keccak-256 哈希的前 4 字节,用于在 EVM 中唯一标识一个函数。函数签名由函数名和参数类型组成,例如 myFunction(uint256,address)
。通过 .selector
属性,可以直接获取函数的 selector。
设计原理:
用途:\ selector 在以下场景中发挥关键作用:
call
、delegatecall
或 staticcall
中,selector 指定要调用的目标函数abi.encodeWithSelector
构造调用数据,调用目标合约函数代码示例:获取和使用 selector:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SelectorExample {
function myFunction(uint256 a, address b) external pure returns (uint256) {
return a;
}
// 返回 Keccak256("myFunction(uint256,address)") 前 4 字节
function getSelector() external pure returns (bytes4) {
return this.myFunction.selector;
}
function computeSelectorManually() external pure returns (bytes4) {
return bytes4(keccak256("myFunction(uint256,address)"));
}
function callWithKnownSelector(
address target,
uint256 a,
address b
) external returns (bool, bytes memory) {
bytes4 selector = this.myFunction.selector;
bytes memory data = abi.encodeWithSelector(selector, a, b);
(bool success, bytes memory result) = target.call(data);
require(success, "Call failed");
return (success, result);
}
function callWithExternalSelector(
bytes4 selector,
address target,
uint256 a,
address b
) external returns (bool, bytes memory) {
bytes memory data = abi.encodeWithSelector(selector, a, b);
(bool success, bytes memory result) = target.call(data);
require(success, "Call failed");
return (success, result);
}
}
注意:
什么是 abi.encodeWithSignature?\
abi.encodeWithSignature
是 Solidity 提供的一个函数,用于根据函数签名(例如 "myFunction(uint256,address)"
)和参数生成 ABI 编码的调用数据。生成的调用数据包含函数的 selector(签名 Keccak-256 哈希的前 4 字节)以及按 ABI 规范编码的参数。这通常与低级调用结合使用,以直接调用另一个合约的指定函数。
设计原理:
call
结合时,返回 success
和 result
,允许手动处理失败好处:
transfer(address,uint256)
等函数代码示例:跨合约调用:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TargetContract {
uint256 public value;
function myFunction(uint256 a, address b) external returns (uint256) {
value = a;
return a;
}
}
contract EncodeWithSignatureExample {
function callWithSignature(
address target,
uint256 value
) external returns (bool, bytes memory) {
bytes memory data = abi.encodeWithSignature(
"myFunction(uint256,address)",
value,
msg.sender
);
(bool success, bytes memory result) = target.call(data);
require(success, "Call failed");
return (success, result);
}
function transferToken(
address token,
address recipient,
uint256 amount
) external returns (bool, bytes memory) {
bytes memory data = abi.encodeWithSignature(
"transfer(address,uint256)",
recipient,
amount
);
(bool success, bytes memory result) = token.call(data);
require(success, "Token transfer failed");
return (success, result);
}
}
注意:
success
函数调用适用性:
bytes4(keccak256("functionName(type1,type2)"))
或 .selector
)。生成的编码数据符合 ABI 规范,适合与 call
、delegatecall
或 staticcall
结合使用代码示例:对比三种编码方式及函数调用:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TargetContract {
uint256 public value;
function setValue(uint256 a, address b) external returns (uint256) {
value = a;
return a;
}
}
contract EncodingComparison {
// 使用 abi.encode 进行函数调用(需手动拼接 selector)
function callWithEncode(
address target,
uint256 a,
address b
) external returns (bool, bytes memory) {
bytes4 selector = bytes4(keccak256("setValue(uint256,address)"));
bytes memory data = abi.encode(a, b); // 仅编码参数
bytes memory callData = abi.encodePacked(selector, data); // 拼接 selector 和参数
(bool success, bytes memory result) = target.call(callData);
require(success, "Call failed");
return (success, result);
}
// 使用 abi.encodePacked 尝试函数调用(不推荐,可能失败)
function callWithEncodePacked(
address target,
uint256 a,
address b
) external returns (bool, bytes memory) {
bytes4 selector = bytes4(keccak256("setValue(uint256,address)"));
bytes memory data = abi.encodePacked(a, b); // 紧密打包参数
bytes memory callData = abi.encodePacked(selector, data); // 拼接 selector
(bool success, bytes memory result) = target.call(callData);
// 注意:执行不报错,返回(false,0x),因为 encodePacked 不符合 ABI 规范
return (success, result);
}
// 使用 abi.encodeWithSignature 进行函数调用(推荐)
function callWithSignature(
address target,
uint256 a,
address b
) external returns (bool, bytes memory) {
bytes memory data = abi.encodeWithSignature(
"setValue(uint256,address)",
a,
b
);
(bool success, bytes memory result) = target.call(data);
require(success, "Call failed");
return (success, result);
}
// 对比三种编码结果
function compareEncoding(
uint256 a,
address b
) external pure returns (bytes memory, bytes memory, bytes memory) {
bytes memory encoded = abi.encode(a, b);
bytes memory packed = abi.encodePacked(a, b);
bytes memory withSignature = abi.encodeWithSignature(
"setValue(uint256,address)",
a,
b
);
return (encoded, packed, withSignature);
}
}
解释:
callWithEncode
:使用 abi.encode
编码参数,需手动拼接 selector,生成标准的 ABI 编码数据,调用成功callWithEncodePacked
:使用 abi.encodePacked
编码参数,紧密打包导致数据长度不符合 ABI 规范,可能导致调用失败(目标函数无法正确解析参数)callWithSignature
:使用 abi.encodeWithSignature
,自动包含 selector 和 ABI 编码参数,最简洁且可靠注意:
abi.encode
进行函数调用时,需确保 selector 正确拼接abi.encodePacked
不适合标准函数调用,仅用于特殊场景(如哈希计算或非 ABI 交互)success
值,确保调用成功abi.encodeWithSelector
使用预计算的 selector 编码参数,适合性能优化。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract EncodeWithSelectorExample {
function callWithSelector(
address target,
uint256 value
) external returns (bool, bytes memory) {
bytes4 selector = bytes4(keccak256("myFunction(uint256,address)"));
bytes memory data = abi.encodeWithSelector(selector, value, msg.sender);
(bool success, bytes memory result) = target.call(data);
require(success, "Call failed");
return (success, result);
}
}
特性 | abi.encodeWithSignature | abi.encodeWithSelector |
---|---|---|
输入 | 函数签名字符串 | 预计算的 selector |
自动生成 selector | 是 | 否 |
使用场景 | 动态调用 | 性能优化 |
可读性 | 高 | 低 |
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SelectorVsSignature {
function encodeBoth(
uint256 a,
address b
) external pure returns (bytes memory, bytes memory) {
bytes memory withSignature = abi.encodeWithSignature(
"myFunction(uint256,address)",
a,
b
);
bytes4 selector = bytes4(keccak256("myFunction(uint256,address)"));
bytes memory withSelector = abi.encodeWithSelector(selector, a, b);
return (withSignature, withSelector);
}
}
低级调用(call
、delegatecall
、staticcall
)通过直接向 EVM 发送调用数据(msg.data
),绕过 Solidity 的类型检查和 ABI 验证,提供高灵活性但风险较高。
设计原理:
(success, result)
,需手动检查调用结果好处:
delegatecall
复用代码,节省部署成本staticcall
确保安全调用 view 或 pure 函数风险:
success
,调用失败不自动回滚call
用于调用目标合约的函数,可附带以太币,修改目标合约状态。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CallExample {
function makeCall(
address target,
uint256 value
) external payable returns (bool, bytes memory) {
bytes memory data = abi.encodeWithSignature("setValue(uint256)", value);
(bool success, bytes memory result) = target.call{value: msg.value}(
data
);
require(success, "Call failed");
return (success, result);
}
}
contract TargetContract {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
解释:
call
向目标地址发送调用数据,执行指定函数(如 setValue
),可转移以太币。transfer
函数。delegatecall
在调用者合约的存储上下文中执行目标合约的代码,适用于代理模式或库合约。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DelegateCallExample {
uint256 public value;
address public libAddress;
function setLib(address _libAddress) external {
libAddress = _libAddress;
}
function updateValue(uint256 _value) external returns (bool, bytes memory) {
bytes memory data = abi.encodeWithSignature(
"setValue(uint256)",
_value
);
(bool success, bytes memory result) = libAddress.delegatecall(data);
require(success, "Delegatecall failed");
return (success, result);
}
}
contract LibraryContract {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
解释:
delegatecall
执行目标合约的代码,但修改调用者合约的存储(如 DelegateCallExample.value
)staticcall
用于只读调用,禁止状态修改,适合查询 view 或 pure 函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StaticCallExample {
function makeStaticCall(
address target,
uint256 value
) external view returns (bool, bytes memory) {
bytes memory data = abi.encodeWithSignature("getValue(uint256)", value);
(bool success, bytes memory result) = target.staticcall(data);
require(success, "Staticcall failed");
return (success, result);
}
}
contract TargetContract {
function getValue(uint256 a) external pure returns (uint256) {
return a * 2;
}
}
解释:
staticcall
确保调用不修改区块链状态,适合查询数据以下表格对比 call
、delegatecall
和 staticcall
的关键区别,帮助开发者选择合适的调用方式:
特性 | call | delegatecall | staticcall |
---|---|---|---|
功能 | 调用目标合约函数,可附带以太币 | 调用目标合约代码,使用调用者存储上下文 | 调用目标合约函数,禁止状态修改 |
存储上下文 | 目标合约的存储 | 调用者合约的存储 | 目标合约的存储(但只读) |
msg.sender | 调用者的 msg.sender |
调用者的 msg.sender |
调用者的 msg.sender |
msg.value | 可转移以太币 | 继承调用者的 msg.value ,不可直接转移 |
不支持以太币转移 |
状态修改 | 允许修改目标合约状态 | 允许修改调用者合约状态 | 禁止任何状态修改 |
EVM 操作码 | CALL |
DELEGATECALL |
STATICCALL |
用途 | 常规跨合约调用(如 ERC20 转账) | 代理模式、库合约(代码复用) | 只读查询(如 view 函数) |
风险 | 重入攻击、目标合约恶意代码 | 存储布局不匹配导致数据覆盖 | 误调用状态修改函数会导致失败 |
返回值 | (bool success, bytes memory result) |
(bool success, bytes memory result) |
(bool success, bytes memory result) |
解释:
call
:通用调用,适合跨合约交互,可修改状态或转移以太币,需防范重入攻击delegatecall
:用于代理或库模式,目标代码操作调用者的存储,需确保存储布局一致staticcall
:专为只读操作设计,安全高效,适合查询数据注意:
success
值,失败不会自动回滚delegatecall
时,需确保目标合约与调用者合约的存储布局兼容staticcall
仅用于 view
或 pure
函数,尝试修改状态会导致调用失败unchecked
关键字(Solidity 0.8.0+)禁用算术溢出检查,优化 gas 消耗。
设计原理:
unchecked
允许在安全场景下跳过检查好处:
风险:需确保输入不会溢出,否则可能引发严重漏洞。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract UncheckedExample {
function safeAdd(uint256 a, uint256 b) external pure returns (uint256) {
return a + b; // 默认溢出检查
}
function unsafeAdd(uint256 a, uint256 b) external pure returns (uint256) {
unchecked {
return a + b; // 无溢出检查
}
}
function sumUnchecked(uint256 n) external pure returns (uint256) {
uint256 total = 0;
unchecked {
for (uint256 i = 0; i < n; i++) {
total += i; // 节省每次循环的溢出检查
}
}
return total;
}
}
优化示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract SafeUnchecked {
function incrementCounter(uint256 count) external pure returns (uint256) {
require(count < 1000, "Input too large"); // 手动验证
uint256 total = 0;
unchecked {
for (uint256 i = 0; i < count; i++) {
total += 1;
}
}
return total;
}
}
定义:\
Solidity 的 Storage 是指存储在区块链上的持久化数据,构成了智能合约的状态,保存在 EVM 的 状态树(state trie) 中,而非传统物理磁盘(如硬盘或 SSD)。每个合约拥有独立的存储空间,理论上可寻址 2^256 个 32 字节插槽(从 0 到 2^256 - 1),由所有以太坊全节点维护以确保共识。实际存储量取决于合约中定义的状态变量和动态数据(如数组、映射)的使用情况,采用稀疏存储,仅非零值占用空间。修改存储触发状态更新,消耗大量 gas(例如,sstore
首次写入 20,000 gas)。与临时性的 memory(函数执行期间)、calldata(交易输入数据)和 stack(EVM 执行时的临时寄存器)不同,storage 是唯一在链上持久化的数据位置。
设计原理:
uint8
)打包到同一个插槽,减少存储占用,降低 gas 成本好处:
注意:
sstore
)成本高,需谨慎设计Solidity 使用四种数据位置:storage、memory、calldata 和 stack,各有不同用途、生命周期和成本。
定义:持久化存储在区块链状态树中,理论上可寻址 2^256 个 32 字节插槽,由全节点维护。
设计原理:
好处:持久性、可审计性、支持复杂数据结构。
定义:临时存储,仅在函数执行期间存在,数据存储在 EVM 内存中,执行后销毁。
设计原理:
0x40
)管理mstore
)仅消耗 3-10 gas好处:高效、安全、支持动态数据。
定义:只读存储,包含交易输入数据(如 selector 和参数),由调用方提供。
设计原理:
msg.data
访问,适合传递参数calldataload
)约 3 gas好处:高效、安全、标准化。
定义:EVM 运行时栈,存储临时变量和操作数,最大深度 1024,每个元素 32 字节。
设计原理:
PUSH
、POP
)成本极低(2-5 gas)好处:高性能、简洁、底层控制。
对比总结:
数据位置 | 持久性 | 读写性 | Gas 成本 | 用途 |
---|---|---|---|---|
Storage | 永久(链上) | 可读写 | 高(20,000 gas 写入) | 持久化状态 |
Memory | 临时(函数执行) | 可读写 | 低(3-10 gas) | 临时数据 |
Calldata | 临时(交易输入) | 只读 | 低(约 3 gas) | 函数参数 |
Stack | 瞬时(指令执行) | 可读写 | 极低(2-5 gas) | EVM 计算 |
代码示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DataLocationExample {
uint256 public storedValue; // storage,链上存储
function processData(uint256[] calldata input) external returns (uint256) {
uint256 sum; // stack,临时变量
uint256[] memory tempArray = new uint256[](input.length); // memory,临时数组
for (uint256 i = 0; i < input.length; i++) {
tempArray[i] = input[i]; // 从 calldata 拷贝到 memory
sum += input[i]; // stack 操作
}
storedValue = sum; // 更新 storage
return sum;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageExample {
uint256 public a; // 插槽 0,链上存储
uint128 public b; // 插槽 1(低 16 字节)
uint128 public c; // 插槽 1(高 16 字节)
uint256 public d; // 插槽 2,链上存储
function getSlotValues() external view returns (uint256, uint256, uint256) {
return (a, b, c);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract DynamicStorage {
uint256[] public arr; // 插槽 0 存储长度,元素存储在 keccak256(0) 开始
mapping(address => uint256) public balances; // 插槽 1,键值对存储在 keccak256(key . 1)
function pushToArray(uint256 value) external {
arr.push(value); // 更新链上存储
}
function setBalance(address user, uint256 amount) external {
balances[user] = amount; // 更新链上存储
}
function getArrayElement(uint256 index) external view returns (uint256) {
return arr[index]; // 读取链上存储
}
}
优化示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract StorageOptimization {
mapping(address => uint256) public balances;
function clearBalance(address user) external {
delete balances[user]; // 置零退还 gas
}
}
除了常规的 Storage(持久化存储),EVM 还支持 Code 存储(存储合约字节码)和 Transient Storage(EIP-1153 引入的临时存储)。这两种存储方式与常规 Storage 共同构成了 EVM 的存储模型,适用于不同场景。
定义:\ Code 存储是指智能合约的运行时字节码(runtime bytecode),存储在区块链的 Code 区域,与合约的 Storage 分开。部署合约时,部署字节码(constructor bytecode)执行后生成运行时字节码,存储在合约地址的 Code 区域,由 EVM 加载执行。
设计原理:
EXTCODECOPY
、EXTCODESIZE
等操作码访问,运行时由 EVM 加载到内存好处:
用途:
注意:
定义:\
Transient Storage(临时存储)由 EIP-1153 引入(以太坊 Cancun 升级,2024 年),是一种仅在单次交易中有效的存储机制,通过 TSTORE
和 TLOAD
操作码操作。数据存储在独立的临时存储空间,交易结束后清空,不写入区块链状态树。
设计原理:
TSTORE
和 TLOAD
成本较低(约 100 gas 和 100 gas),远低于 SSTORE
(20,000 gas)delegatecall
和子调用共享相同的 Transient Storage 上下文好处:
用途:
代码示例:使用 Transient Storage:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20; // 需支持 EIP-1153
contract TransientStorageExample {
// Transient Storage 插槽(任意选择,示例用 0)
uint256 constant TRANSIENT_SLOT = 0;
// 只是示例代码,实际中这里可能重入导致无法得到期待的值,因为可能在其他地方清除了值
function setTransientValue(uint256 value) external {
// 使用内联汇编写入 Transient Storage
assembly {
tstore(TRANSIENT_SLOT, value)
}
}
function getTransientValue() external view returns (uint256) {
// 使用内联汇编读取 Transient Storage
uint256 value;
assembly {
value := tload(TRANSIENT_SLOT)
}
return value;
}
// 示例:防止重入攻击
function nonReentrantCall(
address target,
uint256 value
) external returns (bool, bytes memory) {
// 检查临时锁
uint256 lock;
assembly {
lock := tload(TRANSIENT_SLOT)
}
require(lock == 0, "Reentrancy check");
// 设置临时锁
assembly {
tstore(TRANSIENT_SLOT, 1)
}
// 执行低级调用
bytes memory data = abi.encodeWithSignature("setValue(uint256)", value);
(bool success, bytes memory result) = target.call(data);
// 清除临时锁
assembly {
tstore(TRANSIENT_SLOT, 0)
}
require(success, "Call failed");
return (success, result);
}
}
解释:
tstore
和 tload
操作码直接操作 Transient Storage,插槽索引为 256 位整数nonReentrantCall
使用 Transient Storage 实现简单的重入锁,交易结束后锁自动清零注意:
特性 | Storage | Transient Storage | Code 存储 |
---|---|---|---|
持久性 | 永久(链上状态树) | 临时(单次交易有效) | 永久(链上 Code 区域) |
存储位置 | 状态树(state trie) | 临时存储空间(交易上下文) | Code 区域(CodeHash) |
插槽容量 | 2^256 个 32 字节插槽 | 2^256 个 32 字节插槽 | 字节码大小(最大 24KB) |
操作方式 | SSTORE / SLOAD |
TSTORE / TLOAD |
EXTCODECOPY / EXTCODESIZE |
Gas 成本 | 高(首次写入 20,000 gas) | 低(约 100 gas) | 读取低(约 700 gas),部署高 |
读写性 | 可读写 | 可读写 | 只读(部署后不可改) |
用途 | 持久化状态(如余额、所有权) | 临时数据(如重入锁、中间状态) | 存储合约字节码 |
风险 | 高 gas 成本、状态膨胀 | 插槽冲突、依赖网络支持 | 字节码大小限制、不可升级 |
解释:
Solidity 汇编(Inline Assembly)允许直接使用 EVM 操作码,提供极高灵活性和性能。
设计原理:
好处:
ADD
、MUL
)减少开销风险:汇编代码难以调试,需深入理解 EVM。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract AssemblyExample {
function add(uint256 a, uint256 b) external pure returns (uint256 result) {
assembly {
result := add(a, b) // EVM ADD 操作码
}
}
function computeHash(bytes32 input) external pure returns (bytes32) {
bytes32 hash;
assembly {
mstore(0, input)
hash := keccak256(0, 32)
}
return hash;
}
function getCalldata() external pure returns (uint256) {
uint256 value;
assembly {
// 从调用数据(calldata)的偏移量 4 字节处读取 32 字节(256 位)的数据,并存储到 value 中
// 偏移量 4 是为了跳过调用数据的函数选择器(function selector)
// 这里会返回0,因为 getCalldata() 没有参数,calldata 只包含 4 字节的函数选择器
value := calldataload(4)
}
return value;
}
function storeValue(uint256 slot, uint256 value) external {
assembly {
sstore(slot, value) // 写入存储插槽
}
}
}
优化示例:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract BatchMemory {
// 批量写入函数,接收一个uint256数组,计算其哈希值
function batchWrite(
uint256[] memory values
) external pure returns (bytes32) {
// 声明一个bytes32变量用于存储哈希值
bytes32 hash;
assembly {
// 获取空闲内存指针
let ptr := mload(0x40)
// 遍历输入数组
for {
let i := 0
} lt(i, mload(values)) {
i := add(i, 1)
} {
// 将数组元素写入内存,从ptr开始,偏移i*32字节
mstore(
add(ptr, mul(i, 32)),
mload(add(values, add(32, mul(i, 32))))
)
}
// 计算从ptr开始的数组数据的keccak256哈希值
hash := keccak256(ptr, mul(mload(values), 32))
// 更新空闲内存指针
mstore(0x40, add(ptr, mul(mload(values), 32)))
}
// 返回计算得到的哈希值
return hash;
}
}
<!--StartFragment-->
数据完整性验证:
Merkle 树或数据结构的前置处理:
Gas 优化的内存操作:
链上链下交互:
在链上链下混合系统中,链下生成的数据可以通过哈希值在链上验证。这段代码提供了一种高效的方式来计算链上数据的哈希,用于与链下哈希比对。
加密承诺或签名验证:
哈希值可作为加密承诺(commitment)的一部分,例如在投票、抽奖或隐私保护协议中,链下生成数据,链上验证其哈希
去中心化应用(DApp):
批量数据处理:
链上验证工具:
优化合约性能:
本章深入探讨了函数签名、低级调用、unchecked 关键字、存储原理和汇编。这些知识点构成了 Solidity 开发的核心,涵盖了从 ABI 编码到 EVM 底层操作的方方面面,值得细细琢磨。函数签名部分详细讲解了 selector 的生成与使用,揭示了跨合约调用的动态机制;低级调用通过 call、delegatecall 和 staticcall 提供了灵活的交互方式;unchecked 关键字展示了 gas 优化的可能性;存储原理深入剖析了 Storage、Code 存储和 Transient Storage 的特性和应用场景;汇编则赋予开发者直接操控 EVM 的能力。所有章节均包含正确代码示例和注意事项。学习这些内容需多写代码,结合成功项目的实际案例,举一反三,加深理解。Solidity 开发不可急于求成,需一步一个脚印,通过持续实践和调试,逐步掌握智能合约的精髓,继续迈向 Solidity 大神的境界!
代码仓库https://github.com/BraisedSix/Solidity-Learn
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!