本文介绍了如何使用Yul优化智能合约以节省Gas费用。通过对比纯Solidity、Solidity与内联汇编、以及纯Yul编写的智能合约,展示了Yul在Gas优化中的优势,并通过石头剪刀布游戏的实例详细讲解了各合约的实现和Gas消耗对比。
在本文中,我们将使用 Yul 来优化我们的智能合约,以节省Gas费。我们将查看三个不同的智能合约,这些合约用于玩经典游戏石头、剪子、布。一个合约将纯粹用 Solidity 编写,另一个合约将用 Solidity 和内联汇编编写,最后一个合约将纯粹用 Yul 编写。如果你对 Yul 不熟悉,请查看我撰写的关于 Yul 的文章,以更好地理解它是什么以及如何工作。
初学者的 Yul 指南:https://learnblockchain.cn/article/10872
包含石头、剪子、布的 Github 仓库:https://github.com/marjon-call/Rock-Paper-Scissors
好了,让我们深入游戏结构吧!
要玩这个游戏,用户需要调用 createGame(address _player1, address _player2) external
。这两个参数地址代表参与游戏的两个玩家。一旦游戏创建,玩家可以通过调用 playGame(uint8 _move) external payable
来进行他们的操作。此函数接收一个 uint8
,其值必须在 1 到 3 之间。1 表示石头,2 表示布,3 表示剪子。玩游戏的费用为 0.05 ether,且每位玩家仅允许进行一次操作。一旦两名玩家都进行了他们的操作,将调用 evaluateGame() private
来检查游戏的结果,并将 ether 分配给赢家,或者在游戏平局的情况下平均分配 ether。每次只能进行一局游戏。存储变量 bool public gameInProgress
用于跟踪这一点。我们还有存储变量 uint256 public gameStart
用于记录游戏开始的时间,以及存储变量 uint256 public gameLength
用于记录我们希望的游戏时长。如果游戏进行得太久,任何旁观者都可以调用 terminateGame() external
以便玩一场游戏。在这种情况下,ether 会发送给已进行操作的玩家。
首先,让我们看看我们的 Solidity 智能合约并概述其功能。以下是我们的存储变量。
// 插槽 0
uint256 public gameStart;
// 插槽 1
uint256 public gameCost;
// 插槽 2
address public player1;
uint8 private player1Move;
// 插槽 3
address public player2;
uint8 private player2Move;
bool public gameInProgress;
bool public lockGame;
// 插槽 4
uint256 public gameLength;
event GameOver(address Winner, address Loser);
event GameStart(address Player1, address Player2);
event TieGame(address Player1, address Player2);
event GameTerminated(address Loser1, address Loser2);
请注意,我们正在打包我们的变量以节省Gas成本。我们稍后会深入讨论这一点,但现在请注意这一点。
很好,现在让我们看看构造函数。
constructor() {
gameCost = 0.05 ether;
gameLength = 28800;
}
我们在这里所做的只是将 gameCost
设置为 0.05 ether,并将 gameLength
设置为 28800 个区块。
现在让我们看看 createGame()
。
// 允许用户创建新游戏
function createGame(address _player1, address _player2) external {
require(gameInProgress == false, "游戏仍在进行中。");
gameInProgress = true;
gameStart = block.number;
player1 = _player1;
player2 = _player2;
}
我们首先检查游戏是否正在进行。如果是,则回退。否则,我们设置存储变量以建立我们的游戏。
让我们查看我们的存储,以更好地理解底层发生了什么。如果你想跟随,这里是我用来获取存储布局的函数。
function getStorage() public view returns(bytes32, bytes32, bytes32, bytes32) {
assembly {
mstore(0x00, sload(0))
mstore(0x20, sload(1))
mstore(0x40, sload(2))
mstore(0x60, sload(3))
return(0x00, 0x80)
}
}
让我们在继续之前谈谈存储槽 3。按照右到左的顺序,我们看到前 20 字节是玩家 2 的地址( 0x5b38da6a701c568545dcfcb03fcb875f56beddc4
)。接下来我们看到 1 个空字节( 0x00
),这里存储的是玩家 2 的移动。之后我们看到 0x01
表示 gameInProgress
,这等同于 true。接下来又是一个空字节( 0x00
),因为 lockGame
暂时被设置为 false。然后我们看到 0x7080
,其十进制等同于 28800(构造函数中设置的 gameLength
的值)。
让我们也快速查看内存。
我们在内存中不执行任何操作,所以我们期望它看起来像这样。
很好,现在我们将看看 playGame()
是如何工作的!
function playGame(uint8 _move) external payable {
require(lockGame == false);
require(gameInProgress == true, "游戏未进行。");
require(msg.value >= gameCost, "玩家发送的 ether 不够来玩。");
require(_move > 0 && _move < 4, "无效移动");
lockGame = true;
if (msg.sender == player1 && player1Move == 0) {
player1Move = _move;
} else if(msg.sender == player2 && player2Move == 0) {
player2Move = _move;
} else {
require(1 == 0, "用户无权进行移动。");
}
if (player1Move != 0 && player2Move != 0) {
evaluateGame();
}
lockGame = false;
}
好的,首先我们将使用 require()
语句来验证我们遵守规则。第一个 require()
检查游戏是否被锁定。第二个 require()
检查游戏是否已创建。第三个 require()
检查玩家是否发送了足够的 ether 来玩游戏。最后一个 require()
检查玩家是否发送了有效的移动。在那之后,我们锁定游戏。接下来,我们有一个 if
语句检查玩家 1 是否正在移动。我们还检查他们是否已经进行了移动,方法是检查 player1Move
的值是否设置为 0(记住 Solidity 默认将值初始化为 0)。请注意,我们先检查玩家。我们认为不满足条件的可能性更小,通过这样做,我们节省了不需要检查玩家 1 是否已进行移动的费用(操作更少,Gas成本更低)。如果条件满足,我们就设置玩家 1 的移动。然后我们为玩家 2 执行相同的检查。如果没有满足的 if
语句,我们则回退。否则,我们检查两位玩家是否都已进行移动。如果是的话,我们调用 evaluateGame()
。最后,我们解锁游戏。
如果玩家 1 首先用石头移动,存储的样子如下!
请注意,唯一的变化在于槽 2。从右到左读取,在玩家 1 的地址后面我们看到 0x01
(十进制 1),这等同于石头。还要注意的是,这在事务结束时,因此 lockGame
已经恢复为 0。
同样,内存未使用,所以它看起来与调用 createGame()
时相同。
现在让我们看看在玩家 2 用剪子进行移动之前 evaluateGame()
被调用时存储的情况。
唯一的不同在槽 3。我们看到在 player2
后面,player2Address
被设置为 0x03
(十进制 3),这等于我们游戏中的剪子。还要注意的事情是,在 gameInProgress
和 gameLength
之间,lockGame
已被设置为 0x01
(作为布尔值的真值)。
现在我们知道了 playGame()
的工作机制,让我们看看 evaluateGame()
的具体实现。
function evaluateGame() private {
address _player1 = player1;
uint8 _player1Move = player1Move;
address _player2 = player2;
uint8 _player2Move = player2Move;
if (_player1Move == _player2Move) {
_player1.call{value: address(this).balance / 2}("");
_player2.call{value: address(this).balance}("");
emit TieGame(_player1, _player2);
} else if (
(_player1Move == 1 && _player2Move == 3 ) ||
(_player1Move == 2 && _player2Move == 1) ||
(_player1Move == 3 && _player2Move == 2)) {
_player1.call{value: address(this).balance}("");
emit GameOver(_player1, _player2);
} else {
_player2.call{value: address(this).balance}("");
emit GameOver(_player2, _player1);
}
gameInProgress = false;
player1 = address(0);
player2 = address(0);
player1Move = 0;
player2Move = 0;
gameStart = 0;
}
在 evaluateGame()
中我们首先将存储变量存入栈。为了理解为什么我们要这样做,让我们先讨论一下存储的Gas成本。当提到存储槽时,有两种不同的状态:冷存储和热存储。冷存储是指一个槽在交易中还未被访问(我们已经在 playGame()
中访问了这些变量)。访问冷存储非常昂贵,费用为 2100 gas。一旦你在交易中访问一个存储槽,它被称为热存储。访问热存储的费用为 100 gas,这仍然非常昂贵。这是打包变量带来的优势之一,因为你即使第一次访问特定变量,也可以访问热存储。访问栈中变量的费用可能会有所不同,但在上述示例中,每次读取约为 10 gas。这是一个显著的节省,因此在编写智能合约时请记住这一点。
代码的下一个部分检查谁赢得了游戏。第一个 if
语句检查是否平局。在这种情况下,合约会将一半的 ether 发送给两个玩家。接下来,我们使用一个冗长的 if
语句来检查玩家 1 胜利的情况。如果满足任何条件,我们将 ether 发送给玩家 1。否则,玩家 2 必须获胜,玩家 2 将获得 ether。
此函数的最后部分重置智能合约状态,以便用户可以玩新游戏。如果调用 getStorage()
,你会看到布局与我们在调用 createGame()
时相同。注意我们将槽 0 和槽 2 置为空。这实际上会给我们Gas退款!每将槽置为 0,你可获得 15000 gas。但最高的退款是事务总Gas费用的 ⅕。
我们在 Solidity 合约中需要讨论的最后一个函数是 terminateGame()
。
function terminateGame() external {
require(gameStart + gameLength < block.number, "游戏还有时间。");
require(gameInProgress == true, "游戏未开始");
if(player1Move != 0) {
player1.call{value: address(this).balance}("");
} else if(player2Move != 0) {
player2.call{value: address(this).balance}("");
}
gameInProgress = false;
player1 = address(0);
player2 = address(0);
player1Move = 0;
player2Move = 0;
gameStart = 0;
emit GameTerminated(player1, player2);
}
我们的 require()
语句检查游戏是否超过分配时间,检查是否有游戏可以终止。接下来我们检查任一玩家是否已经进行了移动。如果有,我们就将 ether 返还给他们。最后我们像在 evaluateGame()
中那样重置合约状态。
现在让我们看看如何使用 Yul 来节省一些Gas费用!
混合合约将具有与我们纯 Solidity 合约相同的存储布局。构造函数也将保持不变。
这是我们的更新版 createGame()
函数。
// 允许用户创建新游戏
function createGame(address _player1, address _player2) external {
require(gameInProgress == false, "游戏仍在进行中。");
assembly {
sstore(0, number())
sstore(2, _player1)
let glAndGip := or(0x0000000000000000000001000000000000000000000000000000000000000000, sload(3))
sstore(3, or(glAndGip, _player2))
}
}
require()
相同,但之后我们深入一些 Yul。首先,我们将区块号存储到存储槽 0。这是在设置 gameStart
。接下来我们将玩家 1 的地址存储在槽 2 中。之后,我们需要格式化我们的数据,将其打包到槽 3 中。此时存储槽 3 已经在存储 gameLength
。因此,我们所要做的就是加载槽 3,并将其与一个将 gameInProgress
设置为 true 的 32 字节值( 0x0000000000000000000001000000000000000000000000000000000000000000
) or()
。我们的最后一步是将此值与玩家 2 的地址 or()
,然后存储它。如果调用 getStorage()
,将会看到与使用 Solidity 智能合约时相同的结果。
playGame()
看起来与在 Solidity 合约中相同。然而,evaluateGame()
有一个大变化。
function evaluateGame() private {
address _player1 = player1;
uint8 _player1Move = player1Move;
address _player2 = player2;
uint8 _player2Move = player2Move;
if (_player1Move == _player2Move) {
_player1.call{value: address(this).balance / 2}("");
_player2.call{value: address(this).balance}("");
emit TieGame(_player1, _player2);
} else if ((_player1Move == 1 && _player2Move == 3 ) || (_player1Move == 2 && _player2Move == 1) || (_player1Move == 3 && _player2Move == 2)) {
_player1.call{value: address(this).balance}("");
emit GameOver(_player1, _player2);
} else {
_player2.call{value: address(this).balance}("");
emit GameOver(_player2, _player1);
}
assembly {
sstore(0,0)
sstore(2, 0)
let gameLengthShifted := shr(mul(23,8), sload(3))
let gameLengthVal := shl(mul(23,8), gameLengthShifted)
sstore(3, gameLengthVal);
}
}
合约的前半部分是相同的,但最后的汇编块是我们优化代码的地方。我们稍后将讨论这如何节省Gas成本,但与此同时,让我们完成混合合约的概述。
最后,让我们看看 terminateGame()
。
function terminateGame() external {
require(gameStart + gameLength < block.number, "游戏还有时间。");
require(gameInProgress == true, "游戏未开始");
if(player1Move != 0) {
player1.call{value: address(this).balance}("");
} else if(player2Move != 0) {
player2.call{value: address(this).balance}("");
}
assembly {
sstore(0,0)
sstore(2, 0)
let gameLengthShifted := shr(mul(23,8), sload(3))
let gameLengthVal := shl(mul(23,8), gameLengthShifted)
sstore(3, gameLengthVal);
}
emit GameTerminated(player1, player2);
}
你可能注意到更改非常相似。再次地,我们仅更改一个部分,汇编代码块。那么我们为什么这样做呢?答案是,当我们清除存储槽时,这使我们能够通过一次操作清除两个打包变量,而不是像在 Solidity 合约中那样手动清除每一个。
现在我们有两个合约,让我们比较调用每个合约时的Gas成本。
我们降低了每个函数调用的费用,包括部署费用(1,240,438 与 1,171,912)!同样值得注意的是,对于 playGame()
,如果第二个玩家在调用该函数,则它调用 evaluateGame()
。因此,两者的最大Gas差异 1,232 gas 对用户来说意义重大。
现在我们完成了这一部分,我们准备将合同纯粹用 Yul 编写!
我想在这一部分的开头说,如果你正在跟着进行,我建议使用 remix。Hardhat 不支持纯 Yul 合约,因此你在编译智能合约时会遇到一些问题。此外,当处理完全用 Yul 编写的合约时,你不能像在 Solidity 中那样直接调用函数。例如,你需要手动构建调用数据,并调用函数选择器 0x985d4ac3
。因此,首先让我们看看我们需要如何构造调用数据,以便调用我们的合约。
createGame()
的函数选择器是 0xa6f979ff
。如果你不记得如何派生函数选择器,请查看我在本文开头链接的 “初学者的 Yul 指南”。接下来,我们需要查看如何传递前面提到的两个地址参数。如果你记得,内存工作是以 32 字节一系列的。因此,你需要将两个地址从 20 字节格式化为 32 字节,方法是用 12 个空字节填充左侧。以下是我们调用数据的示例:0xa6f979ff0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2
函数选择器
:0xa6f979ff
_player1
:0000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4
_player2
:000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2
playGame()
要简单得多,因为我们只有一个变量。虽然它是一个 uint8
,但因为只有一个变量,并且我们没有打包它,所以它占用 32 字节。以下是如果我们使用石头时我们的调用数据的样子:0x985d4ac30000000000000000000000000000000000000000000000000000000000000001
函数选择器
:0x985d4ac3
_move
:0000000000000000000000000000000000000000000000000000000000000001
最后是 terminateGame()
。它没有参数,因此我们只需使用函数选择器构建我们的调用数据:0x97661f31
很好,现在我们准备进入代码的细节!
object "RockPaperScissorsYul" {
code {
sstore(1, 0xB1A2BC2EC50000)
sstore(3, 0x000000000000000B400000000000000000000000000000000000000000000000)
datacopy(0, dataoffset("runtime"), datasize("runtime"))
return(0, datasize("runtime"))
}
object "runtime" {
// 存储布局:
// 插槽 0 : gameStart
// 插槽 1 : gameCost
// 插槽 2 : bytes 13 - 32 : player1
// 插槽 2 : bytes 12 : player1Move
// 插槽 3 : bytes 13 - 32 : player2
// 插槽 3 : bytes 12 : player2Move
// 插槽 3 : bytes 11 : gameInProgress
// 插槽 3 : bytes 10 : lockGame
// 插槽 3 : bytes 9 - 8 : gameLength
// 其余代码
}
}
object "RockPaperScissorsYul" {}
声明了我们的智能合约。所有代码都将在其中运行。第一部分 code {}
是一个构造函数在 Solidity 中的表现。在这里我们将 gameCost
存储为 0.05 ether 在槽 1 中。之后,我们在槽 3 存储 gameLength
。请注意,我这次设置了不同的值,你可以根据自己的需要自由设置。接下来的两行是复制运行时代码并返回它。然后,object "runtime" {}
是我们放置代码以在运行时调用的地方。最后,我把存储布局放进去。建议在用 Yul 编写合约时这样做,因为这有助于你跟踪变量存储的位置。
由于这个代码较长,并且格式与 Solidity 不同,我们将分块进行讲解。
code {
let callData := calldataload(0)
let selector := shr(0xe0, callData)
switch selector
// createGame(address, address)
case 0xa6f979ff {
// 获取存储中的 gameInProgress
let gameInProgress := and(0xff, shr( mul( 21, 8), sload(3) ) )
// 如果游戏正在进行,则回退
if eq(gameInProgress, 1) {
revert(0,0)
}
// 从 calldata 复制到内存,不含函数选择器
calldatacopy(0, 4, calldatasize())
// 获取地址 1 和地址 2
let address1 := and(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff, mload(0x00))
let address2 := and(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff, mload(0x20))
// 存储游戏开始的区块号和 player1 到存储中
sstore(0, number())
sstore(2, address1)
// 将 gameLength、gameInProgress 和 player2 打包到槽 3
let gameLengthShifted := shr(mul(23,8), sload(3))
let gameLengthVal := shl(mul(23,8), gameLengthShifted)
let glAndGip := or(0x0000000000000000000001000000000000000000000000000000000000000000, gameLengthVal)
sstore(3, or(glAndGip, address2))
}
// 更多情况
}
code{}
是我们将运行的代码,我们将其他代码包装在其中。calldataload(0)
正在加载调用数据的前 32 字节。然后我们将右移 28 字节,以隔离函数选择器。
Yul 中有一个非常优秀的操作是 Solidity 中不允许的,那就是 switch{}
语句。我们使用 switch 语句来识别正在调用哪个函数。请注意第一个情况,0xa6f979ff
是 createGame()
的函数选择器。查看第一个案例中的第一个操作,我们在访问存储之前读取 sload(3)
,以避免后续再次调用相同的槽。然后我们将槽 3 右移,以格式化 gameInProgress
到第 32 个字节。接下来,我们使用单字节的掩码来隔离我们的变量。然后我们检查是否为 1 (在 Yul 中为 true),如果条件满足则回退。接着,我们从调用数据中加载其余数据。之后,我们使用 and()
和掩码来隔离我们的地址,将其赋值给栈中的变量。然后我们将区块号设置为 gameStart
,并将 address1
设置为 player1
。最后,我们通过将槽 3 与 0x0000000000000000000001000000000000000000000000000000000000000000
进行 or()
,设置 gameInProgress
为 true,然后将 address2
进行另一轮的 or()
并存储这些值到槽 3。
现在,让我们看一下第二个 switch 情况,playGame()
。
// playGame(uint8)
case 0x985d4ac3 {
calldatacopy(0, 4, calldatasize())
// 存储移动和槽变量到栈
let move := mload(0x00)
let slot2 := sload(2)
let slot3 := sload(3)
// 获取存储中的 lockGame
let lockGame := and( 0xff, shr( mul(22, 8), slot3) )
// 检查游戏是否未锁定
if eq(lockGame, 1) {
revert(0,0)
}
// 获取存储中的 gameInProgress
let gameInProgress := and(0xff, shr( mul( 21, 8), slot3 ) )
// 如果游戏未启动,则回退
if iszero(gameInProgress) {
revert(0,0)
}
// 如果未发送足够的 ether 则回退
if gt(sload(1), callvalue()) {
revert(0,0)
}
// 如果无效的移动则回退
if lt(move, 1) {
revert(0,0);
}
// 如果无效的移动则回退
if gt(move, 3) {
revert(0,0);
}
// 从存储中获取 player 1 和 player 2 的移动
let player1Move := shr( mul(20, 8), slot2 )
let player2Move := and(0xff, shr( mul(20, 8), slot3 ) )
// 获取 player1 和 player2
let player1 := and(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff, slot2 )
let player2 := and(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff, slot3 )
// 检查如果是 player1 调用,移动尚未进行,则设置 player1Move
if eq(caller(), player1) {
if gt(player1Move, 0) {
revert(0,0);
}
let moveShifted := shl( mul(20, 8), move)
sstore(2, or(moveShifted, slot2) )
// 若双方都已进行移动
if gt(player2Move, 0) {
// 锁定以防重入攻击
sstore(3, or(0x0000000000000000000100000000000000000000000000000000000000000000, sload(3) ))
evaluateGame()
// 解锁以防重入攻击
sstore(3, and(0xffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffff, sload(3)))
stop()
}
}
// 检查如果是 player2 正在调用,移动尚未进行,则设置 player2Move
if eq(caller(), player2) {
if gt(player2Move, 0) {
revert(0,0);
}
let moveShifted := shl( mul(20, 8), move)
let newSlot3Value := or(moveShifted, slot3)
// 若双方都已进行移动
if gt(player1Move, 0) {
sstore(3, or(0x0000000000000000000100000000000000000000000000000000000000000000, newSlot3Value) )
evaluateGame()
// 解锁以防重入攻击
sstore(3, and(0xffffffffffffffffff00ffffffffffffffffffffffffffffffffffffffffffff, sload(3)))
stop()
}
// 存储新的槽 3 值
sstore(3, newSlot3Value)
}
}
第一件事是将调用数据加载到内存中,并将移动设置为栈变量。接下来,我们将槽 2 和槽 3 存储为栈变量。接下来的 if
语句系列就是我们 Solidity 合约中的 require()
语句。第一个检查游戏是否已锁定。我们通过将槽 3 左移 22 字节,然后掩码一个字节来检查。我们用相似的过程检查游戏是否已经开始。下一个检查是在合约中检查玩家是否发送了足够的 ether。最后两个检查验证移动是否是有效的移动。
在确认调用者遵循游戏规则之后,我们需要获取玩家的移动。对于 player1Move
,我们只需将 玩家 1 的地址右移 20 字节。要获得 player2Move
,我们采用相同的操作,但随后需要与掩码进行 and()
操作,因为槽 3 将其他变量打包到这个槽中。接下来,我们使用掩码和 and()
获取两个玩家的地址。
现在我们需要确定哪个玩家调用合约。我们首先检查玩家 1 是否在进行他们的移动。如果调用者是玩家 1,我们需要验证他们还没有移动。如果他们已经移动了,则回退。否则,我们需要格式化我们的移动,以便它位于将要存储的字节序列的正确位置。之后我们将我们的移动存储到槽 2,并检查玩家 2 是否已经进行了移动。如果是的话,我们需要锁定我们的合约以防重入攻击。接下来我们调用 evaluateGame()
,稍后我们会详细讲解它。
在 evaluateGame()
执行后,我们解锁合约并使用一个 stop()
结束当前函数调用的执行。如果玩家 2 是调用合约的一方,我们再次检查他们是否已进行移动,如果没有,则设置他们的移动。为了防止不必要的 sload()
,我们将槽 3 的新值存储为栈变量。之后我们检查玩家 1 是否进行了移动,使用 or()
操作锁定游戏和 newSlot3Value
,并将该值存储到槽 3。再次调用 evaluateGame()
、解锁合约并停止执行。当然,如果玩家 1 尚未进行移动,我们将简单地将 newSlot3Value
存储到槽 3,而不锁定合约。
在查看 evaluateGame()
之前,让我们先看一下 terminateGame()
的最后一_CASE。
// terminateGame()
case 0x97661f31 {
let slot3 := sload(3)
let slot2 := sload(2)
// 从存储中获取 gameStart 和 gameLength
let gameStart := sload(0)
let gameLength := shr(mul(23,8), slot3)
// 检查块编号是否大于 gameStart + gameLength,否则回退
if iszero( gt( number(), add(gameStart, gameLength)) ) {
revert(0,0)
}
// 获取存储中的 gameInProgress
let gameInProgress := and(0xff, shr( mult( 21, 8), slot3 ) )
// 如果游戏未进行,则回退
if iszero(gameInProgress) {
revert(0,0)
}
// 加载在 player1 的移动,如果已经进行了移动,则将合同的 ether 返还给他们
let player1Move := shr( mult(20, 8), slot2 )
if iszero( eq(player1Move, 0) ) {
let player1 := and(0xffffffffffffffffffffffffffffffffffffffff , slot2)
pop( call(gas(), player1, selfbalance(), 0, 0, 0, 0) )
resetGame()
stop()
}
// 加载 player2 的移动,如果已经进行了移动,则将合同的 ether 返还给他们
let player2Move := and(0xff, shr( mul(20, 8), slot3 ) )
if iszero( eq(player1Move, 0) ) {
let player2 := and(0xffffffffffffffffffffffffffffffffffffffff , slot3)
pop( call(gas(), player2, selfbalance(), 0, 0, 0, 0) )
resetGame()
stop()
}
resetGame()
}
这一步是再次加载槽 2 和槽 3 到栈变量Then we get gameStart
by loading slot 0, and we get gameLength
by shifting slot3
right by 23 bytes to isolate our variable. The first if
is checking that the current block is larger than the sum of gameStart
and gameLength
. If it isn’t we revert. Otherwise, we move on to check if a game is in progress exactly how we did in playGame()
. Now we need to check if either player has made a move yet. We start with Player 1, and if Player 1 has made a move we send them the contracts ether with an empty call to their address and passing selfbalance()
as the msg.value
. We then call resetGame()
, which we will go over later, and stop execution. If Player 1 has not made a move, then we perform the same operation for Player 2. If neither player has made a move, we simply call resetGame()
. The last case is the default. We choose to revert because that means somebody called our contract without using a proper function selector.
好了,我们终于准备好讨论 evaluateGame()
了。
// evaluate game function
function evaluateGame() {
let slot2 := sload(2)
let slot3 := sload(3)
// 从存储中获取 player 1 和 player 2 的移动
let player1Move := shr( mul(20, 8), slot2 )
let player2Move := and(0xff, shr( mul(20, 8), slot3 ) )
let player1 := and(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff, slot2 )
let player2 := and(0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff, slot3 )
(内容未完待续)``` // 如果平局,玩家平分奖金 if eq(player1Move, player2Move) { pop( call(gas(), player1, div(selfbalance(), 2), 0, 0, 0, 0) ) pop( call(gas(), player2, selfbalance(), 0, 0, 0, 0) ) resetGame() leave }
// 如果玩家1出1,玩家2出3,玩家1胜,否则玩家2胜 if eq(player1Move, 1) { if eq(player2Move, 3) { pop( call(gas(), player1, selfbalance(), 0, 0, 0, 0) ) resetGame() leave } pop( call(gas(), player2, selfbalance(), 0, 0, 0, 0) ) resetGame() leave }
// 如果玩家1出2,玩家2出1,玩家1胜,否则玩家2胜 if eq(player1Move, 2) { if eq(player2Move, 1) { pop( call(gas(), player1, selfbalance(), 0, 0, 0, 0) ) resetGame() leave } pop( call(gas(), player2, selfbalance(), 0, 0, 0, 0) ) resetGame() leave }
// 如果玩家1出3,玩家2出2,玩家1胜,否则玩家2胜 if eq(player1Move, 3) { if eq(player2Move, 2) { pop( call(gas(), player1, selfbalance(), 0, 0, 0, 0) ) resetGame() leave } pop( call(gas(), player2, selfbalance(), 0, 0, 0, 0) ) resetGame() leave }
注意,我们必须声明一个函数。这个函数只能在我们的智能合约中使用。函数的第一件事是将我们的存储槽重新加载到栈中。然后我们获取 `player1Move`、`player2Move`、`player1` 和 `player2`,就像我们在之前的函数中那样。接下来,我们检查游戏是否平局。如果是的话,我们平分以太币给两个玩家。否则,我们进行一系列检查以确定谁赢了。由于这些检查看起来非常相似,我们将讨论它们的结构,以便你能理解这个概念。我们在检查玩家1的动作。接下来,我们检查玩家2是否做了让他们输的动作。如果是的话,我们将以太币发送给玩家1,重置游戏,退出该函数。否则,我们将以太币发送给玩家2,重置游戏,退出该函数。
我们Yul合约的最后一部分是 `resetGame()` 函数。
// 重置变量,以便可以进行新游戏 function resetGame() { sstore(0,0) sstore(2, 0) let gameLengthShifted := shr(mul(23,8), sload(3)) let gameLengthVal := shl(mul(23,8), gameLengthShifted) sstore(3, gameLengthVal) }
这看起来和我们在Hybrid合约中重置游戏的方式一样,所以这里不会让你感到惊讶。
这就是我们关于Yul合约部分的总结!
## 结论
现在我们知道如何用Yul编写智能合约了,让我们看看这是否真的为我们节省了Gas费。

现在让我们将这些数字与我们的Solidity和Hybrid合约进行比较!

哇!正如你所看到的,纯粹使用Yul编写智能合约能够为你和你的用户节省大量的Gas成本!
这就是我们的教程的结尾!用Yul编写智能合约是一个高级主题,如果你能坚持到最后,恭喜你完全用Yul编写了你的第一个智能合约!我希望这能帮助你更好地理解Yul和EVM的工作原理。
有关Opcodes及其Gas费用的更多信息,请查看以下资源:
Opcodes: [https://ethereum.org/en/developers/docs/evm/opcodes/](https://ethereum.org/en/developers/docs/evm/opcodes/)
EVM insight: [https://github.com/wolflo/evm-opcodes/blob/main/gas.md#a6-sload](https://github.com/wolflo/evm-opcodes/blob/main/gas.md#a6-sload)
Gas Refunds: [https://eips.ethereum.org/EIPS/eip-3529](https://eips.ethereum.org/EIPS/eip-3529)
如果你有任何问题,或者想让我制作其他主题的教程,请在下面留言。
如果你想支持我制作教程,这里是我的以太坊地址:0xD5FC495fC6C0FF327c1E4e3Bccc4B5987e256794。
>- 原文链接: [coinsbench.com/using-yul...](https://coinsbench.com/using-yul-to-optimize-gas-costs-b4feccdb5172)
>- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,在这里修改,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!