GasToken:我为何不再担心 gas 价格飙升

本文旨在探索 EVM 的 gas 机制,尤其是 GasToken 的 EVM gas 机制。

本文旨在探索 EVM 的 gas 机制,尤其是 GasToken 的 EVM gas 机制。首先,为了降低技术理解的难度,本文需要先给出一段介绍。如果你不想了解底层机制,可以直接跳到后文的 “具体实现细节” 一节开始阅读。 ![](https://img.learnblockchain.cn/2020/11/10/16049784140986.jpg) <center>要是某个表出现反转那就完蛋了 —— 但是 EVM 就不一样了</center> ## 引言—— gas 的基础知识 以太坊使用了一种 gas 计量系统,主要是为了防止停机问题和重入攻击(reentry attack)。这个计量系统似乎是最简单,也是最健壮的(尽管还有其它计量系统,如 EOS 系统)。EVM 的每个操作码都有固定的 gas 消耗量,黄皮书中注明了不同指令的 gas 成本等级:零级(0 gas)、基础级(2 gas)、超低级(3 gas)、低级(5 gas)、高级(10 gas),以及规则更加复杂的特殊等级。 例如,在 EVM 堆栈上添加或删除操作需要花费 3 gas。其中一些操作码在某些硬分叉部署之后经过了重新定价,例如,calldata(EVM 中的只读内存区域 —— 总计的 [4 种存储类型](https://blog.openzeppelin.com/ethereum-in-depth-part-2-6339cf6bddb9/)之一)已经从每字节 68 gas 的价格[下调](https://eips.ethereum.org/EIPS/eip-2028)至每字节 18 gas。重新定价似乎是为了促进二层可扩展方案的实现,因为二层可扩展方案需要链上数据可用性。还有证据表明,操作码的[原始定价](https://github.com/LeastAuthority/ethereum-analyses/blob/master/GasEcon.md)并没有经过充分分析,[依然存在定价不当的问题](https://arxiv.org/abs/1909.07220)。另外,更改操作码的 gas 消耗量也会带来问题: * 降低指令的 gas 价格可能会让重入攻击变得可行 * 提高指令的 gas 价格可能会致使调用失败,因为这会导致 gas 分配量不足以执行调用 一笔交易所使用的操作码会累计出一个总的 gas 使用量。gas 使用量与 gasPrice(以太坊交易中的用户设置字段,即该用户愿意为每单位的 gas 支付的 eth 价格)的乘积会转换成 Wei,也就是以太坊原生代币 ETH。更多关于交易的基础知识,可以参见[这篇文章](https://medium.com/coinmonks/the-business-of-sending-transactions-on-ethereum-e79fd9a2b88)。 ## 区块的 gas 上限 [一般情况](https://ethresear.ch/t/mev-auction-auctioning-transaction-ordering-rights-as-a-solution-to-miner-extractable-value/6788)下,矿工都是依据最高价拍卖模型将交易打包到区块内的。备受期待的 [EIP 1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) 意图将这一动态转变为更有效的结构,另外还有交易费必须使用以太币支付所带来的副作用。然而,这篇文章不是专门介绍 EIP 1559 的,EIP 1559 这个主题本身就具有非常深远的影响。在这篇文章中,我们将聚焦于促成 GasToken 的 gas 机制/经济学。 每个区块都有一个相关的 gas 上限,目前(2020 年 11 月)是 1250 万 gas。因此,由于区块容量有限,形成了一个竞争激烈的 “区块空间” 市场。虽然验证时间占区块传播时间的比例低于 1%,但这个上限的存在还是保证了网络的安全性。将区块 gas 上限定得太高,节点很难赶在下一个区块挖出之前执行完区块中的所有交易(也有可能跳过这些交易 —— 具体参见验证者困境)。将区块 gas 上限定得太低,就会导致网络拥堵和缺乏实用性。关于这里的权衡关系,请参见[这篇文章](https://arxiv.org/pdf/2005.11791.pdf)。 有趣的是,矿工可以使用[节点 cli flag](https://ethereum.stackexchange.com/questions/59669/explanation-of-geths-command-line-option-targetgaslimit/59673#59673) 来标记他们所期望的区块 gas 上限,但是修改 gas 上限(例如最近从 8M 上调至 12.5 M)似乎主要发生在 “社会层面(推特)” 上。正是因为矿工可以上调/下调每个区块 gas 上限的机制,让我们明白了下图为什么会出现峰值: ![](https://img.learnblockchain.cn/2020/11/10/16049784849574.jpg) 上图显示了区块 gas 上限随时间流逝的变化情况。请注意,区块 gas 上限之所以会在 2016 年底大幅降低,是因为当时遭到了 DDoS 攻击。(来源:[https://blog.ethereum.org/2016/09/22/ethereum-network-currently-undergoing-dos-attack/](https://blog.ethereum.org/2016/09/22/ethereum-network-currently-undergoing-dos-attack/)) 区块空间拍卖被认为在经济学/机制设计方面开辟了新的领域,因为传统拍卖理论是以免费投标的假设为前提的。以太坊交易并非如此,交易费率必须在一定的阈值之上,而且一旦交易被广播到点对点网络上,就不受控制了。 ## 接下来进入正题 可以说,最有趣的操作码同时也是成本较高的操作码,如 `SSTORE`、 `CREATE`、 `CREATE2` 和 `SELFDESTRUCT`。这些操作码的共同点是,它们都涉及状态,因此也涉及硬盘读写(以太坊网络的节点通常使用固态硬盘)。这些操作码成本更高,因为它们会影响永久存储和全局状态树。 ## 什么是 GasToken [GasToken](https://gastoken.io/) 巧妙地利用了 gas 定价系统。它利用的是清理状态、清理存储插槽(storage slot)和删除带有自毁操作码的合约(这些操作都可以删减全局状态树)所收到的 gas 退款。这些操作都可以被认为具备负 gas 价格。 * 清理/自毁合约:- 24,000 gas * 清理/删除存储:-15,000 gas 当 EVM 执行这类操作时,gas 退款是通过一个**独立的交易退款计量器来计算的**。gas 退款只会在交易结束时提供。另外,最高 gas 退款量是该交易所消耗 gas 量的一半。 理想情况是在网络 gas 价格较低时写入状态,并在 gas 价格较高时删除状态。由于以 Wei/ETH 为计价单位的总费用是 gas 使用量和 gas 价格的乘积,当 gas 价格较高时,减少 gas 使用量会导致总费用降低。 GasToken 的正统实现很好地体现了名称中的 “token(代币)”部分,因为它与 ERC-20 代币相似,并带有 `approve` 和 `transferFrom`操作码,可以称为多步骤交易的一部分。最初,GasToken 有两种变体,分别采用不同的设计:GST1 和 GST2。GST1 使用的是存储成本和退款机制,GST2 使用的是 `CREATE` 和自毁机制。这些变体采取不同的节约方案,具体取决于 gas 价格差值比(铸造代币和释放代币时的 gas 价格差值比)。由于 gas 价格率更高,GST2 更能节约 gas。 开采或 “铸造” GasToken 就是将其 写入存储/创建合约,而销毁或 “释放” GasToken 就是减少用户持有的 GasToken 数量并删除状态存储插槽。虽然正统的 GasToken 很流行,但是许多开发者选择克隆这一功能,并放到他们自己的系统合约中使用,从而减少成本和设计复杂性。 ## 具体实现细节 ### GST1 —— 基于存储 从智能合约的层面来看,GST1 是什么样的?我们先来看一下 `mint()` 函数: ``` function mint(uint256 value) public { uint256 storage_location_array = STORAGE_LOCATION_ARRAY; // can't use constants inside assembly if (value == 0) { return; } // Read supply uint256 supply; assembly { supply := sload(storage_location_array) } // Set memory locations in interval [l, r] uint256 l = storage_location_array + supply + 1; uint256 r = storage_location_array + supply + value; assert(r >= l); for (uint256 i = l; i <= r; i++) { assembly { sstore(i, 1) } } // Write updated supply & balance assembly { sstore(storage_location_array, add(supply, value)) } s_balances[msg.sender] += value; } ``` 简单来说,我们使用一个存储起点常量来标记 EVM 存储的开始,而且这个常量还包括我们已经写入多少个插槽的值。如果你想了解更多关于 EVM 中永久存储布局的内容,请阅读[这篇文章](https://solidity.readthedocs.io/en/v0.6.8/internals/layout_in_storage.html)。通过第 12 和第 13 行的代码,我们可以计算出新的待写入插槽范围,并在第 17 行的 for 循环中使用 `SSTORE` 操作码来将数据写入这些插槽,存储数值 1(这个值可以替换成任何**非零**值)。然后,我们在第 22 和 24 行代码处更新已写入数据的插槽数量和余额。 自由函数更有趣一点,具备以下功能:`freeFromUpTo(uint value)`、 `freeFrom(uint value)`、 `freeUpTo(uint value)` 和 `free(uint value)`。这类函数在下文统称为 `free*()` 函数,调用内部函数 `freeStorage()`: ``` function freeStorage(uint256 value) internal { uint256 storage_location_array = STORAGE_LOCATION_ARRAY; // can't use constants inside assembly // Read supply uint256 supply; assembly { supply := sload(storage_location_array) } // Clear memory locations in interval [l, r] uint256 l = storage_location_array + supply - value + 1; uint256 r = storage_location_array + supply; for (uint256 i = l; i <= r; i++) { assembly { sstore(i, 0) } } // Write updated supply assembly { sstore(storage_location_array, sub(supply, value)) } } ``` 如你所见,该函数与上文讨论的 `mint()` 函数几乎相同,主要的区别在于第 13 行代码,将值 0 写入存储会导致 EVM 释放存储插槽。这行代码会触发 gas 退款,让 gas 退款计数器增加 15000。更新 ERC-20 类型余额的任务也由 `free*()` 函数承担。 ### GST2 —— 基于合约 与 `mint()` 函数等价的函数,在 GST2 合约里叫做 `makeChild()` ,它是一个内部函数,使用 EVM 来汇编创建一个简单的 “child” 合约,而且该合约只能用 “parent” 合约来摧毁: ``` function makeChild() internal returns (address addr) { assembly { // EVM assembler of runtime portion of child contract: // ;; Pseudocode: if (msg.sender != 0x0000000000b3f879cb30fe243b4dfee438691c04) { throw; } // ;; selfdestruct(msg.sender) // PUSH15 0xb3f879cb30fe243b4dfee438691c04 ;; hardcoded address of this contract // CALLER // XOR // PC // JUMPI // CALLER // SELFDESTRUCT // Or in binary: 6eb3f879cb30fe243b4dfee438691c043318585733ff // Since the binary is so short (22 bytes), we can get away // with a very simple initcode: // PUSH22 0x6eb3f879cb30fe243b4dfee438691c043318585733ff // PUSH1 0 // MSTORE ;; at this point, memory locations mem[10] through // ;; mem[31] contain the runtime portion of the child // ;; contract. all that's left to do is to RETURN this // ;; chunk of memory. // PUSH1 22 ;; length // PUSH1 10 ;; offset // RETURN // Or in binary: 756eb3f879cb30fe243b4dfee438691c043318585733ff6000526016600af3 // Almost done! All we have to do is put this short (31 bytes) blob into // memory and call CREATE with the appropriate offsets. let solidity_free_mem_ptr := mload(0x40) mstore(solidity_free_mem_ptr, 0x00756eb3f879cb30fe243b4dfee438691c043318585733ff6000526016600af3) addr := create(0, add(solidity_free_mem_ptr, 1), 31) } ``` 仔细研究这个汇编代码可以更好地理解 EVM。我个人的观点是,合约开发者在原则上不应该使用汇编,但也有例外,那就是在设计上要求最小化并要求极高效率的合约。这个合约,还有 [EIP-1167](https://eips.ethereum.org/EIPS/eip-1167),就是例子。 **优化** 第 4 行和第 5 行中展示的时 child 合约中的回调函数(fallback function)的伪代码 —— 为什么要用回调函数?因为我们希望 child 合约能尽可能简单,简单到只有一个函数。 从 `PUSH15` 开始:地址本来有 [20 个字节](https://solidity.readthedocs.io/en/v0.5.3/types.html#address),但在这里,我们想把 15 个字节推入这个栈(这是最优实现),因为我们使用了 vanity-address 风格的技巧,它会重复地哈希,直到找到符合需要的地址,所以前面 5 个字节都是 0。剩下还需要 5 个 0,作为默认的一部分填充进去,组成 32 个字节,也就是 EVM 里面 word 的大小。这里的优化是很重要的,因为用来创建 chile 合约所用的 gas 可以认为是整个 GasToken 方案的开销。 下一步, `CALLER` 把合约调用者的地址推入栈中。 `XOR` 会从栈中弹出两个物,然后把这两个值的按位异或运算结果推入栈中。如果这两个值相等,则栈顶为 0,反之则是一个非零的数字。 `PC` 在与此操作对应的增量出现之前从程序计数器处获得一个值,并推入栈中。 `JUMPI` ,一个条件跳转,从栈中取出栈顶的两个值,一个条件和一个目标,如果条件为真,就跳到目标,如果条件不为真,那就失败。 如果 `JUMPI` 的结果不是 `JUMPDEST` 操作码,EVM 就会回滚,这保证了调用者是 parent 合约(满足 `!=` 条件)。失败的路径结束后,就把 parent 合约的地址推入栈中,当下一次 `slefdestruct` 操作执行时,弹出栈顶的 word,作为 gas 退款的目标。 `free*()` 函数调用下列 `destroyChildren()` 实现: ``` function destroyChildren(uint256 value) internal { uint256 tail = s_tail; // tail points to slot behind the last contract in the queue for (uint256 i = tail + 1; i <= tail + value; i++) { mk_contract_address(this, i).call(); } s_tail = tail + value; } ``` [点击此处,查看源代码](https://gist.githubusercontent.com/aodhgan/e53d574a78e7f752e2fab448b422d6fa/raw/5c8f7735bcdb5faf5a39de1344d1141564319fbd/free_gst2.sol) 此处,我们遍历 child 合约,并调用这些合约中的回调函数。正如 GST2 文档所指出的那样,发行代币时,合约必须找到 child 合约的创建地址(存储这些地址的成本会很高,因此我们会即时计算这些地址)。幸运的是,这是有可能做到的,因为使用 `CREATE` 生成的合约地址是根据地址/账户已创建的合约数量(nonce)计算得出的,具有确定性。这些合约地址都是在 `mk_contract_address` 函数中计算得出的,在调用时无需任何参数或值,调用回调函数,然后就像在对应 `mint()` 函数中硬编码的那样,gas 退款会发送至 parent 合约。 ## CHI GasToken 历时 3 年,[CHI](https://github.com/CryptoManiacsZone/chi/blob/master/contracts/ChiToken.sol) GasToken 终于上线。CHI 由去中心化交易所聚合器 [1inch.exchange](http://1inch.exchange/) [开发](https://1inch-exchange.medium.com/everything-you-wanted-to-know-about-chi-gastoken-a1ba0ea55bf3),与传统的 GasToken 类似,但是铸币效率比后者高出 1%,释放代币的效率比后者高出 10%,而且采用新的 `CREATE2` 操作码。该操作码可以提前通过确定性方式来创建链上合约地址,主要用于[反事实](https://blog.openzeppelin.com/getting-the-most-out-of-create2/)的 Layer-2 解决方案。 `CREATE2` 操作码采用 4 个堆栈参数:endowment、memory_start、memory_length 和盐值。生成地址等于 `keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:]`,而非常见的将发送方地址和 nonce 进行哈希计算。由于盐值控制在用户手中,用户可以提前知道地址。 ``` function mint(uint256 value) public { uint256 offset = totalMinted; assembly { mstore(0, 0x746d4946c0e9F43F4Dee607b0eF1fA1c3318585733ff6000526015600bf30000) for {let i := div(value, 32)} i {i := sub(i, 1)} { pop(create2(0, 0, 30, add(offset, 0))) pop(create2(0, 0, 30, add(offset, 1))) pop(create2(0, 0, 30, add(offset, 2))) pop(create2(0, 0, 30, add(offset, 3))) pop(create2(0, 0, 30, add(offset, 4))) pop(create2(0, 0, 30, add(offset, 5))) pop(create2(0, 0, 30, add(offset, 6))) pop(create2(0, 0, 30, add(offset, 7))) pop(create2(0, 0, 30, add(offset, 8))) pop(create2(0, 0, 30, add(offset, 9))) pop(create2(0, 0, 30, add(offset, 10))) pop(create2(0, 0, 30, add(offset, 11))) pop(create2(0, 0, 30, add(offset, 12))) pop(create2(0, 0, 30, add(offset, 13))) pop(create2(0, 0, 30, add(offset, 14))) pop(create2(0, 0, 30, add(offset, 15))) pop(create2(0, 0, 30, add(offset, 16))) pop(create2(0, 0, 30, add(offset, 17))) pop(create2(0, 0, 30, add(offset, 18))) pop(create2(0, 0, 30, add(offset, 19))) pop(create2(0, 0, 30, add(offset, 20))) pop(create2(0, 0, 30, add(offset, 21))) pop(create2(0, 0, 30, add(offset, 22))) pop(create2(0, 0, 30, add(offset, 23))) pop(create2(0, 0, 30, add(offset, 24))) pop(create2(0, 0, 30, add(offset, 25))) pop(create2(0, 0, 30, add(offset, 26))) pop(create2(0, 0, 30, add(offset, 27))) pop(create2(0, 0, 30, add(offset, 28))) pop(create2(0, 0, 30, add(offset, 29))) pop(create2(0, 0, 30, add(offset, 30))) pop(create2(0, 0, 30, add(offset, 31))) offset := add(offset, 32) } for {let i := and(value, 0x1F)} i {i := sub(i, 1)} { pop(create2(0, 0, 30, offset)) offset := add(offset, 1) } } _mint(msg.sender, value); totalMinted = offset; } ``` 这里的一般流程是,将固定的 child 合约字节码存储到 memory 中(第 4 行代码),然后使用 for 循环反复调用 `CREATE2`,直到计算出对应的值为止。 `CREATE2`返回已部署 child 合约的地址,我们不关心这个地址,因此我们只是将这个地址从堆栈中弹出。偏移量计数器被用来计算 child 合约的数量,并将其永久存储在第 33 行代码中。 对应的 `free*()` 函数调用 `_destoryChildren()`: ``` function _destroyChildren(uint256 value) internal { assembly { let i := sload(totalBurned_slot) let end := add(i, value) sstore(totalBurned_slot, end) let data := mload(0x40) mstore(data, 0xff0000000000004946c0e9F43F4Dee607b0eF1fA1c0000000000000000000000) mstore(add(data, 53), 0x3c1644c68e5d6cb380c36d1bf847fdbc0c7ac28030025a2fc5e63cce23c16348) let ptr := add(data, 21) for { } lt(i, end) { i := add(i, 1) } { mstore(ptr, i) pop(call(gas(), keccak256(data, 85), 0, 0, 0, 0, 0)) } } ``` 为简洁起见, `destroyChildren()` 字节码的反汇编是由阅读器来完成的,总的流程与 GST2 类似,但是进行了一些修改来降低 `CREATE2` 目标地址查找的难度 —— 这就是效率提高 10% 的由来。 ## 为什么要关注 GasToken 2020 年之前,几乎没有人公开关注 GasToken 1、2 或 CHI。然而,到了 2020 年,DeFi 热潮引发了 “gas 大战”,gas 费飙升至 500 GWei 以上,并触发了 Geth 的默认设置[内存池溢出](https://www.blocknative.com/blog/mempool-forensics) —— 导致以太坊交易丢失! 然而,在这个默默无闻的以太坊小工具上出现的讽刺事件是,当网络拥堵最严重时,GasToken 的价格(以美元计价)也在 Uniswap 等去中心化交易所上达到顶峰。因此,卖出 GasToken 来赚取利润的生意,因为 gas 本身价格的高涨,并不令人轻松;而且,小数额的卖出,很容易错过一段时间内的高点。(注:这绝不是投资建议。) 根据定义,GasToken 当然是最具实用性的代币,因为它直接充当网络的交易池。有些人[建议](https://medium.com/sunnya97/monetizing-smart-contracts-with-gastoken-abd87f231b23)使用 GasToken 来实现一种基于合约的公益品融资。或许这比 [Near Protocol](https://insights.deribit.com/market-research/transaction-fee-economics-in-near/) 强制规定的智能合约开发者收取基础交易 gas 成本总额的 30%(后者也存在自身的问题,例如,鼓励效率低下的智能合约设计)更好。 ## 非正统 GasToken [DefiSaver](https://defisaver.com/) 旨在为用户提供更加友好的方式,以便其与不同的 DeFi 协议交互。这一工具通过函数修饰符在合约中使用 GasToken。这个修饰符使用正统的 GST2 合约,目前在几乎所有 DefiSaver 包装的协议函数调用中都使用硬编码的值进行调用。一个有趣的分析是,随着时间的推移,这种方法可以节省多少交易费。[Tenderly](https://tenderly.co/) 等新型以太坊工具凭借其优越的 GasProfiler 和仿真模式使之成为可能。 虽然这种硬编码模式肯定有效,但是经过改进的设计需要依赖当前 gas 价格——这时,chainlink 等[信息输入机制](https://feeds.chain.link/fast-gas-gwei)就派上了用场。设计上必须谨慎,因为这可能会带来很高的成本(`lastestAnswer()` 的成本约为 15000 gas)。 其它著名用例/设计有 [GasToken 工厂](https://blog.polymath.network/turning-smart-contracts-into-gastoken-factories-3e947f664e8b)和将 CHI GasToken 纳入 MakerDAO 质押品的[提案](https://forum.makerdao.com/t/chi-chi-gastoken-collateral-onboarding-application/3126/28)。 ## 铸造 GasToken 那么,为什么没有更多合约使用 GasToken?状态膨胀(即,节点的存储量大小)的问题越来越严重,或许这就是 GasToken 被视为有害状态操作的原因。就像一些持纯粹主义的比特币持有者[拒绝](https://news.bitcoin.com/veriblock-captured-close-to-60-of-btcs-op-return-transactions-in-2019/)采用 `OP_RETURN` 比特币脚本操作码来 存储/销毁 比特币区块链上的任意数据的做法,称这会导致不必要的状态膨胀。 [状态租赁](https://ethresear.ch/t/ethereum-state-rent-for-eth-1-x-pre-eip-document/4378)这一想法似乎已经被放弃,一方面是因为可能会引入过多的复杂性,另一方面是因为无状态客户端的出现和 ETH 2.0 有望引入另一种状态存储架构。虽然可能性很低,但是 ETH 1.0 的矿工可能会抵制状态膨胀,选择审查类似 GasToken 的机制的交易,因为状态膨胀会直接增加运行全节点的成本,尽管增加的成本很少 —— 256 比特的存储插槽的真正成本几乎可以忽略不计。 另一个更加实际的因素是,GasToken 从中长期来看存在操作码重新定价的风险。 ## 没有风险的修改提议 由于伊斯坦布尔硬分叉引入了 [EIP 2200](https://eips.ethereum.org/EIPS/eip-2200),存储操作码已经过大规模重组,不过这些更改涉及特定情况下的 记账/计量方式;`SLOAD` 的 gas 价格上涨,`SSTORE` 则没有。 最近,[EIP-2929](https://eips.ethereum.org/EIPS/eip-2929) 提议了一些修改。这些修改源自一篇帝国理工学院(Imperial College London)的论文。此前,这篇论文还被用来详细分析操作码的 gas 定价(过低)问题。这个 EIP 提议增加交易首次使用 `SLOAD`、 `*CALL`、 `BALANCE`、 `EXT*` 和 `SELFDESTRUCT` 所需的 gas 成本,因为考虑到这些操作码读取的状态量和访问状态所需的时间,它们都存在定价过低的问题。 特别要指出的是,这个 EIP 流程提议增加交易范围内的 `addresses_accessed`和 `accessed_storage_keys` 集合,以便区分冷热状态访问,向冷账户/状态访问收取额外的 2600 gas,并将热状态存储访问的 gas 成本减少至 100 gas。 由于 `COLD_SLOAD_COST` 是基于 `SSTORE_RESET_GAS`收费的,基于存储的 **GasToken1 的经济机制就不那么有吸引力了。**GasToken 1 似乎不常用,因为它只能在较小的 GasPrice 率范围内节省成本。所以再见了,GasToken1。 对 `SELFDESTRUCT` 的修改提议不会影响 GST2 或 CHI 和 `free*()` 部分,因为 gas 退款的接收方 parent 合约已经在 `addresses_accessed`集合内。但是,如果该机制采用不同的设计,如,接收退款的地址不在 `addresses_accessed` 集合内,那就不同了。但是,所有这些都不是断言 GasToken 的经济模型会改变,或是使之不那么 有效/可行。 ## Gas 在 2020 年发生了什么 ? Eth 1.x 社区有一个[提议](https://ethresear.ch/t/oil-adding-a-second-fuel-source-to-the-evm-pre-eip-v1-1/7425)是,增加一个记账单位,以便进行计算。这个单位被命名为 oil(石油),与 gas 并行运作(操作码成本和初始限制相同),但是存在以下几点关键区别: * 如果交易在执行过程中将 oil 耗尽,交易可还原。在 gas 机制中,如果 gas 耗尽,交易只能还原当前帧,并让调用者检查结果。相比之下,如果 oil 耗尽,整个交易都会还原(所有帧)。 * 不同于 gas,调用者合约无法限制被调用者合约可以使用的 oil 数量。 * 交易所退回的以太币数量将基于剩余的 oil 而非 gas 来计算。 上文所提到的 “帧(frame)” 指的是合约环境,即,执行合约时涉及到的 内存/状态 区域。这当然是一个有趣的提议,也许会增加复杂性,但是从最初的方案来看,似乎不会打破正统的 GasToken 合约(请注意,这是 oil 机制的目标),可以保证大多数合约的向后兼容性。关于 oil 概念/机制的更多内容不在本文的讨论范围内,但是各位读者可以关注一下。 ### 玩梗时间 破产(Broke):在网络发生拥堵时支付 gas 价格 未雨绸缪(Woke):在网络通畅时铸造 GasToken,在网络拥堵时释放 GasToken 定制化(Bespoke):将 GasToken 动态整合到你的合约设计中,使用链上 gas 价格输入机制在有利可图时触发 GasToken 机制 ![](https://img.learnblockchain.cn/2020/11/10/16049786116820.jpg) <center>要找到一张有趣的燃气表的图片真的太难了</center> 关于 GasToken 及其动态还有很多可以说。我支持那些提出这一想法的[人](https://gastoken.io/)。GasToken 不仅有趣,还能鼓励人们更好地理解 EVM,推动对状态维护、合约设计和 gas 市场动态的深入思考。 如果你了解更多炫酷的 GasToken 应用/用例,请通过[推特](https://twitter.com/gawnieg)联系我,或在评论区写下评论。 (完) * * * **原文链接:** [https://medium.com/coinmonks/gastoken-or-how-i-learned-to-stop-worrying-and-love-gas-price-surges-6aaee9fb0ba3](https://medium.com/coinmonks/gastoken-or-how-i-learned-to-stop-worrying-and-love-gas-price-surges-6aaee9fb0ba3) **作者:** Aodhgan Gleeson **翻译&校对:** 闵敏 & 阿剑 * * *

本文旨在探索 EVM 的 gas 机制,尤其是 GasToken 的 EVM gas 机制。首先,为了降低技术理解的难度,本文需要先给出一段介绍。如果你不想了解底层机制,可以直接跳到后文的 “具体实现细节” 一节开始阅读。

<center>要是某个表出现反转那就完蛋了 —— 但是 EVM 就不一样了</center>

引言—— gas 的基础知识

以太坊使用了一种 gas 计量系统,主要是为了防止停机问题和重入攻击(reentry attack)。这个计量系统似乎是最简单,也是最健壮的(尽管还有其它计量系统,如 EOS 系统)。EVM 的每个操作码都有固定的 gas 消耗量,黄皮书中注明了不同指令的 gas 成本等级:零级(0 gas)、基础级(2 gas)、超低级(3 gas)、低级(5 gas)、高级(10 gas),以及规则更加复杂的特殊等级。

例如,在 EVM 堆栈上添加或删除操作需要花费 3 gas。其中一些操作码在某些硬分叉部署之后经过了重新定价,例如,calldata(EVM 中的只读内存区域 —— 总计的 4 种存储类型之一)已经从每字节 68 gas 的价格下调至每字节 18 gas。重新定价似乎是为了促进二层可扩展方案的实现,因为二层可扩展方案需要链上数据可用性。还有证据表明,操作码的原始定价并没有经过充分分析,依然存在定价不当的问题。另外,更改操作码的 gas 消耗量也会带来问题:

  • 降低指令的 gas 价格可能会让重入攻击变得可行
  • 提高指令的 gas 价格可能会致使调用失败,因为这会导致 gas 分配量不足以执行调用

一笔交易所使用的操作码会累计出一个总的 gas 使用量。gas 使用量与 gasPrice(以太坊交易中的用户设置字段,即该用户愿意为每单位的 gas 支付的 eth 价格)的乘积会转换成 Wei,也就是以太坊原生代币 ETH。更多关于交易的基础知识,可以参见这篇文章。

区块的 gas 上限

一般情况下,矿工都是依据最高价拍卖模型将交易打包到区块内的。备受期待的 EIP 1559 意图将这一动态转变为更有效的结构,另外还有交易费必须使用以太币支付所带来的副作用。然而,这篇文章不是专门介绍 EIP 1559 的,EIP 1559 这个主题本身就具有非常深远的影响。在这篇文章中,我们将聚焦于促成 GasToken 的 gas 机制/经济学。

每个区块都有一个相关的 gas 上限,目前(2020 年 11 月)是 1250 万 gas。因此,由于区块容量有限,形成了一个竞争激烈的 “区块空间” 市场。虽然验证时间占区块传播时间的比例低于 1%,但这个上限的存在还是保证了网络的安全性。将区块 gas 上限定得太高,节点很难赶在下一个区块挖出之前执行完区块中的所有交易(也有可能跳过这些交易 —— 具体参见验证者困境)。将区块 gas 上限定得太低,就会导致网络拥堵和缺乏实用性。关于这里的权衡关系,请参见这篇文章。

有趣的是,矿工可以使用节点 cli flag 来标记他们所期望的区块 gas 上限,但是修改 gas 上限(例如最近从 8M 上调至 12.5 M)似乎主要发生在 “社会层面(推特)” 上。正是因为矿工可以上调/下调每个区块 gas 上限的机制,让我们明白了下图为什么会出现峰值:

上图显示了区块 gas 上限随时间流逝的变化情况。请注意,区块 gas 上限之所以会在 2016 年底大幅降低,是因为当时遭到了 DDoS 攻击。(来源:https://blog.ethereum.org/2016/09/22/ethereum-network-currently-undergoing-dos-attack/)

区块空间拍卖被认为在经济学/机制设计方面开辟了新的领域,因为传统拍卖理论是以免费投标的假设为前提的。以太坊交易并非如此,交易费率必须在一定的阈值之上,而且一旦交易被广播到点对点网络上,就不受控制了。

接下来进入正题

可以说,最有趣的操作码同时也是成本较高的操作码,如 SSTORECREATECREATE2SELFDESTRUCT。这些操作码的共同点是,它们都涉及状态,因此也涉及硬盘读写(以太坊网络的节点通常使用固态硬盘)。这些操作码成本更高,因为它们会影响永久存储和全局状态树。

什么是 GasToken

GasToken 巧妙地利用了 gas 定价系统。它利用的是清理状态、清理存储插槽(storage slot)和删除带有自毁操作码的合约(这些操作都可以删减全局状态树)所收到的 gas 退款。这些操作都可以被认为具备负 gas 价格。

  • 清理/自毁合约:- 24,000 gas
  • 清理/删除存储:-15,000 gas

当 EVM 执行这类操作时,gas 退款是通过一个独立的交易退款计量器来计算的。gas 退款只会在交易结束时提供。另外,最高 gas 退款量是该交易所消耗 gas 量的一半。

理想情况是在网络 gas 价格较低时写入状态,并在 gas 价格较高时删除状态。由于以 Wei/ETH 为计价单位的总费用是 gas 使用量和 gas 价格的乘积,当 gas 价格较高时,减少 gas 使用量会导致总费用降低。

GasToken 的正统实现很好地体现了名称中的 “token(代币)”部分,因为它与 ERC-20 代币相似,并带有 approvetransferFrom操作码,可以称为多步骤交易的一部分。最初,GasToken 有两种变体,分别采用不同的设计:GST1 和 GST2。GST1 使用的是存储成本和退款机制,GST2 使用的是 CREATE 和自毁机制。这些变体采取不同的节约方案,具体取决于 gas 价格差值比(铸造代币和释放代币时的 gas 价格差值比)。由于 gas 价格率更高,GST2 更能节约 gas。

开采或 “铸造” GasToken 就是将其 写入存储/创建合约,而销毁或 “释放” GasToken 就是减少用户持有的 GasToken 数量并删除状态存储插槽。虽然正统的 GasToken 很流行,但是许多开发者选择克隆这一功能,并放到他们自己的系统合约中使用,从而减少成本和设计复杂性。

具体实现细节

GST1 —— 基于存储

从智能合约的层面来看,GST1 是什么样的?我们先来看一下 mint() 函数:

function mint(uint256 value) public {
    uint256 storage_location_array = STORAGE_LOCATION_ARRAY;  // can't use constants inside assembly
    if (value == 0) {
        return;
    }
    // Read supply
    uint256 supply;
    assembly {
        supply := sload(storage_location_array)
    }
    // Set memory locations in interval [l, r]
    uint256 l = storage_location_array + supply + 1;
    uint256 r = storage_location_array + supply + value;
    assert(r >= l);
    for (uint256 i = l; i &lt;= r; i++) {
        assembly {
            sstore(i, 1)
        }
    }
    // Write updated supply & balance
    assembly {
        sstore(storage_location_array, add(supply, value))
    }
    s_balances[msg.sender] += value;
}

简单来说,我们使用一个存储起点常量来标记 EVM 存储的开始,而且这个常量还包括我们已经写入多少个插槽的值。如果你想了解更多关于 EVM 中永久存储布局的内容,请阅读这篇文章。通过第 12 和第 13 行的代码,我们可以计算出新的待写入插槽范围,并在第 17 行的 for 循环中使用 SSTORE 操作码来将数据写入这些插槽,存储数值 1(这个值可以替换成任何非零值)。然后,我们在第 22 和 24 行代码处更新已写入数据的插槽数量和余额。

自由函数更有趣一点,具备以下功能:freeFromUpTo(uint value)freeFrom(uint value)freeUpTo(uint value)free(uint value)。这类函数在下文统称为 free*() 函数,调用内部函数 freeStorage()

function freeStorage(uint256 value) internal {
    uint256 storage_location_array = STORAGE_LOCATION_ARRAY;  // can't use constants inside assembly
    // Read supply
    uint256 supply;
    assembly {
        supply := sload(storage_location_array)
    }
    // Clear memory locations in interval [l, r]
    uint256 l = storage_location_array + supply - value + 1;
    uint256 r = storage_location_array + supply;
    for (uint256 i = l; i &lt;= r; i++) {
        assembly {
            sstore(i, 0)
        }
    }
    // Write updated supply
    assembly {
        sstore(storage_location_array, sub(supply, value))
    }
}

如你所见,该函数与上文讨论的 mint() 函数几乎相同,主要的区别在于第 13 行代码,将值 0 写入存储会导致 EVM 释放存储插槽。这行代码会触发 gas 退款,让 gas 退款计数器增加 15000。更新 ERC-20 类型余额的任务也由 free*() 函数承担。

GST2 —— 基于合约

mint() 函数等价的函数,在 GST2 合约里叫做 makeChild() ,它是一个内部函数,使用 EVM 来汇编创建一个简单的 “child” 合约,而且该合约只能用 “parent” 合约来摧毁:

function makeChild() internal returns (address addr) {
    assembly {
        // EVM assembler of runtime portion of child contract:
        //     ;; Pseudocode: if (msg.sender != 0x0000000000b3f879cb30fe243b4dfee438691c04) { throw; }
        //     ;;             selfdestruct(msg.sender)
        //     PUSH15 0xb3f879cb30fe243b4dfee438691c04 ;; hardcoded address of this contract
        //     CALLER
        //     XOR
        //     PC
        //     JUMPI
        //     CALLER
        //     SELFDESTRUCT
        // Or in binary: 6eb3f879cb30fe243b4dfee438691c043318585733ff
        // Since the binary is so short (22 bytes), we can get away
        // with a very simple initcode:
        //     PUSH22 0x6eb3f879cb30fe243b4dfee438691c043318585733ff
        //     PUSH1 0
        //     MSTORE ;; at this point, memory locations mem[10] through
        //            ;; mem[31] contain the runtime portion of the child
        //            ;; contract. all that's left to do is to RETURN this
        //            ;; chunk of memory.
        //     PUSH1 22 ;; length
        //     PUSH1 10 ;; offset
        //     RETURN
        // Or in binary: 756eb3f879cb30fe243b4dfee438691c043318585733ff6000526016600af3
        // Almost done! All we have to do is put this short (31 bytes) blob into
        // memory and call CREATE with the appropriate offsets.
        let solidity_free_mem_ptr := mload(0x40)
        mstore(solidity_free_mem_ptr, 0x00756eb3f879cb30fe243b4dfee438691c043318585733ff6000526016600af3)
        addr := create(0, add(solidity_free_mem_ptr, 1), 31)
    }

仔细研究这个汇编代码可以更好地理解 EVM。我个人的观点是,合约开发者在原则上不应该使用汇编,但也有例外,那就是在设计上要求最小化并要求极高效率的合约。这个合约,还有 EIP-1167,就是例子。

优化

第 4 行和第 5 行中展示的时 child 合约中的回调函数(fallback function)的伪代码 —— 为什么要用回调函数?因为我们希望 child 合约能尽可能简单,简单到只有一个函数。

PUSH15 开始:地址本来有 20 个字节,但在这里,我们想把 15 个字节推入这个栈(这是最优实现),因为我们使用了 vanity-address 风格的技巧,它会重复地哈希,直到找到符合需要的地址,所以前面 5 个字节都是 0。剩下还需要 5 个 0,作为默认的一部分填充进去,组成 32 个字节,也就是 EVM 里面 word 的大小。这里的优化是很重要的,因为用来创建 chile 合约所用的 gas 可以认为是整个 GasToken 方案的开销。

下一步, CALLER 把合约调用者的地址推入栈中。 XOR 会从栈中弹出两个物,然后把这两个值的按位异或运算结果推入栈中。如果这两个值相等,则栈顶为 0,反之则是一个非零的数字。 PC 在与此操作对应的增量出现之前从程序计数器处获得一个值,并推入栈中。 JUMPI ,一个条件跳转,从栈中取出栈顶的两个值,一个条件和一个目标,如果条件为真,就跳到目标,如果条件不为真,那就失败。

如果 JUMPI 的结果不是 JUMPDEST 操作码,EVM 就会回滚,这保证了调用者是 parent 合约(满足 != 条件)。失败的路径结束后,就把 parent 合约的地址推入栈中,当下一次 slefdestruct 操作执行时,弹出栈顶的 word,作为 gas 退款的目标。

free*() 函数调用下列 destroyChildren() 实现:

function destroyChildren(uint256 value) internal {
    uint256 tail = s_tail;
    // tail points to slot behind the last contract in the queue
    for (uint256 i = tail + 1; i &lt;= tail + value; i++) {
        mk_contract_address(this, i).call();
    }

    s_tail = tail + value;
}

点击此处,查看源代码

此处,我们遍历 child 合约,并调用这些合约中的回调函数。正如 GST2 文档所指出的那样,发行代币时,合约必须找到 child 合约的创建地址(存储这些地址的成本会很高,因此我们会即时计算这些地址)。幸运的是,这是有可能做到的,因为使用 CREATE 生成的合约地址是根据地址/账户已创建的合约数量(nonce)计算得出的,具有确定性。这些合约地址都是在 mk_contract_address 函数中计算得出的,在调用时无需任何参数或值,调用回调函数,然后就像在对应 mint() 函数中硬编码的那样,gas 退款会发送至 parent 合约。

CHI GasToken

历时 3 年,CHI GasToken 终于上线。CHI 由去中心化交易所聚合器 1inch.exchange 开发,与传统的 GasToken 类似,但是铸币效率比后者高出 1%,释放代币的效率比后者高出 10%,而且采用新的 CREATE2 操作码。该操作码可以提前通过确定性方式来创建链上合约地址,主要用于反事实的 Layer-2 解决方案。

CREATE2 操作码采用 4 个堆栈参数:endowment、memory_start、memory_length 和盐值。生成地址等于 keccak256( 0xff ++ address ++ salt ++ keccak256(init_code))[12:],而非常见的将发送方地址和 nonce 进行哈希计算。由于盐值控制在用户手中,用户可以提前知道地址。

function mint(uint256 value) public {
    uint256 offset = totalMinted;
    assembly {
        mstore(0, 0x746d4946c0e9F43F4Dee607b0eF1fA1c3318585733ff6000526015600bf30000)

        for {let i := div(value, 32)} i {i := sub(i, 1)} {
            pop(create2(0, 0, 30, add(offset, 0))) pop(create2(0, 0, 30, add(offset, 1)))
            pop(create2(0, 0, 30, add(offset, 2))) pop(create2(0, 0, 30, add(offset, 3)))
            pop(create2(0, 0, 30, add(offset, 4))) pop(create2(0, 0, 30, add(offset, 5)))
            pop(create2(0, 0, 30, add(offset, 6))) pop(create2(0, 0, 30, add(offset, 7)))
            pop(create2(0, 0, 30, add(offset, 8))) pop(create2(0, 0, 30, add(offset, 9)))
            pop(create2(0, 0, 30, add(offset, 10))) pop(create2(0, 0, 30, add(offset, 11)))
            pop(create2(0, 0, 30, add(offset, 12))) pop(create2(0, 0, 30, add(offset, 13)))
            pop(create2(0, 0, 30, add(offset, 14))) pop(create2(0, 0, 30, add(offset, 15)))
            pop(create2(0, 0, 30, add(offset, 16))) pop(create2(0, 0, 30, add(offset, 17)))
            pop(create2(0, 0, 30, add(offset, 18))) pop(create2(0, 0, 30, add(offset, 19)))
            pop(create2(0, 0, 30, add(offset, 20))) pop(create2(0, 0, 30, add(offset, 21)))
            pop(create2(0, 0, 30, add(offset, 22))) pop(create2(0, 0, 30, add(offset, 23)))
            pop(create2(0, 0, 30, add(offset, 24))) pop(create2(0, 0, 30, add(offset, 25)))
            pop(create2(0, 0, 30, add(offset, 26))) pop(create2(0, 0, 30, add(offset, 27)))
            pop(create2(0, 0, 30, add(offset, 28))) pop(create2(0, 0, 30, add(offset, 29)))
            pop(create2(0, 0, 30, add(offset, 30))) pop(create2(0, 0, 30, add(offset, 31)))
            offset := add(offset, 32)
        }

        for {let i := and(value, 0x1F)} i {i := sub(i, 1)} {
            pop(create2(0, 0, 30, offset))
            offset := add(offset, 1)
        }
    }

    _mint(msg.sender, value);
    totalMinted = offset;
}

这里的一般流程是,将固定的 child 合约字节码存储到 memory 中(第 4 行代码),然后使用 for 循环反复调用 CREATE2,直到计算出对应的值为止。 CREATE2返回已部署 child 合约的地址,我们不关心这个地址,因此我们只是将这个地址从堆栈中弹出。偏移量计数器被用来计算 child 合约的数量,并将其永久存储在第 33 行代码中。

对应的 free*() 函数调用 _destoryChildren()

function _destroyChildren(uint256 value) internal {
    assembly {
        let i := sload(totalBurned_slot)
        let end := add(i, value)
        sstore(totalBurned_slot, end)

        let data := mload(0x40)
        mstore(data, 0xff0000000000004946c0e9F43F4Dee607b0eF1fA1c0000000000000000000000)
        mstore(add(data, 53), 0x3c1644c68e5d6cb380c36d1bf847fdbc0c7ac28030025a2fc5e63cce23c16348)
        let ptr := add(data, 21)
        for { } lt(i, end) { i := add(i, 1) } {
            mstore(ptr, i)
            pop(call(gas(), keccak256(data, 85), 0, 0, 0, 0, 0))
        }
    }

为简洁起见, destroyChildren() 字节码的反汇编是由阅读器来完成的,总的流程与 GST2 类似,但是进行了一些修改来降低 CREATE2 目标地址查找的难度 —— 这就是效率提高 10% 的由来。

为什么要关注 GasToken

2020 年之前,几乎没有人公开关注 GasToken 1、2 或 CHI。然而,到了 2020 年,DeFi 热潮引发了 “gas 大战”,gas 费飙升至 500 GWei 以上,并触发了 Geth 的默认设置内存池溢出 —— 导致以太坊交易丢失!

然而,在这个默默无闻的以太坊小工具上出现的讽刺事件是,当网络拥堵最严重时,GasToken 的价格(以美元计价)也在 Uniswap 等去中心化交易所上达到顶峰。因此,卖出 GasToken 来赚取利润的生意,因为 gas 本身价格的高涨,并不令人轻松;而且,小数额的卖出,很容易错过一段时间内的高点。(注:这绝不是投资建议。)

根据定义,GasToken 当然是最具实用性的代币,因为它直接充当网络的交易池。有些人建议使用 GasToken 来实现一种基于合约的公益品融资。或许这比 Near Protocol 强制规定的智能合约开发者收取基础交易 gas 成本总额的 30%(后者也存在自身的问题,例如,鼓励效率低下的智能合约设计)更好。

非正统 GasToken

DefiSaver 旨在为用户提供更加友好的方式,以便其与不同的 DeFi 协议交互。这一工具通过函数修饰符在合约中使用 GasToken。这个修饰符使用正统的 GST2 合约,目前在几乎所有 DefiSaver 包装的协议函数调用中都使用硬编码的值进行调用。一个有趣的分析是,随着时间的推移,这种方法可以节省多少交易费。Tenderly 等新型以太坊工具凭借其优越的 GasProfiler 和仿真模式使之成为可能。

虽然这种硬编码模式肯定有效,但是经过改进的设计需要依赖当前 gas 价格——这时,chainlink 等信息输入机制就派上了用场。设计上必须谨慎,因为这可能会带来很高的成本(lastestAnswer() 的成本约为 15000 gas)。

其它著名用例/设计有 GasToken 工厂和将 CHI GasToken 纳入 MakerDAO 质押品的提案。

铸造 GasToken

那么,为什么没有更多合约使用 GasToken?状态膨胀(即,节点的存储量大小)的问题越来越严重,或许这就是 GasToken 被视为有害状态操作的原因。就像一些持纯粹主义的比特币持有者拒绝采用 OP_RETURN 比特币脚本操作码来 存储/销毁 比特币区块链上的任意数据的做法,称这会导致不必要的状态膨胀。

状态租赁这一想法似乎已经被放弃,一方面是因为可能会引入过多的复杂性,另一方面是因为无状态客户端的出现和 ETH 2.0 有望引入另一种状态存储架构。虽然可能性很低,但是 ETH 1.0 的矿工可能会抵制状态膨胀,选择审查类似 GasToken 的机制的交易,因为状态膨胀会直接增加运行全节点的成本,尽管增加的成本很少 —— 256 比特的存储插槽的真正成本几乎可以忽略不计。

另一个更加实际的因素是,GasToken 从中长期来看存在操作码重新定价的风险。

没有风险的修改提议

由于伊斯坦布尔硬分叉引入了 EIP 2200,存储操作码已经过大规模重组,不过这些更改涉及特定情况下的 记账/计量方式;SLOAD 的 gas 价格上涨,SSTORE 则没有。

最近,EIP-2929 提议了一些修改。这些修改源自一篇帝国理工学院(Imperial College London)的论文。此前,这篇论文还被用来详细分析操作码的 gas 定价(过低)问题。这个 EIP 提议增加交易首次使用 SLOAD*CALLBALANCEEXT*SELFDESTRUCT 所需的 gas 成本,因为考虑到这些操作码读取的状态量和访问状态所需的时间,它们都存在定价过低的问题。

特别要指出的是,这个 EIP 流程提议增加交易范围内的 addresses_accessedaccessed_storage_keys 集合,以便区分冷热状态访问,向冷账户/状态访问收取额外的 2600 gas,并将热状态存储访问的 gas 成本减少至 100 gas。

由于 COLD_SLOAD_COST 是基于 SSTORE_RESET_GAS收费的,基于存储的 GasToken1 的经济机制就不那么有吸引力了。GasToken 1 似乎不常用,因为它只能在较小的 GasPrice 率范围内节省成本。所以再见了,GasToken1。

SELFDESTRUCT 的修改提议不会影响 GST2 或 CHI 和 free*() 部分,因为 gas 退款的接收方 parent 合约已经在 addresses_accessed集合内。但是,如果该机制采用不同的设计,如,接收退款的地址不在 addresses_accessed 集合内,那就不同了。但是,所有这些都不是断言 GasToken 的经济模型会改变,或是使之不那么 有效/可行。

Gas 在 2020 年发生了什么 ?

Eth 1.x 社区有一个提议是,增加一个记账单位,以便进行计算。这个单位被命名为 oil(石油),与 gas 并行运作(操作码成本和初始限制相同),但是存在以下几点关键区别:

  • 如果交易在执行过程中将 oil 耗尽,交易可还原。在 gas 机制中,如果 gas 耗尽,交易只能还原当前帧,并让调用者检查结果。相比之下,如果 oil 耗尽,整个交易都会还原(所有帧)。
  • 不同于 gas,调用者合约无法限制被调用者合约可以使用的 oil 数量。
  • 交易所退回的以太币数量将基于剩余的 oil 而非 gas 来计算。

上文所提到的 “帧(frame)” 指的是合约环境,即,执行合约时涉及到的 内存/状态 区域。这当然是一个有趣的提议,也许会增加复杂性,但是从最初的方案来看,似乎不会打破正统的 GasToken 合约(请注意,这是 oil 机制的目标),可以保证大多数合约的向后兼容性。关于 oil 概念/机制的更多内容不在本文的讨论范围内,但是各位读者可以关注一下。

玩梗时间

破产(Broke):在网络发生拥堵时支付 gas 价格

未雨绸缪(Woke):在网络通畅时铸造 GasToken,在网络拥堵时释放 GasToken

定制化(Bespoke):将 GasToken 动态整合到你的合约设计中,使用链上 gas 价格输入机制在有利可图时触发 GasToken 机制

<center>要找到一张有趣的燃气表的图片真的太难了</center>

关于 GasToken 及其动态还有很多可以说。我支持那些提出这一想法的人。GasToken 不仅有趣,还能鼓励人们更好地理解 EVM,推动对状态维护、合约设计和 gas 市场动态的深入思考。

如果你了解更多炫酷的 GasToken 应用/用例,请通过推特联系我,或在评论区写下评论。

(完)

原文链接: https://medium.com/coinmonks/gastoken-or-how-i-learned-to-stop-worrying-and-love-gas-price-surges-6aaee9fb0ba3 作者: Aodhgan Gleeson 翻译&校对: 闵敏 & 阿剑

区块链技术网。

  • 发表于 2020-11-09 15:10
  • 阅读 ( 1166 )
  • 学分 ( 45 )
  • 分类:以太坊

评论