<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>NealZhu</title>
        <link>https://paragraph.com/@nealzhu</link>
        <description>web3 菜鸟，编程爱好者</description>
        <lastBuildDate>Fri, 28 Aug 2026 06:20:42 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>NealZhu</title>
            <url>https://storage.googleapis.com/papyrus_images/d179f20b523aab5cbc4c41d02007e7cbb3af66b02727a4f1ed0bebe477cad659.png</url>
            <link>https://paragraph.com/@nealzhu</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[BSC meme 三明治机器人代码学习]]></title>
            <link>https://paragraph.com/@nealzhu/bsc-meme</link>
            <guid>O2S4tWSRiv6AalEJ9FiU</guid>
            <pubDate>Wed, 04 Sep 2024 07:48:32 GMT</pubDate>
            <description><![CDATA[写在前面: 本文是 https://github.com/fuzzland/fourmeme-god.git 的学习笔记 三明治机器人原理有人使用较大的滑点进行交易, 称此交易为 victim tx ，后面简称为 vtx滑点代表的是对价格上升的容忍程度，较大的滑点导致在 vtx 买入代币，并且在 vtx 之后卖出代币的模式变得有利可图三明治机器人构造 buy tx 和 sell tx（可能还有额外的 approve tx，以保证卖出交易的成功），并且将这些交易与原始的 vtx 按照如下顺序排列：[buy tx, vtx, approve tx(optional), sell tx]找一个类似 flashbots 的交易捆绑服务商，将上面的交易 bundle(即上述的 tx 数组) 提交构造一个三明治机器人的主要难点：检测到有利可图的交易、协议在合理的时间范围内构造出利润最大化的 bundle个人感觉第 1 点，尤其对于短暂出现的 alpha 机会, 才是真正的难点。比如本例中的 meme 协议，合并并未开源，如何从一份未开源合约中提取到完成套利机器人的相关信息，是很复杂的.如何完...]]></description>
            <content:encoded><![CDATA[<p>写在前面: 本文是 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/fuzzland/fourmeme-god.git">https://github.com/fuzzland/fourmeme-god.git</a> 的学习笔记</p><p>三明治机器人原理</p><ol><li><p>有人使用较大的滑点进行交易, 称此交易为 victim tx ，后面简称为 vtx</p></li><li><p>滑点代表的是对价格上升的容忍程度，较大的滑点导致在 vtx 买入代币，并且在 vtx 之后卖出代币的模式变得有利可图</p></li><li><p>三明治机器人构造 buy tx 和 sell tx（可能还有额外的 approve tx，以保证卖出交易的成功），并且将这些交易与原始的 vtx 按照如下顺序排列：[buy tx, vtx, approve tx(optional), sell tx]</p></li><li><p>找一个类似 flashbots 的交易捆绑服务商，将上面的交易 bundle(即上述的 tx 数组) 提交</p></li></ol><p>构造一个三明治机器人的主要难点：</p><ol><li><p>检测到有利可图的交易、协议</p></li><li><p>在合理的时间范围内构造出利润最大化的 bundle</p></li></ol><p>个人感觉第 1 点，尤其对于短暂出现的 alpha 机会, 才是真正的难点。比如本例中的 meme 协议，合并并未开源，如何从一份未开源合约中提取到完成套利机器人的相关信息，是很复杂的.如何完成这一点在本文中不再展开，我们只需要知道 meme 协议是一个常量乘积 AMM 算法：即</p><blockquote><p>x * y = k 并且会收取一定的交易手续费（体现在后续代码中的 fee_rate 和 min_fee）</p></blockquote><p>下面主要讲套利交易构造部分: fourmeme-god 是基于 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/tonyke-bot/burberry.git">burberry</a> (一个模块化的 mev 开发框架) 开发的。burberry 中, 一个 mev 项目被模块化的分解为:</p><ul><li><p>collector(s), 负责搜集数据。支持多方数据源，异构数据源等</p></li><li><p>strategy, 负责解析搜集的数据,并且根据套利算法进行套利交易的构造</p></li><li><p>executor, 负责执行套利交易的最终执行</p></li></ul><p>在本项目中, collectors 采用了两个 burberry 内置的数据采集类，主要负责采集最新的区块信息和 mempool pending tx 信息.两个数据源的主要作用分别为:</p><ul><li><p>block 信息用于探测其他机器人竞争者，并且在后续的三明治套利中避免处理这些机器人地址的相关交易</p></li><li><p>pending tx 信息用于识别潜在的 victim tx.</p></li></ul><p>在 strategy 中整体流程如下:</p><ol><li><p>检查交易是不是一个 meme::Buy tx （通过解析 tx.data）</p></li><li><p>对于 buy 交易，根据 meme 当前池子信息和这个 buy tx 信息，计算最优的三明治交易组合(即计算提前买入多少)</p></li><li><p>计算一下加上 gas 消耗以后是否还有利可图(gas 根据链上交易设定固定值进行估算)</p></li><li><p>构造 bundle 提交到区块链上完成交易(未实现，可以自己选择 bundle 服务提供商)</p></li></ol><p>这里重点讲一下 <code>search::go</code> 的逻辑。在不考虑任何数学和代码之前，很容易得到一个感性的结论:</p><ul><li><p>在一个临界点之前, 三明治机器人的利润肯定是随着买入 mev 机器人提前买入的代币数量的增加而增加。</p></li><li><p>这个临界点存在的原因是买入太多会导致滑点太大，进而让 victim tx 失败 那如何找到这个临界点呢? 这里简单使用了一个类似二分搜索法的搜索: 将输入区间标记为 4 个点</p></li></ul><pre data-type="codeBlock" text="起点----起点+m(A)----起点+2m(B)----终点
"><code>起点<span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>起点<span class="hljs-operator">+</span>m(A)<span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>起点<span class="hljs-operator">+</span>2m(B)<span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span><span class="hljs-operator">-</span>终点
</code></pre><p>我们只需要反复比较买入量为 A 和 B 时候，机器人的利润，然后再进行区间的收缩即可。即如果 Porfit(B) &lt; Profit(A) ，则代表 B 已经超过了上面提到的临界点，最优解必然在起点到 B 之间的一个点, 我们重新对这部分输入进行等分搜索。通过反复迭代，我们可以找到最优解。(你可以问问 ai 看看有什么更好的寻求解的办法)需要指出的是，因为 mev 本质上还是需要和时间赛跑，而且最优解也没有那么重要，所以迭代次数被设置为 100，这个数据可以根据你的 CPU 性能进行调优（比如机器差，就改小点，机器好就调大一点）.</p><p>至于如何计算利润也比较简单，流程也是比较明朗的，给定你希望买入的 eth 数量(也就是 amount_in):</p><ol><li><p>先计算可以 buy tx 的 amount_out(买入的代币数量)</p></li><li><p>模拟更新池子信息(内存中的数据)</p></li><li><p>按照 victim tx 进行计算, 得到 victim tx 真正可以得到的代币</p></li><li><p>模拟更新池子信息</p></li><li><p>构造卖出交易(卖出数量为 1 中的 amount_out), 得到卖出获取的 eth 数量</p></li><li><p>计算 mev 机器人的整体收益，即最终卖出 ETH 的数量减去 amount_in + fee</p></li></ol><p>至此主要的逻辑我已经梳理完成,不过在读代码的过程中，我也遇到一些不理解的地方:</p><ol><li><p>在搜索最优解的时候，<code>trial_ultimate(&amp;mut context, lower_bound + m)</code> 函数调用使用的相同的 context 引用，这会持续修改 context(也就是内存中虚拟池子的信息),但是这个修改其实是不应该发生的，也就是计算不同点的利润的时候，输入都应该是相同的 context. 不确定是代码确实有问题还是我的理解有误. 后面希望可以和作者请教讨论</p></li></ol>]]></content:encoded>
            <author>nealzhu@newsletter.paragraph.com (NealZhu)</author>
        </item>
        <item>
            <title><![CDATA[爆改！都是科技与狠活]]></title>
            <link>https://paragraph.com/@nealzhu/ml0oBaDKZhSyeF2jqyfF</link>
            <guid>ml0oBaDKZhSyeF2jqyfF</guid>
            <pubDate>Wed, 12 Oct 2022 19:28:00 GMT</pubDate>
            <description><![CDATA[通过阅读本文你可以学到:如何应用 EIP1167 来低成本的创建大量的代理合约以及其需要注意的点immutable 的妙用所有代码均在代码仓库中 https://github.com/neal-zhu/batcher.git 找到欢迎大家 clone & 玩耍 & star 在文章 https://mirror.xyz/0x3dbb624861C0f62BdE573a33640ca016E4c65Ff7/VoBIa7fC_lNLw6TPutj16KztvnQffDdBOv_A1Z2AxUw 中，我们实现了一个简单的通用型薅羊毛合约。但是在后续的测试过程中发现其有一个致命问题: 一次无法创建大量子合约地址。比如如果我们试图创建 100 个地址，则会报错（即使将 gasLimit 设置为 3000w）：子合约太长导致报错这搞毛啊，说好的批量撸羊毛的，100 个地址都做不到，那还怎么玩？好在天无绝人之路，在各个 web3 技术群中发现了一个叫做 mini proxy 的神器，具体文档参考下面链接： https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEF...]]></description>
            <content:encoded><![CDATA[<p>通过阅读本文你可以学到:</p><ol><li><p>如何应用 EIP1167 来低成本的创建大量的代理合约以及其需要注意的点</p></li><li><p>immutable 的妙用</p></li></ol><p>所有代码均在代码仓库中</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/neal-zhu/batcher.git">https://github.com/neal-zhu/batcher.git</a></p><p>找到欢迎大家 clone &amp; 玩耍 &amp; star</p><p>在文章</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/0x3dbb624861C0f62BdE573a33640ca016E4c65Ff7/VoBIa7fC_lNLw6TPutj16KztvnQffDdBOv_A1Z2AxUw">https://mirror.xyz/0x3dbb624861C0f62BdE573a33640ca016E4c65Ff7/VoBIa7fC_lNLw6TPutj16KztvnQffDdBOv_A1Z2AxUw</a></p><p>中，我们实现了一个简单的通用型薅羊毛合约。但是在后续的测试过程中发现其有一个致命问题: 一次无法创建大量子合约地址。比如如果我们试图创建 100 个地址，则会报错（即使将 gasLimit 设置为 3000w）：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/28bdc20624d837be99d1bf8840004c2e1e9fe3a6ea4f5f8013ee19634d29fd49.png" alt="" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p>子合约太长导致报错这搞毛啊，说好的批量撸羊毛的，100 个地址都做不到，那还怎么玩？好在天无绝人之路，在各个 web3 技术群中发现了一个叫做 mini proxy 的神器，具体文档参考下面链接：</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8">https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8</a></p><p>限于篇幅，本文不会仔细的分析 mini proxy 的原理，大家只需要记住几个关键点：</p><ol><li><p>mini proxy 合约非常短，只有 45 个字节</p></li><li><p>mini proxy 合约创建后没有存储任何数据（也就是未指定 owner），所以可以被 EOA 地址直接调用</p></li><li><p>mini proxy 会将所有调用，通过 delegatecall 转发到 logic 合约中</p></li></ol><p>看看如何集成 miniproxy，代码(有 BUG 版本）如下：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import &quot;hardhat/console.sol&quot;;

contract BatcherV2 {
    // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1167.md
    bytes32 byteCode;
    uint n;
    address private immutable deployer;
    
    constructor(uint _n) {
        deployer = msg.sender;
        n = _n;
        createProxies(_n);
    }

    function createProxies(uint _n) internal {
        bytes memory miniProxy = bytes.concat(bytes20(0x3D602d80600A3D3981F3363d3d373d3D3D363d73), bytes20(address(this)), bytes15(0x5af43d82803e903d91602b57fd5bf3));
        byteCode = keccak256(abi.encodePacked(miniProxy));  
        address proxy;
        for(uint i=0; i&lt;_n; i++) {
            bytes32 salt = keccak256(abi.encodePacked(msg.sender, i));
            assembly {
                proxy := create2(0, add(miniProxy, 32), mload(miniProxy), salt)
            }
        }
    } 

    function execute(address target, bytes memory data) external {
        require(msg.sender == deployer, &quot;Only deployer can call this function.&quot;);
        for(uint i=0; i&lt;n; i++) {
            address proxy = proxyFor(msg.sender, i);
            BatcherV2(proxy).callback(target, data);
        }
    }

    function callback(address target, bytes memory data) external {
        (bool success, ) = target.call(data);
        require(success, &quot;Transaction failed.&quot;);
    }


    function proxyFor(address sender, uint i) public view returns (address proxy) {
        bytes32 salt = keccak256(abi.encodePacked(sender, i));
        proxy = address(uint160(uint(keccak256(abi.encodePacked(
                hex&apos;ff&apos;,
                address(this),
                salt,
                byteCode
            )))));
    }

}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.7;</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"hardhat/console.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">BatcherV2</span> </span>{
    <span class="hljs-comment">// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1167.md</span>
    <span class="hljs-keyword">bytes32</span> byteCode;
    <span class="hljs-keyword">uint</span> n;
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">immutable</span> deployer;
    
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> _n</span>) </span>{
        deployer <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        n <span class="hljs-operator">=</span> _n;
        createProxies(_n);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createProxies</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> _n</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> miniProxy <span class="hljs-operator">=</span> <span class="hljs-built_in">bytes</span>.<span class="hljs-built_in">concat</span>(<span class="hljs-keyword">bytes20</span>(<span class="hljs-number">0x3D602d80600A3D3981F3363d3d373d3D3D363d73</span>), <span class="hljs-keyword">bytes20</span>(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)), <span class="hljs-keyword">bytes15</span>(<span class="hljs-number">0x5af43d82803e903d91602b57fd5bf3</span>));
        byteCode <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(miniProxy));  
        <span class="hljs-keyword">address</span> proxy;
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint</span> i<span class="hljs-operator">=</span><span class="hljs-number">0</span>; i<span class="hljs-operator">&#x3C;</span>_n; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-keyword">bytes32</span> salt <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, i));
            <span class="hljs-keyword">assembly</span> {
                proxy <span class="hljs-operator">:=</span> <span class="hljs-built_in">create2</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">add</span>(miniProxy, <span class="hljs-number">32</span>), <span class="hljs-built_in">mload</span>(miniProxy), salt)
            }
        }
    } 

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">execute</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> target, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> deployer, <span class="hljs-string">"Only deployer can call this function."</span>);
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint</span> i<span class="hljs-operator">=</span><span class="hljs-number">0</span>; i<span class="hljs-operator">&#x3C;</span>n; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-keyword">address</span> proxy <span class="hljs-operator">=</span> proxyFor(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, i);
            BatcherV2(proxy).callback(target, data);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">callback</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> target, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> target.<span class="hljs-built_in">call</span>(data);
        <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Transaction failed."</span>);
    }


    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">proxyFor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> sender, <span class="hljs-keyword">uint</span> i</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span> proxy</span>) </span>{
        <span class="hljs-keyword">bytes32</span> salt <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(sender, i));
        proxy <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-keyword">uint160</span>(<span class="hljs-keyword">uint</span>(<span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(
                <span class="hljs-string">hex'ff'</span>,
                <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>),
                salt,
                byteCode
            )))));
    }

}
</code></pre><p>关于这次的代码，架构要比之前的更加复杂，BatcherV2 不但是我们的门面合约，而且还作为 mini proxy 的 logic 合约。目前的调用时序图如下:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/215ac7135718b8417ce3ce61e6b9b8027bc37ac59f13b504e844e8e8d1e21c38.png" alt="调用情况" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">调用情况</figcaption></figure><p>现在我们来对上述合约的改动进行逐步分析:</p><ol><li><p>createProxies 里面就是 MiniProxy 的创建代码，更为细致的解释可以参考上面的 github 链接。同时注意我们这里使用了 create2 来创建确定性地址合约。</p></li><li><p>callback 很简单，就是执行传来的任意 tx将部署 proxy 数量改为 100，</p></li><li><p>fallback 函数没有了，取而代之的是 execute 函数，其职能基本与之前的 fallback 一样，就是将调用转发到每一个 mini proxy 合约。有两点需要着重强调下：</p><ol><li><p>BatcherV2(proxy).callback(target, data); 这行代码值得详细说一下：因为 Batcher 是 MiniProxy 的 logic 合约，所以其实对 MiniProxy 的调用，最终都会落到 Batcher 自己身上，所以我们可以直接将 proxy 转换为一个 BatcherV2 调用 callback ，但是请务必注意，这个 fallback 是 MiniProxy 通过 delegatecall 调用的！</p></li><li><p>proxy 的创建是使用 create2，所以我们在 proxyFor 处，动态的计算每个 MiniProxy 合约的地址</p></li></ol></li></ol><p>在仓库中运行</p><pre data-type="codeBlock" text="npx hardhat test
"><code>npx hardhat <span class="hljs-built_in">test</span>
</code></pre><p>可以得到测试通过的输出</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/90110e1556456602bfb55622dca4d35c3b25a16cdbf0cc361743547919e0d5a1.png" alt="搞定了吗？" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">搞定了吗？</figcaption></figure><p>成功，非常好！除了我们的合约还存在一个<strong>极大的安全隐患</strong>！前面提到过 MiniProxy 刚创建时并为指定 owner，所以任何人都可以直接对其调用 claimMintRewardAndShare 方法。看下面的具体测试代码:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ac66c5365cd400489d028ea3d672c7b82de4a582d832a26c0650f491b5ecaa70.png" alt="小心黑客！" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">小心黑客！</figcaption></figure><p>黑客直接调用 proxy对应的测试输出如下:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d80a5add867a65770211347072af30ea97eb7bb74fef5788b25cea949e6eb607.png" alt="还我 XEN" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">还我 XEN</figcaption></figure><p>一个黑客直接获取到了我们一个（当然也可以全部拿走） mini proxy 的地址，然后直接在 mini proxy 合约上调用 callback() 取走了我们的 XEN。</p><p>那这个问题如何修复呢？一个很直接的想法就是，添加断言，只允许 Batcher 调用 MiniProxy，对应修复代码如下</p><pre data-type="codeBlock" text=":...skip
// 添加变量到第一个位置, 记录 batcher 地址
address private original;
    constructor(uint _n) {
        original = address(this);
        deployer = msg.sender;
        n = _n;
        createProxies(_n);
    }
    function callback(address target, bytes memory data) external {
        require(msg.sender == original, &quot;Only original can call this function.&quot;);
        (bool success, ) = target.call(data);
        require(success, &quot;Transaction failed.&quot;);
    }
"><code>:...skip
<span class="hljs-comment">// 添加变量到第一个位置, 记录 batcher 地址</span>
<span class="hljs-keyword">address</span> <span class="hljs-keyword">private</span> original;
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> _n</span>) </span>{
        original <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>);
        deployer <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        n <span class="hljs-operator">=</span> _n;
        createProxies(_n);
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">callback</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> target, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> original, <span class="hljs-string">"Only original can call this function."</span>);
        (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> target.<span class="hljs-built_in">call</span>(data);
        <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Transaction failed."</span>);
    }
</code></pre><p>看起来好像没问题，但是测试结果却给我们当头一棒，输出结果如下:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9e8c50d1a91a868bc7fa783525d915b0ce1fd57ff4f2aa4ff137de65eaafdc49.png" alt="claim 也因为断言失败了" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">claim 也因为断言失败了</figcaption></figure><p>黑客确实偷不走了，合约也报废了！</p><p>这是为什么呢？其实接触过 proxy 模式的同学想必已经发现了问题所在：因为 delegatecall 会保留当前执行的上下文，即虽然 callback 是 Batcher 中的代码，但是执行的时候，却在 MiniProxy 的存储上下文：msg.sender == original 其实会被翻译成 msg.sender == slot0，而很不幸， MiniProxy 什么都没存（可以通过检查 MiniProxy 的 slot 来确定）。所以这个断言永远不可能成立。这怎么办呢？</p><p>一个最直观的办法是在 logic 合约（也就是本文的 Batcher）里面提供一个设置 owner 的接口，在每个 mini proxy 创建之后再调用一下设置 owner 的接口即可。</p><p>但是本人无意间发现了一个神奇的修饰词: immutable，其与 const 有一个重要特性:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/03eba0b59c931c8d7e627d99ef7b8c7d0c9376eddf6bad0f13e345d1621305cd.png" alt="immutable &amp; const 不占用存储空间！" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">immutable &amp; const 不占用存储空间！</figcaption></figure><p>太神奇了，immutable 变量在初始化之后，后续的使用都会被直接替换而不需要存储在 slot 中。msg.sender == original 的判断，将不再需要读取 storage，退化成类似和一个常量的比较！将 original 改为 immutable 之后，重新测试，终于完美通过:</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7baeaaaa7147ada0e5ee58421761dbcc96999109377564e26f7cd384d92165f2.png" alt="乌拉！" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">乌拉！</figcaption></figure><p>最后让我们总结一下技术要点：</p><ol><li><p>mini proxy 可以低成本创建大量的 proxy 合约，但是需要额外的机制保证不被黑客直接调用 proxy 合约</p></li><li><p>代理模式下一定要注意权限的控制，以及上下文切换时一些违反直觉的情况</p></li><li><p>immutable 变量不占用任何 storage slot，可以与 mini proxy 完美配合，实现对 proxy 使用权的控制</p></li></ol>]]></content:encoded>
            <author>nealzhu@newsletter.paragraph.com (NealZhu)</author>
        </item>
        <item>
            <title><![CDATA[简单实现一个通用型薅羊毛合约]]></title>
            <link>https://paragraph.com/@nealzhu/UivRhAR6Gew5gJVfurU7</link>
            <guid>UivRhAR6Gew5gJVfurU7</guid>
            <pubDate>Tue, 11 Oct 2022 08:49:59 GMT</pubDate>
            <description><![CDATA[通过阅读本文你可以学到:如何实现一个通用的、可复用的薅羊毛合约（仅供参考，本人是 Solidity 菜鸡， 相关代码可以在 <https://github.com/neal-zhu/batcher > 找到）对于一些空投 token，free mint nft 项目，项目方有时候可能会无意（甚至是有意）的给科学家留下发挥的空间。这方面比较出名的有早前的小学生项目 RND，近有最近罕见的热点项目 Xen。这些项目的主要特点是合约代码中未进行 tx.origin == msg.sender 的判断。 有文章 <https://mirror.xyz/hackbot.eth/plbO7co90A6JhKSRSMsstrcP-X8CpfnaRs7k3HI3loY > 中有介绍如何通过运行时创建大量子合约来批量领取大量 RND 代币。主要代码如下：// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.9; // Uncomment this line to use console.log // import "hardhat/...]]></description>
            <content:encoded><![CDATA[<p>通过阅读本文你可以学到:</p><ol><li><p>如何实现一个通用的、可复用的薅羊毛合约（仅供参考，本人是 Solidity 菜鸡， 相关代码可以在</p><p>&lt;<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/neal-zhu/batcher">https://github.com/neal-zhu/batcher</a> &gt;</p><p>找到）</p></li></ol><p>对于一些空投 token，free mint nft 项目，项目方有时候可能会无意（甚至是有意）的给科学家留下发挥的空间。这方面比较出名的有早前的小学生项目 RND，近有最近罕见的热点项目 Xen。这些项目的主要特点是合约代码中未进行 tx.origin == msg.sender 的判断。</p><p>有文章</p><p>&lt;<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/hackbot.eth/plbO7co90A6JhKSRSMsstrcP-X8CpfnaRs7k3HI3loY">https://mirror.xyz/hackbot.eth/plbO7co90A6JhKSRSMsstrcP-X8CpfnaRs7k3HI3loY</a> &gt;</p><p>中有介绍如何通过运行时创建大量子合约来批量领取大量 RND 代币。主要代码如下：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
// import &quot;hardhat/console.sol&quot;;

interface airdrop {
    function transfer(address recipient, uint256 amount) external;
    function balanceOf(address account) external view returns (uint256);
    function claim() external;
}

contract multiCall{
    //                                此处填写 RND 代币的合约地址
    address constant contra = address(0xcb33F7FB101E377a4b0e19fD647F391fAD14d0B5);
    function call(uint256 times) public {
        for(uint i=0;i&lt;times;++i){
            new claimer(contra);
        }
    }
}
contract claimer{
    constructor(address contra){
        airdrop(contra).claim();
        uint256 balance = airdrop(contra).balanceOf(address(this));
        airdrop(contra).transfer(address(tx.origin), balance);
        selfdestruct(payable(address(msg.sender)));
    }
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: UNLICENSED</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.9;</span>

<span class="hljs-comment">// Uncomment this line to use console.log</span>
<span class="hljs-comment">// import "hardhat/console.sol";</span>

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">airdrop</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transfer</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> recipient, <span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claim</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">multiCall</span></span>{
    <span class="hljs-comment">//                                此处填写 RND 代币的合约地址</span>
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">constant</span> contra <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0xcb33F7FB101E377a4b0e19fD647F391fAD14d0B5</span>);
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">call</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> times</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint</span> i<span class="hljs-operator">=</span><span class="hljs-number">0</span>;i<span class="hljs-operator">&#x3C;</span>times;<span class="hljs-operator">+</span><span class="hljs-operator">+</span>i){
            <span class="hljs-keyword">new</span> claimer(contra);
        }
    }
}
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">claimer</span></span>{
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> contra</span>)</span>{
        airdrop(contra).claim();
        <span class="hljs-keyword">uint256</span> balance <span class="hljs-operator">=</span> airdrop(contra).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
        airdrop(contra).<span class="hljs-built_in">transfer</span>(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>), balance);
        <span class="hljs-built_in">selfdestruct</span>(<span class="hljs-keyword">payable</span>(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>)));
    }
}
</code></pre><p>上面的代码实现清晰明了，但是也有一个明显的问题，即不具备可复用性。因为项目方不同，而且在空投这件事情上，也并不存在所谓的 ERCxxx 标准，所以可以撸 RND 项目的合约代码，往往在其他项目上就失效了。这也是为什么这种合约往往在撸毛结束以后，都会销毁批量创建的子合约。</p><p>这样导致了一些问题：</p><ol><li><p>面对不同项目时候，需要做更多重复的工作（部署合约，对合约进行小修改）</p></li><li><p>多个项目，反复的批量创建、销毁合约， gas 消耗会更高</p></li><li><p>大家都在撸的时候，进行的一些列包括部署、调用等操作，成本会更高</p></li></ol><p>比如最近大火的 Xen，除了合约地址不同，其交互流程也更为复杂：</p><ol><li><p>调用 claimRank(uint256)</p></li><li><p>挖矿时间足够以后，调用 claimMintReward() or claimMintRewardAndShare(address, uint256)</p></li></ol><p>大家只好再起炉灶，赶紧写一份大体一致的代码，然后开始部署调用等操作。比如下面这份用来撸 Xen 的代码：</p><pre data-type="codeBlock" text="/**
 *Submitted for verification at Etherscan.io on 2022-10-10
*/

// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

interface IXEN1{
    function claimRank(uint256 term) external;
    function claimMintReward() external;
    function approve(address spender, uint256 amount) external returns (bool);
}

interface IXEN2{
    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
}

contract GET{
    IXEN1 private constant xen = IXEN1(0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8);

    constructor() {
        xen.approve(msg.sender,~uint256(0));
    }
    
    function claimRank(uint256 term) public {
        xen.claimRank(term);
    }

    function claimMintReward() public {
        xen.claimMintReward();
        selfdestruct(payable(tx.origin));
    }
}
/// @author 捕鲸船社区 加入社区添加微信:Whaler_man 关注推特 @Whaler_DAO
contract GETXEN {
    mapping (address=&gt;mapping (uint256=&gt;address[])) public userContracts;
    IXEN2 private constant xen = IXEN2(0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8);
    address private constant whaler = 0x918Cb3c935d82eE20F4986158dFA755048F41d47;

    function claimRank(uint256 times, uint256 term) external {
        address user = tx.origin;
        for(uint256 i; i&lt;times; ++i){
            GET get = new GET();
            get.claimRank(term);
            userContracts[user][term].push(address(get));
        }
    }

    function claimMintReward(uint256 times, uint256 term) external {
        address user = tx.origin;
        for(uint256 i; i&lt;times; ++i){
            uint256 count = userContracts[user][term].length;
            address get = userContracts[user][term][count - 1];
            GET(get).claimMintReward();
            address owner = tx.origin;
            uint256 balance = xen.balanceOf(get);
            xen.transferFrom(get, whaler, balance * 10 / 100);
            xen.transferFrom(get, owner, balance * 90 / 100);
            userContracts[user][term].pop();
        }
    }
}
"><code><span class="hljs-comment">/**
 *Submitted for verification at Etherscan.io on 2022-10-10
*/</span>

<span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.17;</span>

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IXEN1</span></span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claimRank</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> term</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claimMintReward</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">approve</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> spender, <span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>)</span>;
}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IXEN2</span></span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferFrom</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>,
        <span class="hljs-keyword">address</span> to,
        <span class="hljs-keyword">uint256</span> amount
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>)</span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>;
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">GET</span></span>{
    IXEN1 <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> xen <span class="hljs-operator">=</span> IXEN1(<span class="hljs-number">0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8</span>);

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        xen.approve(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>,<span class="hljs-operator">~</span><span class="hljs-keyword">uint256</span>(<span class="hljs-number">0</span>));
    }
    
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claimRank</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> term</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        xen.claimRank(term);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claimMintReward</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        xen.claimMintReward();
        <span class="hljs-built_in">selfdestruct</span>(<span class="hljs-keyword">payable</span>(<span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>));
    }
}
<span class="hljs-comment">/// @author 捕鲸船社区 加入社区添加微信:Whaler_man 关注推特 @Whaler_DAO</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">GETXEN</span> </span>{
    <span class="hljs-keyword">mapping</span> (<span class="hljs-keyword">address</span><span class="hljs-operator">=</span><span class="hljs-operator">></span><span class="hljs-keyword">mapping</span> (<span class="hljs-keyword">uint256</span><span class="hljs-operator">=</span><span class="hljs-operator">></span><span class="hljs-keyword">address</span>[])) <span class="hljs-keyword">public</span> userContracts;
    IXEN2 <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> xen <span class="hljs-operator">=</span> IXEN2(<span class="hljs-number">0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8</span>);
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> whaler <span class="hljs-operator">=</span> <span class="hljs-number">0x918Cb3c935d82eE20F4986158dFA755048F41d47</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claimRank</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> times, <span class="hljs-keyword">uint256</span> term</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-keyword">address</span> user <span class="hljs-operator">=</span> <span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>;
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&#x3C;</span>times; <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i){
            GET get <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> GET();
            get.claimRank(term);
            userContracts[user][term].<span class="hljs-built_in">push</span>(<span class="hljs-keyword">address</span>(get));
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">claimMintReward</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> times, <span class="hljs-keyword">uint256</span> term</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-keyword">address</span> user <span class="hljs-operator">=</span> <span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>;
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&#x3C;</span>times; <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i){
            <span class="hljs-keyword">uint256</span> count <span class="hljs-operator">=</span> userContracts[user][term].<span class="hljs-built_in">length</span>;
            <span class="hljs-keyword">address</span> get <span class="hljs-operator">=</span> userContracts[user][term][count <span class="hljs-operator">-</span> <span class="hljs-number">1</span>];
            GET(get).claimMintReward();
            <span class="hljs-keyword">address</span> owner <span class="hljs-operator">=</span> <span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>;
            <span class="hljs-keyword">uint256</span> balance <span class="hljs-operator">=</span> xen.balanceOf(get);
            xen.transferFrom(get, whaler, balance <span class="hljs-operator">*</span> <span class="hljs-number">10</span> <span class="hljs-operator">/</span> <span class="hljs-number">100</span>);
            xen.transferFrom(get, owner, balance <span class="hljs-operator">*</span> <span class="hljs-number">90</span> <span class="hljs-operator">/</span> <span class="hljs-number">100</span>);
            userContracts[user][term].<span class="hljs-built_in">pop</span>();
        }
    }
}
</code></pre><p>不难发现，两份合约代码主体框架是极为相似的，完全可以用一种更加通用的合约来尝试替代。实现思路也很简单：尽量让所有 hardcode 的部分动态化，作为参数传入。同时，因为这些合约已经可以复用，我们不再每次都动态批量创建合约，而是可以反复复用。实现的代码也很简单:</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// Uncomment this line to use console.log
import &quot;hardhat/console.sol&quot;;

// ERC20 interface
interface ERC20 {
    function transfer(address recipient, uint256 amount) external;
    function balanceOf(address account) external view returns (uint256);
}

// Proxy contract to execute multiple transactions
contract Proxy {
    address public owner;

    constructor(address _owner) {
        owner = _owner;
    }

    // only owner modifier
    modifier onlyOwner() {
        require(msg.sender == owner, &quot;Only owner can call this function.&quot;);
        _;
    }

    // withdraw all tokens
    function withdraw(address token) public onlyOwner {
        uint256 balance = ERC20(token).balanceOf(address(this));
        ERC20(token).transfer(tx.origin, balance);
    }

    // withdraw all ETH
    function withdrawETH() public onlyOwner {
        payable(tx.origin).transfer(address(this).balance);
    }

    // execute encodeed transaction
    function execute(address target, bytes memory data)
        public
        payable
        onlyOwner
    {
        (bool success, ) = target.call(data);
        require(success, &quot;Transaction failed.&quot;);
    }

    // Destroys this contract instance
    function destroy(address payable recipient) public onlyOwner {
        selfdestruct(recipient);
    }
}

contract Batcher {
    address public owner;
    Proxy[] public proxies;

    constructor(uint256 _n) {
        owner = msg.sender;
        // create proxy contracts, we will not destroy them
        for (uint256 i = 0; i &lt; _n; i++) {
            // create with salt
            Proxy proxy = new Proxy{salt: bytes32(uint256(i))}(address(this));
            // append to proxies
            proxies.push(proxy);
        }
    }

    function getBytecode() public view returns (bytes memory) {
        bytes memory bytecode = type(Proxy).creationCode;
        return abi.encodePacked(bytecode, abi.encode(msg.sender));
    }

    function getAddress(uint256 _salt) public view returns (address) {
        // Get a hash concatenating args passed to encodePacked
        bytes32 hash = keccak256(
            abi.encodePacked(
                bytes1(0xff), // 0
                address(this), // address of factory contract
                _salt, // a random salt
                keccak256(getBytecode()) // the wallet contract bytecode
            )
        );
        // Cast last 20 bytes of hash to address
        return address(uint160(uint256(hash)));
    }

    fallback() external payable {
        require(owner == msg.sender, &quot;Only owner can call this function.&quot;);
        // delegatecall to proxy contracts
        for (uint256 i = 0; i &lt; proxies.length; i++) {
            address proxy = address(proxies[i]);
            (bool success, ) = proxy.call(msg.data);
            require(success, &quot;Transaction failed.&quot;);
        }
    }

    receive() external payable {}
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: UNLICENSED</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.9;</span>

<span class="hljs-comment">// Uncomment this line to use console.log</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">"hardhat/console.sol"</span>;

<span class="hljs-comment">// ERC20 interface</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">ERC20</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transfer</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> recipient, <span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>;
}

<span class="hljs-comment">// Proxy contract to execute multiple transactions</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Proxy</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> owner;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _owner</span>) </span>{
        owner <span class="hljs-operator">=</span> _owner;
    }

    <span class="hljs-comment">// only owner modifier</span>
    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">onlyOwner</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> owner, <span class="hljs-string">"Only owner can call this function."</span>);
        <span class="hljs-keyword">_</span>;
    }

    <span class="hljs-comment">// withdraw all tokens</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> token</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-keyword">uint256</span> balance <span class="hljs-operator">=</span> ERC20(token).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
        ERC20(token).<span class="hljs-built_in">transfer</span>(<span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>, balance);
    }

    <span class="hljs-comment">// withdraw all ETH</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdrawETH</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>).<span class="hljs-built_in">transfer</span>(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>);
    }

    <span class="hljs-comment">// execute encodeed transaction</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">execute</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> target, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data</span>)
        <span class="hljs-title"><span class="hljs-keyword">public</span></span>
        <span class="hljs-title"><span class="hljs-keyword">payable</span></span>
        <span class="hljs-title">onlyOwner</span>
    </span>{
        (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> target.<span class="hljs-built_in">call</span>(data);
        <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Transaction failed."</span>);
    }

    <span class="hljs-comment">// Destroys this contract instance</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">destroy</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> <span class="hljs-keyword">payable</span> recipient</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        <span class="hljs-built_in">selfdestruct</span>(recipient);
    }
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Batcher</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> owner;
    Proxy[] <span class="hljs-keyword">public</span> proxies;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _n</span>) </span>{
        owner <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        <span class="hljs-comment">// create proxy contracts, we will not destroy them</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&#x3C;</span> _n; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-comment">// create with salt</span>
            Proxy proxy <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Proxy{<span class="hljs-built_in">salt</span>: <span class="hljs-keyword">bytes32</span>(<span class="hljs-keyword">uint256</span>(i))}(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
            <span class="hljs-comment">// append to proxies</span>
            proxies.<span class="hljs-built_in">push</span>(proxy);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getBytecode</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span></span>) </span>{
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> bytecode <span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(Proxy).<span class="hljs-built_in">creationCode</span>;
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(bytecode, <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>));
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getAddress</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _salt</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span></span>) </span>{
        <span class="hljs-comment">// Get a hash concatenating args passed to encodePacked</span>
        <span class="hljs-keyword">bytes32</span> hash <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(
                <span class="hljs-keyword">bytes1</span>(<span class="hljs-number">0xff</span>), <span class="hljs-comment">// 0</span>
                <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), <span class="hljs-comment">// address of factory contract</span>
                _salt, <span class="hljs-comment">// a random salt</span>
                <span class="hljs-built_in">keccak256</span>(getBytecode()) <span class="hljs-comment">// the wallet contract bytecode</span>
            )
        );
        <span class="hljs-comment">// Cast last 20 bytes of hash to address</span>
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">address</span>(<span class="hljs-keyword">uint160</span>(<span class="hljs-keyword">uint256</span>(hash)));
    }

    <span class="hljs-function"><span class="hljs-keyword">fallback</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        <span class="hljs-built_in">require</span>(owner <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-string">"Only owner can call this function."</span>);
        <span class="hljs-comment">// delegatecall to proxy contracts</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&#x3C;</span> proxies.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-keyword">address</span> proxy <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(proxies[i]);
            (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> proxy.<span class="hljs-built_in">call</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>);
            <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Transaction failed."</span>);
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">receive</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{}
}
</code></pre><p>在我们的合约中主要有两个角色:</p><ol><li><p>Proxy，即批量创建的合约，其主要作用就是动态的执行任何传入的 tx 数据，这样我们可以让 Proxy 以任何我们想要的姿势和项目方代码进行交互。</p></li><li><p>Batcher，门面合约，负责将交互 tx 转发到所有创建的 Proxy。任何对此合约的调用，最终都会转换为在 N 个 Proxy 合约上的操作。</p></li></ol><p>以交互 Xen 为例，我们需要在 hardhat 中添加类似代码来实现交互:</p><pre data-type="codeBlock" text="require(&quot;@nomicfoundation/hardhat-toolbox&quot;);

const xenAddress = &quot;0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8&quot;;
const xenABI = [] // skip

const batcherABI = [
  &quot;function execute(address, bytes) payable&quot;,
  &quot;function withdraw(address)&quot;,
  &quot;function withdrawETH(address)&quot;,
  &quot;function destroy()&quot;,
]

async function increaseTime(value) {
  if (!ethers.BigNumber.isBigNumber(value)) {
    value = ethers.BigNumber.from(value);
  }
  await ethers.provider.send(&apos;evm_increaseTime&apos;, [value.toNumber()]);
  await ethers.provider.send(&apos;evm_mine&apos;);
}

task(&quot;xen_claim_rank&quot;, &quot;claimRank for xen&quot;).setAction(async (taskArgs, hre) =&gt; {
  const signer = (await hre.ethers.getSigners())[0];
  const xen = new hre.ethers.Contract(xenAddress, xenABI, signer);

  const batcher = new hre.ethers.Contract(taskArgs.batcher, batcherABI, signer);
  let tx = await batcher.execute(xenAddress, xen.interface.encodeFunctionData(&quot;claimRank&quot;, [1]));
  await tx.wait();

  console.log(&quot;XEN balance: &quot;, (await xen.balanceOf(signer.address)).toString());

}).addParam(&quot;batcher&quot;, &quot;address of batcher contract&quot;)

task(&quot;xen_claim_reward&quot;, &quot;claim reward for xen&quot;).setAction(async (taskArgs, hre) =&gt; {
  const signer = (await hre.ethers.getSigners())[0];

  const xen = new hre.ethers.Contract(xenAddress, xenABI, signer);

  // increase and mine time so we can claimReward
  // FOR TEST ONLY
  await increaseTime(24*60*60);

  const batcher = new hre.ethers.Contract(taskArgs.batcher, batcherABI, signer);
  tx = await batcher.execute(xen.address, xen.interface.encodeFunctionData(&quot;claimMintRewardAndShare&quot;, [signer.address, 100]));
  await tx.wait();

  console.log(&quot;XEN balance: &quot;, (await xen.balanceOf(signer.address)).toString());

}).addParam(&quot;batcher&quot;, &quot;address of batcher contract&quot;)
"><code><span class="hljs-built_in">require</span>(<span class="hljs-string">"@nomicfoundation/hardhat-toolbox"</span>);

const xenAddress <span class="hljs-operator">=</span> <span class="hljs-string">"0x06450dEe7FD2Fb8E39061434BAbCFC05599a6Fb8"</span>;
const xenABI <span class="hljs-operator">=</span> [] <span class="hljs-comment">// skip</span>

const batcherABI <span class="hljs-operator">=</span> [
  <span class="hljs-string">"function execute(address, bytes) payable"</span>,
  <span class="hljs-string">"function withdraw(address)"</span>,
  <span class="hljs-string">"function withdrawETH(address)"</span>,
  <span class="hljs-string">"function destroy()"</span>,
]

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increaseTime</span>(<span class="hljs-params">value</span>) </span>{
  <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>ethers.BigNumber.isBigNumber(value)) {
    value <span class="hljs-operator">=</span> ethers.BigNumber.from(value);
  }
  await ethers.provider.<span class="hljs-built_in">send</span>(<span class="hljs-string">'evm_increaseTime'</span>, [value.toNumber()]);
  await ethers.provider.<span class="hljs-built_in">send</span>(<span class="hljs-string">'evm_mine'</span>);
}

task(<span class="hljs-string">"xen_claim_rank"</span>, <span class="hljs-string">"claimRank for xen"</span>).setAction(async (taskArgs, hre) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
  const signer <span class="hljs-operator">=</span> (await hre.ethers.getSigners())[<span class="hljs-number">0</span>];
  const xen <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> hre.ethers.Contract(xenAddress, xenABI, signer);

  const batcher <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> hre.ethers.Contract(taskArgs.batcher, batcherABI, signer);
  let <span class="hljs-built_in">tx</span> <span class="hljs-operator">=</span> await batcher.execute(xenAddress, xen.interface.encodeFunctionData(<span class="hljs-string">"claimRank"</span>, [<span class="hljs-number">1</span>]));
  await <span class="hljs-built_in">tx</span>.wait();

  console.log(<span class="hljs-string">"XEN balance: "</span>, (await xen.balanceOf(signer.<span class="hljs-built_in">address</span>)).toString());

}).addParam(<span class="hljs-string">"batcher"</span>, <span class="hljs-string">"address of batcher contract"</span>)

task(<span class="hljs-string">"xen_claim_reward"</span>, <span class="hljs-string">"claim reward for xen"</span>).setAction(async (taskArgs, hre) <span class="hljs-operator">=</span><span class="hljs-operator">></span> {
  const signer <span class="hljs-operator">=</span> (await hre.ethers.getSigners())[<span class="hljs-number">0</span>];

  const xen <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> hre.ethers.Contract(xenAddress, xenABI, signer);

  <span class="hljs-comment">// increase and mine time so we can claimReward</span>
  <span class="hljs-comment">// FOR TEST ONLY</span>
  await increaseTime(<span class="hljs-number">24</span><span class="hljs-operator">*</span><span class="hljs-number">60</span><span class="hljs-operator">*</span><span class="hljs-number">60</span>);

  const batcher <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> hre.ethers.Contract(taskArgs.batcher, batcherABI, signer);
  <span class="hljs-built_in">tx</span> <span class="hljs-operator">=</span> await batcher.execute(xen.<span class="hljs-built_in">address</span>, xen.interface.encodeFunctionData(<span class="hljs-string">"claimMintRewardAndShare"</span>, [signer.<span class="hljs-built_in">address</span>, <span class="hljs-number">100</span>]));
  await <span class="hljs-built_in">tx</span>.wait();

  console.log(<span class="hljs-string">"XEN balance: "</span>, (await xen.balanceOf(signer.<span class="hljs-built_in">address</span>)).toString());

}).addParam(<span class="hljs-string">"batcher"</span>, <span class="hljs-string">"address of batcher contract"</span>)
</code></pre><p>我们只需要依次执行:</p><ol><li><p>npx hardhat --network localhost run scripts/deploy.js # 如果已经部署过 batcher 合约，则不需要重复部署</p></li><li><p>npx hardhat --network localhost xen_claim_rank --batcher {batcherAddress}</p></li><li><p>npx hardhat --network localhost xen_claim_reward --batcher {batcherAddress}</p></li></ol><p>观察 hardhat 中的脚本代码，不难看出，我们这次的重构本质是: 将链上代码搬移到链下。本来写在 Solidity 合约中的业务逻辑，出现在了 javascript 脚本中。</p><p>最后来总结下改造之后对比之前方案的优劣:</p><ul><li><p>优点：</p><ul><li><p>可以在 gas 低的时候创建好批量合约，等热点项目出来的时候降低 gas 成本</p></li><li><p>减少了部分重复性操作（比如大体框架一致的合约代码开发，新合约部署等）</p></li><li><p>javascript 代码的调试、迭代比 Solidity 更加便利</p></li></ul></li><li><p>缺点：</p><ul><li><p>合约代码明显更加复杂，总体的开发量并没有减少</p></li></ul></li></ul>]]></content:encoded>
            <author>nealzhu@newsletter.paragraph.com (NealZhu)</author>
        </item>
    </channel>
</rss>