university.alchemy
学习资料: https://university.alchemy.com/course/solidity/
安全最大化
gas最小化
EVM opcodes: https://www.evm.codes/ 是什么
简单来说,evm.codes 是一个用于查看和学习 Ethereum Virtual Machine(EVM)操作码(opcodes)的交互式参考网站/工具。(EVM Codes)
✅ evm.codes 是什么
- evm.codes 提供了一个完整的 EVM opcode 表 — 列出所有 EVM 支持的基本指令(操作码)、对应的十六进制值 (0x00–0xFF)、以及它们的功能/含义。(EVM Codes)
- 除了静态表,它还是“交互式 (interactive)” — 用户可以方便地查找、阅读、对比各个 opcode(例如算术操作、存储/内存操作、合约调用、日志、控制流等)。(EVM Codes)
- evm.codes 还提供 “Playground” (演练场) 功能,让你写一些 EVM bytecode(或查看编译结果),然后观察它如何被 opcode 执行 — 对理解 EVM 内部工作机制特别有帮助。(EVM Codes)
🧠 背景:为什么 EVM 和 opcodes 很重要
- EVM 是以太坊网络上执行智能合约 (smart contract) 的“虚拟机 (VM)”。合约(例如用 Solidity 写的合约)在部署前会被编译成“字节码 (bytecode)” —— 这些字节码最终由 EVM 解释执行。(EVM Codes)
- “Opcodes” 是 EVM 的最底层指令 (instructions)。这些是 EVM 能识别和执行的基本操作,比如加减乘除 (ADD, SUB, MUL, DIV),内存读写 (MLOAD / MSTORE),日志 (LOG), 合约调用 (CALL / DELEGATECALL / STATICCALL) 等。(ethereum.org)
- 因为智能合约会被编译为这些 opcodes,理解 opcodes 有助于你更深入掌握底层执行逻辑 — 对安全审计 (audit)、gas 优化、反编译、漏洞分析、以及编译器/工具链开发都非常有用。(CSDN)
🎯 evm.codes 对谁有用 / 适合什么场景
- 如果你是 Solidity / EVM 智能合约开发者 — 用它查看编译出来的 bytecode 里到底是什么 opcode,理解 gas 成本 & 底层执行。
- 如果你从事合约安全、审计、漏洞分析 — 想更底层地了解合约如何执行、可能出错、如何优化 gas 或规避安全问题。
- 如果你对 EVM 工作机制 (虚拟机 + 字节码 + 执行模型) 感兴趣 — evm.codes 是一个直观、交互式的入口。
- 做教育 / 学习 /研究 — 比起抽象语言 (Solidity) better:直接理解 “机器语言层面” 的合约行为。
如果你愿意,我可以推荐几个学习 EVM opcodes 的中文 / 英文资源(包括教程、可视化工具、深入解释等),帮助你快速上手。你要我帮你整理吗?
EVM opcodes是solidity下层的语言,类似于汇编和c的关系
是的,EVM opcodes 确实可以看作是 Solidity 的底层语言,类似于汇编语言和高级语言(如 C 语言)之间的关系。我们可以这样理解:
Solidity 和 EVM Opcodes 的关系:
- Solidity 是一种高级语言,它让开发者能够编写易于理解和管理的智能合约代码。就像 C 语言是为程序员设计的,而不直接对硬件进行操作一样,Solidity 主要为开发人员提供了一个高层次的接口来编写合约。
- EVM Opcodes 则是 Solidity 编译后生成的机器级指令,相当于智能合约在以太坊虚拟机(EVM)中执行时所用的“汇编”代码。它们是以太坊网络能够直接执行的操作码,类似于汇编语言在传统计算机中的角色。
类比汇编和 C 语言:
- C 语言(高级语言)编写的程序最终会被编译器转化为对应的汇编语言,然后再由汇编器(assembler)转化为机器码,这些机器码最终能被处理器执行。
- 类似地,Solidity(高级语言)编写的智能合约会被Solidity 编译器(如
solc)编译成 EVM 字节码,这些字节码会被解析为EVM Opcodes,最终由以太坊网络上的虚拟机(EVM)执行。
重要的对比:
- Solidity 编译器(如
solc)的工作类似于一个编译器,它将 Solidity 源代码翻译为 EVM 字节码(opcode)。 - EVM Opcodes 则类似于传统计算机中的汇编语言指令,执行这些操作码的“机器”就是 EVM(Ethereum Virtual Machine)。EVM 类似于计算机的 CPU,负责执行这些低层次的命令。
为什么理解 EVM Opcodes 很重要:
- Gas 优化:因为每个 EVM 操作码的执行都有对应的 gas 消耗(即执行费用),理解这些操作码能帮助开发者优化合约的执行效率,减少不必要的 gas 消耗。
- 合约安全性:了解底层操作,可以帮助你识别潜在的安全漏洞。某些 EVM 操作可能会导致安全问题,如重入攻击(Reentrancy)等。
- 反向工程与审计:通过分析 EVM 字节码和操作码,可以进行合约反向工程,理解其实现方式,帮助进行合约的安全审计。
总结:
Solidity 是用来编写智能合约的高级语言,EVM opcodes 是其编译结果的底层执行指令,类似于汇编语言和 C 语言的关系。这种层级结构使得开发者可以专注于更高效和易于理解的代码,而 EVM 则处理底层的执行和优化。
如果你有兴趣深入理解 EVM 操作码或进一步学习如何优化 Solidity 合约的执行,evm.codes 是一个非常实用的参考工具。
视频笔记
https://university.alchemy.com/course/solidity/md/64e7b20d71fe4b0002cf49f4
- uint是uint256的缩写,或者别名
- int8 //-128 到127
constructor在智能合约中只运行一次,那么一般用来写什么逻辑,做什么呢?
在智能合约中,**constructor** 是一个特殊的函数,它在合约部署时执行,只运行一次。constructor 通常用来初始化合约的状态,设置合约的初始条件,或进行一些合约部署时必须完成的操作。
constructor 的作用
- 初始化状态变量:通常用于设置合约的初始状态,或者初始化重要的合约参数。例如,可以在部署合约时传入一些参数,并用这些参数初始化状态变量。
- 授权设置:通常在合约的构造函数中指定合约的拥有者、管理员或授权人。这对于权限控制非常重要,确保只有特定的地址能够调用某些特权函数。
- 合约依赖关系设置:如果合约需要与其他合约交互或依赖其他合约,可以在构造函数中传入其他合约的地址,设置依赖关系。
- 合约初始状态的设定:可以通过 constructor 来设置初始状态,比如余额、交易的最大限制、费用的初始值等。
常见的构造函数用途:
设置合约的拥有者(Owner)
- 通常在智能合约中,会有一个
owner状态变量,指定合约的创建者或管理员。在构造函数中通常会将owner设置为部署合约的地址,以便控制权限或管理合约。
1
2
3
4
5address public owner;
constructor() {
owner = msg.sender; // msg.sender 是部署合约的地址
}- 通常在智能合约中,会有一个
初始化合约参数
- 合约部署时可以接收参数,通过构造函数将这些参数传入并初始化合约的状态变量。例如,设置合约的初始余额、费用、某些设定等。
1
2
3
4
5uint256 public initialBalance;
constructor(uint256 _balance) {
initialBalance = _balance;
}设置管理员或授权角色
- 如果合约需要有多种角色(比如管理员、普通用户),可以在构造函数中指定某些用户的权限。例如,管理员可以有更高权限进行某些操作。
1
2
3
4
5mapping(address => bool) public admins;
constructor(address _admin) {
admins[_admin] = true;
}合约依赖的地址(其他合约的地址)
- 如果合约需要依赖其他合约(比如通过地址与其他合约交互),可以在构造函数中传入其他合约的地址并将其设置为依赖项。
1
2
3
4
5address public anotherContract;
constructor(address _contractAddress) {
anotherContract = _contractAddress;
}初始化集合或数组
- 可以在构造函数中初始化一些合约的数据集合,如数组或映射(mapping),以便在合约生命周期内使用。
1
2
3
4
5
6
7mapping(address => uint256) public balances;
constructor(address[] memory _addresses, uint256[] memory _initialBalances) {
for (uint i = 0; i < _addresses.length; i++) {
balances[_addresses[i]] = _initialBalances[i];
}
}
需要注意的几点:
- 只能调用一次:
constructor只会在合约部署时执行一次,之后不可再调用。因此,它的逻辑通常用于初始化,而不是执行常规的合约功能。 - 参数化部署:在合约部署时,构造函数的参数是可以设置的,部署者可以根据需要传递不同的值来影响合约的初始化行为。
- 与
fallback和receive函数不同:constructor只在合约创建时执行一次,而fallback和receive函数则用于接收 Ether 并响应特定的调用。
示例:一个简单的智能合约的构造函数
1 | |
在这个例子中:
owner会被设置为部署合约的账户地址。totalSupply会被初始化为部署时传入的参数_initialSupply。
总结
constructor 在 Solidity 合约中用于合约的初始化操作,通常包括设置初始状态、赋值权限、配置依赖等。它在合约部署时执行一次,不可再被调用,确保合约的初始化逻辑只运行一次。
constructor在 Solidity 合约中用于合约的初始化操作,只运行一次。但智能合约不一定要有constructor对吗
是的,**智能合约并不一定需要有 constructor**。在 Solidity 中,constructor 是可选的,如果不定义构造函数,合约仍然可以正常部署和运行。
什么时候可以不使用 constructor?
- 不需要初始化状态变量:如果你的合约没有特别需要在部署时初始化的状态变量,那么就可以省略
constructor。在这种情况下,Solidity 会自动使用默认值初始化所有状态变量(例如0、false、""等)。 - 合约逻辑无需部署时设置特殊参数:如果合约的初始化不依赖于外部参数(如传入初始值),那么也可以省略
constructor,让合约的行为从合约部署后的第一个函数调用开始。
默认情况下的初始化行为
如果合约没有定义构造函数(constructor),Solidity 会自动为状态变量赋予默认值:
- 数值类型(如
uint256、int)会初始化为0。 - 布尔类型(
bool)会初始化为false。 - 地址类型(
address)会初始化为0x0000000000000000000000000000000000000000。 - 字符串、字节数组等引用类型会初始化为空(
""或bytes(""))。
示例:没有 constructor 的合约
1 | |
在这个例子中:
owner会被自动初始化为0x0000000000000000000000000000000000000000(空地址)。balance会被自动初始化为0。
需要注意的地方
访问权限和初始化:如果你的合约有特定的权限(如只有某个地址才能执行某些操作),通常使用
constructor来初始化这些权限。例如,在constructor中设置合约的拥有者。没有constructor时,你可能需要设计一个额外的函数来初始化这些权限。依赖合约地址:如果合约需要依赖其他合约(比如依赖某个预先部署的合约地址),通常也会通过
constructor来传入这些依赖项。如果没有constructor,则可能需要其他方式来设置这些依赖。
总结
虽然 constructor 是一个非常常见和有用的功能,用于在部署时初始化合约状态,但它并不是强制性的。智能合约可以没有 constructor,并且它的状态变量会被自动初始化为默认值。如果合约不需要初始化操作或部署时参数,可以省略构造函数。
你的目标:创建无符号整数!
在我们的合约中创建三个公共存储的无符号整数:a、b 和 sum。
- 将变量
a定义为uint8类型,其初始值应在 0 到 255 之间。 - 将变量
b定义为uint16类型,其值应至少为 256。 - 变量
sum应该是uint256类型,表示a和b存储的值之和。
答案:
1 | |
你的目标:创建带符号整数!
创建三个公共存储的整数:a、b 和 difference。
- 将变量
a和b定义为int8类型,其中一个值必须是正数,另一个值必须是负数。 - 将变量
difference定义为int16类型,它表示a和b之间的绝对差值。
1 | |
你的目标:Hello World
是时候在 Solidity 中实现 Hello World 了!
- 创建一个公共的
bytes32存储变量msg1,它存储字符串字面量"Hello World"。 - 创建一个公共的
string存储变量msg2,它存储一个需要超过 32 字节来存储的字符串字面量。
答案:
1 | |
你的目标:做一些食物!
在提供的 enum 中,你将看到四种类型的食物。
将这些值分别存储在 food1、food2、food3 和 food4 中。
1 | |
你的目标:无符号整数构造函数
创建一个构造函数,该构造函数将一个 uint 作为参数传入。
将这个 uint 值存储在一个名为 x 的公共存储变量中。
答案:
1 | |
变量遮蔽
让我们来看一个变量遮蔽的例子:
1 | |
在这种情况下,状态变量 name 被参数 name 遮蔽了!
编译器会在我们这样做时发出警告。编译器会提示:“警告:此声明遮蔽了一个已存在的声明。”,并且会显示被遮蔽的变量和已存在的声明。编译器的警告对于调试和避免常见错误非常有帮助。
从技术上讲,有一种方法可以解决这个问题:
1 | |
现在我们使用对 MyContract 的引用来更新状态变量。这不是典型的做法。通常,你会看到参数前加上下划线(例如 _variableName),而不是其他方式。
解释:
- 通过
MyContract.name显式地引用合约的状态变量,可以避免与构造函数参数name的遮蔽。 - 然而,这种方法并不常见,通常开发者会使用带下划线的命名约定(如
_name)来区分参数和状态变量。
Solidity 参数
我们首先要讨论的是构造函数:
1 | |
在这里,我们在合约部署时设置了一个存储变量的值。
Solidity 合约的构造函数与许多面向对象语言中的类的构造函数非常相似。构造函数仅在合约部署时调用一次,之后再也不会调用。它通常用于设置合约的初始值。
如果我们想让合约的部署者决定 isOpen 的值呢?
我们可以向构造函数传递一个参数!让我们看看如何实现:
1 | |
看看!现在合约的部署者可以决定 isOpen 的值了。
注意:参数名称(_isOpen)前面有一个下划线吗?这防止了变量与存储变量同名。当名字冲突时,这叫做“变量遮蔽(variable shadowing)”。在 Solidity 中,这种情况经常发生,因为我们可以在不使用 this 的情况下引用存储变量。让我们深入探讨一下这个问题。
你的目标:添加一个无符号整数
创建一个外部的 view 函数 add,该函数接收一个 uint 参数,并返回该参数与状态变量 x 的和。
答案:
1 | |
你的目标:返回秘密
在 winningNumber 函数中传递给你一个消息。你可以使用 console.log 将这个消息显示到控制台。它会告诉你接下来该怎么做!
我们还没有讨论 calldata 关键字。我们将在接下来的关于引用类型的课程中介绍这个关键字。现在,请了解对于引用类型(如 string),calldata 指定了数据的位置,具体来说,它表示该值位于消息调用数据中!
Your Goal: Return the Secret
There is a message being passed to you in the winningNumber function. You can use console.log to display this message to the console. It will tell you what to do from there!
1 | |
Your Goal: Double Uint
Create an external, pure function called double which takes a uint parameter and doubles it. It should return this doubled uint value.
1 | |
你的目标:重载 Double
创建另一个 pure 类型的外部函数 double,该函数接收两个 uint 参数。
将这两个参数分别乘以 2,并按传入函数的顺序返回这两个值。
1 | |
Message Calls
Message Calls 是指在以太坊智能合约中通过 调用 来发送数据和执行函数的过程。这些调用可以携带 以太币(Ether) 和 数据,并且会触发智能合约内的特定函数执行。
在 Solidity 和以太坊中,message call 是一种由外部账户或合约发送到另一个合约的请求,可以包括:
- 数据(比如函数参数)
- 价值(即转账一定数量的以太币)
主要特点:
传递数据和价值:
- 数据:通常是发送给合约函数的参数,或者是调用某个特定函数的指令。
- 价值:可以携带一定数量的以太币(ETH)与消息一起发送,以太币的转账可以通过这种方式进行。
调用函数:
- 当发送消息时,通常会触发目标合约中的某个函数,函数的执行逻辑就会根据传入的参数进行处理。
消息调用的发送方式:
- 外部账户(Externally Owned Accounts, EOAs) 可以直接发送消息(包括以太币)到智能合约。
- 智能合约之间 也可以通过消息调用进行交互。
两种主要的消息调用类型:
普通调用(Call):
- 无值(没有以太币):如果你只是调用合约的函数而不发送任何以太币,那么这个调用就是普通的消息调用。
- 返回值:这种调用通常有一个返回值(例如函数计算后的结果)。
示例:
1
2// 普通消息调用的例子
(bool success, bytes memory data) = address(myContract).call(abi.encodeWithSignature("functionName(uint256)", 123));带值的调用(Transfer):
- 发送以太币:这种调用不仅触发合约的函数执行,还可以将一定数量的以太币发送给目标合约。
- 这种调用通常使用
transfer或send来发送 ETH。
示例:
1
2// 向合约发送 1 ether 的例子
address(myContract).transfer(1 ether);
message call 的常见用途:
- 合约间交互:一个合约可以调用另一个合约的函数并传递参数。
- 合约与外部账户的交互:外部账户可以向合约发送以太币,同时触发合约中的函数。
示例:发送消息并调用函数
假设你有一个合约 MyContract,其中有一个函数 setNumber 用来设置一个数值 x,并且这个函数能够接收 ETH:
1 | |
外部账户可以通过 message call 发送交易并传递数据,如下:
1 | |
在这个示例中:
setNumber是一个可以接收 ETH 和数据(_x)的函数。- 外部账户调用
setNumber时,发送了1 ether和42作为参数,触发了合约中的setNumber函数,并且x被设置为 42。
总结:
Message calls 是智能合约之间或外部账户与智能合约之间的交互方式,通过这种方式,我们可以传递数据和以太币,并调用合约中的函数。这是以太坊生态系统中智能合约交互的核心机制之一。