# EIP-1167 详解

By [xyyme.eth](https://paragraph.com/@xyyme) · 2022-08-26

---

概述
--

`EIP-1167`，又称 `Minimal Proxy Contract`，提供了一种低成本复制合约的方法。它有什么意义呢？我们先来看个例子：

    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, 'UniswapV2: IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'UniswapV2: ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'UniswapV2: PAIR_EXISTS'); // single check is sufficient
        bytes memory bytecode = type(UniswapV2Pair).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        // 通过原始 bytecode 创建合约
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        IUniswapV2Pair(pair).initialize(token0, token1);
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair; // populate mapping in the reverse direction
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }
    

在 Uniswap v2 的工厂合约中，需要创建交易对合约，这里的方法是直接使用交易对合约，即 `UniswapV2Pair` 合约的 `creationCode` 进行创建。由于是创建原始的合约内容，因此有一个缺点就是耗费的 gas 取决于 `Pair` 合约的大小，`Pair` 合约的内容越多，耗费的 gas 越多。

那么有没有什么改进方法呢？答案是有的，就是通过代理合约来转发调用。从创建原始合约变为创建代理合约，而代理合约只需要负责将调用转发给原始合约就行了，实际执行的逻辑仍然使用最原始的合约。接下来的内容需要大家了解内存布局和 `delegatecall` 相关的内容，不了解的朋友可以看看[这里](https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec)和[这里](https://mirror.xyz/xyyme.eth/0gmFpVZVlHhwb2YlmaSY8Dyv5r3Z24sKIks38cyQRFk)。

原理
--

提到代理，可能大家想到的第一反应就是合约升级，但是我们今天说的代理并不涉及合约升级，它仅仅负责合约调用的转发。

我们先来看看可升级合约的代理合约架构：

![可升级合约架构](https://storage.googleapis.com/papyrus_images/1762f3357bd2853e25905e075ec5be92dfbf9e0ede84fe698229a32dd7a603f2.png)

可升级合约架构

整个架构中存在一个代理合约和 N 个逻辑合约，只有一套数据，需要升级时则替换逻辑合约，同一时间只能存在一个逻辑合约。

再来看看今天提到的 `Minimal Proxy Contract` 架构：

![Minimal Proxy Contract 架构](https://storage.googleapis.com/papyrus_images/b579b49fafe7b447b8328f1c0bbaf9f327ec3899e96ddb0f4701f70ea4010034.png)

Minimal Proxy Contract 架构

整个架构中存在 N 个代理合约和一个逻辑合约，有多套数据分别存储在不同的代理合约中，所有代理合约共享逻辑合约的执行逻辑，同一时间存在多个代理合约。`Minimal Proxy Contract` 的原理就是将代理合约作为逻辑合约的复制品，各个代理合约存储各自的数据，需要多少份复制品就创建多少个代理合约即可。而代理合约本身只负责请求转发，因此其内容很少，从而耗费更少的 gas。

我们来看一个例子（注意这里是为了简单起见直接使用了构造函数，实际应用中不应该使用构造函数，因为这部分不会在代理合约中运行，即不会被初始化。因此如果有初始化逻辑，需要放在 `initialize` 函数中额外调用）：

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity 0.8.15;
    
    contract Demo {
        uint256 public a;
    
        constructor() {
            a = 1000;
        }
    
        function setA(uint256 _a) external {
            a = _a;
        }
    }
    

现在我们要对这个 `Demo` 合约进行复制，可以借助 OZ 的 `Clones` 合约来完成：

    // SPDX-License-Identifier: MIT
    // OpenZeppelin Contracts v4.4.1 (proxy/Clones.sol)
    
    pragma solidity ^0.8.0;
    library Clones {
        /**
         * @dev Deploys and returns the address of a clone that mimics the behaviour of `implementation`.
         *
         * This function uses the create opcode, which should never revert.
         */
        function clone(address implementation) internal returns (address instance) {
            assembly {
                let ptr := mload(0x40)
                mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
                mstore(add(ptr, 0x14), shl(0x60, implementation))
                mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
                instance := create(0, ptr, 0x37)
            }
            require(instance != address(0), "ERC1167: create failed");
        }
        ///....
    }
    

`clone` 方法需要接受一个原始逻辑合约的地址，然后返回新生成的克隆合约（也就是我们上面说的代理合约）地址。

我们来看看上面代码是什么意思，首先 `let ptr := mload(0x40)` 获取内存中空闲内存指针的位置，下一行的 `mstore(ptr, 0x3d602d80600a3d3981f33…)`，将该数据存入内存中，接下来的 `shl(0x60, implementation)` 将地址左移 `0x60`，即 `96` 位。由于 `address` 类型实际只占用 20 个字节，而传入的参数是通过 0 补齐到 32 字节的。假设传入的地址是

`0xbebebebebebebebebebebebebebebebebebebebe`，则补齐的内容为：

`0x000000000000000000000000bebebebebebebebebebebebebebebebebebebebe`

左移 `96` 位之后恰好获得原始的地址数据。

接下来将地址数据 `mstore` 储存到内存中，内存位置为 `0x14`，即 `20`。这个数据恰好是上面一行 `0x3d602d80600a3d39` 中截断后面零值的长度。

第三个 `mstore` 再将 `0x5af43d82803e903d…` 拼接到前面的数据所在内存位置后面。三个 `mstore` 操作下来，此时内存中的数据为：

即三部分数据拼接到一起的结果。但是在合约地址前后拼接的这两部分是什么意思呢？

首先我们将前 `10` 个字节 `3d602d80600a3d3981f3` [反编译](https://ethervm.io/decompile?address=&network=)一下得到：

    contract Contract {
        function main() {
            var var0 = returndata.length;
            memory[returndata.length:returndata.length + 0x2d] = code[0x0a:0x37];
            return memory[var0:var0 + 0x2d];
        }
    }
    

即克隆合约的构造方法，内容是将整个克隆合约的字节码返回给 EVM。

再将后面的 `45` 字节数据 `363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3` 反编译：

    contract Contract {
        function main() {
            var temp0 = msg.data.length;
            memory[returndata.length:returndata.length + temp0] = msg.data[returndata.length:returndata.length + temp0];
            var temp1 = returndata.length;
            var temp2;
            temp2, memory[returndata.length:returndata.length + returndata.length] = address(0xbebebebebebebebebebebebebebebebebebebebe).delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length]);
            var temp3 = returndata.length;
            memory[temp1:temp1 + temp3] = returndata[temp1:temp1 + temp3];
            var var1 = temp1;
            var var0 = returndata.length;
        
            if (temp2) { return memory[var1:var1 + var0]; }
            else { revert(memory[var1:var1 + var0]); }
        }
    }
    

这部分内容是利用 `delegatecall` 将调用进行转发的逻辑。

`clone` 方法的最后一行使用了 `create` 方法创建克隆合约，长度 `0x37` 即 `55` 是内存中克隆合约字节码的长度。

实践
--

现在我们来实际操作一下，首先编写克隆工厂合约:

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.0;
    
    import "./Clones.sol";
    
    contract MyClonesFactory {
        using Clones for address;
    
        event ProxyGenerated(address proxy);
    
        function clone(address implementation) external {
            address proxy = implementation.clone();
            emit ProxyGenerated(proxy);
        }
    }
    

接下来先部署原始 `Demo` 合约，然后部署 `MyClonesFactory` 合约，随后调用 `clone` 方法，传入 `Demo` 合约地址，即可创建克隆合约。

创建原始 `Demo` 合约时耗费了 14 万 gas：

![创建原始合约](https://storage.googleapis.com/papyrus_images/cf78844389ad8061dc046220c4b883e3898cfae8857c56777e63054b644a8e41.png)

创建原始合约

而 `clone` 方法创建克隆合约只花费了 6 万 gas：

![创建克隆合约](https://storage.googleapis.com/papyrus_images/47df04712caa2738270b064977183ccbbb155e681ee01efb3f9c8d3fe2a17326.png)

创建克隆合约

注意克隆合约是通过内部交易创建的：

![内部交易创建克隆合约](https://storage.googleapis.com/papyrus_images/f97948aa5510578cdf810bbc34472c6f76e0b4b5bac786bdf43a3fe74bc2192c.png)

内部交易创建克隆合约

Etherscan 也已经支持了 `EIP-1167` 的合约验证，只要原始合约代码已经验证，那么克隆合约代码自动验证。

原始合约验证代码之前的克隆合约：

![克隆合约的字节码](https://storage.googleapis.com/papyrus_images/2dc36f8e0c42bb0e63e6e0ea667a2155900b2224305fe1181031a7152f064a5d.png)

克隆合约的字节码

可以看到克隆合约的字节码就是去除构造方法字节码之后其余的部分，然后我们验证一下原始合约的代码，这时克隆合约就已经自动验证了，并且标注了 `Minimal Proxy Contract`：

![ ](https://storage.googleapis.com/papyrus_images/9d637f7876f2a55bbe752e5982ddc2388e407a78527cf847b69e38ec5343cc09.png)

注意需要直接验证原始逻辑合约，如果验证克隆合约会失败。

总结
--

`EIP-1167` 提供了一种低成本克隆合约的方法，并且 Etherscan 也已经提供了支持，在需要创建多个合约的场景下，不失为一种好方法。

关于我
---

欢迎[和我交流](https://linktr.ee/xyymeeth)

参考
--

[https://medium.com/coinmonks/diving-into-smart-contracts-minimal-proxy-eip-1167-3c4e7f1a41b8](https://medium.com/coinmonks/diving-into-smart-contracts-minimal-proxy-eip-1167-3c4e7f1a41b8)

[

💎🎉 OpenZeppelin Clones 解説 🎉💎
--------------------------------

OpenZeppelinのUpgradeable Contractsに関するドキュメントを読んでいて、Clonesの存在を知りました。 ClonesはUpgradable Contractsの仕組みの一部であるProxyの一種で、Implementation Contractの更新機能を省いたProxyだと言えます。（つまり、Implementation ...

https://zenn.dev

![](https://storage.googleapis.com/papyrus_images/287f4e9919f9513320a784f2534fb1a56fc784b61444733215b2c55daec24cb4.png)

](https://zenn.dev/yukista/articles/0bff3879ed0263)

[

EIP- 1167, Minimal Proxy Contract on Etherscan
----------------------------------------------

EIP- 1167, Minimal Proxy Contract on Etherscan Etherscan now supports Minimal Proxy Contract in EIP-1167. EIP-1167 define a standard where a minimal proxy contract forwards all calls and 100% of the ...

https://medium.com

![](https://storage.googleapis.com/papyrus_images/9faca8c0548aafe308e13e6f829553e72c0dbd487e13cc3d05d9948fc44c989e.jpg)

](https://medium.com/etherscan-blog/eip-1167-minimal-proxy-contract-on-etherscan-3eaedd85ef50)

---

*Originally published on [xyyme.eth](https://paragraph.com/@xyyme/eip-1167)*
