这篇文章详细介绍了Yul语言,作为以太坊上智能合约的低级语言,具有更高的代码执行效率。文章包括Yul的语法、数据类型、控制流、函数以及Yul和Solidity的对比,并提供了一些示例代码和编译工具的推荐,以帮助开发者理解和应用Yul语言。文章结构清晰,内容丰富,有助于熟悉低级编程概念的开发者进行智能合约优化。
Solidity 一直是以太坊上编写智能合约的主导语言,但随着生态系统的持续发展和成熟,开发者们正在探索优化和定制代码的新方法。Yul 是一种低级语言,提供了更细粒度的控制智能合约执行的能力,是需要从其代码中挤出每一分效率的开发者的理想选择。
你需要的准备
你将做的事情
Yul 是一种中间语言,编译到以太坊虚拟机(EVM)字节码。它被设计为一种低级语言,使开发者能够对其智能合约的执行进行高度控制。它在很多方面类似于汇编语言,但具有更高级的特性,使其更易于使用。
Yul 包括几个内置数据类型,用于存储和操作值。以下是 Yul 中一些最常见的数据类型:
整数: 整数用于表示整数。在 Yul 中,整数可以是有符号或无符号,并且可以具有不同的位大小。例如,uint256 是一个 256 位的无符号整数。
字节数组: 字节数组用于存储字节序列。它们通常用于表示数据,如哈希或公钥。在 Yul 中,字节数组使用 bytes 关键字声明,后面跟上要分配的字节数。
结构: 结构用于将相关数据组合在一起。它们类似于其他编程语言中的对象。在 Yul 中,结构使用 struct 关键字声明,后面跟上结构中每个字段的名称和类型。
以下是如何在 Yul 中声明和使用结构的示例:
struct Person {
string name;
uint age;
}
变量和常量用于在 Yul 中存储和操作数据。以下是关于如何在 Yul 中声明和使用变量和常量的快速概述:
变量: 变量使用 let 关键字声明,后面跟上变量名称和值。以下是如何在 Yul 中声明变量的示例:
let x = 100
let x //初始值为 0 赋值
Yul 中的 运算符 用于对值执行算术和逻辑运算。以下是 Yul 中一些最常见的运算符:
加法 (+): 将两个值相加。例如:
add(x,y) // x + y
减法 (-): 从一个值中减去另一个值。例如:
sub(x,y) // x - y
*乘法 ()**: 将两个值相乘。例如:
mul(x,y) // x * y
除法 (/): 将一个值除以另一个值。例如:
div(x,y) // x / y
取模 (%): 计算除法操作的余数。例如:
mod(x,y) // x % y
等价: 检查该值是否相等。例如:
x := y // x = y
Yul 中的控制流语句用于控制合约中的执行流。以下是 Yul 中一些最常见的控制流语句。注意,不能定义 “else” 块。如果需要多个选项,请考虑使用 “switch”(见下文)。
如果语句:
if lt(calldatasize(), 4) { revert(0, 0) }
开关语句:
{
let x := 0
switch calldataload(4)
case 0 {
x := calldataload(0x24)
}
default {
x := calldataload(0x44)
}
sstore(0, div(x, 2))
}
循环:
// 简单的 for 循环
for {let i := 0} lt(i, 10) {i := add(i, 10)} {
let p := funcCall(i, 10)
if eq(p, 5) {
continue
}
if eq(p, 90) {
break
}
}
Yul 中的函数用于封装可以在以后执行的代码块。以下是如何在 Yul 中声明和使用函数的示例:
function sum(a, b) -> ret : u64
{
ret := add(b, a)
}
注意,Yul 函数仅限于操作作为参数传递给它们的变量,因为它们无法访问任何超出其范围的变量。可以指定参数和返回值的类型,如果未指定类型,编译器将默认使用 u256 类型。
Yul 还包括一种内联汇编语言,允许你在 Yul 合约中直接编写低级代码。这对于优化某些操作或直接与以太坊虚拟机(EVM)交互是非常有用的。以下是如何在 Yul 中使用内联汇编的示例:
assembly {
let x := 0
let y := 1
add(x, y)
}
其他内置函数包括:
剩余的列表可以在 这里 找到。此外,可以像在 Solidity 中一样使用注释,在 Yul 中可以使用 // 和 // 来表示注释,但一个例外是 Yul 中的标识符可以包含点(例如,.)。
有关 Yul 语法的更多信息,请查看 文档。
Yul 代码是在文本编辑器中编写,并使用 solc Solidity 编译器编译的。Yul 代码由一系列指令组成,每个指令在 EVM 上执行特定操作。这些操作包括算术和逻辑操作、内存管理和控制流指令。
以下是一个 ERC-20 代币智能合约的示例(来源,用 Yul 和 Solidity 编写):
object "Token" {
code {
// 将创建者存储在槽零中。
sstore(0, caller())
// 部署合约
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
code {
// 防止发送以太币
require(iszero(callvalue()))
// 调度器
switch selector()
case 0x70a08231 /* "balanceOf(address)" */ {
returnUint(balanceOf(decodeAsAddress(0)))
}
case 0x18160ddd /* "totalSupply()" */ {
returnUint(totalSupply())
}
case 0xa9059cbb /* "transfer(address,uint256)" */ {
transfer(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0x23b872dd /* "transferFrom(address,address,uint256)" */ {
transferFrom(decodeAsAddress(0), decodeAsAddress(1), decodeAsUint(2))
returnTrue()
}
case 0x095ea7b3 /* "approve(address,uint256)" */ {
approve(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
case 0xdd62ed3e /* "allowance(address,address)" */ {
returnUint(allowance(decodeAsAddress(0), decodeAsAddress(1)))
}
case 0x40c10f19 /* "mint(address,uint256)" */ {
mint(decodeAsAddress(0), decodeAsUint(1))
returnTrue()
}
default {
revert(0, 0)
}
function mint(account, amount) {
require(calledByOwner())
mintTokens(amount)
addToBalance(account, amount)
emitTransfer(0, account, amount)
}
function transfer(to, amount) {
executeTransfer(caller(), to, amount)
}
function approve(spender, amount) {
revertIfZeroAddress(spender)
setAllowance(caller(), spender, amount)
emitApproval(caller(), spender, amount)
}
function transferFrom(from, to, amount) {
decreaseAllowanceBy(from, caller(), amount)
executeTransfer(from, to, amount)
}
function executeTransfer(from, to, amount) {
revertIfZeroAddress(to)
deductFromBalance(from, amount)
addToBalance(to, amount)
emitTransfer(from, to, amount)
}
/* ---------- calldata 解码函数 ----------- */
function selector() -> s {
s := div(calldataload(0), 0x100000000000000000000000000000000000000000000000000000000)
}
function decodeAsAddress(offset) -> v {
v := decodeAsUint(offset)
if iszero(iszero(and(v, not(0xffffffffffffffffffffffffffffffffffffffff)))) {
revert(0, 0)
}
}
function decodeAsUint(offset) -> v {
let pos := add(4, mul(offset, 0x20))
if lt(calldatasize(), add(pos, 0x20)) {
revert(0, 0)
}
v := calldataload(pos)
}
/* ---------- calldata 编码函数 ---------- */
function returnUint(v) {
mstore(0, v)
return(0, 0x20)
}
function returnTrue() {
returnUint(1)
}
/* -------- 事件 ---------- */
function emitTransfer(from, to, amount) {
let signatureHash := 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
emitEvent(signatureHash, from, to, amount)
}
function emitApproval(from, spender, amount) {
let signatureHash := 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925
emitEvent(signatureHash, from, spender, amount)
}
function emitEvent(signatureHash, indexed1, indexed2, nonIndexed) {
mstore(0, nonIndexed)
log3(0, 0x20, signatureHash, indexed1, indexed2)
}
/* -------- 存储布局 ---------- */
function ownerPos() -> p { p := 0 }
function totalSupplyPos() -> p { p := 1 }
function accountToStorageOffset(account) -> offset {
offset := add(0x1000, account)
}
function allowanceStorageOffset(account, spender) -> offset {
offset := accountToStorageOffset(account)
mstore(0, offset)
mstore(0x20, spender)
offset := keccak256(0, 0x40)
}
/* -------- 存储访问 ---------- */
function owner() -> o {
o := sload(ownerPos())
}
function totalSupply() -> supply {
supply := sload(totalSupplyPos())
}
function mintTokens(amount) {
sstore(totalSupplyPos(), safeAdd(totalSupply(), amount))
}
function balanceOf(account) -> bal {
bal := sload(accountToStorageOffset(account))
}
function addToBalance(account, amount) {
let offset := accountToStorageOffset(account)
sstore(offset, safeAdd(sload(offset), amount))
}
function deductFromBalance(account, amount) {
let offset := accountToStorageOffset(account)
let bal := sload(offset)
require(lte(amount, bal))
sstore(offset, sub(bal, amount))
}
function allowance(account, spender) -> amount {
amount := sload(allowanceStorageOffset(account, spender))
}
function setAllowance(account, spender, amount) {
sstore(allowanceStorageOffset(account, spender), amount)
}
function decreaseAllowanceBy(account, spender, amount) {
let offset := allowanceStorageOffset(account, spender)
let currentAllowance := sload(offset)
require(lte(amount, currentAllowance))
sstore(offset, sub(currentAllowance, amount))
}
/* ---------- 实用程序函数 ---------- */
function lte(a, b) -> r {
r := iszero(gt(a, b))
}
function gte(a, b) -> r {
r := iszero(lt(a, b))
}
function safeAdd(a, b) -> r {
r := add(a, b)
if or(lt(r, a), lt(r, b)) { revert(0, 0) }
}
function calledByOwner() -> cbo {
cbo := eq(owner(), caller())
}
function revertIfZeroAddress(addr) {
require(addr)
}
function require(condition) {
if iszero(condition) { revert(0, 0) }
}
}
}
}
// SPDX-License-Identifier-Identifier: MIT
// OpenZeppelin Contracts (最后更新于 v4.8.0) (token/ERC20/ERC20.sol)
pragma solidity ^0.8.0;
import "./IERC20.sol";
import "./extensions/IERC20Metadata.sol";
import "../../utils/Context.sol";
/**
* @dev 实现 {IERC20} 接口。
*
* 该实现与创建代币的方式无关。这意味着在派生合约中必须添加供给机制,使用 {_mint}。
* 对于通用机制,请参见 {ERC20PresetMinterPauser}。
*
* 提示:有关详细写法,请参见我们的指南
* https://forum.openzeppelin.com/t/how-to-implement-erc20-supply-mechanisms/226[如何实现供给机制]。
*
* {decimals} 的默认值为 18。要更改此值,你应该覆盖此函数以返回一个不同的值。
*
* 我们遵循了通用 OpenZeppelin Contracts 指南:函数在失败时会 revert,而不是返回 `false`。这种行为是传统的,与 ERC20 应用程序的期望不冲突。
*
* 此外,在调用 {transferFrom} 时会发出 {Approval} 事件。这允许应用程序通过监听这些事件重新构建所有账户的额度。EIP 的其他实现可能不会发出这些事件,因为规范并不要求。
*
* 最后,已添加非标准的 {decreaseAllowance} 和 {increaseAllowance} 函数,以应对众所周知的关于设置允许额的问题。请参见 {IERC20-approve}。
*/
contract ERC20 is Context, IERC20, IERC20Metadata {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
/**
* @dev 设置 {name} 和 {symbol} 的值。
*
* 这两个值都是不可变的:在构造时只能设置一次。
*/
constructor(string memory name_, string memory symbol_) {
_name = name_;
_symbol = symbol_;
}
/**
* @dev 返回代币的名称。
*/
function name() public view virtual override returns (string memory) {
return _name;
}
/**
* @dev 返回代币的符号,通常是名称的简短版本。
*/
function symbol() public view virtual override returns (string memory) {
return _symbol;
}
/**
* @dev 返回用于获取用户表示的十进制数的位数。
* 例如,如果 `decimals` 等于 `2`,则 `505` 代币的余额应显示为 `5.05` (`505 / 10 ** 2`)。
*
* 代币通常选择 18 作为值,以模仿以太币和 Wei 之间的关系。除非被覆盖,否则这是该函数返回的默认值。
*
* 注意:该信息仅用于 _显示_ 目的:它并不会影响合约的任何算术,包括 {IERC20-balanceOf} 和 {IERC20-transfer}。
*/
function decimals() public view virtual override returns (uint8) {
return 18;
}
/**
* @dev 查看 {IERC20-totalSupply}。
*/
function totalSupply() public view virtual override returns (uint256) {
return _totalSupply;
}
/**
* @dev 查看 {IERC20-balanceOf}。
*/
function balanceOf(address account) public view virtual override returns (uint256) {
return _balances[account];
}
/**
* @dev 查看 {IERC20-transfer}。
*
* 需求:
*
* - `to` 不能是零地址。
* - 调用者的余额必须至少为 `amount`。
*/
function transfer(address to, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_transfer(owner, to, amount);
return true;
}
/**
* @dev 查看 {IERC20-allowance}。
*/
function allowance(address owner, address spender) public view virtual override returns (uint256) {
return _allowances[owner][spender];
}
/**
* @dev 查看 {IERC20-approve}。
*
* 注意:如果 `amount` 是最大 `uint256`,在 `transferFrom` 时不会更新允许额。这在语义上等同于无限批准。
*
* 需求:
*
* - `spender` 不能是零地址。
*/
function approve(address spender, uint256 amount) public virtual override returns (bool) {
address owner = _msgSender();
_approve(owner, spender, amount);
return true;
}
/**
* @dev 查看 {IERC20-transferFrom}。
*
* 发出 {Approval} 事件,指示更新的允许额。这不是 EIP 要求。请参见 {ERC20} 开头的说明。
*
* 注意:如果当前允许额是最大 `uint256`,则不会更新允许额。
*
* 需求:
*
* - `from` 和 `to` 不能是零地址。
* - `from` 必须至少有 `amount` 的余额。
* - 调用者必须有 `from` 的代币的至少 `amount` 的允许额。
*/
function transferFrom(address from, address to, uint256 amount) public virtual override returns (bool) {
address spender = _msgSender();
_spendAllowance(from, spender, amount);
_transfer(from, to, amount);
return true;
}
/**
* @dev 原子性增加授予 `spender` 的允许额。
*
* 这是 {approve} 的替代方案,可用于减轻 {IERC20-approve} 中描述的问题。
*
* 发出 {Approval} 事件,指示更新的允许额。
*
* 需求:
*
* - `spender` 不能是零地址。
*/
function increaseAllowance(address spender, uint256 addedValue) public virtual returns (bool) {
address owner = _msgSender();
_approve(owner, spender, allowance(owner, spender) + addedValue);
return true;
}
/**
* @dev 原子性减少授予 `spender` 的允许额。
*
* 这是 {approve} 的替代方案,可用于减轻 {IERC20-approve} 中描述的问题。
*
* 发出 {Approval} 事件,指示更新的允许额。
*
* 需求:
*
* - `spender` 不能是零地址。
* - `spender` 必须对调用者有至少 `subtractedValue` 的允许额。
*/
function decreaseAllowance(address spender, uint256 subtractedValue) public virtual returns (bool) {
address owner = _msgSender();
uint256 currentAllowance = allowance(owner, spender);
require(currentAllowance >= subtractedValue, "ERC20: decreased allowance below zero");
unchecked {
_approve(owner, spender, currentAllowance - subtractedValue);
}
return true;
}
/**
* @dev 将 `amount` 的代币从 `from` 移动到 `to`。
*
* 此内部功能相当于 {transfer},可以用来实现自动代币费用、削减机制等。
*
* 发出 {Transfer} 事件。
*
* 需求:
*
* - `from` 不能是零地址。
* - `to` 不能是零地址。
* - `from` 必须至少有 `amount` 的余额。
*/
function _transfer(address from, address to, uint256 amount) internal virtual {
require(from != address(0), "ERC20: transfer from the zero address");
require(to != address(0), "ERC20: transfer to the zero address");
_beforeTokenTransfer(from, to, amount);
uint256 fromBalance = _balances[from];
require(fromBalance >= amount, "ERC20: transfer amount exceeds balance");
unchecked {
_balances[from] = fromBalance - amount;
// 溢出不可能发生:所有余额的总和由总供给限制,并且通过
// 先递减然后递增的方法保持总和。
_balances[to] += amount;
}
emit Transfer(from, to, amount);
_afterTokenTransfer(from, to, amount);
}
/** @dev 创建 `amount` 代币并将其分配给 `account`,增加
* 总供应量。
*
* 发出 {Transfer} 事件,发送自零地址。
*
* 需求:
*
* - `account` 不能是零地址。
*/
function _mint(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: mint to the zero address");
_beforeTokenTransfer(address(0), account, amount);
_totalSupply += amount;
unchecked {
// 溢出不可能:余额 + 数量最多为总供应 + 数量,已在上面检查。
_balances[account] += amount;
}
emit Transfer(address(0), account, amount);
_afterTokenTransfer(address(0), account, amount);
}
/**
* @dev 从 `account` 销毁 `amount` 代币,减少
* 总供应量。
*
* 发出 {Transfer} 事件,发往零地址。
*
* 需求:
*
* - `account` 不能是零地址。
* - `account` 必须至少有 `amount` 代币。
*/
function _burn(address account, uint256 amount) internal virtual {
require(account != address(0), "ERC20: burn from the zero address");
_beforeTokenTransfer(account, address(0), amount);
uint256 accountBalance = _balances[account];
require(accountBalance >= amount, "ERC20: burn amount exceeds balance");
unchecked {
_balances[account] = accountBalance - amount;
// 溢出不可能:数量 <= 账户余额 <= 总供应量。
_totalSupply -= amount;
}
emit Transfer(account, address(0), amount);
_afterTokenTransfer(account, address(0), amount);
}
/**
* @dev 设置 `spender` 对 `owner` 代币的允许 `amount`。
*
* 此内部功能相当于 `approve`,可用于
* 设置某些子系统的自动允许额等。
*
* 发出 {Approval} 事件。
*
* 需求:
*
* - `owner` 不能是零地址。
* - `spender` 不能是零地址。
*/
function _approve(address owner, address spender, uint256 amount) internal virtual {
require(owner != address(0), "ERC20: approve from the zero address");
require(spender != address(0), "ERC20: approve to the zero address");
_allowances[owner][spender] = amount;
emit Approval(owner, spender, amount);
}
/**
* @dev 基于消费的 `amount` 更新 `owner` 的 `spender` 的允许额。
*
* 在无限允许的情况下不更新允许额。如果没有足够的允许额,会 revert。
*
* 可能会发出 {Approval} 事件。
*/
function _spendAllowance(address owner, address spender, uint256 amount) internal virtual {
uint256 currentAllowance = allowance(owner, spender);
if (currentAllowance != type(uint256).max) {
require(currentAllowance >= amount, "ERC20: insufficient allowance");
unchecked {
_approve(owner, spender, currentAllowance - amount);
}
}
}
/**
* @dev 在任何代币转移之前调用的钩子。这包括
* 创建和销毁。
*
* 调用条件:
*
* - 当 `from` 和 `to` 都是非零时,将 ``from`` 的 `amount` 代币转移到 `to`。
* - 当 `from` 为零时,将 `amount` 代币发送给 `to`。
* - 当 `to` 为零时,将 ``from`` 的 `amount` 代币销毁。
* - `from` 和 `to` 都永远不会是零。
*
* 要了解有关钩子的更多信息,请参阅 xref:ROOT:extending-contracts.adoc#using-hooks[使用钩子]。
*/
function _beforeTokenTransfer(address from, address to, uint256 amount) internal virtual {}
/**
* @dev 在任何代币转移之后调用的钩子。这包括
* 创建和销毁。
*
* 调用条件:
*
* - 当 `from` 和 `to` 都是非零时,``from`` 的 `amount` 代币已转移到 `to`。
* - 当 `from` 为零时,将 `amount` 代币已发行给 `to`。
* - 当 `to` 为零时,``from`` 的 `amount` 代币已被销毁。
* - `from` 和 `to` 都永远不会是零。
*
* 要了解有关钩子的更多信息,请参阅 xref:ROOT:extending-contracts.adoc#using-hooks[使用钩子]。
*/
function _afterTokenTransfer(address from, address to, uint256 amount) internal virtual {}
}
花几分钟时间查看上面的代码。我们在 Yul 语法 部分中涵盖的大部分语法在上面的代码中已有实现。你还可以切换到 Solidity 中的 ERC-20 选项卡,查看该合约如何用 Solidity 编写。
Yul 并不是 Solidity 的替代品,而是一种可以与之协同使用的补充语言。Yul 的层次要比 Solidity 低得多,并旨在提供更直接的合约执行控制。这使得 Yul 代码比等效的 Solidity 代码更高效,但同时也更难以编写和理解。
相反,Solidity 是一种高级语言,提供了更多的抽象和便于开发者使用的特性。它旨在让不熟悉低级编程概念的开发者更易上手。
如果你有兴趣了解更多关于 Yul 的信息以及如何在智能合约开发中使用 Yul,有许多在线资源可供参考。Solidity 文档包括 Yul 的一个部分以及 Yul 教程。此外,还有许多博客文章、文献和视频更详细地介绍了 Yul。
有多种可用于 Yul 的工具,例如:
Yul 是一个强大的工具,为需要优化其代码的效率和性能的智能合约开发者而设计。虽然它可能并不适合所有开发者,但那些对低级编程概念感到舒适,并希望微调其代码的人会发现 Yul 是他们工具包中一个宝贵的补充。我们希望本指南能够帮助你了解 Yul 及其功能,并鼓励你进一步探索,看看 Yul 能为你带来什么!
如果你遇到问题,或者有任何疑问,或只是想讨论自己正在构建的内容,请在 Discord 或 Twitter 上给我们留言!
如果你对本指南有任何反馈,请 告诉我们。我们期待你的反馈!
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!