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 很重要:

  1. Gas 优化:因为每个 EVM 操作码的执行都有对应的 gas 消耗(即执行费用),理解这些操作码能帮助开发者优化合约的执行效率,减少不必要的 gas 消耗。
  2. 合约安全性:了解底层操作,可以帮助你识别潜在的安全漏洞。某些 EVM 操作可能会导致安全问题,如重入攻击(Reentrancy)等。
  3. 反向工程与审计:通过分析 EVM 字节码和操作码,可以进行合约反向工程,理解其实现方式,帮助进行合约的安全审计。

总结:

Solidity 是用来编写智能合约的高级语言,EVM opcodes 是其编译结果的底层执行指令,类似于汇编语言和 C 语言的关系。这种层级结构使得开发者可以专注于更高效和易于理解的代码,而 EVM 则处理底层的执行和优化。

如果你有兴趣深入理解 EVM 操作码或进一步学习如何优化 Solidity 合约的执行,evm.codes 是一个非常实用的参考工具。

视频笔记

https://university.alchemy.com/course/solidity/md/64e7b20d71fe4b0002cf49f4

  1. uint是uint256的缩写,或者别名
  2. int8 //-128 到127

constructor在智能合约中只运行一次,那么一般用来写什么逻辑,做什么呢?

在智能合约中,**constructor** 是一个特殊的函数,它在合约部署时执行,只运行一次。constructor 通常用来初始化合约的状态,设置合约的初始条件,或进行一些合约部署时必须完成的操作。

constructor 的作用

  • 初始化状态变量:通常用于设置合约的初始状态,或者初始化重要的合约参数。例如,可以在部署合约时传入一些参数,并用这些参数初始化状态变量。
  • 授权设置:通常在合约的构造函数中指定合约的拥有者、管理员或授权人。这对于权限控制非常重要,确保只有特定的地址能够调用某些特权函数。
  • 合约依赖关系设置:如果合约需要与其他合约交互或依赖其他合约,可以在构造函数中传入其他合约的地址,设置依赖关系。
  • 合约初始状态的设定:可以通过 constructor 来设置初始状态,比如余额、交易的最大限制、费用的初始值等。

常见的构造函数用途:

  1. 设置合约的拥有者(Owner)

    • 通常在智能合约中,会有一个 owner 状态变量,指定合约的创建者或管理员。在构造函数中通常会将 owner 设置为部署合约的地址,以便控制权限或管理合约。
    1
    2
    3
    4
    5
    address public owner;

    constructor() {
    owner = msg.sender; // msg.sender 是部署合约的地址
    }
  2. 初始化合约参数

    • 合约部署时可以接收参数,通过构造函数将这些参数传入并初始化合约的状态变量。例如,设置合约的初始余额、费用、某些设定等。
    1
    2
    3
    4
    5
    uint256 public initialBalance;

    constructor(uint256 _balance) {
    initialBalance = _balance;
    }
  3. 设置管理员或授权角色

    • 如果合约需要有多种角色(比如管理员、普通用户),可以在构造函数中指定某些用户的权限。例如,管理员可以有更高权限进行某些操作。
    1
    2
    3
    4
    5
    mapping(address => bool) public admins;

    constructor(address _admin) {
    admins[_admin] = true;
    }
  4. 合约依赖的地址(其他合约的地址)

    • 如果合约需要依赖其他合约(比如通过地址与其他合约交互),可以在构造函数中传入其他合约的地址并将其设置为依赖项。
    1
    2
    3
    4
    5
    address public anotherContract;

    constructor(address _contractAddress) {
    anotherContract = _contractAddress;
    }
  5. 初始化集合或数组

    • 可以在构造函数中初始化一些合约的数据集合,如数组或映射(mapping),以便在合约生命周期内使用。
    1
    2
    3
    4
    5
    6
    7
    mapping(address => uint256) public balances;

    constructor(address[] memory _addresses, uint256[] memory _initialBalances) {
    for (uint i = 0; i < _addresses.length; i++) {
    balances[_addresses[i]] = _initialBalances[i];
    }
    }

需要注意的几点:

  1. 只能调用一次constructor 只会在合约部署时执行一次,之后不可再调用。因此,它的逻辑通常用于初始化,而不是执行常规的合约功能。
  2. 参数化部署:在合约部署时,构造函数的参数是可以设置的,部署者可以根据需要传递不同的值来影响合约的初始化行为。
  3. fallbackreceive 函数不同constructor 只在合约创建时执行一次,而 fallbackreceive 函数则用于接收 Ether 并响应特定的调用。

示例:一个简单的智能合约的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.8.0;

contract MyContract {
address public owner;
uint256 public totalSupply;

// 通过构造函数初始化合约
constructor(uint256 _initialSupply) {
owner = msg.sender; // 将合约部署者设为拥有者
totalSupply = _initialSupply; // 初始化代币总供应量
}
}

在这个例子中:

  • owner 会被设置为部署合约的账户地址。
  • totalSupply 会被初始化为部署时传入的参数 _initialSupply

总结

constructor 在 Solidity 合约中用于合约的初始化操作,通常包括设置初始状态、赋值权限、配置依赖等。它在合约部署时执行一次,不可再被调用,确保合约的初始化逻辑只运行一次。

constructor在 Solidity 合约中用于合约的初始化操作,只运行一次。但智能合约不一定要有constructor对吗

是的,**智能合约并不一定需要有 constructor**。在 Solidity 中,constructor 是可选的,如果不定义构造函数,合约仍然可以正常部署和运行。

什么时候可以不使用 constructor

  • 不需要初始化状态变量:如果你的合约没有特别需要在部署时初始化的状态变量,那么就可以省略 constructor。在这种情况下,Solidity 会自动使用默认值初始化所有状态变量(例如 0false"" 等)。
  • 合约逻辑无需部署时设置特殊参数:如果合约的初始化不依赖于外部参数(如传入初始值),那么也可以省略 constructor,让合约的行为从合约部署后的第一个函数调用开始。

默认情况下的初始化行为

如果合约没有定义构造函数(constructor),Solidity 会自动为状态变量赋予默认值:

  • 数值类型(如 uint256int)会初始化为 0
  • 布尔类型(bool)会初始化为 false
  • 地址类型(address)会初始化为 0x0000000000000000000000000000000000000000
  • 字符串、字节数组等引用类型会初始化为空(""bytes(""))。

示例:没有 constructor 的合约

1
2
3
4
5
6
7
8
pragma solidity ^0.8.0;

contract NoConstructorContract {
address public owner;
uint256 public balance;

// 合约没有构造函数,状态变量会被初始化为默认值
}

在这个例子中:

  • owner 会被自动初始化为 0x0000000000000000000000000000000000000000(空地址)。
  • balance 会被自动初始化为 0

需要注意的地方

  • 访问权限和初始化:如果你的合约有特定的权限(如只有某个地址才能执行某些操作),通常使用 constructor 来初始化这些权限。例如,在 constructor 中设置合约的拥有者。没有 constructor 时,你可能需要设计一个额外的函数来初始化这些权限。

  • 依赖合约地址:如果合约需要依赖其他合约(比如依赖某个预先部署的合约地址),通常也会通过 constructor 来传入这些依赖项。如果没有 constructor,则可能需要其他方式来设置这些依赖。

总结

虽然 constructor 是一个非常常见和有用的功能,用于在部署时初始化合约状态,但它并不是强制性的。智能合约可以没有 constructor,并且它的状态变量会被自动初始化为默认值。如果合约不需要初始化操作或部署时参数,可以省略构造函数。

你的目标:创建无符号整数!

在我们的合约中创建三个公共存储的无符号整数:absum

  • 将变量 a 定义为 uint8 类型,其初始值应在 0 到 255 之间。
  • 将变量 b 定义为 uint16 类型,其值应至少为 256。
  • 变量 sum 应该是 uint256 类型,表示 ab 存储的值之和。

答案:

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
uint8 public a = 254;
uint16 public b = 256;
uint256 public sum = a + b;
}

你的目标:创建带符号整数!

创建三个公共存储的整数:abdifference

  • 将变量 ab 定义为 int8 类型,其中一个值必须是正数,另一个值必须是负数。
  • 将变量 difference 定义为 int16 类型,它表示 ab 之间的绝对差值。
1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
int8 public a = 10;
int8 public b = -10;
int16 public difference = a - b;
}

你的目标:Hello World

是时候在 Solidity 中实现 Hello World 了!

  • 创建一个公共的 bytes32 存储变量 msg1,它存储字符串字面量 "Hello World"
  • 创建一个公共的 string 存储变量 msg2,它存储一个需要超过 32 字节来存储的字符串字面量。

答案:

1
2
3
4
5
6
7
8
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
bytes32 public msg1 = "Hello World";
string public msg2 = "ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
}

你的目标:做一些食物!

在提供的 enum 中,你将看到四种类型的食物。

将这些值分别存储在 food1food2food3food4 中。

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
enum Foods { Apple, Pizza, Bagel, Banana }

Foods public food1 = Foods.Apple;
Foods public food2 = Foods.Pizza;
Foods public food3 = Foods.Bagel;
Foods public food4 = Foods.Banana;
}

你的目标:无符号整数构造函数

创建一个构造函数,该构造函数将一个 uint 作为参数传入。
将这个 uint 值存储在一个名为 x 的公共存储变量中。

答案:

1
2
3
4
5
6
7
8
9
10
11
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
uint public x;

constructor(uint _x) {
x = _x; // 直接修改状态变量 x
}
}

变量遮蔽

让我们来看一个变量遮蔽的例子:

1
2
3
4
5
string public name;

constructor(string name) {
// name 被遮蔽了!
}

在这种情况下,状态变量 name 被参数 name 遮蔽了!

编译器会在我们这样做时发出警告。编译器会提示:“警告:此声明遮蔽了一个已存在的声明。”,并且会显示被遮蔽的变量和已存在的声明。编译器的警告对于调试和避免常见错误非常有帮助。

从技术上讲,有一种方法可以解决这个问题:

1
2
3
4
5
6
7
contract MyContract {
string public name;

constructor(string name) {
MyContract.name = name;
}
}

现在我们使用对 MyContract 的引用来更新状态变量。这不是典型的做法。通常,你会看到参数前加上下划线(例如 _variableName),而不是其他方式。


解释:

  • 通过 MyContract.name 显式地引用合约的状态变量,可以避免与构造函数参数 name 的遮蔽。
  • 然而,这种方法并不常见,通常开发者会使用带下划线的命名约定(如 _name)来区分参数和状态变量。

Solidity 参数

我们首先要讨论的是构造函数:

1
2
3
4
5
bool public isOpen;

constructor() {
isOpen = true;
}

在这里,我们在合约部署时设置了一个存储变量的值。

Solidity 合约的构造函数与许多面向对象语言中的类的构造函数非常相似。构造函数仅在合约部署时调用一次,之后再也不会调用。它通常用于设置合约的初始值。


如果我们想让合约的部署者决定 isOpen 的值呢?

我们可以向构造函数传递一个参数!让我们看看如何实现:

1
2
3
4
5
bool public isOpen;

constructor(bool _isOpen) {
isOpen = _isOpen;
}

看看!现在合约的部署者可以决定 isOpen 的值了。


注意:参数名称(_isOpen)前面有一个下划线吗?这防止了变量与存储变量同名。当名字冲突时,这叫做“变量遮蔽(variable shadowing)”。在 Solidity 中,这种情况经常发生,因为我们可以在不使用 this 的情况下引用存储变量。让我们深入探讨一下这个问题。

你的目标:添加一个无符号整数

创建一个外部的 view 函数 add,该函数接收一个 uint 参数,并返回该参数与状态变量 x 的和。

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
uint public x;

constructor(uint _x) {
x = _x; // 直接修改状态变量 x
}
function increment() external {
x+=1;
}
function add(uint y) external view returns(uint){
return y+x;
}
}

你的目标:返回秘密

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
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;

import "forge-std/console.sol";

contract Contract {
function winningNumber(string calldata secretMessage) external returns(uint) {
console.log(secretMessage);
return 794;
}
}

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
2
3
4
5
6
7
8
9
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
function double(uint x) external pure
returns(uint sum) {
sum = 2*x;
}
}

你的目标:重载 Double

创建另一个 pure 类型的外部函数 double,该函数接收两个 uint 参数。
将这两个参数分别乘以 2,并按传入函数的顺序返回这两个值。

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Contract {
function double(uint x) external pure
returns(uint sum) {
sum = 2*x;
}
function double(uint x, uint y) public pure returns (uint, uint) {
return (2 * x, 2 * y);
}
}

Message Calls

Message Calls 是指在以太坊智能合约中通过 调用 来发送数据和执行函数的过程。这些调用可以携带 以太币(Ether)数据,并且会触发智能合约内的特定函数执行。

在 Solidity 和以太坊中,message call 是一种由外部账户或合约发送到另一个合约的请求,可以包括:

  • 数据(比如函数参数)
  • 价值(即转账一定数量的以太币)

主要特点:

  1. 传递数据和价值

    • 数据:通常是发送给合约函数的参数,或者是调用某个特定函数的指令。
    • 价值:可以携带一定数量的以太币(ETH)与消息一起发送,以太币的转账可以通过这种方式进行。
  2. 调用函数

    • 当发送消息时,通常会触发目标合约中的某个函数,函数的执行逻辑就会根据传入的参数进行处理。
  3. 消息调用的发送方式

    • 外部账户(Externally Owned Accounts, EOAs) 可以直接发送消息(包括以太币)到智能合约。
    • 智能合约之间 也可以通过消息调用进行交互。

两种主要的消息调用类型:

  1. 普通调用(Call)

    • 无值(没有以太币):如果你只是调用合约的函数而不发送任何以太币,那么这个调用就是普通的消息调用。
    • 返回值:这种调用通常有一个返回值(例如函数计算后的结果)。

    示例:

    1
    2
    // 普通消息调用的例子
    (bool success, bytes memory data) = address(myContract).call(abi.encodeWithSignature("functionName(uint256)", 123));
  2. 带值的调用(Transfer)

    • 发送以太币:这种调用不仅触发合约的函数执行,还可以将一定数量的以太币发送给目标合约。
    • 这种调用通常使用 transfersend 来发送 ETH。

    示例:

    1
    2
    // 向合约发送 1 ether 的例子
    address(myContract).transfer(1 ether);

message call 的常见用途:

  • 合约间交互:一个合约可以调用另一个合约的函数并传递参数。
  • 合约与外部账户的交互:外部账户可以向合约发送以太币,同时触发合约中的函数。

示例:发送消息并调用函数

假设你有一个合约 MyContract,其中有一个函数 setNumber 用来设置一个数值 x,并且这个函数能够接收 ETH:

1
2
3
4
5
6
7
8
9
10
11
pragma solidity ^0.8.20;

contract MyContract {
uint public x;

// 接收 ETH 的函数并设置 x
function setNumber(uint _x) external payable {
require(msg.value == 1 ether, "You need to send exactly 1 ether");
x = _x;
}
}

外部账户可以通过 message call 发送交易并传递数据,如下:

1
2
// 使用 web3.js 或 ethers.js 等库调用这个合约函数
myContract.setNumber(42, { value: web3.utils.toWei("1", "ether") });

在这个示例中:

  • setNumber 是一个可以接收 ETH 和数据(_x)的函数。
  • 外部账户调用 setNumber 时,发送了 1 ether42 作为参数,触发了合约中的 setNumber 函数,并且 x 被设置为 42。

总结:

Message calls 是智能合约之间或外部账户与智能合约之间的交互方式,通过这种方式,我们可以传递数据和以太币,并调用合约中的函数。这是以太坊生态系统中智能合约交互的核心机制之一。