# 爆改！都是科技与狠活

By [NealZhu](https://paragraph.com/@nealzhu) · 2022-10-12

---

通过阅读本文你可以学到:

1.  如何应用 EIP1167 来低成本的创建大量的代理合约以及其需要注意的点
    
2.  immutable 的妙用
    

所有代码均在代码仓库中

[https://github.com/neal-zhu/batcher.git](https://github.com/neal-zhu/batcher.git)

找到欢迎大家 clone & 玩耍 & star

在文章

[https://mirror.xyz/0x3dbb624861C0f62BdE573a33640ca016E4c65Ff7/VoBIa7fC\_lNLw6TPutj16KztvnQffDdBOv\_A1Z2AxUw](https://mirror.xyz/0x3dbb624861C0f62BdE573a33640ca016E4c65Ff7/VoBIa7fC_lNLw6TPutj16KztvnQffDdBOv_A1Z2AxUw)

中，我们实现了一个简单的通用型薅羊毛合约。但是在后续的测试过程中发现其有一个致命问题: 一次无法创建大量子合约地址。比如如果我们试图创建 100 个地址，则会报错（即使将 gasLimit 设置为 3000w）：

![](https://storage.googleapis.com/papyrus_images/28bdc20624d837be99d1bf8840004c2e1e9fe3a6ea4f5f8013ee19634d29fd49.png)

子合约太长导致报错这搞毛啊，说好的批量撸羊毛的，100 个地址都做不到，那还怎么玩？好在天无绝人之路，在各个 web3 技术群中发现了一个叫做 mini proxy 的神器，具体文档参考下面链接：

[https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8](https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8)

限于篇幅，本文不会仔细的分析 mini proxy 的原理，大家只需要记住几个关键点：

1.  mini proxy 合约非常短，只有 45 个字节
    
2.  mini proxy 合约创建后没有存储任何数据（也就是未指定 owner），所以可以被 EOA 地址直接调用
    
3.  mini proxy 会将所有调用，通过 delegatecall 转发到 logic 合约中
    

看看如何集成 miniproxy，代码(有 BUG 版本）如下：

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.7;
    import "hardhat/console.sol";
    
    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<_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, "Only deployer can call this function.");
            for(uint i=0; i<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, "Transaction failed.");
        }
    
    
        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'ff',
                    address(this),
                    salt,
                    byteCode
                )))));
        }
    
    }
    

关于这次的代码，架构要比之前的更加复杂，BatcherV2 不但是我们的门面合约，而且还作为 mini proxy 的 logic 合约。目前的调用时序图如下:

![调用情况](https://storage.googleapis.com/papyrus_images/215ac7135718b8417ce3ce61e6b9b8027bc37ac59f13b504e844e8e8d1e21c38.png)

调用情况

现在我们来对上述合约的改动进行逐步分析:

1.  createProxies 里面就是 MiniProxy 的创建代码，更为细致的解释可以参考上面的 github 链接。同时注意我们这里使用了 create2 来创建确定性地址合约。
    
2.  callback 很简单，就是执行传来的任意 tx将部署 proxy 数量改为 100，
    
3.  fallback 函数没有了，取而代之的是 execute 函数，其职能基本与之前的 fallback 一样，就是将调用转发到每一个 mini proxy 合约。有两点需要着重强调下：
    
    1.  BatcherV2(proxy).callback(target, data); 这行代码值得详细说一下：因为 Batcher 是 MiniProxy 的 logic 合约，所以其实对 MiniProxy 的调用，最终都会落到 Batcher 自己身上，所以我们可以直接将 proxy 转换为一个 BatcherV2 调用 callback ，但是请务必注意，这个 fallback 是 MiniProxy 通过 delegatecall 调用的！
        
    2.  proxy 的创建是使用 create2，所以我们在 proxyFor 处，动态的计算每个 MiniProxy 合约的地址
        

在仓库中运行

    npx hardhat test
    

可以得到测试通过的输出

![搞定了吗？](https://storage.googleapis.com/papyrus_images/90110e1556456602bfb55622dca4d35c3b25a16cdbf0cc361743547919e0d5a1.png)

搞定了吗？

成功，非常好！除了我们的合约还存在一个**极大的安全隐患**！前面提到过 MiniProxy 刚创建时并为指定 owner，所以任何人都可以直接对其调用 claimMintRewardAndShare 方法。看下面的具体测试代码:

![小心黑客！](https://storage.googleapis.com/papyrus_images/ac66c5365cd400489d028ea3d672c7b82de4a582d832a26c0650f491b5ecaa70.png)

小心黑客！

黑客直接调用 proxy对应的测试输出如下:

![还我 XEN](https://storage.googleapis.com/papyrus_images/d80a5add867a65770211347072af30ea97eb7bb74fef5788b25cea949e6eb607.png)

还我 XEN

一个黑客直接获取到了我们一个（当然也可以全部拿走） mini proxy 的地址，然后直接在 mini proxy 合约上调用 callback() 取走了我们的 XEN。

那这个问题如何修复呢？一个很直接的想法就是，添加断言，只允许 Batcher 调用 MiniProxy，对应修复代码如下

    :...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, "Only original can call this function.");
            (bool success, ) = target.call(data);
            require(success, "Transaction failed.");
        }
    

看起来好像没问题，但是测试结果却给我们当头一棒，输出结果如下:

![claim 也因为断言失败了](https://storage.googleapis.com/papyrus_images/9e8c50d1a91a868bc7fa783525d915b0ce1fd57ff4f2aa4ff137de65eaafdc49.png)

claim 也因为断言失败了

黑客确实偷不走了，合约也报废了！

这是为什么呢？其实接触过 proxy 模式的同学想必已经发现了问题所在：因为 delegatecall 会保留当前执行的上下文，即虽然 callback 是 Batcher 中的代码，但是执行的时候，却在 MiniProxy 的存储上下文：msg.sender == original 其实会被翻译成 msg.sender == slot0，而很不幸， MiniProxy 什么都没存（可以通过检查 MiniProxy 的 slot 来确定）。所以这个断言永远不可能成立。这怎么办呢？

一个最直观的办法是在 logic 合约（也就是本文的 Batcher）里面提供一个设置 owner 的接口，在每个 mini proxy 创建之后再调用一下设置 owner 的接口即可。

但是本人无意间发现了一个神奇的修饰词: immutable，其与 const 有一个重要特性:

![immutable & const 不占用存储空间！](https://storage.googleapis.com/papyrus_images/03eba0b59c931c8d7e627d99ef7b8c7d0c9376eddf6bad0f13e345d1621305cd.png)

immutable & const 不占用存储空间！

太神奇了，immutable 变量在初始化之后，后续的使用都会被直接替换而不需要存储在 slot 中。msg.sender == original 的判断，将不再需要读取 storage，退化成类似和一个常量的比较！将 original 改为 immutable 之后，重新测试，终于完美通过:

![乌拉！](https://storage.googleapis.com/papyrus_images/7baeaaaa7147ada0e5ee58421761dbcc96999109377564e26f7cd384d92165f2.png)

乌拉！

最后让我们总结一下技术要点：

1.  mini proxy 可以低成本创建大量的 proxy 合约，但是需要额外的机制保证不被黑客直接调用 proxy 合约
    
2.  代理模式下一定要注意权限的控制，以及上下文切换时一些违反直觉的情况
    
3.  immutable 变量不占用任何 storage slot，可以与 mini proxy 完美配合，实现对 proxy 使用权的控制

---

*Originally published on [NealZhu](https://paragraph.com/@nealzhu/ml0oBaDKZhSyeF2jqyfF)*
