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

(续前)GST2
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;
}
此处,我们遍历 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、 *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 社区有一个提议是,增加一个记账单位,以便进行计算。这个单位被命名为 oil(石油),与 gas 并行运作(操作码成本和初始限制相同),但是存在以下几点关键区别:
· 如果交易在执行过程中将 oil 耗尽,交易可还原。在 gas 机制中,如果 gas 耗尽,交易只能还原当前帧,并让调用者检查结果。相比之下,如果 oil 耗尽,整个交易都会还原(所有帧)。
· 不同于 gas,调用者合约无法限制被调用者合约可以使用的 oil 数量。
· 交易所退回的以太币数量将基于剩余的 oil 而非 gas 来计算。
上文所提到的 “帧(frame)” 指的是合约环境,即,执行合约时涉及到的 内存/状态 区域。这当然是一个有趣的提议,也许会增加复杂性,但是从最初的方案来看,似乎不会打破正统的 GasToken 合约(请注意,这是 oil 机制的目标),可以保证大多数合约的向后兼容性。关于 oil 概念/机制的更多内容不在本文的讨论范围内,但是各位读者可以关注一下。
玩梗时间
破产(Broke):在网络发生拥堵时支付 gas 价格
未雨绸缪(Woke):在网络通畅时铸造 GasToken,在网络拥堵时释放 GasToken

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

关于 GasToken 及其动态还有很多可以说。我支持那些提出这一想法的人。GasToken 不仅有趣,还能鼓励人们更好地理解 EVM,推动对状态维护、合约设计和 gas 市场动态的深入思考。
如果你了解更多炫酷的 GasToken 应用/用例,请通过推特联系我,或在评论区写下评论。