#byte #building-block #gas #contract #dapp #l2 #calldata

app solid-grinder

一个与智能合约构建块一起使用的CLI。配合我们前端代码片段,此工具箱可以通过为dApp开发编码calldata来尽可能减少L2燃气成本。

12个版本 (4个稳定版)

1.1.5 2024年5月16日
1.1.4 2024年4月6日
1.0.0 2023年11月28日
0.4.0 2023年10月31日
0.0.4 2023年8月14日

#311 in 神奇豆子

Download history 4/week @ 2024-04-13 159/week @ 2024-05-11 18/week @ 2024-05-18 4/week @ 2024-05-25 1/week @ 2024-06-08 7/week @ 2024-07-06 53/week @ 2024-07-27

每月60次下载

MIT许可证

255KB
3K SLoC

Solidity 1.5K SLoC // 0.1% comments Handlebars 516 SLoC Rust 514 SLoC // 0.0% comments JavaScript 228 SLoC // 0.1% comments

calldata燃气优化器!!

一个与智能合约构建块一起使用的CLI。配合我们前端代码片段,此工具箱可以通过为dApp开发编码calldata来尽可能减少L2燃气成本。

[!警告] 代码尚未经过审计。请在生产环境中谨慎使用。

这是做什么用的 ?

此dApp构建块旨在通过使用calldata优化范式显著降低L2燃气成本。

要了解更多关于 calldata优化 的信息,请查看这篇技术文章 !!

虽然安全性是我们的首要任务,但我们旨在提高开发者体验,以便整个协议无需从头开始重写。

您需要做的是指定如何将参数打包成一个单独的calldata,然后我们的CLI将为您生成所需的文件!!

它是如何工作的

它通过使用尽可能少的calldata字节来优化calldata。

具体来说,我们的创新组件包括以下内容

  1. Solidity片段:一个用于链上编码调用数据的合约。另一个用于解码它。此组件具有以下功能

    • AddressTable:用于存储 地址与索引之间的映射,允许
      • 地址 注册到合约,然后生成索引。
      • 生成的ID然后可以在压缩的 调用数据 解码 过程中查找已注册的地址
    • 数据序列化,允许
      • 编码后的calldata可以在足够的数据大小下反序列化为正确的类型。
      • 例如,如果我们选择通过发送uint40(5字节)类型的time period作为参数来减少calldata,而不是使用uint256,那么calldata应该在正确的偏移量处被切割,并且在后续步骤中可以正确使用。
  2. 前端代码片段:将编码和解码组件原子性地连接到单个调用中

  3. CLI:生成上述Solidity代码片段(包括Encoder和Decoder合约)。唯一的任务是在确保安全性的同时指定要打包calldata的数据类型。

基准

基准测试 - 在主网上!!

我们通过部署和交互以下合约进行了基准测试:未优化版本/UniswapV2Router02.sol优化版本 /UniswapV2Router02_Optimized.sol

然后,我们在以下交易中比较了gas成本的差异

  1. https://optimistic.etherscan.io/tx/0x446b8d7f091ff258d16dfbac751797210ad1edeb5f856c0ac0686b80d32516a5

然后,L2支付的费用是0.00011025。

Unoptimized

  1. https://optimistic.etherscan.io/tx/0x778a6beb856540c5534d7516fa168e0b26b09086e414317748ac01c153e81f01 Optimized

然后,L2支付的费用是0.00007481。

可以看到,L1 gas节省了约36%(从0.000029 ETH到0.000018 ETH),但总体成本更高。然而,在L1网络拥堵时,节省的gas量可能会更高。

例如,如果L1 gas价格增加到100 Gwei,并且L1费率标量调整为1,则数字将从0.000424 ETH变为0.000263 ETH。

根据公式

总燃气量

未优化合约的总费用为 0.00007481 + 0.000424 = 0.0004988。

优化合约的总费用为 0.00011025 + 0.000263 = 0.00034325。

燃气节省了 31 %

基准测试 - 背后情况

我们提供了UniswapV2的router如何优化的方法如下

    /** ... */
    contract UniswapV2Router02 is IUniswapV2Router02 {

        /** ... */

        function addLiquidity(
            address tokenA,
            address tokenB,
            uint256 amountADesired,
            uint256 amountBDesired,
            uint256 amountAMin,
            uint256 amountBMin,
            address to,
            uint256 deadline
        ) public virtual override ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
            /** ... */
        }
        /** ... */
    }


    /** ... */
    contract UniswapV2Router02_Optimized is UniswapV2Router02, Ownable, UniswapV2Router02_Decoder {

        /** ... */

        function addLiquidityCompressed(bytes calldata _payload)
            external
            payable
            returns (uint256 amountA, uint256 amountB, uint256 liquidity)
        {
            (addLiquidityData memory addLiquidityData,) = _decode_addLiquidityData(_payload, 0);

            return UniswapV2Router02.addLiquidity(
                addLiquidityData.tokenA,
                addLiquidityData.tokenB,
                addLiquidityData.amountADesired,
                addLiquidityData.amountBDesired,
                addLiquidityData.amountAMin,
                addLiquidityData.amountBMin,
                addLiquidityData.to,
                addLiquidityData.deadline
            );
        }
        /** ... */
  
    }

第二个是 UniswapV2Router02_Encoder.sol


    /** ... */
   contract UniswapV2Router02_Encoder {
    IAddressTable public immutable addressTable;

        /** ... */

        function encode_addLiquidityData(
            address tokenA,
            address tokenB,
            uint256 amountADesired,
            uint256 amountBDesired,
            uint256 amountAMin,
            uint256 amountBMin,
            address to,
            uint256 deadline
        )
            external
            view
            returns (
                bytes memory _compressedPayload
            )
        {
            /** ... */
        }

        /** ... */

    }

如上图所示,原始合约的各种输入参数通过 编码器 压缩成一个单一的 calldata。然后它被解码用于后面的 解码器。因此,calldata 几乎减少了近一半的字节。

这可以通过以下方式说明

  • 此命令显示了如何使用 Solidity 编码具有参数的原始函数
cast calldata "addLiquidity(address,address,uint256,uint256,uint256,uint256,address,uint256)" 0x106EABe0298ec286Adf962994f0Dcf250c4BB763 0xEbfc763Eb9e1d1ab09Eb2f70549b66682AfD9aa5 1200000000000000000000 2500000000000000000000 1000000000000000000000 2000000000000000000000 0x095E7BAea6a6c7c4c2DfeB977eFac326aF552d87 100
  • 结果的总字节数为 520 十六进制 = 520/2 = 260 字节
0xe8e33700000000000000000000000000106eabe0298ec286adf962994f0dcf250c4bb763000000000000000000000000ebfc763eb9e1d1ab09eb2f70549b66682afd9aa50000000000000000000000000000000000000000000000410d586a20a4c000000000000000000000000000000000000000000000000000878678326eac90000000000000000000000000000000000000000000000000003635c9adc5dea0000000000000000000000000000000000000000000000000006c6b935b8bbd400000000000000000000000000000095e7baea6a6c7c4c2dfeb977efac326af552d870000000000000000000000000000000000000000000000000000000000000064
  • 此命令显示了我们的优化版本如何将各种输入参数编码成单个紧密压缩的 calldata。
cast calldata "addLiquidityCompressed(bytes)" 000001000002000000410d586a20a4c00000000000878678326eac9000000000003635c9adc5dea000000000006c6b935b8bbd4000000000030000000064
  • 结果的总字节数为 264 十六进制 = 264/2 = 132 字节
0x2feccbed0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003e000001000002000000410d586a20a4c00000000000878678326eac9000000000003635c9adc5dea000000000006c6b935b8bbd40000000000300000000640000

因此,这节省了约 50% 的字节数。这个数字在实现 L2 上部署的 dApp(如 Arbitrum/Optimism)时非常有影响,因为 L2 用户支付了显著的 L1 批次提交安全成本。L1 燃气可能占总燃气成本(L1 + 2 燃气)的大部分。

这意味着,发送的调用数据字节数越少或调用数据打包越紧密,L2 上的用户支付的燃气费用就越低。

因此,我们优化的 UniswapV2 路由器版本有可能在 L2 上节省近 50% 的燃气。

[!注意] 节省的燃气量很大程度上取决于 L1 的安全成本,这可能会根据 L1 的拥堵程度而变化。

从数学上讲,总燃气费用是L2执行费和L1数据/安全费用的总和,这个数字的计算方式因不同的L2链而异。

Arbitrum

$\ \text{Total Gas Fee}_{\text{Layer2}} = \text{Execution Fee}_{\text{Layer2}} + \text{Data Fee}_{\text{Layer1}} $

其中 $\ \text{Execution Fee}_{\text{Layer2}} $ 是

$\ \text{Execution Fee}_{\text{Layer2}} = \text{Gas Price}_{\text{Layer2}} \times \text{ Gas Used}_{\text{Layer2}}$

$\ text{Gas Price}_{\text{Layer2}} = min( \text{Base Fee}_{\text{Layer2}} + \text{Priority Fee}_{\text{Layer2}}, \text{Gas Price Floor} )$

$$\ \text{where} \quad \text{Gas Price Floor} = \begin{cases} 0.1 \, \text{Gwei}, & \text{on Arbitrum One} \\ 0.01 \, \text{Gwei}, & \text{on Nova} \end{cases}$$

并且 $\ \text{Data Fee}_{\text{Layer1}} $ 是

$$\ \text{Data Fee}_{\text{Layer1}} = \text{Gas Price}_{\text{Layer1}} \times \text{brotli-zero-algorithm}_{\text{txdata}}(txdata) \times 16$$

$$\ \text{where } \text{brotli-zero-algorithm}_{\text{txdata}}(txdata) 是用于奖励用户发布可压缩交易。

Optimism

这里是一些(简单的)数学

$\ \text{总汽油费}_{\text{Layer2}} = \text{执行费}_{\text{Layer2}} + \text{数据费}_{\text{Layer1}} $

其中 $\ \text{Execution Fee}_{\text{Layer2}} $ 是

$\ \text{Execution Fee}_{\text{Layer2}} = \text{Gas Price}_{\text{Layer2}} \times \text{ Gas Used}_{\text{Layer2}}$

$\ \text{汽油价格}_{\text{Layer2}} = \text{基础费}_{\text{Layer2}} + \text{优先费}_{\text{Layer2}}$

并且 $\ \text{Data Fee}_{\text{Layer1}} $ 是

$\ \text{数据费}_{\text{Layer1}} = \text{汽油价格}_{\text{Layer1}} \times \text{ [交易数据汽油量 + 固定开销]} \times \text{动态开销}$

其中 $\ \text{交易数据汽油量} $ 是

$\ \text{Tx Data Gas} = \text{count-zero-bytes}_{\text{txdata}}(txdata) \times 4 + \text{count-non-zero-bytes}_{\text{txdata}}(txdata) \times 16 $

安装

有2种方式:不使用npm和使用npm

使用npm

我们假设您已经根据foundry指南hardhat指南设置好了工作环境,使用了cd进入

cd my-project;
  1. 使用您喜欢的包管理器添加solid-grinder,例如使用Yarn
yarn add -D solid-grinder

这将自动添加solid-grinder二进制文件和所需的模板到/templates

  1. package.json中添加以下行

  "scripts": {
    "solid-grinder": "solid-grinder"
    }

  1. 安装完成后,我们可以检查是否安装成功。
yarn solid-grinder -V
  1. 添加以下行的remappings.txt
@solid-grinder/=node_modules/solid-grinder/

不使用npm

我们假设您已经有一个forge项目

mkdir my-project;
cd my-project;
forge init;
  1. 添加solid-grindercrates
forge install Ratimon/[email protected];
  1. 直接从lib/solid-grinder构建cli
cd lib/solid-grinder;
cargo build --release;
cp target/release/solid-grinder ../../solid-grinder;
  1. 安装完成后,我们可以检查是否安装成功。
./solid-grinder -V
  1. 添加以下行的remappings.txt
@solid-grinder/=lib/solid-grinder/contracts/
  1. 我们需要手动复制所需的模板以使其工作
cd lib/solid-grinder;
cp -a cli/templates/. ../../templates/;

快速入门

为了简单起见,我们以Benchmarks中提到的UniswapV2的router为例。

  1. 选择一个要优化的函数,然后进行calldata位打包。这与存储位打包的概念相同。主要目标是将参数打包到单个256位中,使位数最低,尽可能最小化calldata。

对于uint类型,例如,我们可以将时间周期最小化为uint40类型(5字节)。这是安全的,因为上限大约是35k年,足够长了。

对于address类型,位大小指定为uint24,假设地址表可以存储最多16,777,216个id。

以下是我们如何定义参数范围的指南。

    // 24-bit, 16,777,216 possible
    // 32-bit, 4,294,967,296  possible
    // 40-bit, 1,099,511,627,776 => ~35k years
    // 72-bit, 4,722 (18 decimals)
    // 88-bit, 309m (18 decimals)
    // 96-bit, 79b or 79,228,162,514 (18 decimals)

[!IMPORTANT] 目前,该工具在每个迭代中只生成一个函数。如果您打算优化两个函数,您仍然可以使用两次,然后将第二个添加到第一个。

  1. 作为一个说明,将文件夹 examples复制到您的/contracts
├── contracts
   ├── examples/
  1. 生成decoder合约
yarn solid-grinder gen-decoder --source 'contracts/solc_0_8/examples/uniswapv2/UniswapV2Router02.sol' --output 'contracts/solc_0_8/examples/uniswapv2' --contract-name 'UniswapV2Router02' --function-name 'addLiquidity' --arg-bits '24 24 96 96 96 96 24 40' --compiler-version 'solc_0_8'
  1. 生成encoder合约
yarn solid-grinder gen-encoder --source 'contracts/solc_0_8/examples/uniswapv2/UniswapV2Router02.sol' --output 'contracts/solc_0_8/examples/uniswapv2' --contract-name 'UniswapV2Router02' --function-name 'addLiquidity' --arg-bits '24 24 96 96 96 96 24 40' --compiler-version 'solc_0_8'

[!IMPORTANT] 目前,我们只支持0.8.X版本,并且需要将--compiler-version的参数设置为solc_0_8

  1. 进行linting是一个好习惯,例如。
forge fmt
  1. 成为一个calldata gas优化器!!
import {UniswapV2Router02_Decoder} from "@solid-grinder/solc_0_8/examples/uniswapv2/decoder/UniswapV2Router02_Decoder.g.sol";

【提示】建议手动将原始(未优化)合约的可见性更改为公开。从用户的角度来看,仍然可以包含原始版本,这意味着在紧急情况下(例如前端部分出现问题)用户可以直接快速通过 Etherscan 进行交互。这是因为通过 Etherscan 与优化版本交互很困难,因为用户必须自己手动压缩参数成单个有效负载。

    /** ... */
    contract UniswapV2Router02 is IUniswapV2Router02 {

        /** ... */

        function addLiquidity(
            /** ... */
        ) public virtual override ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
            /** ... */
        }
        /** ... */
    }

贡献

查看我们的贡献指南

如果您想表示感谢或/和支持 Solid Grinder 的积极开发

  • 给项目的GitHub Star
  • 在推特上关于 Solid Grinder 的消息。
  • Medium或您的个人博客上撰写有关项目的有趣文章。

依赖项

~6.5–8.5MB
~160K SLoC