<?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>xyyme.eth</title>
        <link>https://paragraph.com/@xyyme</link>
        <description>Smart Contract Developer</description>
        <lastBuildDate>Tue, 21 Jul 2026 16:25:56 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>xyyme.eth</title>
            <url>https://storage.googleapis.com/papyrus_images/c073643d3e3d840e3f81d0a835237dac46e537926dfe9fd74f275d4d012cea45.png</url>
            <link>https://paragraph.com/@xyyme</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[ERC-6551 详解]]></title>
            <link>https://paragraph.com/@xyyme/erc-6551</link>
            <guid>pz0J87nYHEfVdHEMIRBg</guid>
            <pubDate>Fri, 07 Jul 2023 09:13:43 GMT</pubDate>
            <description><![CDATA[最近 ERC-6551 的热度很高，我们就来聊聊 ERC-6551 到底是什么。这篇文章我想从它的原理，代码实现，以及应用案例来讲解 6551。相信看完这篇文章后大家应该会对 6551 有一个比较全面的认识。什么是 ERC-6551简单来说，6551 是一个为 NFT 创建钱包的标准。这是什么意思呢？ 我们考虑一个场景，假设在游戏中，我的地址 A 拥有一个角色 Bob，这个角色它本身是一个 ERC721 的 NFT，同时它的身上也有许多道具，例如帽子，鞋子，武器等，也可能拥有一些资产例如金元宝等。这些资产本身也是 ERC20，ERC721 等类型的 token。这些道具和资产在游戏逻辑中是属于我的角色 Bob 的，但是在实际的底层合约实现中，其实它们都是属于我的地址 A 的。如果我想将我的角色 Bob 出售给别人，我就需要分别将 Bob 还有它所拥有的所有资产都一一转账给买家。这在逻辑和操作上都不是很合理。 6551 标准的目的就是为角色 Bob 创建一个钱包，使得它所拥有的道具都是属于角色 Bob 的，这样看起来就合理多了。我们可以看看官方 EIP 里给到的示例图：ERC-65...]]></description>
            <content:encoded><![CDATA[<p>最近 ERC-6551 的热度很高，我们就来聊聊 ERC-6551 到底是什么。这篇文章我想从它的原理，代码实现，以及应用案例来讲解 6551。相信看完这篇文章后大家应该会对 6551 有一个比较全面的认识。</p><h2 id="h-erc-6551" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">什么是 ERC-6551</h2><p>简单来说，6551 是一个为 NFT 创建钱包的标准。这是什么意思呢？</p><p>我们考虑一个场景，假设在游戏中，我的地址 A 拥有一个角色 Bob，这个角色它本身是一个 ERC721 的 NFT，同时它的身上也有许多道具，例如帽子，鞋子，武器等，也可能拥有一些资产例如金元宝等。这些资产本身也是 ERC20，ERC721 等类型的 token。这些道具和资产在游戏逻辑中是属于我的角色 Bob 的，但是在实际的底层合约实现中，其实它们都是属于我的地址 A 的。如果我想将我的角色 Bob 出售给别人，我就需要分别将 Bob 还有它所拥有的所有资产都一一转账给买家。这在逻辑和操作上都不是很合理。</p><p>6551 标准的目的就是为角色 Bob 创建一个钱包，使得它所拥有的道具都是属于角色 Bob 的，这样看起来就合理多了。我们可以看看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-6551">官方 EIP</a> 里给到的示例图：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1bf710a634688e5fef752474d27ba7512d0a4070eade5921714eb470d9cfd1e4.png" alt="ERC-6551" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">ERC-6551</figcaption></figure><p>用户账户中拥有两个 NFT，分别是 A#123 和 B#456，其中 A#123 拥有两个账户（ A 和 B），B#456 拥有一个账户（C）。</p><p>我们再来看看图中右边这一块是什么意思。6551 协议中提供了一个 <code>Registry</code> 合约，它是用来创建 NFT 钱包的合约，通过调用它的 <code>createAccount</code> 函数就可以对该 NFT 创建一个合约钱包。由于合约钱包的写法可以各式各样，因此该函数需要我们提供一个合约钱包的 Implementation。也就是说我们可以自定义钱包的合约，例如可以是 AA 钱包，可以是 Safe 钱包。图中还有 <code>proxies</code> 关键字，这表示 <code>Registry</code> 在创建钱包的时候是创建了 Implementation 的代理，而不是原模原样将其复制了一份，这样做的目的是为了节省 Gas。这块应用到了 EIP-1167 的知识，对其不熟悉的朋友可以看看我之前的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8">文章</a>。</p><p>6551 EIP 中把为 NFT 创建的钱包称为 token bound accounts。它有一个很重要的特性是可以向前兼容 NFT，对于之前链上已经部署的标准 NFT 合约，都可以兼容 6551。</p><p>我们平时使用的钱包，不论是 EOA 还是合约钱包，都是钱包本身拥有资产。但在 NFT 账户中，NFT 本身是不拥有资产的，而是它拥有一个合约钱包，该钱包是拥有资产的主体。也就是说 NFT 本身更类似于<code>人</code>的角色。用图说明应该更清晰一点：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9d16ca2ecb70a2986cfac9c24ef94b6cd9ad905aee90a64636be098e0ea4fdbc.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><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代码实现</h2><p>6551 标准建议 <code>Registry</code> 实现 <code>IERC6551Registry</code> 接口：</p><pre data-type="codeBlock" text="interface IERC6551Registry {
    /// @dev The registry SHALL emit the AccountCreated event upon successful account creation
    event AccountCreated(
        address account,
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt
    );

    /// @dev Creates a token bound account for an ERC-721 token.
    ///
    /// If account has already been created, returns the account address without calling create2.
    ///
    /// If initData is not empty and account has not yet been created, calls account with
    /// provided initData after creation.
    ///
    /// Emits AccountCreated event.
    ///
    /// @return the address of the account
    function createAccount(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt,
        bytes calldata initData
    ) external returns (address);

    /// @dev Returns the computed address of a token bound account
    ///
    /// @return The computed address of the account
    function account(
        address implementation,
        uint256 chainId,
        address tokenContract,
        uint256 tokenId,
        uint256 salt
    ) external view returns (address);
}
"><code><span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IERC6551Registry</span> </span>{
    <span class="hljs-comment">/// @dev The registry SHALL emit the AccountCreated event upon successful account creation</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">AccountCreated</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> account,
        <span class="hljs-keyword">address</span> implementation,
        <span class="hljs-keyword">uint256</span> chainId,
        <span class="hljs-keyword">address</span> tokenContract,
        <span class="hljs-keyword">uint256</span> tokenId,
        <span class="hljs-keyword">uint256</span> salt
    </span>)</span>;

    <span class="hljs-comment">/// @dev Creates a token bound account for an ERC-721 token.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// If account has already been created, returns the account address without calling create2.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// If initData is not empty and account has not yet been created, calls account with</span>
    <span class="hljs-comment">/// provided initData after creation.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// Emits AccountCreated event.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// @return the address of the account</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createAccount</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> implementation,
        <span class="hljs-keyword">uint256</span> chainId,
        <span class="hljs-keyword">address</span> tokenContract,
        <span class="hljs-keyword">uint256</span> tokenId,
        <span class="hljs-keyword">uint256</span> salt,
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> initData
    </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">address</span></span>)</span>;

    <span class="hljs-comment">/// @dev Returns the computed address of a token bound account</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// @return The computed address of the account</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">account</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> implementation,
        <span class="hljs-keyword">uint256</span> chainId,
        <span class="hljs-keyword">address</span> tokenContract,
        <span class="hljs-keyword">uint256</span> tokenId,
        <span class="hljs-keyword">uint256</span> salt
    </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">address</span></span>)</span>;
}
</code></pre><p><br><code>createAccount</code> 函数用来为 NFT 创建合约钱包，<code>account</code> 函数用来计算合约钱包地址。它们都利用了 create2 机制，因此可以返回一个确定的地址。对 create2 不了解的朋友可以看我之前的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/czipkrvqRwxHUjQey7zeEScfWDEVUYNajMAEo5e7Myw">文章</a>。</p><p>6551 官方给出了一个已经部署的 <code>Registry</code> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x02101dfB77FDE026414827Fdc604ddAF224F0921#code">实现</a>可供参考。</p><p>其中的 <code>getCreationCode</code> 函数值得留意一下：</p><pre data-type="codeBlock" text="function getCreationCode(
    address implementation_,
    uint256 chainId_,
    address tokenContract_,
    uint256 tokenId_,
    uint256 salt_
) internal pure returns (bytes memory) {
    // 将 salt, chainId, tokenContract, tokenId 拼接在 bytecode 后面
    return
        abi.encodePacked(
            hex&quot;3d60ad80600a3d3981f3363d3d373d3d3d363d73&quot;,
            implementation_,
            hex&quot;5af43d82803e903d91602b57fd5bf3&quot;,
            abi.encode(salt_, chainId_, tokenContract_, tokenId_)
        );
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCreationCode</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> implementation_,
    <span class="hljs-keyword">uint256</span> chainId_,
    <span class="hljs-keyword">address</span> tokenContract_,
    <span class="hljs-keyword">uint256</span> tokenId_,
    <span class="hljs-keyword">uint256</span> salt_
</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</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-comment">// 将 salt, chainId, tokenContract, tokenId 拼接在 bytecode 后面</span>
    <span class="hljs-keyword">return</span>
        <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(
            <span class="hljs-string">hex"3d60ad80600a3d3981f3363d3d373d3d3d363d73"</span>,
            implementation_,
            <span class="hljs-string">hex"5af43d82803e903d91602b57fd5bf3"</span>,
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(salt_, chainId_, tokenContract_, tokenId_)
        );
}
</code></pre><p><br>它是用来组装 proxy 字节码的函数，可以看到在最后将 <code>salt_</code>、<code>chainId_</code>、<code>tokenContract_</code>、<code>tokenId_</code> 等数据拼接在了 EIP-1167 的代理字节码后面。这里的目的是为了在创建的合约钱包中可以直接通过字节码读取到这些数据，我们之前学习过的 SudoSwap 代码中也使用了类似的写法，参考<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/3y87pgyV5BKoZuGRtBmC6AJRxLJcfFe9focEEY_JbEM">这里</a>。</p><p>对于创建的 NFT 钱包合约（即 Implementation 合约），6551 也作出了一些要求：</p><ul><li><p>应该通过 Registry 创建</p></li><li><p>必须实现 <code>ERC-165</code> 接口</p></li><li><p>必须实现 <code>ERC-1271</code> 接口</p></li><li><p>必须实现下面的 <code>IERC6551Account</code> 接口：</p></li></ul><pre data-type="codeBlock" text="/// @dev the ERC-165 identifier for this interface is `0x400a0398`
interface IERC6551Account {
    /// @dev Token bound accounts MUST implement a `receive` function.
    ///
    /// Token bound accounts MAY perform arbitrary logic to restrict conditions
    /// under which Ether can be received.
    receive() external payable;

    /// @dev Executes `call` on address `to`, with value `value` and calldata
    /// `data`.
    ///
    /// MUST revert and bubble up errors if call fails.
    ///
    /// By default, token bound accounts MUST allow the owner of the ERC-721 token
    /// which owns the account to execute arbitrary calls using `executeCall`.
    ///
    /// Token bound accounts MAY implement additional authorization mechanisms
    /// which limit the ability of the ERC-721 token holder to execute calls.
    ///
    /// Token bound accounts MAY implement additional execution functions which
    /// grant execution permissions to other non-owner accounts.
    ///
    /// @return The result of the call
    function executeCall(
        address to,
        uint256 value,
        bytes calldata data
    ) external payable returns (bytes memory);

    /// @dev Returns identifier of the ERC-721 token which owns the
    /// account
    ///
    /// The return value of this function MUST be constant - it MUST NOT change
    /// over time.
    ///
    /// @return chainId The EIP-155 ID of the chain the ERC-721 token exists on
    /// @return tokenContract The contract address of the ERC-721 token
    /// @return tokenId The ID of the ERC-721 token
    function token()
        external
        view
        returns (
            uint256 chainId,
            address tokenContract,
            uint256 tokenId
        );

    /// @dev Returns the owner of the ERC-721 token which controls the account
    /// if the token exists.
    ///
    /// This is value is obtained by calling `ownerOf` on the ERC-721 contract.
    ///
    /// @return Address of the owner of the ERC-721 token which owns the account
    function owner() external view returns (address);

    /// @dev Returns a nonce value that is updated on every successful transaction
    ///
    /// @return The current account nonce
    function nonce() external view returns (uint256);
}
"><code><span class="hljs-comment">/// @dev the ERC-165 identifier for this interface is `0x400a0398`</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IERC6551Account</span> </span>{
    <span class="hljs-comment">/// @dev Token bound accounts MUST implement a `receive` function.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// Token bound accounts MAY perform arbitrary logic to restrict conditions</span>
    <span class="hljs-comment">/// under which Ether can be received.</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>;

    <span class="hljs-comment">/// @dev Executes `call` on address `to`, with value `value` and calldata</span>
    <span class="hljs-comment">/// `data`.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// MUST revert and bubble up errors if call fails.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// By default, token bound accounts MUST allow the owner of the ERC-721 token</span>
    <span class="hljs-comment">/// which owns the account to execute arbitrary calls using `executeCall`.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// Token bound accounts MAY implement additional authorization mechanisms</span>
    <span class="hljs-comment">/// which limit the ability of the ERC-721 token holder to execute calls.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// Token bound accounts MAY implement additional execution functions which</span>
    <span class="hljs-comment">/// grant execution permissions to other non-owner accounts.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// @return The result of the call</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">executeCall</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> to,
        <span class="hljs-keyword">uint256</span> value,
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> data
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</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-comment">/// @dev Returns identifier of the ERC-721 token which owns the</span>
    <span class="hljs-comment">/// account</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// The return value of this function MUST be constant - it MUST NOT change</span>
    <span class="hljs-comment">/// over time.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// @return chainId The EIP-155 ID of the chain the ERC-721 token exists on</span>
    <span class="hljs-comment">/// @return tokenContract The contract address of the ERC-721 token</span>
    <span class="hljs-comment">/// @return tokenId The ID of the ERC-721 token</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">token</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">view</span></span>
        <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params">
            <span class="hljs-keyword">uint256</span> chainId,
            <span class="hljs-keyword">address</span> tokenContract,
            <span class="hljs-keyword">uint256</span> tokenId
        </span>)</span>;

    <span class="hljs-comment">/// @dev Returns the owner of the ERC-721 token which controls the account</span>
    <span class="hljs-comment">/// if the token exists.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// This is value is obtained by calling `ownerOf` on the ERC-721 contract.</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// @return Address of the owner of the ERC-721 token which owns the account</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">owner</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">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">/// @dev Returns a nonce value that is updated on every successful transaction</span>
    <span class="hljs-comment">///</span>
    <span class="hljs-comment">/// @return The current account nonce</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">nonce</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">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>;
}
</code></pre><p><br><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x1a0e97dae78590b7e967e725a5c848ed034f5510#code">这里</a>是一个已经实现的合约钱包示例。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">应用案例</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://opensea.io/collection/sapienz">Sapienz</a> NFT 系列使用了 ERC 6551 标准，我们来看看它是怎样应用 6551 的，代码<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x28e46378249685ff5593391502af2fbfaeac735d#code">在此</a>。</p><p>我们主要来看其中的 <code>_mintWithTokens</code> 函数，它的作用是，用户需要使用一些白名单的 NFT（例如 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://opensea.io/collection/stapleverse-hood-squad">PIGEON</a>）来 mint Sapienz NFT。具体逻辑是，函数将用户拥有的这些白名单的 NFT 转入到即将生成的 Sapienz NFT 对应的合约钱包中（即 token bound accounts），然后再为用户 mint Sapienz NFT。也就是说这些转入的白名单 NFT 是归属于生成的 Sapienz NFT 的。</p><p>我们来看看主要代码：</p><pre data-type="codeBlock" text="function _mintWithTokens(
    address tokenAddress,
    uint256[] memory tokenIds,
    bytes32[][] memory proof,
    address recipient
) internal {
    // ...
    uint256 quantity = tokenIds.length;

    for (uint256 i = 0; i &lt; quantity; i++) {
        uint256 tokenId = tokenIds[i];

        // 计算出 当前 tokenId（是当前 Sapienz 合约的 tokenId，不是传入的 tokenId）所有对应 tba 的地址
        address tba = erc6551Registry.account(
            erc6551AccountImplementation,
            block.chainid,
            address(this),
            startTokenId + i,
            0
        );

        // ....

        // 将白名单 NFT#tokenId 从 msg.sender 转移到 tba 中
        IERC721Upgradeable(tokenAddress).safeTransferFrom(
            msg.sender,
            tba,
            tokenId
        );
    }

    _safeMint(recipient, quantity);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_mintWithTokens</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> tokenAddress,
    <span class="hljs-keyword">uint256</span>[] <span class="hljs-keyword">memory</span> tokenIds,
    <span class="hljs-keyword">bytes32</span>[][] <span class="hljs-keyword">memory</span> proof,
    <span class="hljs-keyword">address</span> recipient
</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">uint256</span> quantity <span class="hljs-operator">=</span> tokenIds.<span class="hljs-built_in">length</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> quantity; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        <span class="hljs-keyword">uint256</span> tokenId <span class="hljs-operator">=</span> tokenIds[i];

        <span class="hljs-comment">// 计算出 当前 tokenId（是当前 Sapienz 合约的 tokenId，不是传入的 tokenId）所有对应 tba 的地址</span>
        <span class="hljs-keyword">address</span> tba <span class="hljs-operator">=</span> erc6551Registry.account(
            erc6551AccountImplementation,
            <span class="hljs-built_in">block</span>.<span class="hljs-built_in">chainid</span>,
            <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>),
            startTokenId <span class="hljs-operator">+</span> i,
            <span class="hljs-number">0</span>
        );

        <span class="hljs-comment">// ....</span>

        <span class="hljs-comment">// 将白名单 NFT#tokenId 从 msg.sender 转移到 tba 中</span>
        IERC721Upgradeable(tokenAddress).safeTransferFrom(
            <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>,
            tba,
            tokenId
        );
    }

    _safeMint(recipient, quantity);
}
</code></pre><p><br>我们在前面的 <code>IERC6551Registry</code> 接口中看到，<code>account</code> 函数是用来计算生成的合约钱包地址的，即代码中的 <code>tba</code> 就是合约钱包的地址，然后在后面会通过 <code>tokenAddress</code> 的 <code>safeTransferFrom</code> 函数将白名单 NFT 转入 tba 中。</p><p>我们注意到，代码中仅仅是计算出了 tba 的合约地址，但是并没有部署合约这一步操作，也就是说，这里的转入操作的目标地址可能是一个没有实际部署代码的地址，仅仅是一个预留地址而已。例如这个 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0xb46b2e4fbd408c4202c343964e03d03270021cb8">tba</a>，它是拥有 NFT 的，但是却并没有合约代码（不过在你看到的时候可能已经发生了状态变化）。如果我们希望该地址是有合约代码的话，需要调用 Registry 的 <code>createAccount</code> 函数来创建部署合约。例如这个 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x8e4d8b4689f7fabd32fc2862165c8556e1a4cc88#code">tba</a> 就是已经部署的合约地址。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9a912ba8d82f58a5219c2fd53f04f7339e82f16046053c1bea5db375de77f52f.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>其中字节码的前半部分是 EIP-1167 的拼接字节码，后面的阴影部分是我们前面提到的 <code>getCreationCode</code> 函数中拼接的<code>salt_</code>、<code>chainId_</code>、<code>tokenContract_</code>、<code>tokenId_</code> 数据。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>我们通过原理，代码实现和实际案例学习了 ERC-6551，它本身内容并不复杂，提供了一个创建 NFT 钱包的新思路。我觉得它最友好的一点就是可以向前兼容 NFT，没有侵入性，这样使得 NFT 本身也不用去关心 6551 的逻辑，只要按照自己的业务开发就行。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://eips.ethereum.org/EIPS/eip-6551" data="{&quot;provider_url&quot;:&quot;https://eips.ethereum.org&quot;,&quot;description&quot;:&quot;An interface and registry for smart contract accounts owned by non-fungible tokens&quot;,&quot;title&quot;:&quot;ERC-6551: Non-fungible Token Bound Accounts&quot;,&quot;url&quot;:&quot;https://eips.ethereum.org/EIPS/eip-6551&quot;,&quot;author_name&quot;:&quot;Ethereum Improvement Proposals&quot;,&quot;thumbnail_width&quot;:2614,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/ee90b3c0f1471672e67e1f79fd384562d04e53ad73f9effa654c9f25fde478c9.png&quot;,&quot;author_url&quot;:&quot;https://eips.ethereum.org/&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Ethereum Improvement Proposals&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:1528,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:2614,&quot;height&quot;:1528,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/ee90b3c0f1471672e67e1f79fd384562d04e53ad73f9effa654c9f25fde478c9.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/ee90b3c0f1471672e67e1f79fd384562d04e53ad73f9effa654c9f25fde478c9.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://eips.ethereum.org/EIPS/eip-6551" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>ERC-6551: Non-fungible Token Bound Accounts</h2><p>An interface and registry for smart contract accounts owned by non-fungible tokens</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://eips.ethereum.org</span></div><img src="https://storage.googleapis.com/papyrus_images/ee90b3c0f1471672e67e1f79fd384562d04e53ad73f9effa654c9f25fde478c9.png"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[Sudoswap 中对于 EIP-1167 的应用]]></title>
            <link>https://paragraph.com/@xyyme/sudoswap-eip-1167</link>
            <guid>ELFE172PmDRLleftBe9t</guid>
            <pubDate>Tue, 07 Feb 2023 15:43:47 GMT</pubDate>
            <description><![CDATA[最近在读 Sudoswap 的合约代码，发现其中应用了 EIP-1167 的玩法，有些写法感觉很有意思，因此想特地写篇文章来记录分享一下。对于 EIP-1167 还不太了解的朋友可以看看我之前写的这篇文章。工厂合约在 Sudoswap 代码中，LSSVMPairFactory 合约是 Pair 的工厂合约，其中可以通过 createPairETH 创建 ETH 的交易对，通过 createPairERC20 创建 ERC20 的交易对。在创建交易对的过程中，调用 LSSVMPairCloner 库合约，其中应用了 EIP-1167 协议。 我们来看看库合约中创建 Pair 的方法：function cloneETHPair( address implementation, ILSSVMPairFactoryLike factory, ICurve bondingCurve, IERC721 nft, uint8 poolType ) internal returns (address instance) { assembly { let ptr := mload(0x40) ms...]]></description>
            <content:encoded><![CDATA[<p>最近在读 Sudoswap 的合约代码，发现其中应用了 EIP-1167 的玩法，有些写法感觉很有意思，因此想特地写篇文章来记录分享一下。对于 EIP-1167 还不太了解的朋友可以看看我之前写的这篇<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8">文章</a>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">工厂合约</h2><p>在 Sudoswap 代码中，<code>LSSVMPairFactory</code> 合约是 Pair 的工厂合约，其中可以通过 <code>createPairETH</code> 创建 ETH 的交易对，通过 <code>createPairERC20</code> 创建 ERC20 的交易对。在创建交易对的过程中，调用 <code>LSSVMPairCloner</code> 库合约，其中应用了 EIP-1167 协议。</p><p>我们来看看库合约中创建 Pair 的方法：</p><pre data-type="codeBlock" text="function cloneETHPair(
    address implementation,
    ILSSVMPairFactoryLike factory,
    ICurve bondingCurve,
    IERC721 nft,
    uint8 poolType
) internal returns (address instance) {
    assembly {
        let ptr := mload(0x40)

        mstore(
            ptr,
            hex&quot;60_72_3d_81_60_09_3d_39_f3_3d_3d_3d_3d_36_3d_3d_37_60_3d_60_35_36_39_36_60_3d_01_3d_73_00_00_00&quot;
        )
        mstore(add(ptr, 0x1d), shl(0x60, implementation))

        mstore(
            add(ptr, 0x31),
            hex&quot;5a_f4_3d_3d_93_80_3e_60_33_57_fd_5b_f3_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00&quot;
        )

        mstore(add(ptr, 0x3e), shl(0x60, factory))
        mstore(add(ptr, 0x52), shl(0x60, bondingCurve))
        mstore(add(ptr, 0x66), shl(0x60, nft))
        mstore8(add(ptr, 0x7a), poolType)

        instance := create(0, ptr, 0x7b)
    }
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">cloneETHPair</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> implementation,
    ILSSVMPairFactoryLike factory,
    ICurve bondingCurve,
    IERC721 nft,
    <span class="hljs-keyword">uint8</span> poolType
</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span> instance</span>) </span>{
    <span class="hljs-keyword">assembly</span> {
        <span class="hljs-keyword">let</span> ptr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)

        <span class="hljs-built_in">mstore</span>(
            ptr,
            <span class="hljs-string">hex"60_72_3d_81_60_09_3d_39_f3_3d_3d_3d_3d_36_3d_3d_37_60_3d_60_35_36_39_36_60_3d_01_3d_73_00_00_00"</span>
        )
        <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x1d</span>), <span class="hljs-built_in">shl</span>(<span class="hljs-number">0x60</span>, implementation))

        <span class="hljs-built_in">mstore</span>(
            <span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x31</span>),
            <span class="hljs-string">hex"5a_f4_3d_3d_93_80_3e_60_33_57_fd_5b_f3_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00"</span>
        )

        <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x3e</span>), <span class="hljs-built_in">shl</span>(<span class="hljs-number">0x60</span>, factory))
        <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x52</span>), <span class="hljs-built_in">shl</span>(<span class="hljs-number">0x60</span>, bondingCurve))
        <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x66</span>), <span class="hljs-built_in">shl</span>(<span class="hljs-number">0x60</span>, nft))
        <span class="hljs-built_in">mstore8</span>(<span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x7a</span>), poolType)

        instance <span class="hljs-operator">:=</span> <span class="hljs-built_in">create</span>(<span class="hljs-number">0</span>, ptr, <span class="hljs-number">0x7b</span>)
    }
}
</code></pre><p>其中前三个 <code>mstore</code> 与我们之前的文章介绍的 EIP-1167 的写法类似，但是后面又多了几行 <code>mstore</code> 以及 <code>mstore8</code>，这些都是什么意思呢？</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">字节码分析</h2><p>我们先来看看前三个 <code>mstore</code>，这里的具体字节码与之前我们介绍的 1167 写法有些出入，这三行组合出的字节码是（长度为 62 字节）：</p><pre data-type="codeBlock" text="60723d8160093d39f33d3d3d3d363d3d37603d6035363936603d013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603357fd5bf3
"><code></code></pre><p>分割一下，得到：</p><pre data-type="codeBlock" text="1 -&gt; 60723d8160093d39f3
2 -&gt; 3d3d3d3d363d3d37603d6035363936603d013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603357fd5bf3
"><code><span class="hljs-number">1</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 60723d8160093d39f3
<span class="hljs-number">2</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 3d3d3d3d363d3d37603d6035363936603d013d73bebebebebebebebebebebebebebebebebebebebe5af43d3d93803e603357fd5bf3
</code></pre><p>其中第一部分的九个字节，反编译得到的结果是：</p><pre data-type="codeBlock" text="contract Contract {
    function main() {
        memory[returndata.length:returndata.length + 0x72] = code[0x09:0x7b];
        return memory[returndata.length:returndata.length + 0x72];
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Contract</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> <span class="hljs-number">0x72</span>] <span class="hljs-operator">=</span> code[<span class="hljs-number">0x09</span>:<span class="hljs-number">0x7b</span>];
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> <span class="hljs-number">0x72</span>];
    }
}
</code></pre><p>可以看到，合约初始化时将字节码中从 9 字节到 123（0x7b） 字节的数据返回到 EVM 中，但是我们前面的字节码只有 62 字节，后面还有 61 字节都是什么呢？</p><p>此时我们再回看前面的 <code>cloneETHPair</code> 代码，没错，后面的 61 字节就是来自于其最后的几个 <code>mstore</code>，其中 <code>factory</code>、<code>bondingCurve</code> 以及 <code>nft</code> 都是地址类型，分别占据 20 字节，<code>poolType</code> 是 bool 类型，占据 1 个字节。</p><p>我们再将上面字节码的第二部分进行反编译：</p><pre data-type="codeBlock" text="contract Contract {
    function main() {
        var temp0 = returndata.length;
        var var1 = returndata.length;
        var temp1 = msg.data.length;
        memory[returndata.length:returndata.length + temp1] = msg.data[returndata.length:returndata.length + temp1];
        memory[msg.data.length:msg.data.length + 0x3d] = code[0x35:0x72];
        var temp2;
        temp2, memory[returndata.length:returndata.length + returndata.length] = 
            address(0xbebebebebebebebebebebebebebebebebebebebe)
            .delegatecall.gas(msg.gas)(memory[returndata.length:returndata.length + msg.data.length + 0x3d]);
        var temp3 = returndata.length;
        var var0 = returndata.length;
        memory[temp0:temp0 + temp3] = returndata[temp0:temp0 + temp3];
    
        if (temp2) { return memory[var1:var1 + var0]; }
        else { revert(memory[var1:var1 + var0]); }
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Contract</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">var</span> temp0 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">var</span> var1 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">var</span> temp1 <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> temp1] <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> temp1];
        <span class="hljs-keyword">memory</span>[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>.<span class="hljs-built_in">length</span>:<span class="hljs-built_in">msg</span>.data.<span class="hljs-built_in">length</span> + <span class="hljs-number">0x3d</span>] <span class="hljs-operator">=</span> code[<span class="hljs-number">0x35</span>:<span class="hljs-number">0x72</span>];
        <span class="hljs-keyword">var</span> temp2;
        temp2, <span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> returndata.<span class="hljs-built_in">length</span>] <span class="hljs-operator">=</span> 
            <span class="hljs-keyword">address</span>(<span class="hljs-number">0xbebebebebebebebebebebebebebebebebebebebe</span>)
            .<span class="hljs-built_in">delegatecall</span>.<span class="hljs-built_in">gas</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">gas</span>)(<span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>.<span class="hljs-built_in">length</span> + <span class="hljs-number">0x3d</span>]);
        <span class="hljs-keyword">var</span> temp3 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">var</span> var0 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">memory</span>[temp0:temp0 <span class="hljs-operator">+</span> temp3] <span class="hljs-operator">=</span> returndata[temp0:temp0 <span class="hljs-operator">+</span> temp3];
    
        <span class="hljs-keyword">if</span> (temp2) { <span class="hljs-keyword">return</span> <span class="hljs-keyword">memory</span>[var1:var1 <span class="hljs-operator">+</span> var0]; }
        <span class="hljs-keyword">else</span> { <span class="hljs-keyword">revert</span>(<span class="hljs-keyword">memory</span>[var1:var1 <span class="hljs-operator">+</span> var0]); }
    }
}
</code></pre><p>从上面代码中可以看到，代理合约在调用逻辑合约的时候，总会在最后加上 61 （0x3d）字节的 calldata，正是前面的 <code>factory</code>、<code>bondingCurve</code> 、<code>nft</code> 以及 <code>poolType</code>。那么也就是说，任何通过代理合约转发到逻辑合约的调用，最后都会加上这段 calldata，意味着在逻辑合约的方法中，总是可以获取到这段 calldata。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">获取数据</h2><p>我们来看看 Pair 合约中是如何获取这几个字段值的，以 <code>bondingCurve()</code> 为例：</p><pre data-type="codeBlock" text="function bondingCurve() public pure returns (ICurve _bondingCurve) {
    // 这里 ETH Pair 是 61，ERC20 Pair 是 81
    uint256 paramsLength = _immutableParamsLength();
    assembly {
        _bondingCurve := shr(
            0x60,
            calldataload(add(sub(calldatasize(), paramsLength), 20))
        )
    }
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">bondingCurve</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">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params">ICurve _bondingCurve</span>) </span>{
    <span class="hljs-comment">// 这里 ETH Pair 是 61，ERC20 Pair 是 81</span>
    <span class="hljs-keyword">uint256</span> paramsLength <span class="hljs-operator">=</span> _immutableParamsLength();
    <span class="hljs-keyword">assembly</span> {
        _bondingCurve <span class="hljs-operator">:=</span> <span class="hljs-built_in">shr</span>(
            <span class="hljs-number">0x60</span>,
            <span class="hljs-built_in">calldataload</span>(<span class="hljs-built_in">add</span>(<span class="hljs-built_in">sub</span>(<span class="hljs-built_in">calldatasize</span>(), paramsLength), <span class="hljs-number">20</span>))
        )
    }
}
</code></pre><p><code>_immutableParamsLength()</code> 返回常量，ETH Pair 返回 61，ERC20 Pair 返回 81。我们前面参考的方法是 <code>cloneETHPair</code>，因此我们这里默认其值为 61。</p><p>接下来的一段汇编代码，首先最里面的 <code>calldatasize()</code> 获取整个 calldata 的长度，其中是包含后面 61 字节的，然后用 <code>calldatasize()</code> 减去 <code>paramsLength</code>，得到的值就是调用合约方法的基础 calldata 长度，再加上 20（这里的 20 是前面 <code>factory</code> 的地址长度），获取到 <code>bondingCurve</code> 地址在整个 calldata 中的偏移量。然后调用 <code>calldataload</code>，将偏移量作为参数，可以获取到 <code>bondingCurve</code> 地址起始的 32 字节数据，其中前 20 字节是 <code>bondingCurve</code> 的地址，后面的 12 （0x60 / 8）字节通过 <code>shr</code> 操作移除，最终得到的就是 <code>bondingCurve</code> 的地址。</p><p>我们来看看图示，更加易懂：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0be47ac03c7a620f79dbf0bd78f6033011b775583e4e338db04b18ecb5752667.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>其余的 <code>factory</code> 等获取方法同理，只是偏移量不同，最后的 <code>add</code> 数值不同。</p><p>看懂这块，我们同理就可以分析 ERC20 Pair 的操作，唯一的区别是在 <code>cloneERC20Pair</code> 方法中，最后还要在 <code>mstore</code> token 地址，因此 calldata 会多 20，即 81。</p><p>那么我们思考一下，为什么 Sudoswap 要采用这么复杂的操作，而不是直接将这几个字段直接存储到 Pair 中呢？最重要的就是 gas 的原因，Solidity 中处理 calldata 的操作耗费的 gas 很少，而如果将这些字段存储到 Pair 中，首先 <code>sstore</code> 操作就要耗费大量 gas，其次每次读取 <code>sload</code> 也会耗费大量 gas，与在 calldata 中操作的 gas 消耗可以说不是一个数量级的。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>我们看到，Sudoswap 中采取了一个几乎极致的方法来节省 gas，这其中确实是有炫技的成分，不过里面的方法与思想还是值得我们学习的。随着合约开发逐渐卷起来，我觉得这种字节码层级操作的代码以后会越来越多，这块还是值得深入研究的。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[Lens protocol 合约代码浅析]]></title>
            <link>https://paragraph.com/@xyyme/lens-protocol</link>
            <guid>FZSDGEeWMsvhPun3hLhs</guid>
            <pubDate>Fri, 25 Nov 2022 14:45:07 GMT</pubDate>
            <description><![CDATA[Lens protocol 是 Aave 团队出品的 SocialFi 项目，我们今天结合它的文档来聊聊它的业务逻辑和合约代码。这篇文章主要着重于对大家看 Lens 代码的一个引导，不会详细解读代码的每一个点，主要是方便大家在看完文章后能对代码更熟悉一些，更快地理解代码。代码分析在大体业务上来说，Lens 的业务逻辑有些类似于 Twitter，主要包括了下面几个方面：Profile，一个地址可以拥有多个 Profile，创建一个 Profile 就可以拥有一个对应 NFT，类似于一个人可以拥有多个账户，可以指定默认 ProfilePublication，发表内容Comment，对内容发表评论，也可以对评论发表评论Mirror，类似于转发功能Collect，将内容铸造为 NFTFollow，关注其他 Profile，关注后可以获得 FollowNFT相对推特来说，主要就是多了 Collect 功能，接下来我们分功能来看看各自的主要逻辑。 用户的主要操作逻辑入口都是在 LensHub 合约中，其中也包含了许多 setter 方法。主要实现逻辑是在 PublishingLogic 和 ...]]></description>
            <content:encoded><![CDATA[<p>Lens protocol 是 Aave 团队出品的 SocialFi 项目，我们今天结合它的文档来聊聊它的业务逻辑和合约代码。这篇文章主要着重于对大家看 Lens 代码的一个引导，不会详细解读代码的每一个点，主要是方便大家在看完文章后能对代码更熟悉一些，更快地理解代码。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代码分析</h2><p>在大体业务上来说，Lens 的业务逻辑有些类似于 Twitter，主要包括了下面几个方面：</p><ul><li><p><code>Profile</code>，一个地址可以拥有多个 Profile，创建一个 Profile 就可以拥有一个对应 NFT，类似于一个人可以拥有多个账户，可以指定默认 Profile</p></li><li><p><code>Publication</code>，发表内容</p></li><li><p><code>Comment</code>，对内容发表评论，也可以对评论发表评论</p></li><li><p><code>Mirror</code>，类似于转发功能</p></li><li><p><code>Collect</code>，将内容铸造为 NFT</p></li><li><p><code>Follow</code>，关注其他 Profile，关注后可以获得 FollowNFT</p></li></ul><p>相对推特来说，主要就是多了 <code>Collect</code> 功能，接下来我们分功能来看看各自的主要逻辑。</p><p>用户的主要操作逻辑入口都是在 <code>LensHub</code> 合约中，其中也包含了许多 <code>setter</code> 方法。主要实现逻辑是在 <code>PublishingLogic</code> 和 <code>InteractionLogic</code> 库合约中。</p><h3 id="h-profile" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Profile</h3><p>创建 Profile</p><pre data-type="codeBlock" text="function createProfile(DataTypes.CreateProfileData calldata vars)
    external
    override
    whenNotPaused
    returns (uint256)
{
    // 白名单中的用户才能创建
    if (!_profileCreatorWhitelisted[msg.sender]) 
        revert Errors.ProfileCreatorNotWhitelisted();
    unchecked {
        // ++ 表示从 1 开始
        uint256 profileId = ++_profileCounter;
        _mint(vars.to, profileId);
        PublishingLogic.createProfile(
            vars,
            profileId,
            _profileIdByHandleHash,
            _profileById,
            _followModuleWhitelisted
        );
        return profileId;
    }
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createProfile</span>(<span class="hljs-params">DataTypes.CreateProfileData <span class="hljs-keyword">calldata</span> vars</span>)
    <span class="hljs-title"><span class="hljs-keyword">external</span></span>
    <span class="hljs-title"><span class="hljs-keyword">override</span></span>
    <span class="hljs-title">whenNotPaused</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">// 白名单中的用户才能创建</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>_profileCreatorWhitelisted[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>]) 
        <span class="hljs-keyword">revert</span> Errors.ProfileCreatorNotWhitelisted();
    <span class="hljs-keyword">unchecked</span> {
        <span class="hljs-comment">// ++ 表示从 1 开始</span>
        <span class="hljs-keyword">uint256</span> profileId <span class="hljs-operator">=</span> <span class="hljs-operator">+</span><span class="hljs-operator">+</span>_profileCounter;
        _mint(vars.to, profileId);
        PublishingLogic.createProfile(
            vars,
            profileId,
            _profileIdByHandleHash,
            _profileById,
            _followModuleWhitelisted
        );
        <span class="hljs-keyword">return</span> profileId;
    }
}
</code></pre><p>创建 profile，只有白名单中的地址可以创建，这是为了防止用户名被恶意占领。每次创建一个 profile 会获得一个 NFT。</p><blockquote><p>Profiles can only be minted by addresses that have been whitelisted by governance. This ensures that, given the low-fee environment present on Polygon, the namespace is not reserved by squatters.</p></blockquote><p>我们来看看入参 <code>CreateProfileData</code> 的结构：</p><pre data-type="codeBlock" text="struct CreateProfileData {
    // 接收 profile 的地址
    address to;
    // 可以理解为用户名
    string handle;
    // 头像地址
    string imageURI;
    address followModule;
    bytes followModuleInitData;
    string followNFTURI;
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">CreateProfileData</span> {
    <span class="hljs-comment">// 接收 profile 的地址</span>
    <span class="hljs-keyword">address</span> to;
    <span class="hljs-comment">// 可以理解为用户名</span>
    <span class="hljs-keyword">string</span> handle;
    <span class="hljs-comment">// 头像地址</span>
    <span class="hljs-keyword">string</span> imageURI;
    <span class="hljs-keyword">address</span> followModule;
    <span class="hljs-keyword">bytes</span> followModuleInitData;
    <span class="hljs-keyword">string</span> followNFTURI;
}
</code></pre><p>前面的几个都是一些比较基础的数据字段，后面的几个 follow 字段下文再解释。</p><p>下面是创建 profile 的主要逻辑：</p><pre data-type="codeBlock" text="function createProfile(
    DataTypes.CreateProfileData calldata vars,
    uint256 profileId,
    mapping(bytes32 =&gt; uint256) storage _profileIdByHandleHash,
    mapping(uint256 =&gt; DataTypes.ProfileStruct) storage _profileById,
    mapping(address =&gt; bool) storage _followModuleWhitelisted
) external {
    // 校验 handle 的内容，只在创建 profile 的时候校验
    _validateHandle(vars.handle);

    // 要求 imageURI 的长度不能大于限制
    if (bytes(vars.imageURI).length &gt; Constants.MAX_PROFILE_IMAGE_URI_LENGTH)
        revert Errors.ProfileImageURILengthInvalid();

    bytes32 handleHash = keccak256(bytes(vars.handle));

    // handle 必须唯一
    if (_profileIdByHandleHash[handleHash] != 0) revert Errors.HandleTaken();

    // handle hash 对应 profileId
    _profileIdByHandleHash[handleHash] = profileId;

    // 存储 profileId 对应的数据，handle，imageURI，followNFTURI
    _profileById[profileId].handle = vars.handle;
    _profileById[profileId].imageURI = vars.imageURI;
    _profileById[profileId].followNFTURI = vars.followNFTURI;

    bytes memory followModuleReturnData;
    if (vars.followModule != address(0)) {
        _profileById[profileId].followModule = vars.followModule;
        followModuleReturnData = _initFollowModule(
            profileId,
            vars.followModule,
            vars.followModuleInitData,
            _followModuleWhitelisted
        );
    }

    _emitProfileCreated(profileId, vars, followModuleReturnData);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createProfile</span>(<span class="hljs-params">
    DataTypes.CreateProfileData <span class="hljs-keyword">calldata</span> vars,
    <span class="hljs-keyword">uint256</span> profileId,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> => <span class="hljs-keyword">uint256</span></span>) <span class="hljs-keyword">storage</span> _profileIdByHandleHash,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.ProfileStruct</span>) <span class="hljs-keyword">storage</span> _profileById,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> => <span class="hljs-keyword">bool</span></span>) <span class="hljs-keyword">storage</span> _followModuleWhitelisted
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// 校验 handle 的内容，只在创建 profile 的时候校验</span>
    _validateHandle(vars.handle);

    <span class="hljs-comment">// 要求 imageURI 的长度不能大于限制</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">bytes</span>(vars.imageURI).<span class="hljs-built_in">length</span> <span class="hljs-operator">></span> Constants.MAX_PROFILE_IMAGE_URI_LENGTH)
        <span class="hljs-keyword">revert</span> Errors.ProfileImageURILengthInvalid();

    <span class="hljs-keyword">bytes32</span> handleHash <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(vars.handle));

    <span class="hljs-comment">// handle 必须唯一</span>
    <span class="hljs-keyword">if</span> (_profileIdByHandleHash[handleHash] <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) <span class="hljs-keyword">revert</span> Errors.HandleTaken();

    <span class="hljs-comment">// handle hash 对应 profileId</span>
    _profileIdByHandleHash[handleHash] <span class="hljs-operator">=</span> profileId;

    <span class="hljs-comment">// 存储 profileId 对应的数据，handle，imageURI，followNFTURI</span>
    _profileById[profileId].handle <span class="hljs-operator">=</span> vars.handle;
    _profileById[profileId].imageURI <span class="hljs-operator">=</span> vars.imageURI;
    _profileById[profileId].followNFTURI <span class="hljs-operator">=</span> vars.followNFTURI;

    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> followModuleReturnData;
    <span class="hljs-keyword">if</span> (vars.followModule <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
        _profileById[profileId].followModule <span class="hljs-operator">=</span> vars.followModule;
        followModuleReturnData <span class="hljs-operator">=</span> _initFollowModule(
            profileId,
            vars.followModule,
            vars.followModuleInitData,
            _followModuleWhitelisted
        );
    }

    _emitProfileCreated(profileId, vars, followModuleReturnData);
}
</code></pre><p>代码相对也比较好理解，最后的 <code>_initFollowModule</code> 调用我们同样放在后面再讲解，大家记住这里出现过就行。</p><p>这里有一个小细节，方法入参中有 <code>mapping</code> 类型，这是只有在 <code>library</code> 库合约中的方法才能实现的，普通的合约方法不能接收 <code>mapping</code> 类型的参数。</p><h3 id="h-publication" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Publication</h3><p>发表内容</p><pre data-type="codeBlock" text="function post(DataTypes.PostData calldata vars)
    external
    override
    whenPublishingEnabled
    returns (uint256)
{
    // dispatcher 可以替 owner 为其 post
    _validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
    return
        _createPost(
            vars.profileId,
            vars.contentURI,
            vars.collectModule,
            vars.collectModuleInitData,
            vars.referenceModule,
            vars.referenceModuleInitData
        );
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">post</span>(<span class="hljs-params">DataTypes.PostData <span class="hljs-keyword">calldata</span> vars</span>)
    <span class="hljs-title"><span class="hljs-keyword">external</span></span>
    <span class="hljs-title"><span class="hljs-keyword">override</span></span>
    <span class="hljs-title">whenPublishingEnabled</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">// dispatcher 可以替 owner 为其 post</span>
    _validateCallerIsProfileOwnerOrDispatcher(vars.profileId);
    <span class="hljs-keyword">return</span>
        _createPost(
            vars.profileId,
            vars.contentURI,
            vars.collectModule,
            vars.collectModuleInitData,
            vars.referenceModule,
            vars.referenceModuleInitData
        );
}
</code></pre><p><code>_validateCallerIsProfileOwnerOrDispatcher</code> 是校验只有用户本人或者用户的 dispatcher 可以调用，类似于 ERC721 中的 operator 角色：</p><pre data-type="codeBlock" text="function _validateCallerIsProfileOwnerOrDispatcher(uint256 profileId) internal view {
    if (msg.sender == ownerOf(profileId) || msg.sender == _dispatcherByProfile[profileId]) {
        return;
    }
    revert Errors.NotProfileOwnerOrDispatcher();
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_validateCallerIsProfileOwnerOrDispatcher</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> profileId</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> </span>{
    <span class="hljs-keyword">if</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> ownerOf(profileId) <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-operator">=</span><span class="hljs-operator">=</span> _dispatcherByProfile[profileId]) {
        <span class="hljs-keyword">return</span>;
    }
    <span class="hljs-keyword">revert</span> Errors.NotProfileOwnerOrDispatcher();
}
</code></pre><p>我们来看看入参 <code>PostData</code> 的结构：</p><pre data-type="codeBlock" text="struct PostData {
    uint256 profileId;
    // 存储内容的 URI
    string contentURI;
    address collectModule;
    bytes collectModuleInitData;
    address referenceModule;
    bytes referenceModuleInitData;
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">PostData</span> {
    <span class="hljs-keyword">uint256</span> profileId;
    <span class="hljs-comment">// 存储内容的 URI</span>
    <span class="hljs-keyword">string</span> contentURI;
    <span class="hljs-keyword">address</span> collectModule;
    <span class="hljs-keyword">bytes</span> collectModuleInitData;
    <span class="hljs-keyword">address</span> referenceModule;
    <span class="hljs-keyword">bytes</span> referenceModuleInitData;
}
</code></pre><p>后面的几个 collect 和 reference 相关内容我们下文再说。</p><pre data-type="codeBlock" text="function _createPost(
    uint256 profileId,
    string memory contentURI,
    address collectModule,
    bytes memory collectModuleData,
    address referenceModule,
    bytes memory referenceModuleData
) internal returns (uint256) {
    unchecked {
        // 更新发布数量
        uint256 pubId = ++_profileById[profileId].pubCount;
        PublishingLogic.createPost(
            profileId,
            contentURI,
            collectModule,
            collectModuleData,
            referenceModule,
            referenceModuleData,
            pubId,
            _pubByIdByProfile,
            _collectModuleWhitelisted,
            _referenceModuleWhitelisted
        );
        return pubId;
    }
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_createPost</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span> profileId,
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span> contentURI,
    <span class="hljs-keyword">address</span> collectModule,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> collectModuleData,
    <span class="hljs-keyword">address</span> referenceModule,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> referenceModuleData
</span>) <span class="hljs-title"><span class="hljs-keyword">internal</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-keyword">unchecked</span> {
        <span class="hljs-comment">// 更新发布数量</span>
        <span class="hljs-keyword">uint256</span> pubId <span class="hljs-operator">=</span> <span class="hljs-operator">+</span><span class="hljs-operator">+</span>_profileById[profileId].pubCount;
        PublishingLogic.createPost(
            profileId,
            contentURI,
            collectModule,
            collectModuleData,
            referenceModule,
            referenceModuleData,
            pubId,
            _pubByIdByProfile,
            _collectModuleWhitelisted,
            _referenceModuleWhitelisted
        );
        <span class="hljs-keyword">return</span> pubId;
    }
}
</code></pre><p><code>_createPost</code> 方法仅仅更新了发布数量就进入了库合约中的逻辑：</p><pre data-type="codeBlock" text="function createPost(
    uint256 profileId,
    string memory contentURI,
    address collectModule,
    bytes memory collectModuleInitData,
    address referenceModule,
    bytes memory referenceModuleInitData,
    uint256 pubId,
    mapping(uint256 =&gt; mapping(uint256 =&gt; DataTypes.PublicationStruct))
        storage _pubByIdByProfile,
    mapping(address =&gt; bool) storage _collectModuleWhitelisted,
    mapping(address =&gt; bool) storage _referenceModuleWhitelisted
) external {
    _pubByIdByProfile[profileId][pubId].contentURI = contentURI;

    // 新建 post 的时候需要初始化 collet module 和 reference module
    // Collect module initialization
    bytes memory collectModuleReturnData = _initPubCollectModule(
        profileId,
        pubId,
        collectModule,
        collectModuleInitData,
        _pubByIdByProfile,
        _collectModuleWhitelisted
    );

    // Reference module initialization
    bytes memory referenceModuleReturnData = _initPubReferenceModule(
        profileId,
        pubId,
        referenceModule,
        referenceModuleInitData,
        _pubByIdByProfile,
        _referenceModuleWhitelisted
    );

    emit Events.PostCreated(
        profileId,
        pubId,
        contentURI,
        collectModule,
        collectModuleReturnData,
        referenceModule,
        referenceModuleReturnData,
        block.timestamp
    );
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createPost</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span> profileId,
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span> contentURI,
    <span class="hljs-keyword">address</span> collectModule,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> collectModuleInitData,
    <span class="hljs-keyword">address</span> referenceModule,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> referenceModuleInitData,
    <span class="hljs-keyword">uint256</span> pubId,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.PublicationStruct</span>)</span>)
        <span class="hljs-keyword">storage</span> _pubByIdByProfile,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> => <span class="hljs-keyword">bool</span></span>) <span class="hljs-keyword">storage</span> _collectModuleWhitelisted,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> => <span class="hljs-keyword">bool</span></span>) <span class="hljs-keyword">storage</span> _referenceModuleWhitelisted
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    _pubByIdByProfile[profileId][pubId].contentURI <span class="hljs-operator">=</span> contentURI;

    <span class="hljs-comment">// 新建 post 的时候需要初始化 collet module 和 reference module</span>
    <span class="hljs-comment">// Collect module initialization</span>
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> collectModuleReturnData <span class="hljs-operator">=</span> _initPubCollectModule(
        profileId,
        pubId,
        collectModule,
        collectModuleInitData,
        _pubByIdByProfile,
        _collectModuleWhitelisted
    );

    <span class="hljs-comment">// Reference module initialization</span>
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> referenceModuleReturnData <span class="hljs-operator">=</span> _initPubReferenceModule(
        profileId,
        pubId,
        referenceModule,
        referenceModuleInitData,
        _pubByIdByProfile,
        _referenceModuleWhitelisted
    );

    <span class="hljs-keyword">emit</span> Events.PostCreated(
        profileId,
        pubId,
        contentURI,
        collectModule,
        collectModuleReturnData,
        referenceModule,
        referenceModuleReturnData,
        <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>
    );
}
</code></pre><p>我们来看看 <code>_pubByIdByProfile</code> 的结构：</p><pre data-type="codeBlock" text="mapping(uint256 =&gt; mapping(uint256 =&gt; DataTypes.PublicationStruct)) internal _pubByIdByProfile;
"><code><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">mapping</span>(<span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> DataTypes.PublicationStruct)) <span class="hljs-keyword">internal</span> _pubByIdByProfile;
</code></pre><p>它的结构是 <code>profileId</code> → <code>pubId</code> → <code>PublicationStruct</code>，其中 <code>PublicationStruct</code> 的结构是：</p><pre data-type="codeBlock" text="struct PublicationStruct {
    uint256 profileIdPointed;
    uint256 pubIdPointed;

    string contentURI;
    address referenceModule;
    address collectModule;
    address collectNFT;
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">PublicationStruct</span> {
    <span class="hljs-keyword">uint256</span> profileIdPointed;
    <span class="hljs-keyword">uint256</span> pubIdPointed;

    <span class="hljs-keyword">string</span> contentURI;
    <span class="hljs-keyword">address</span> referenceModule;
    <span class="hljs-keyword">address</span> collectModule;
    <span class="hljs-keyword">address</span> collectNFT;
}
</code></pre><p>前面两个 pointed 数据代表的是该内容指向的原始内容的 id，只有在当前内容为 <code>Mirror</code> 或者 <code>Comment</code> 类型的时候才有值。</p><p>两个 <code>_init</code> 方法我们下文再说。</p><p>代码中还有很多的 <code>WithSig</code> 结尾的方法，例如 <code>postWithSig</code>，这是利用 EIP-712 实现的利用签名可以让其他人代替发送交易的方法，不了解的朋友可以看看我的这篇<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/cJX3zqiiUg2dxB1nmbXbDcQ1DSdajHP5iNgBc6wEZz4">文章</a>。</p><h3 id="h-comment" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Comment</h3><p>发表评论，它的逻辑与 Post 的逻辑很类似，我们主要来看看最核心的部分：</p><pre data-type="codeBlock" text="function createComment(
    DataTypes.CommentData memory vars,
    uint256 pubId,
    mapping(uint256 =&gt; DataTypes.ProfileStruct) storage _profileById,
    mapping(uint256 =&gt; mapping(uint256 =&gt; DataTypes.PublicationStruct))
        storage _pubByIdByProfile,
    mapping(address =&gt; bool) storage _collectModuleWhitelisted,
    mapping(address =&gt; bool) storage _referenceModuleWhitelisted
) external {
    // Validate existence of the pointed publication
    // 校验传入的数据是否正确
    uint256 pubCount = _profileById[vars.profileIdPointed].pubCount;
    if (pubCount &lt; vars.pubIdPointed || vars.pubIdPointed == 0)
        revert Errors.PublicationDoesNotExist();

    // Ensure the pointed publication is not the comment being created
    // 不能指向自己的这条 comment
    if (vars.profileId == vars.profileIdPointed &amp;&amp; vars.pubIdPointed == pubId)
        revert Errors.CannotCommentOnSelf();

    _pubByIdByProfile[vars.profileId][pubId].contentURI = vars.contentURI;
    _pubByIdByProfile[vars.profileId][pubId].profileIdPointed = vars.profileIdPointed;
    _pubByIdByProfile[vars.profileId][pubId].pubIdPointed = vars.pubIdPointed;

    // Collect Module Initialization
    bytes memory collectModuleReturnData = _initPubCollectModule(
        vars.profileId,
        pubId,
        vars.collectModule,
        vars.collectModuleInitData,
        _pubByIdByProfile,
        _collectModuleWhitelisted
    );

    // Reference module initialization
    bytes memory referenceModuleReturnData = _initPubReferenceModule(
        vars.profileId,
        pubId,
        vars.referenceModule,
        vars.referenceModuleInitData,
        _pubByIdByProfile,
        _referenceModuleWhitelisted
    );

    // Reference module validation
    address refModule = _pubByIdByProfile[vars.profileIdPointed][vars.pubIdPointed]
        .referenceModule;
    if (refModule != address(0)) {
        IReferenceModule(refModule).processComment(
            vars.profileId,
            vars.profileIdPointed,
            vars.pubIdPointed,
            vars.referenceModuleData
        );
    }

    // Prevents a stack too deep error
    _emitCommentCreated(vars, pubId, collectModuleReturnData, referenceModuleReturnData);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createComment</span>(<span class="hljs-params">
    DataTypes.CommentData <span class="hljs-keyword">memory</span> vars,
    <span class="hljs-keyword">uint256</span> pubId,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.ProfileStruct</span>) <span class="hljs-keyword">storage</span> _profileById,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.PublicationStruct</span>)</span>)
        <span class="hljs-keyword">storage</span> _pubByIdByProfile,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> => <span class="hljs-keyword">bool</span></span>) <span class="hljs-keyword">storage</span> _collectModuleWhitelisted,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> => <span class="hljs-keyword">bool</span></span>) <span class="hljs-keyword">storage</span> _referenceModuleWhitelisted
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// Validate existence of the pointed publication</span>
    <span class="hljs-comment">// 校验传入的数据是否正确</span>
    <span class="hljs-keyword">uint256</span> pubCount <span class="hljs-operator">=</span> _profileById[vars.profileIdPointed].pubCount;
    <span class="hljs-keyword">if</span> (pubCount <span class="hljs-operator">&#x3C;</span> vars.pubIdPointed <span class="hljs-operator">|</span><span class="hljs-operator">|</span> vars.pubIdPointed <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>)
        <span class="hljs-keyword">revert</span> Errors.PublicationDoesNotExist();

    <span class="hljs-comment">// Ensure the pointed publication is not the comment being created</span>
    <span class="hljs-comment">// 不能指向自己的这条 comment</span>
    <span class="hljs-keyword">if</span> (vars.profileId <span class="hljs-operator">=</span><span class="hljs-operator">=</span> vars.profileIdPointed <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> vars.pubIdPointed <span class="hljs-operator">=</span><span class="hljs-operator">=</span> pubId)
        <span class="hljs-keyword">revert</span> Errors.CannotCommentOnSelf();

    _pubByIdByProfile[vars.profileId][pubId].contentURI <span class="hljs-operator">=</span> vars.contentURI;
    _pubByIdByProfile[vars.profileId][pubId].profileIdPointed <span class="hljs-operator">=</span> vars.profileIdPointed;
    _pubByIdByProfile[vars.profileId][pubId].pubIdPointed <span class="hljs-operator">=</span> vars.pubIdPointed;

    <span class="hljs-comment">// Collect Module Initialization</span>
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> collectModuleReturnData <span class="hljs-operator">=</span> _initPubCollectModule(
        vars.profileId,
        pubId,
        vars.collectModule,
        vars.collectModuleInitData,
        _pubByIdByProfile,
        _collectModuleWhitelisted
    );

    <span class="hljs-comment">// Reference module initialization</span>
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> referenceModuleReturnData <span class="hljs-operator">=</span> _initPubReferenceModule(
        vars.profileId,
        pubId,
        vars.referenceModule,
        vars.referenceModuleInitData,
        _pubByIdByProfile,
        _referenceModuleWhitelisted
    );

    <span class="hljs-comment">// Reference module validation</span>
    <span class="hljs-keyword">address</span> refModule <span class="hljs-operator">=</span> _pubByIdByProfile[vars.profileIdPointed][vars.pubIdPointed]
        .referenceModule;
    <span class="hljs-keyword">if</span> (refModule <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
        IReferenceModule(refModule).processComment(
            vars.profileId,
            vars.profileIdPointed,
            vars.pubIdPointed,
            vars.referenceModuleData
        );
    }

    <span class="hljs-comment">// Prevents a stack too deep error</span>
    _emitCommentCreated(vars, pubId, collectModuleReturnData, referenceModuleReturnData);
}
</code></pre><p>与 Post 相比，多了指定 <code>profileIdPointed</code> 和 <code>pubIdPointed</code> 的部分，其指向的就是对应的原始内容 Id。还多了 <code>processComment</code> 的调用，我们下文再讲。</p><p>入参 <code>CommentData</code> 的结构：</p><pre data-type="codeBlock" text="struct CommentData {
    uint256 profileId;
    string contentURI;
    uint256 profileIdPointed;
    uint256 pubIdPointed;

    bytes referenceModuleData;
    address collectModule;
    bytes collectModuleInitData;
    address referenceModule;
    bytes referenceModuleInitData;
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">CommentData</span> {
    <span class="hljs-keyword">uint256</span> profileId;
    <span class="hljs-keyword">string</span> contentURI;
    <span class="hljs-keyword">uint256</span> profileIdPointed;
    <span class="hljs-keyword">uint256</span> pubIdPointed;

    <span class="hljs-keyword">bytes</span> referenceModuleData;
    <span class="hljs-keyword">address</span> collectModule;
    <span class="hljs-keyword">bytes</span> collectModuleInitData;
    <span class="hljs-keyword">address</span> referenceModule;
    <span class="hljs-keyword">bytes</span> referenceModuleInitData;
}
</code></pre><h3 id="h-mirror" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Mirror</h3><p>转发，它的逻辑与前面也是大同小异：</p><pre data-type="codeBlock" text="function createMirror(
    DataTypes.MirrorData memory vars,
    uint256 pubId,
    mapping(uint256 =&gt; mapping(uint256 =&gt; DataTypes.PublicationStruct))
        storage _pubByIdByProfile,
    mapping(address =&gt; bool) storage _referenceModuleWhitelisted
) external {
    // 这里是获取最原始的 id，例如 mirror 另外一个 mirror，这里需要获得最原始的数据
    (uint256 rootProfileIdPointed, uint256 rootPubIdPointed, ) = Helpers.getPointedIfMirror(
        vars.profileIdPointed,
        vars.pubIdPointed,
        _pubByIdByProfile
    );

    _pubByIdByProfile[vars.profileId][pubId].profileIdPointed = rootProfileIdPointed;
    _pubByIdByProfile[vars.profileId][pubId].pubIdPointed = rootPubIdPointed;

    // Reference module initialization
    bytes memory referenceModuleReturnData = _initPubReferenceModule(
        vars.profileId,
        pubId,
        vars.referenceModule,
        vars.referenceModuleInitData,
        _pubByIdByProfile,
        _referenceModuleWhitelisted
    );

    // Reference module validation
    address refModule = _pubByIdByProfile[rootProfileIdPointed][rootPubIdPointed]
        .referenceModule;
    if (refModule != address(0)) {
        IReferenceModule(refModule).processMirror(
            vars.profileId,
            rootProfileIdPointed,
            rootPubIdPointed,
            vars.referenceModuleData
        );
    }

    emit Events.MirrorCreated(
        vars.profileId,
        pubId,
        rootProfileIdPointed,
        rootPubIdPointed,
        vars.referenceModuleData,
        vars.referenceModule,
        referenceModuleReturnData,
        block.timestamp
    );
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createMirror</span>(<span class="hljs-params">
    DataTypes.MirrorData <span class="hljs-keyword">memory</span> vars,
    <span class="hljs-keyword">uint256</span> pubId,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.PublicationStruct</span>)</span>)
        <span class="hljs-keyword">storage</span> _pubByIdByProfile,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> => <span class="hljs-keyword">bool</span></span>) <span class="hljs-keyword">storage</span> _referenceModuleWhitelisted
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// 这里是获取最原始的 id，例如 mirror 另外一个 mirror，这里需要获得最原始的数据</span>
    (<span class="hljs-keyword">uint256</span> rootProfileIdPointed, <span class="hljs-keyword">uint256</span> rootPubIdPointed, ) <span class="hljs-operator">=</span> Helpers.getPointedIfMirror(
        vars.profileIdPointed,
        vars.pubIdPointed,
        _pubByIdByProfile
    );

    _pubByIdByProfile[vars.profileId][pubId].profileIdPointed <span class="hljs-operator">=</span> rootProfileIdPointed;
    _pubByIdByProfile[vars.profileId][pubId].pubIdPointed <span class="hljs-operator">=</span> rootPubIdPointed;

    <span class="hljs-comment">// Reference module initialization</span>
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> referenceModuleReturnData <span class="hljs-operator">=</span> _initPubReferenceModule(
        vars.profileId,
        pubId,
        vars.referenceModule,
        vars.referenceModuleInitData,
        _pubByIdByProfile,
        _referenceModuleWhitelisted
    );

    <span class="hljs-comment">// Reference module validation</span>
    <span class="hljs-keyword">address</span> refModule <span class="hljs-operator">=</span> _pubByIdByProfile[rootProfileIdPointed][rootPubIdPointed]
        .referenceModule;
    <span class="hljs-keyword">if</span> (refModule <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
        IReferenceModule(refModule).processMirror(
            vars.profileId,
            rootProfileIdPointed,
            rootPubIdPointed,
            vars.referenceModuleData
        );
    }

    <span class="hljs-keyword">emit</span> Events.MirrorCreated(
        vars.profileId,
        pubId,
        rootProfileIdPointed,
        rootPubIdPointed,
        vars.referenceModuleData,
        vars.referenceModule,
        referenceModuleReturnData,
        <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>
    );
}
</code></pre><p>多了 <code>getPointedIfMirror</code> 方法，用于获取最原始的 Id，例如 A 文章是原创，B Mirror 了 A，C 又 Mirror 了 B，那么这里不论 Mirror 了多少层，总是获得 A 的 Id。同时后面也多了 <code>processMirror</code> 方法的调用。</p><p>入参 <code>MirrorData</code> 的结构：</p><pre data-type="codeBlock" text="struct MirrorData {
    // 发表在哪个 profile 的名下
    uint256 profileId;
    // mirror 指向的 profile id
    uint256 profileIdPointed;
    // mirror 指向的 pub id
    uint256 pubIdPointed;
    
    bytes referenceModuleData;
    address referenceModule;
    bytes referenceModuleInitData;
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">MirrorData</span> {
    <span class="hljs-comment">// 发表在哪个 profile 的名下</span>
    <span class="hljs-keyword">uint256</span> profileId;
    <span class="hljs-comment">// mirror 指向的 profile id</span>
    <span class="hljs-keyword">uint256</span> profileIdPointed;
    <span class="hljs-comment">// mirror 指向的 pub id</span>
    <span class="hljs-keyword">uint256</span> pubIdPointed;
    
    <span class="hljs-keyword">bytes</span> referenceModuleData;
    <span class="hljs-keyword">address</span> referenceModule;
    <span class="hljs-keyword">bytes</span> referenceModuleInitData;
}
</code></pre><h3 id="h-collect" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Collect</h3><p>Collect 是将内容铸造为 NFT 的过程，例如用户认为某个内容很好，想将其铸造为 NFT，可以类比为将喜爱的照片装裱的过程。</p><p>来看看代码：</p><pre data-type="codeBlock" text="function collect(
    address collector,
    uint256 profileId,
    uint256 pubId,
    bytes calldata collectModuleData,
    address collectNFTImpl,
    mapping(uint256 =&gt; mapping(uint256 =&gt; DataTypes.PublicationStruct))
        storage _pubByIdByProfile,
    mapping(uint256 =&gt; DataTypes.ProfileStruct) storage _profileById
) external returns (uint256) {
    // 这里是获取最原始的 pub 数据，包括 profileId， pubId，collectModule
    (uint256 rootProfileId, uint256 rootPubId, address rootCollectModule) = Helpers
        .getPointedIfMirror(profileId, pubId, _pubByIdByProfile);

    uint256 tokenId;
    // Avoids stack too deep
    {
        address collectNFT = _pubByIdByProfile[rootProfileId][rootPubId].collectNFT;
        if (collectNFT == address(0)) {
            // 如果是第一次 collect，则部署一个新的 collect
            collectNFT = _deployCollectNFT(
                rootProfileId,
                rootPubId,
                _profileById[rootProfileId].handle,
                collectNFTImpl
            );
            _pubByIdByProfile[rootProfileId][rootPubId].collectNFT = collectNFT;
        }
        // 第一次 collect 需要部署合约，后面的不需要，直接 mint 就行
        tokenId = ICollectNFT(collectNFT).mint(collector);
    }

    ICollectModule(rootCollectModule).processCollect(
        profileId,
        collector,
        rootProfileId,
        rootPubId,
        collectModuleData
    );
    _emitCollectedEvent(
        collector,
        profileId,
        pubId,
        rootProfileId,
        rootPubId,
        collectModuleData
    );

    return tokenId;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">collect</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> collector,
    <span class="hljs-keyword">uint256</span> profileId,
    <span class="hljs-keyword">uint256</span> pubId,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> collectModuleData,
    <span class="hljs-keyword">address</span> collectNFTImpl,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.PublicationStruct</span>)</span>)
        <span class="hljs-keyword">storage</span> _pubByIdByProfile,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.ProfileStruct</span>) <span class="hljs-keyword">storage</span> _profileById
</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">uint256</span></span>) </span>{
    <span class="hljs-comment">// 这里是获取最原始的 pub 数据，包括 profileId， pubId，collectModule</span>
    (<span class="hljs-keyword">uint256</span> rootProfileId, <span class="hljs-keyword">uint256</span> rootPubId, <span class="hljs-keyword">address</span> rootCollectModule) <span class="hljs-operator">=</span> Helpers
        .getPointedIfMirror(profileId, pubId, _pubByIdByProfile);

    <span class="hljs-keyword">uint256</span> tokenId;
    <span class="hljs-comment">// Avoids stack too deep</span>
    {
        <span class="hljs-keyword">address</span> collectNFT <span class="hljs-operator">=</span> _pubByIdByProfile[rootProfileId][rootPubId].collectNFT;
        <span class="hljs-keyword">if</span> (collectNFT <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
            <span class="hljs-comment">// 如果是第一次 collect，则部署一个新的 collect</span>
            collectNFT <span class="hljs-operator">=</span> _deployCollectNFT(
                rootProfileId,
                rootPubId,
                _profileById[rootProfileId].handle,
                collectNFTImpl
            );
            _pubByIdByProfile[rootProfileId][rootPubId].collectNFT <span class="hljs-operator">=</span> collectNFT;
        }
        <span class="hljs-comment">// 第一次 collect 需要部署合约，后面的不需要，直接 mint 就行</span>
        tokenId <span class="hljs-operator">=</span> ICollectNFT(collectNFT).mint(collector);
    }

    ICollectModule(rootCollectModule).processCollect(
        profileId,
        collector,
        rootProfileId,
        rootPubId,
        collectModuleData
    );
    _emitCollectedEvent(
        collector,
        profileId,
        pubId,
        rootProfileId,
        rootPubId,
        collectModuleData
    );

    <span class="hljs-keyword">return</span> tokenId;
}
</code></pre><p>既然 collect 会铸造 NFT，那么肯定就有 mint NFT 的过程。我们在上面代码中看到，从 <code>_pubByIdByProfile</code> 中获取 <code>collectNFT</code> 的地址，如果为空，说明该内容是第一次被 collect，此时需要部署一个 <code>collectNFT</code> 的合约，如果不为空，则直接 mint 即可。部署合约这块运用了 EIP-1167 的内容，可以节省 Gas 费用，不了解的朋友可以看看我的这篇<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8">文章</a>。</p><p><code>processCollect</code> 是执行 collect 的一些逻辑。我们前面涉及到 <code>collectModule</code> 的部分一直没讲，现在来看看这一块究竟是什么逻辑。实际上 <code>collectModule</code> 就是对 collect 过程的个性化定制模块。例如，内容创造者要求最多只能 collect 100 份，或者是他想要对 collect 进行收费，其他用户必须缴纳一定费用才能进行 collect。<code>collectModule</code> 就是用来实现这些多种多样的功能。目前 Lens 官方配置了下面几种 collect 配置：</p><ul><li><p><code>FreeCollectModule</code>，免费 collect</p></li><li><p><code>FeeCollectModule</code>，用户需要支付一定量的费用（Token）才能 collect</p></li><li><p><code>TimedFeeCollectModule</code>，用户只能在内容发布后的一段时间内进行 collect，且需要支付费用</p></li><li><p><code>LimitedFeeCollectModule</code>，用户需要支付费用，并且有数量上限，例如最多只能 collect 500份</p></li><li><p><code>LimitedTimedFeeCollectModule</code>，用户需要支付费用，并且有数量上限，同时也有时间限制</p></li><li><p><code>RevertCollectModule</code>，任何情况下都不能 collect，否则交易失败</p></li></ul><p>上面这几个，除了最后一个 <code>RevertCollectModule</code>，其他的都有一个先决条件是先判断是否只有 follower 才能 collect，这个配置是在用户创建 profile 的时候配置的，也就是说用户可以设置是否只有 follower 才能 collect，这个设置是在 <code>followModule</code> 中。</p><p><code>collectModule</code> 主要有两个模块，<code>init</code> 和 <code>process</code>。<code>init</code> 会初始化一些参数，例如如果用户使用的是 <code>FeeCollectModule</code>，那么 <code>init</code> 就会根据用户传入的参数指定 Token 的类型以及数量等，<code>process</code> 是执行 collect 时进行的逻辑，在这里就会执行转账操作。我们前面看到的 <code>_initPubCollectModule</code> 方法就属于 <code>init</code> 部分，用户会在创建 post 或者 comment 的时候调用 <code>init</code>，在 collect 的时候执行 <code>process</code>。</p><h3 id="h-follow" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Follow</h3><p>Follow 顾名思义就是关注其他 profile，与 collect 相同的是 follow 时也会铸造 NFT，类似于粉丝拥有了一个明星的徽章。</p><pre data-type="codeBlock" text="function follow(
    address follower,
    uint256[] calldata profileIds,
    bytes[] calldata followModuleDatas,
    mapping(uint256 =&gt; DataTypes.ProfileStruct) storage _profileById,
    mapping(bytes32 =&gt; uint256) storage _profileIdByHandleHash
) external returns (uint256[] memory) {
    if (profileIds.length != followModuleDatas.length) revert Errors.ArrayMismatch();
    uint256[] memory tokenIds = new uint256Unsupported embed;
    for (uint256 i = 0; i &lt; profileIds.length; ) {
        string memory handle = _profileById[profileIds[i]].handle;
        if (_profileIdByHandleHash[keccak256(bytes(handle))] != profileIds[i])
            revert Errors.TokenDoesNotExist();

        address followModule = _profileById[profileIds[i]].followModule;
        address followNFT = _profileById[profileIds[i]].followNFT;

        if (followNFT == address(0)) {
            followNFT = _deployFollowNFT(profileIds[i]);
            _profileById[profileIds[i]].followNFT = followNFT;
        }

        tokenIds[i] = IFollowNFT(followNFT).mint(follower);

        if (followModule != address(0)) {
            IFollowModule(followModule).processFollow(
                follower,
                profileIds[i],
                followModuleDatas[i]
            );
        }
        unchecked {
            ++i;
        }
    }
    emit Events.Followed(follower, profileIds, followModuleDatas, block.timestamp);
    return tokenIds;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">follow</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> follower,
    <span class="hljs-keyword">uint256</span>[] <span class="hljs-keyword">calldata</span> profileIds,
    <span class="hljs-keyword">bytes</span>[] <span class="hljs-keyword">calldata</span> followModuleDatas,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> => DataTypes.ProfileStruct</span>) <span class="hljs-keyword">storage</span> _profileById,
    <span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> => <span class="hljs-keyword">uint256</span></span>) <span class="hljs-keyword">storage</span> _profileIdByHandleHash
</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">uint256</span>[] <span class="hljs-keyword">memory</span></span>) </span>{
    <span class="hljs-keyword">if</span> (profileIds.<span class="hljs-built_in">length</span> <span class="hljs-operator">!</span><span class="hljs-operator">=</span> followModuleDatas.<span class="hljs-built_in">length</span>) <span class="hljs-keyword">revert</span> Errors.ArrayMismatch();
    <span class="hljs-keyword">uint256</span>[] <span class="hljs-keyword">memory</span> tokenIds <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> uint256Unsupported embed;
    <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> profileIds.<span class="hljs-built_in">length</span>; ) {
        <span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span> handle <span class="hljs-operator">=</span> _profileById[profileIds[i]].handle;
        <span class="hljs-keyword">if</span> (_profileIdByHandleHash[<span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(handle))] <span class="hljs-operator">!</span><span class="hljs-operator">=</span> profileIds[i])
            <span class="hljs-keyword">revert</span> Errors.TokenDoesNotExist();

        <span class="hljs-keyword">address</span> followModule <span class="hljs-operator">=</span> _profileById[profileIds[i]].followModule;
        <span class="hljs-keyword">address</span> followNFT <span class="hljs-operator">=</span> _profileById[profileIds[i]].followNFT;

        <span class="hljs-keyword">if</span> (followNFT <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
            followNFT <span class="hljs-operator">=</span> _deployFollowNFT(profileIds[i]);
            _profileById[profileIds[i]].followNFT <span class="hljs-operator">=</span> followNFT;
        }

        tokenIds[i] <span class="hljs-operator">=</span> IFollowNFT(followNFT).mint(follower);

        <span class="hljs-keyword">if</span> (followModule <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
            IFollowModule(followModule).processFollow(
                follower,
                profileIds[i],
                followModuleDatas[i]
            );
        }
        <span class="hljs-keyword">unchecked</span> {
            <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i;
        }
    }
    <span class="hljs-keyword">emit</span> Events.Followed(follower, profileIds, followModuleDatas, <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>);
    <span class="hljs-keyword">return</span> tokenIds;
}
</code></pre><p>follow 方法支持批量 follow，因此接收的是一个 <code>profileIds</code> 的数组，遍历这一个数组，逐个进行 follow。同样的，如果是该 profile 第一次被 follow，则需要部署 NFT 合约，否则直接 mint 即可。</p><p>follow 与 collect 同样都支持定制化模块，follow 目前支持的模块如下：</p><ul><li><p><code>ProfileFollowModule</code>，正常 follow，没有限制</p></li><li><p><code>ApprovalFollowModule</code>，只有授权过的地址可以 follow</p></li><li><p><code>FeeFollowModule</code>，follow 需要付费</p></li><li><p><code>RevertFollowModule</code>，不允许 follow，否则直接失败</p></li></ul><p><code>followModule</code> 同样包含 <code>init</code> 和 <code>process</code> 两个模块。用户可以在创建 profile 的时候调用 <code>init</code>，即 <code>_initFollowModule</code>，也可以后期修改。在 follow 的时候执行 <code>process</code>。</p><h3 id="h-reference" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Reference</h3><p>其实 <code>reference</code> 不算是一个额外的功能，它指的是 <code>comment</code> 或者 <code>mirror</code> 操作。但是它也有自己对应的 module，所以这里把它单独拿出来讲讲。在用户创建 <code>post</code>，<code>comment</code>，<code>mirror</code> 的时候，可以指定当前创建的这个新 pub 在被 <code>reference</code> 时的要求。目前只支持一个模块：</p><ul><li><p><code>FollowerOnlyReferenceModule</code>，只有 follower 可以进行 <code>reference</code></p></li></ul><p><code>referenceModule</code> 也是包含 <code>init</code> 和 <code>process</code> 两个模块。在用户创建 <code>post</code>，<code>comment</code>，<code>mirror</code> 的时候，会调用 <code>init</code>，即 <code>_initPubReferenceModule</code>。在用户进行 <code>comment</code>，<code>mirror</code> 的时候会调用 <code>process</code>。注意前后两个 <code>comment</code>，<code>mirror</code> 对应的不是一个对象，<code>init</code> 时是指定当该 pub 被 <code>reference</code> 时的要求，<code>process</code> 时是执行当前 <code>reference</code> 对象的要求。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>我们已经将 Lens 的主要逻辑代码看完，整体来说不算难，业务逻辑相对也比较简单，没有特别绕的逻辑。希望能对大家起一个代码导读的作用，接下来看代码会更加容易。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://docs.lens.xyz/docs" data="{&quot;provider_url&quot;:&quot;https://lens.xyz&quot;,&quot;description&quot;:&quot;Scalable SocialFi on Ethereum. Lens is a high-performance blockchain stack built for SocialFi, combining modular Social Primitives, fast settlement, and decentralized storage. The Lens stack is composed of Lens Chain, Social Protocol, and Storage Nodes. As a layer 2 leveraging ZKsync, Avail, and Ethereum&apos;s security, Lens delivers the fastest, most cost-effective, and scalable blockchain for developers.&quot;,&quot;title&quot;:&quot;Lens Chain&quot;,&quot;mean_alpha&quot;:249,&quot;thumbnail_width&quot;:1080,&quot;url&quot;:&quot;https://lens.xyz/docs/chain/overview&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/d15b3ff39578c26e897f2f4b9be2ff4ce689c553ea2f14d405fd67bc552bb616.png&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Docs&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:570,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1080,&quot;height&quot;:570,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/d15b3ff39578c26e897f2f4b9be2ff4ce689c553ea2f14d405fd67bc552bb616.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/d15b3ff39578c26e897f2f4b9be2ff4ce689c553ea2f14d405fd67bc552bb616.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://docs.lens.xyz/docs" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Lens Chain</h2><p>Scalable SocialFi on Ethereum. Lens is a high-performance blockchain stack built for SocialFi, combining modular Social Primitives, fast settlement, and decentralized storage. The Lens stack is composed of Lens Chain, Social Protocol, and Storage Nodes. As a layer 2 leveraging ZKsync, Avail, and Ethereum&#x27;s security, Lens delivers the fastest, most cost-effective, and scalable blockchain for developers.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://lens.xyz</span></div><img src="https://storage.googleapis.com/papyrus_images/d15b3ff39578c26e897f2f4b9be2ff4ce689c553ea2f14d405fd67bc552bb616.png"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[XEN 合约代码深入解读]]></title>
            <link>https://paragraph.com/@xyyme/xen</link>
            <guid>5r3o2MTDxpsY1bwojW20</guid>
            <pubDate>Mon, 10 Oct 2022 13:17:09 GMT</pubDate>
            <description><![CDATA[这两天 XEN 特别火，看了看代码，相对比较简单。这篇文章就来结合文档来解读一下合约代码，仅做学习交流用。对于玩法还不熟悉的朋友可以先看看我昨天发的推文。 整个玩法分成两部分，我这里将其区别为：时间挖矿（claim to mint），也就是在参与时指定时间，时间到期后即可领取对应的 XEN，唯一付出的成本就是 gas 费用和等待的时间stake 挖矿，通过质押 XEN 来挖矿时间挖矿先来看第一部分，时间挖矿。用户通过调用 claimRank(uint256 term) 来参与，term 代表用户想要挖矿的天数，在这个时间到期之后才能领取 XEN 奖励。function claimRank(uint256 term) external { // SECONDS_IN_DAY = 3_600 * 24; // 将天数转化为秒数 uint256 termSec = term * SECONDS_IN_DAY; // 最短挖一天 // MIN_TERM = 1 * SECONDS_IN_DAY - 1; require(termSec > MIN_TERM, "CRank: Term l...]]></description>
            <content:encoded><![CDATA[<p>这两天 XEN 特别火，看了看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x06450dee7fd2fb8e39061434babcfc05599a6fb8#code">代码</a>，相对比较简单。这篇文章就来结合文档来解读一下合约代码，仅做学习交流用。对于玩法还不熟悉的朋友可以先看看我昨天发的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/xyymeeth/status/1579107974722637824?s=20&amp;t=WiWjzyzI9usqCeteCWQmHA">推文</a>。</p><p>整个玩法分成两部分，我这里将其区别为：</p><ol><li><p>时间挖矿（claim to mint），也就是在参与时指定时间，时间到期后即可领取对应的 XEN，唯一付出的成本就是 gas 费用和等待的时间</p></li><li><p>stake 挖矿，通过质押 XEN 来挖矿</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">时间挖矿</h2><p>先来看第一部分，<code>时间挖矿</code>。用户通过调用 <code>claimRank(uint256 term)</code> 来参与，<code>term</code> 代表用户想要挖矿的天数，在这个时间到期之后才能领取 XEN 奖励。</p><pre data-type="codeBlock" text="function claimRank(uint256 term) external {
    // SECONDS_IN_DAY = 3_600 * 24;
    // 将天数转化为秒数
    uint256 termSec = term * SECONDS_IN_DAY;
    // 最短挖一天
    // MIN_TERM = 1 * SECONDS_IN_DAY - 1;
    require(termSec &gt; MIN_TERM, &quot;CRank: Term less than min&quot;);
    // 最长可参与的天数需要根据参与人数实时计算
    require(termSec &lt; _calculateMaxTerm() + 1, &quot;CRank: Term more than current max term&quot;);
    require(userMints[_msgSender()].rank == 0, &quot;CRank: Mint already in progress&quot;);

    // create and store new MintInfo
    MintInfo memory mintInfo = MintInfo({
        user: _msgSender(),
        term: term,
        maturityTs: block.timestamp + termSec,
        rank: globalRank,
        amplifier: _calculateRewardAmplifier(),
        eaaRate: _calculateEAARate()
    });
    userMints[_msgSender()] = mintInfo;
    activeMinters++;
    emit RankClaimed(_msgSender(), term, globalRank++);
}
"><code><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-comment">// SECONDS_IN_DAY = 3_600 * 24;</span>
    <span class="hljs-comment">// 将天数转化为秒数</span>
    <span class="hljs-keyword">uint256</span> termSec <span class="hljs-operator">=</span> term <span class="hljs-operator">*</span> SECONDS_IN_DAY;
    <span class="hljs-comment">// 最短挖一天</span>
    <span class="hljs-comment">// MIN_TERM = 1 * SECONDS_IN_DAY - 1;</span>
    <span class="hljs-built_in">require</span>(termSec <span class="hljs-operator">></span> MIN_TERM, <span class="hljs-string">"CRank: Term less than min"</span>);
    <span class="hljs-comment">// 最长可参与的天数需要根据参与人数实时计算</span>
    <span class="hljs-built_in">require</span>(termSec <span class="hljs-operator">&#x3C;</span> _calculateMaxTerm() <span class="hljs-operator">+</span> <span class="hljs-number">1</span>, <span class="hljs-string">"CRank: Term more than current max term"</span>);
    <span class="hljs-built_in">require</span>(userMints[_msgSender()].rank <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">"CRank: Mint already in progress"</span>);

    <span class="hljs-comment">// create and store new MintInfo</span>
    MintInfo <span class="hljs-keyword">memory</span> mintInfo <span class="hljs-operator">=</span> MintInfo({
        user: _msgSender(),
        term: term,
        maturityTs: <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">+</span> termSec,
        rank: globalRank,
        amplifier: _calculateRewardAmplifier(),
        eaaRate: _calculateEAARate()
    });
    userMints[_msgSender()] <span class="hljs-operator">=</span> mintInfo;
    activeMinters<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;
    <span class="hljs-keyword">emit</span> RankClaimed(_msgSender(), term, globalRank<span class="hljs-operator">+</span><span class="hljs-operator">+</span>);
}
</code></pre><p>其中全局变量 <code>globalRank</code> 代表的是全局参与的总人数，只增不减。<code>activeMinters</code> 代表正在参与挖矿的人数，当用户参与<code>时间挖矿</code>时增加 <code>1</code>，到期领取奖励后减少 <code>1</code>。<code>userMints</code> 代表用户的挖矿参数。我们看到，这里最短需要参与一天，最多参与的天数是通过 <code>_calculateMaxTerm()</code> 实时计算出来的。</p><pre data-type="codeBlock" text="function _calculateMaxTerm() private view returns (uint256) {
    // TERM_AMPLIFIER_THRESHOLD = 5_000;
    // TERM_AMPLIFIER = 15;
    // MAX_TERM_START = 100 * SECONDS_IN_DAY;
    // MAX_TERM_END = 1_000 * SECONDS_IN_DAY;
    if (globalRank &gt; TERM_AMPLIFIER_THRESHOLD) {
        // 如果参与的人数大于 5000
        // globalRank.fromUInt()，先将 globalRank 转换为 int128 类型
        // 然后再对其进行对数 log_2() 计算
        // 即 log_2_globalRank * 15
        uint256 delta = globalRank.fromUInt().log_2().mul(TERM_AMPLIFIER.fromUInt()).toUInt();
        // newMax = 100 天 + delta 天
        uint256 newMax = MAX_TERM_START + delta * SECONDS_IN_DAY;
        // 取 1000 天 和 newMax 的最小值，也就是最多只能挖 1000 天
        return Math.min(newMax, MAX_TERM_END);
    }
    // 如果参与的人数没有超过 5000，则最大只能挖 100 天
    return MAX_TERM_START;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_calculateMaxTerm</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">private</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">// TERM_AMPLIFIER_THRESHOLD = 5_000;</span>
    <span class="hljs-comment">// TERM_AMPLIFIER = 15;</span>
    <span class="hljs-comment">// MAX_TERM_START = 100 * SECONDS_IN_DAY;</span>
    <span class="hljs-comment">// MAX_TERM_END = 1_000 * SECONDS_IN_DAY;</span>
    <span class="hljs-keyword">if</span> (globalRank <span class="hljs-operator">></span> TERM_AMPLIFIER_THRESHOLD) {
        <span class="hljs-comment">// 如果参与的人数大于 5000</span>
        <span class="hljs-comment">// globalRank.fromUInt()，先将 globalRank 转换为 int128 类型</span>
        <span class="hljs-comment">// 然后再对其进行对数 log_2() 计算</span>
        <span class="hljs-comment">// 即 log_2_globalRank * 15</span>
        <span class="hljs-keyword">uint256</span> delta <span class="hljs-operator">=</span> globalRank.fromUInt().log_2().mul(TERM_AMPLIFIER.fromUInt()).toUInt();
        <span class="hljs-comment">// newMax = 100 天 + delta 天</span>
        <span class="hljs-keyword">uint256</span> newMax <span class="hljs-operator">=</span> MAX_TERM_START <span class="hljs-operator">+</span> delta <span class="hljs-operator">*</span> SECONDS_IN_DAY;
        <span class="hljs-comment">// 取 1000 天 和 newMax 的最小值，也就是最多只能挖 1000 天</span>
        <span class="hljs-keyword">return</span> Math.<span class="hljs-built_in">min</span>(newMax, MAX_TERM_END);
    }
    <span class="hljs-comment">// 如果参与的人数没有超过 5000，则最大只能挖 100 天</span>
    <span class="hljs-keyword">return</span> MAX_TERM_START;
}
</code></pre><p>首先如果全部参与人数没有超过 5000，那么最多只能挖 100 天。如果达到了 5000，通过对参与人数进行对数运算，计算出对应的最大天数。对应于文档中的：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/baf34cc06e65d51e4b731ce85190ccd80d05d2a3a612f2784bbc0cec28347c66.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>代码中的 <code>fromUInt()</code> 和 <code>log_2()</code> 都来自于 <code>ABDKMath64x64</code> 库（<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/abdk-consulting/abdk-libraries-solidity/blob/master/ABDKMath64x64.sol">代码</a>）。其中 <code>fromUInt()</code> 的代码：</p><pre data-type="codeBlock" text="function fromUInt (uint256 x) internal pure returns (int128) {
    unchecked {
        require (x &lt;= 0x7FFFFFFFFFFFFFFF);
        return int128 (int256 (x &lt;&lt; 64));
    }
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fromUInt</span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> x</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">int128</span></span>) </span>{
    <span class="hljs-keyword">unchecked</span> {
        <span class="hljs-built_in">require</span> (x <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> <span class="hljs-number">0x7FFFFFFFFFFFFFFF</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">int128</span> (<span class="hljs-keyword">int256</span> (x <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">&#x3C;</span> <span class="hljs-number">64</span>));
    }
}
</code></pre><p>入参 x 有限制，这个最大值转换为 10 进制是 <code>9223372036854775807</code>，全部参与人数不可能超过这个数，所有可以安全使用。</p><p>在构造的挖矿系数 <code>mintInfo</code> 中，<code>_calculateRewardAmplifier()</code> 和 <code>_calculateEAARate()</code> 也是实时计算的。</p><pre data-type="codeBlock" text="function _calculateRewardAmplifier() private view returns (uint256) {
    // genesisTs 是合约部署时间，也就是初始时间
    // 这里是计算出距离开始时间过去了多少天
    uint256 amplifierDecrease = (block.timestamp - genesisTs) / SECONDS_IN_DAY;
    // REWARD_AMPLIFIER_START = 3000
    // REWARD_AMPLIFIER_END = 1;
    if (amplifierDecrease &lt; REWARD_AMPLIFIER_START) {
        // 如果是在 3000 天以内
        return Math.max(REWARD_AMPLIFIER_START - amplifierDecrease, REWARD_AMPLIFIER_END);
    } else {
        // 如果超过 3000 天，则使用 1
        return REWARD_AMPLIFIER_END;
    }
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_calculateRewardAmplifier</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">private</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">// genesisTs 是合约部署时间，也就是初始时间</span>
    <span class="hljs-comment">// 这里是计算出距离开始时间过去了多少天</span>
    <span class="hljs-keyword">uint256</span> amplifierDecrease <span class="hljs-operator">=</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">-</span> genesisTs) <span class="hljs-operator">/</span> SECONDS_IN_DAY;
    <span class="hljs-comment">// REWARD_AMPLIFIER_START = 3000</span>
    <span class="hljs-comment">// REWARD_AMPLIFIER_END = 1;</span>
    <span class="hljs-keyword">if</span> (amplifierDecrease <span class="hljs-operator">&#x3C;</span> REWARD_AMPLIFIER_START) {
        <span class="hljs-comment">// 如果是在 3000 天以内</span>
        <span class="hljs-keyword">return</span> Math.<span class="hljs-built_in">max</span>(REWARD_AMPLIFIER_START <span class="hljs-operator">-</span> amplifierDecrease, REWARD_AMPLIFIER_END);
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// 如果超过 3000 天，则使用 1</span>
        <span class="hljs-keyword">return</span> REWARD_AMPLIFIER_END;
    }
}
</code></pre><p>可以看到，越早参与，可以获得到的 <code>AMP</code> 就越多，最开始一天是 <code>3000</code>，每过一天会减少 <code>1</code>，最终超过 3000 天就会恒定为 <code>1</code>。</p><p>对应于文档中 <code>AMP</code> 的计算方式：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ca4d491b3b2c027a9517212d4014b00b10e66a4a2d3af9524a322615bd0b4457.png" alt="AMP 计算公式" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">AMP 计算公式</figcaption></figure><pre data-type="codeBlock" text="function _calculateEAARate() private view returns (uint256) {
    // EAA_PM_STEP = 1
    // EAA_RANK_STEP = 100000
    uint256 decrease = (EAA_PM_STEP * globalRank) / EAA_RANK_STEP;
    // EAA_PM_START = 100
    // 也就是说，如果参与人数大于 1000 万，返回 0
    if (decrease &gt; EAA_PM_START) return 0;
    // 否则返回 100 - 人数 / 100000
    return EAA_PM_START - decrease;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_calculateEAARate</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">private</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">// EAA_PM_STEP = 1</span>
    <span class="hljs-comment">// EAA_RANK_STEP = 100000</span>
    <span class="hljs-keyword">uint256</span> decrease <span class="hljs-operator">=</span> (EAA_PM_STEP <span class="hljs-operator">*</span> globalRank) <span class="hljs-operator">/</span> EAA_RANK_STEP;
    <span class="hljs-comment">// EAA_PM_START = 100</span>
    <span class="hljs-comment">// 也就是说，如果参与人数大于 1000 万，返回 0</span>
    <span class="hljs-keyword">if</span> (decrease <span class="hljs-operator">></span> EAA_PM_START) <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    <span class="hljs-comment">// 否则返回 100 - 人数 / 100000</span>
    <span class="hljs-keyword">return</span> EAA_PM_START <span class="hljs-operator">-</span> decrease;
}
</code></pre><p>从 <code>100</code> 开始，每当有 <code>十万人</code> 参与时，下降 <code>1</code>，最终若达到 <code>一千万人</code> 参与，则恒定为 <code>0</code>。同样也是越早参与越好。对应于文档：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/38b1b6b1c95d7c76c01d6f04ce92659a32ec1d7cd88b76646e7c58e0a260c27c.png" alt="EAA 计算公式" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">EAA 计算公式</figcaption></figure><p>由于 Solidity 中没有小数，因此在代码中将其放大了 <code>1000</code> 倍，后面在 <code>getGrossReward</code> 方法中会再缩小 <code>1000</code> 倍。</p><p>到这里，我们可以看到，在用户参与<code>时间挖矿</code>时，已经确定的数据有</p><ol><li><p>用户在全局中的位置（<code>rank</code>）</p></li><li><p>参与时长（<code>term</code>），由用户在参与时指定</p></li><li><p><code>AMP</code>，越早参与越大</p></li><li><p><code>EAA</code>，越早参与越大</p></li></ol><p>接下来我们来看用户领取奖励时的方法 <code>claimMintReward()</code>：</p><pre data-type="codeBlock" text="function claimMintReward() external {
    // 获取用户的挖矿系数
    MintInfo memory mintInfo = userMints[_msgSender()];
    require(mintInfo.rank &gt; 0, &quot;CRank: No mint exists&quot;);
    // 要求满足时间限制
    require(block.timestamp &gt; mintInfo.maturityTs, &quot;CRank: Mint maturity not reached&quot;);

    // calculate reward and mint tokens
    uint256 rewardAmount = _calculateMintReward(
        mintInfo.rank,
        mintInfo.term,
        mintInfo.maturityTs,
        mintInfo.amplifier,
        mintInfo.eaaRate
    ) * 1 ether;
    _mint(_msgSender(), rewardAmount);

    // 清除掉用户的挖矿数据
    _cleanUpUserMint();
    emit MintClaimed(_msgSender(), rewardAmount);
}
"><code><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-comment">// 获取用户的挖矿系数</span>
    MintInfo <span class="hljs-keyword">memory</span> mintInfo <span class="hljs-operator">=</span> userMints[_msgSender()];
    <span class="hljs-built_in">require</span>(mintInfo.rank <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"CRank: No mint exists"</span>);
    <span class="hljs-comment">// 要求满足时间限制</span>
    <span class="hljs-built_in">require</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">></span> mintInfo.maturityTs, <span class="hljs-string">"CRank: Mint maturity not reached"</span>);

    <span class="hljs-comment">// calculate reward and mint tokens</span>
    <span class="hljs-keyword">uint256</span> rewardAmount <span class="hljs-operator">=</span> _calculateMintReward(
        mintInfo.rank,
        mintInfo.term,
        mintInfo.maturityTs,
        mintInfo.amplifier,
        mintInfo.eaaRate
    ) <span class="hljs-operator">*</span> <span class="hljs-number">1</span> <span class="hljs-literal">ether</span>;
    _mint(_msgSender(), rewardAmount);

    <span class="hljs-comment">// 清除掉用户的挖矿数据</span>
    _cleanUpUserMint();
    <span class="hljs-keyword">emit</span> MintClaimed(_msgSender(), rewardAmount);
}
</code></pre><p>校验限制后，计算可得奖励数量，然后 <code>_mint</code> 给用户，计算奖励数量的主要计算逻辑在 <code>_calculateMintReward()</code> 中：</p><pre data-type="codeBlock" text="function _calculateMintReward(
    uint256 cRank,
    uint256 term,
    uint256 maturityTs,
    uint256 amplifier,
    uint256 eeaRate
) private view returns (uint256) {
    // 计算当前时间与到期时间的差值
    uint256 secsLate = block.timestamp - maturityTs;
    // 根据时间差值计算需要扣除的数量
    uint256 penalty = _penalty(secsLate);
    // 计算用户后面还有多少人参与游戏
    uint256 rankDelta = Math.max(globalRank - cRank, 2);
    uint256 EAA = (1_000 + eeaRate);
    uint256 reward = getGrossReward(rankDelta, amplifier, term, EAA);
    return (reward * (100 - penalty)) / 100;
}

function getGrossReward(
    uint256 rankDelta,
    uint256 amplifier,
    uint256 term,
    uint256 eaa
) public pure returns (uint256) {
    // log_2_rankDelta
    int128 log128 = rankDelta.fromUInt().log_2();
    int128 reward128 = log128.mul(amplifier.fromUInt()).mul(term.fromUInt()).mul(eaa.fromUInt());
    return reward128.div(uint256(1_000).fromUInt()).toUInt();
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_calculateMintReward</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span> cRank,
    <span class="hljs-keyword">uint256</span> term,
    <span class="hljs-keyword">uint256</span> maturityTs,
    <span class="hljs-keyword">uint256</span> amplifier,
    <span class="hljs-keyword">uint256</span> eeaRate
</span>) <span class="hljs-title"><span class="hljs-keyword">private</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">// 计算当前时间与到期时间的差值</span>
    <span class="hljs-keyword">uint256</span> secsLate <span class="hljs-operator">=</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">-</span> maturityTs;
    <span class="hljs-comment">// 根据时间差值计算需要扣除的数量</span>
    <span class="hljs-keyword">uint256</span> penalty <span class="hljs-operator">=</span> _penalty(secsLate);
    <span class="hljs-comment">// 计算用户后面还有多少人参与游戏</span>
    <span class="hljs-keyword">uint256</span> rankDelta <span class="hljs-operator">=</span> Math.<span class="hljs-built_in">max</span>(globalRank <span class="hljs-operator">-</span> cRank, <span class="hljs-number">2</span>);
    <span class="hljs-keyword">uint256</span> EAA <span class="hljs-operator">=</span> (<span class="hljs-number">1_000</span> <span class="hljs-operator">+</span> eeaRate);
    <span class="hljs-keyword">uint256</span> reward <span class="hljs-operator">=</span> getGrossReward(rankDelta, amplifier, term, EAA);
    <span class="hljs-keyword">return</span> (reward <span class="hljs-operator">*</span> (<span class="hljs-number">100</span> <span class="hljs-operator">-</span> penalty)) <span class="hljs-operator">/</span> <span class="hljs-number">100</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getGrossReward</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span> rankDelta,
    <span class="hljs-keyword">uint256</span> amplifier,
    <span class="hljs-keyword">uint256</span> term,
    <span class="hljs-keyword">uint256</span> eaa
</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</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">// log_2_rankDelta</span>
    <span class="hljs-keyword">int128</span> log128 <span class="hljs-operator">=</span> rankDelta.fromUInt().log_2();
    <span class="hljs-keyword">int128</span> reward128 <span class="hljs-operator">=</span> log128.mul(amplifier.fromUInt()).mul(term.fromUInt()).mul(eaa.fromUInt());
    <span class="hljs-keyword">return</span> reward128.div(<span class="hljs-keyword">uint256</span>(<span class="hljs-number">1_000</span>).fromUInt()).toUInt();
}
</code></pre><p>这里我们先忽略 <code>penalty</code> 这一块，其他部分的计算正好对应于文档中的：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e61be9bfa5f9705cee04f5d71cfb4d9c2d16334f79a3cc0ab9b7497465c5870b.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><pre data-type="codeBlock" text="cRG - cRu
"><code>cRG <span class="hljs-operator">-</span> cRu
</code></pre><p>就会越大，同样说明越早参与越好。</p><p>我们再来看 <code>penalty</code> 这部分，这块其实就是系统限制用户必须在到期后一定时间内领取走，如果没有领取则会随着时间越来越少，最终归零。</p><pre data-type="codeBlock" text="function _penalty(uint256 secsLate) private pure returns (uint256) {
    // =MIN(2^(daysLate+3)/window-1,99)
    // 计算这个差值有多少天
    uint256 daysLate = secsLate / SECONDS_IN_DAY;
    // WITHDRAWAL_WINDOW_DAYS = 7
    // 如果这个差值在七天及以上，返回 99
    if (daysLate &gt; WITHDRAWAL_WINDOW_DAYS - 1) return MAX_PENALTY_PCT;
    // 也就是 2 ^ (daysLate + 3) / 7 - 1
    uint256 penalty = (uint256(1) &lt;&lt; (daysLate + 3)) / WITHDRAWAL_WINDOW_DAYS - 1;
    return Math.min(penalty, MAX_PENALTY_PCT);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_penalty</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> secsLate</span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</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">// =MIN(2^(daysLate+3)/window-1,99)</span>
    <span class="hljs-comment">// 计算这个差值有多少天</span>
    <span class="hljs-keyword">uint256</span> daysLate <span class="hljs-operator">=</span> secsLate <span class="hljs-operator">/</span> SECONDS_IN_DAY;
    <span class="hljs-comment">// WITHDRAWAL_WINDOW_DAYS = 7</span>
    <span class="hljs-comment">// 如果这个差值在七天及以上，返回 99</span>
    <span class="hljs-keyword">if</span> (daysLate <span class="hljs-operator">></span> WITHDRAWAL_WINDOW_DAYS <span class="hljs-operator">-</span> <span class="hljs-number">1</span>) <span class="hljs-keyword">return</span> MAX_PENALTY_PCT;
    <span class="hljs-comment">// 也就是 2 ^ (daysLate + 3) / 7 - 1</span>
    <span class="hljs-keyword">uint256</span> penalty <span class="hljs-operator">=</span> (<span class="hljs-keyword">uint256</span>(<span class="hljs-number">1</span>) <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">&#x3C;</span> (daysLate <span class="hljs-operator">+</span> <span class="hljs-number">3</span>)) <span class="hljs-operator">/</span> WITHDRAWAL_WINDOW_DAYS <span class="hljs-operator">-</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">return</span> Math.<span class="hljs-built_in">min</span>(penalty, MAX_PENALTY_PCT);
}
</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/aef938b3bfec6aaa72ae59ddfcac2a59c36abd5adbdfb06c9ad1a1ee7239dbc8.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>文档中显示超过七天就全部不能领取，但是代码中显示最多只会扣除 <code>99%</code>。</p><p>到这里，我们就介绍完了<code>时间挖矿</code>的代码部分，接下来我们来看看 stake 挖矿的部分。</p><h2 id="h-stake" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">stake 挖矿</h2><p>这里的 stake 其实比常见的挖矿计算逻辑要简单。常见的挖矿 <code>APY</code> 是根据用户质押数量占比以及参与时间来计算的，属于随挖随走类型的。而这里的 stake 挖矿的 <code>APY</code> 在参与时就已经固定了，且需要在参与时就指定参与时间，在时间到期后才能领取奖励，如果没有到期就领取，只能取回本金，没有任何的奖励。</p><p>用户可以在前面<code>时间挖矿</code>到期时调用 <code>claimMintRewardAndStake</code> 同时领取奖励并进行 stake，或者单独调用 <code>stake(uint256 amount, uint256 term)</code> 进行 stake 挖矿：</p><pre data-type="codeBlock" text="function stake(uint256 amount, uint256 term) external {
    require(balanceOf(_msgSender()) &gt;= amount, &quot;XEN: not enough balance&quot;);
    // XEN_MIN_STAKE = 0
    require(amount &gt; XEN_MIN_STAKE, &quot;XEN: Below min stake&quot;);
    // 最少 stake 1 天，最多 stake 1000 天
    require(term * SECONDS_IN_DAY &gt; MIN_TERM, &quot;XEN: Below min stake term&quot;);
    require(term * SECONDS_IN_DAY &lt; MAX_TERM_END + 1, &quot;XEN: Above max stake term&quot;);
    require(userStakes[_msgSender()].amount == 0, &quot;XEN: stake exists&quot;);

    // burn staked XEN
    // 直接 burn，而不是从用户手里转账
    _burn(_msgSender(), amount);
    // create XEN Stake
    _createStake(amount, term);
    emit Staked(_msgSender(), amount, term);
}

function _createStake(uint256 amount, uint256 term) private {
    userStakes[_msgSender()] = StakeInfo({
        term: term,
        maturityTs: block.timestamp + term * SECONDS_IN_DAY,
        amount: amount,
        apy: _calculateAPY()
    });
    activeStakes++;
    totalXenStaked += amount;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">stake</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amount, <span class="hljs-keyword">uint256</span> term</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-built_in">require</span>(balanceOf(_msgSender()) <span class="hljs-operator">></span><span class="hljs-operator">=</span> amount, <span class="hljs-string">"XEN: not enough balance"</span>);
    <span class="hljs-comment">// XEN_MIN_STAKE = 0</span>
    <span class="hljs-built_in">require</span>(amount <span class="hljs-operator">></span> XEN_MIN_STAKE, <span class="hljs-string">"XEN: Below min stake"</span>);
    <span class="hljs-comment">// 最少 stake 1 天，最多 stake 1000 天</span>
    <span class="hljs-built_in">require</span>(term <span class="hljs-operator">*</span> SECONDS_IN_DAY <span class="hljs-operator">></span> MIN_TERM, <span class="hljs-string">"XEN: Below min stake term"</span>);
    <span class="hljs-built_in">require</span>(term <span class="hljs-operator">*</span> SECONDS_IN_DAY <span class="hljs-operator">&#x3C;</span> MAX_TERM_END <span class="hljs-operator">+</span> <span class="hljs-number">1</span>, <span class="hljs-string">"XEN: Above max stake term"</span>);
    <span class="hljs-built_in">require</span>(userStakes[_msgSender()].amount <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">"XEN: stake exists"</span>);

    <span class="hljs-comment">// burn staked XEN</span>
    <span class="hljs-comment">// 直接 burn，而不是从用户手里转账</span>
    _burn(_msgSender(), amount);
    <span class="hljs-comment">// create XEN Stake</span>
    _createStake(amount, term);
    <span class="hljs-keyword">emit</span> Staked(_msgSender(), amount, term);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_createStake</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amount, <span class="hljs-keyword">uint256</span> term</span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> </span>{
    userStakes[_msgSender()] <span class="hljs-operator">=</span> StakeInfo({
        term: term,
        maturityTs: <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">+</span> term <span class="hljs-operator">*</span> SECONDS_IN_DAY,
        amount: amount,
        apy: _calculateAPY()
    });
    activeStakes<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;
    totalXenStaked <span class="hljs-operator">+</span><span class="hljs-operator">=</span> amount;
}
</code></pre><p>整体的逻辑也比较简单，参与的时候需要指定时间 <code>term</code>。有一个小细节是在 <code>stake</code> 的时候直接 <code>burn</code> 掉了用户的 token，而不是通过转账的方法，这样可以少一步授权操作。由于合约本身既包含了挖矿操作，同时也是 ERC20，因此可以实现这个逻辑。</p><p>接下来我们看看计算 APY 的方法 <code>_calculateAPY()</code>：</p><pre data-type="codeBlock" text="function _calculateAPY() private view returns (uint256) {
    // 这里 genesisTs 是合约部署时间
    // (SECONDS_IN_DAY * XEN_APY_DAYS_STEP) -&gt; 86400 * 90，也就是 90 天
    // 也就是计算，当前时间距离最开始的合约部署时间有多少个 90 天的差距
    uint256 decrease = (block.timestamp - genesisTs) / (SECONDS_IN_DAY * XEN_APY_DAYS_STEP);
    // XEN_APY_START = 20
    // XEN_APY_END = 2
    // 如果 decrease 差值大于 (20 - 2)，也就是上面的时间差值大于 18 * 90 = 1620 天
    // 则返回 2
    if (XEN_APY_START - XEN_APY_END &lt; decrease) return XEN_APY_END;
    // 否则返回 20 - decrease
    return XEN_APY_START - decrease;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_calculateAPY</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">private</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">// 这里 genesisTs 是合约部署时间</span>
    <span class="hljs-comment">// (SECONDS_IN_DAY * XEN_APY_DAYS_STEP) -> 86400 * 90，也就是 90 天</span>
    <span class="hljs-comment">// 也就是计算，当前时间距离最开始的合约部署时间有多少个 90 天的差距</span>
    <span class="hljs-keyword">uint256</span> decrease <span class="hljs-operator">=</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">-</span> genesisTs) <span class="hljs-operator">/</span> (SECONDS_IN_DAY <span class="hljs-operator">*</span> XEN_APY_DAYS_STEP);
    <span class="hljs-comment">// XEN_APY_START = 20</span>
    <span class="hljs-comment">// XEN_APY_END = 2</span>
    <span class="hljs-comment">// 如果 decrease 差值大于 (20 - 2)，也就是上面的时间差值大于 18 * 90 = 1620 天</span>
    <span class="hljs-comment">// 则返回 2</span>
    <span class="hljs-keyword">if</span> (XEN_APY_START <span class="hljs-operator">-</span> XEN_APY_END <span class="hljs-operator">&#x3C;</span> decrease) <span class="hljs-keyword">return</span> XEN_APY_END;
    <span class="hljs-comment">// 否则返回 20 - decrease</span>
    <span class="hljs-keyword">return</span> XEN_APY_START <span class="hljs-operator">-</span> decrease;
}
</code></pre><p>基本逻辑也是类似于上面计算 <code>EAA</code> 的方法，一次函数递减，参与的时间越早，相对应的 <code>APY</code> 就越大。初始值为 <code>20</code>，每过 <code>90</code> 天，减少 <code>1</code>。最终在 <code>1620</code> 天后，恒定为 <code>2</code>。对应于文档：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6952f2f8aa48c95a2bb7a709e17d80fb241e31bc7342b076f87ec12dcd4b084d.png" alt="APY 时间关系" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">APY 时间关系</figcaption></figure><p>最终在 <code>stake</code> 到期后，可以调用 <code>withdraw()</code> 取出本金和奖励：</p><pre data-type="codeBlock" text="function withdraw() external {
    StakeInfo memory userStake = userStakes[_msgSender()];
    require(userStake.amount &gt; 0, &quot;XEN: no stake exists&quot;);

    uint256 xenReward = _calculateStakeReward(
        userStake.amount,
        userStake.term,
        userStake.maturityTs,
        userStake.apy
    );
    activeStakes--;
    totalXenStaked -= userStake.amount;

    // mint staked XEN (+ reward)
    _mint(_msgSender(), userStake.amount + xenReward);
    emit Withdrawn(_msgSender(), userStake.amount, xenReward);
    delete userStakes[_msgSender()];
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    StakeInfo <span class="hljs-keyword">memory</span> userStake <span class="hljs-operator">=</span> userStakes[_msgSender()];
    <span class="hljs-built_in">require</span>(userStake.amount <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"XEN: no stake exists"</span>);

    <span class="hljs-keyword">uint256</span> xenReward <span class="hljs-operator">=</span> _calculateStakeReward(
        userStake.amount,
        userStake.term,
        userStake.maturityTs,
        userStake.apy
    );
    activeStakes<span class="hljs-operator">-</span><span class="hljs-operator">-</span>;
    totalXenStaked <span class="hljs-operator">-</span><span class="hljs-operator">=</span> userStake.amount;

    <span class="hljs-comment">// mint staked XEN (+ reward)</span>
    _mint(_msgSender(), userStake.amount <span class="hljs-operator">+</span> xenReward);
    <span class="hljs-keyword">emit</span> Withdrawn(_msgSender(), userStake.amount, xenReward);
    <span class="hljs-keyword">delete</span> userStakes[_msgSender()];
}
</code></pre><p>其中计算奖励数量的方法 <code>_calculateStakeReward()</code>：</p><pre data-type="codeBlock" text="function _calculateStakeReward(
    uint256 amount,
    uint256 term,
    uint256 maturityTs,
    uint256 apy
) private view returns (uint256) {
    if (block.timestamp &gt; maturityTs) {
        // DAYS_IN_YEAR = 365
        uint256 rate = (apy * term * 1_000_000) / DAYS_IN_YEAR;
        return (amount * rate) / 100_000_000;
    }
    return 0;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_calculateStakeReward</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span> amount,
    <span class="hljs-keyword">uint256</span> term,
    <span class="hljs-keyword">uint256</span> maturityTs,
    <span class="hljs-keyword">uint256</span> apy
</span>) <span class="hljs-title"><span class="hljs-keyword">private</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-keyword">if</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">></span> maturityTs) {
        <span class="hljs-comment">// DAYS_IN_YEAR = 365</span>
        <span class="hljs-keyword">uint256</span> rate <span class="hljs-operator">=</span> (apy <span class="hljs-operator">*</span> term <span class="hljs-operator">*</span> <span class="hljs-number">1_000_000</span>) <span class="hljs-operator">/</span> DAYS_IN_YEAR;
        <span class="hljs-keyword">return</span> (amount <span class="hljs-operator">*</span> rate) <span class="hljs-operator">/</span> <span class="hljs-number">100_000_000</span>;
    }
    <span class="hljs-keyword">return</span> <span class="hljs-number">0</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/112abfc072cd5fdc5c78b96ba759ec5bd05193cb48107be554cb2088e2004dc7.png" alt="stake 奖励计算公式" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">stake 奖励计算公式</figcaption></figure><p>对于 stake 挖矿而言，没有领取的限制，奖励数量不会变化。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>到这里我们就看完了主要的逻辑代码。这个玩法有意思的地方在于越早参与获得的奖励越多，相当于普通的挖头矿，但是同时也取决于总体的参与人数，如果后面没有人参与，那么也没啥意义。必须是参与的早且后面还有更多人参与的情况下，奖励才会更多。目前时刻总参与人数已经快达到 50 万了，热度确实很高。</p><p>同时，前面的<code>时间挖矿</code>和后面的 stake 挖矿也存在博弈关系，如果前面选择的时间越长，获得的奖励就越多，但是来到后面的 stake 挖矿的 APY 就会降低，需要大家自行抉择。</p><p>合约本身代码没啥难度，但是整体机制比较有趣，值得花点时间了解。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[流动性挖矿-合约原理详解]]></title>
            <link>https://paragraph.com/@xyyme/PBEbOKmAq1Jgs9MNn5Xk</link>
            <guid>PBEbOKmAq1Jgs9MNn5Xk</guid>
            <pubDate>Tue, 30 Aug 2022 15:47:56 GMT</pubDate>
            <description><![CDATA[流动性挖矿应该是上个牛市最火热的内容，基本上整个 DeFi 都是在围绕着流动性挖矿展开的，今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。流动性挖矿简介首先我们先从用户的角度来理解一下流动性挖矿是什么，实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如，SushiSwap 最初推出的 DEX 流动性挖矿，用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢？我们今天就来研究一下这部分内容。 先来看几个例子： 一：假设有一个流动性挖矿的合约，可以质押 A token 赚取 B token。它在 0 秒时开始活动，每秒奖励 R 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token，并且之后没有其他人参与，在第 8 秒时取出 token，图示：那么他在此时获得的收益就是：5R = (2 / 2) * (8 - 3) * R 其中，第一个 2 是用户 A 质押的数量，第二个 2 是合约中质押的总量，(8-3)是用户 ...]]></description>
            <content:encoded><![CDATA[<p>流动性挖矿应该是上个牛市最火热的内容，基本上整个 DeFi 都是在围绕着流动性挖矿展开的，今天我们就来看看它到底是什么以及合约代码层面是怎么实现的。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">流动性挖矿简介</h2><p>首先我们先从用户的角度来理解一下流动性挖矿是什么，实际上就是用户通过在合约中质押一个 token 从而赚取另一个 token 的过程。例如，SushiSwap 最初推出的 DEX 流动性挖矿，用户可以通过将 SushiSwap 的 LP token 质押到合约中赚取 Sushi token。那么这个奖励具体是怎么发放以及如何实现的呢？我们今天就来研究一下这部分内容。</p><p>先来看几个例子：</p><p>一：假设有一个流动性挖矿的合约，可以质押 A token 赚取 B token。它在 0 秒时开始活动，每秒奖励 <code>R</code> 个 B token。此时有用户 Alice 在第 3 秒时质押了 2 个 A token，并且之后没有其他人参与，在第 8 秒时取出 token，图示：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cd660641f2845aa22f25eb789fdbd2ff6b6e1ad63cab4f91d51819e5e5fabcef.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>那么他在此时获得的收益就是：</p><pre data-type="codeBlock" text="5R = (2 / 2) * (8 - 3) * R
"><code>5R <span class="hljs-operator">=</span> (<span class="hljs-number">2</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span>) <span class="hljs-operator">*</span> (<span class="hljs-number">8</span> <span class="hljs-operator">-</span> <span class="hljs-number">3</span>) <span class="hljs-operator">*</span> R
</code></pre><p>其中，第一个 <code>2</code> 是用户 A 质押的数量，第二个 <code>2</code> 是合约中质押的总量，<code>(8-3)</code>是用户 Alice 质押的时间，<code>R</code> 是每秒发放的奖励。</p><p>二：同样是上面的合约，用户 Alice 在第 3 秒时质押了 2 个 A token，在第 6 秒时，用户 Bob 也质押了 2 个 A token。Alice 在第 8 秒时离场，Bob 在第 10 秒时离场。图示：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/62bf0ec3b84f670d47b3cc3427672c7b1ecd56efd7ab32cd0b09598890790638.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>那么此时 Alice 可以获得的奖励是：</p><pre data-type="codeBlock" text="4R = (2 / 2) * (6 - 3) * R + (2 / 4) * (8 - 6) * R
"><code>4R <span class="hljs-operator">=</span> (<span class="hljs-number">2</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span>) <span class="hljs-operator">*</span> (<span class="hljs-number">6</span> <span class="hljs-operator">-</span> <span class="hljs-number">3</span>) <span class="hljs-operator">*</span> R <span class="hljs-operator">+</span> (<span class="hljs-number">2</span> <span class="hljs-operator">/</span> <span class="hljs-number">4</span>) <span class="hljs-operator">*</span> (<span class="hljs-number">8</span> <span class="hljs-operator">-</span> <span class="hljs-number">6</span>) <span class="hljs-operator">*</span> R
</code></pre><p>Bob 可以获得的奖励是：</p><pre data-type="codeBlock" text="3R = (2 / 4) * (8 - 6) * R + (2 / 2) * (10 - 8) * R
"><code>3R <span class="hljs-operator">=</span> (<span class="hljs-number">2</span> <span class="hljs-operator">/</span> <span class="hljs-number">4</span>) <span class="hljs-operator">*</span> (<span class="hljs-number">8</span> <span class="hljs-operator">-</span> <span class="hljs-number">6</span>) <span class="hljs-operator">*</span> R <span class="hljs-operator">+</span> (<span class="hljs-number">2</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span>) <span class="hljs-operator">*</span> (<span class="hljs-number">10</span> <span class="hljs-operator">-</span> <span class="hljs-number">8</span>) <span class="hljs-operator">*</span> R
</code></pre><p>对于 Alice，3～6 秒独占所有奖励，6～8 由于有 Bob 参与，因此需要计算自己在整个池子中的占比，再去计算奖励总额。</p><p>Bob 同理，6～8 秒与 Alice 分享，8～10 秒独享奖励。</p><p>同时，两个人的奖励总和是 <code>7R</code>，是这 7 秒时间的奖励发放总量。</p><p>我们思考一下这部分计算逻辑在代码中如何实现。首先，用户自己的本金数量在一段时间内（下次增加，减少质押数量之前）是不会变化的，但是由于有其它用户的参与，池子中质押的总数量是一直在变化的，而我们计算最终奖励的时候是需要用到池子总数量的，因此需要在代码中一直维护这个变量。</p><p>在上面的例子中，我们可以看到，3～6 秒中总量是 <code>2</code>，6～8 秒中总量是 <code>4</code>。这个简单的例子中，由于总量变化了一次，因此可以分解为两部分，但是如果说在 Alice 质押的这段时间，有一万个用户参与。那么总量变化次数将难以估计，这种情况下，如果我们要计算 Alice 可以获得的奖励，就需要知道每一个小区间的质押总量，然后再计算 Alice 的质押占比，再乘上各个小区间的时间长度，最后加起来，便是 Alice 的可以获得的奖励数量。这个计算量在普通的后端计算中也许可以实现，但是在合约中是不可能的，仅仅在 gas 消耗这方面就否定了这个方案。</p><blockquote><p>注：这里的区间是指时间轴上每两个最近的时间点组成的时间段</p></blockquote><p>那么有没有办法在合约中计算出奖励数量呢？答案是可以的。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">优化数学原理</h2><p>我们换一个思路，对于用户来说，他把 token 质押进来这段时间内（下次增加，减少质押数量之前），他所占的份额可能会发生变化，但是数量是没有变的。如果我们知道了每一单位质押 token 在每个区间内可以获得的奖励数量，那么把这些区间内的所有单位奖励都加起来，最后再乘上用户质押的数量，就是最终的奖励，即：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/20118a72441bf4add84160e9582d2ea33f750f65fec79ea4cb5877691bc9d194.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>其中，<code>k</code> 是用户质押的数量，<code>At</code> 是每一单位在整个池子中的占比，<code>Tt</code> 是每个区间的时间长度，<code>R * At * Tt</code> 是每个区间每单位质押 token 可以获得的奖励。我们以区间将时间轴进行划分，假设用户在第 <code>a</code> 个区间存入，在第 <code>b</code> 个区间后取出，因此上面公式的跨度是从 <code>a</code> 到 <code>b</code>。</p><p>这个公式对于我们在合约中实现似乎没有什么帮助，因为需要计算该用户在每个区间内的质押占比，仍然是一笔不小的工作量。不过我们可以将上面公式稍微转化一下：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/05131d8f72929d8054f42463fe2da4f14f330c067bc2e1a370cc839676196d3e.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>可以消去常量，即：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6cdb37f9c424a62591eb19a7148146327f04d3fd29c28ab05572c2e69a9ce64b.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>对于 <code>0~b</code> 与 <code>0~(a-1)</code>这部分，由于它们是从 <code>0</code> 区间开始累加的，因此是一个不断递增的变量。我们思考一下，对于用户在任何时刻的操作，此时的时间轴都是由 N 个区间构成的，且当前时刻是最后一个区间的右端点（因为区间就是这么定义的）。对于任何用户的操作，我们可以记录下以当前时刻点为右端点的区间的 <code>At</code>，并累加。这部分是不难计算的，因为 <code>At</code> 是每一单位在整个池子中的占比，所以容易算出：</p><blockquote><p>At = 1 / Lt</p></blockquote><p>其中 <code>Lt</code> 是当前区间质押总量，这个是已知的。<code>Tt</code> 是当前区间的时间长度，也是已知的。</p><p>对于 Alice 单一用户而言，在他对池子有操作的时候（增加，减少质押数量），在用户个人维度上记录一下当前的单位数量奖励累加值，再在用户下一次操作的时候，用最新的累加值减去上一次用户个人维度记录的累加值，就是这段时间内用户个人单位数量可以获得的单位奖励，再乘上之前的 <code>kR</code>，就可以算出这段时间内用户获得奖励数量。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">实践</h2><p>我们再来看看前面的例子验证一下：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/25d39a11fa4709537873742f87cf43e747954033a26366c65dca8cffd2a3f209.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>第 3 秒时，Alice 入场，此刻左边的区间，总质押量为 <code>0</code>，因此单位数量获得的单位奖励为</p><pre data-type="codeBlock" text="s = 0
"><code><span class="hljs-attr">s</span> = <span class="hljs-number">0</span>
</code></pre><p>同时将 Alice 的单位累加值记为 <code>0</code></p><p>第 6 秒时，Bob 入场，此刻左边的区间，总质押量为 <code>2</code>，因此单位数量获得的奖励为：</p><pre data-type="codeBlock" text="s = s + 1 / 2 * (6 - 3) = 1.5
"><code>s <span class="hljs-operator">=</span> s <span class="hljs-operator">+</span> <span class="hljs-number">1</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> (<span class="hljs-number">6</span> <span class="hljs-operator">-</span> <span class="hljs-number">3</span>) <span class="hljs-operator">=</span> <span class="hljs-number">1.5</span>
</code></pre><p>同时将 Bob 的单位累加值记为 <code>1.5</code></p><p>第 9 秒时，Alice 离场，此刻之前的区间，总质押量为 <code>4</code>，因此单位数量获得的奖励为：</p><pre data-type="codeBlock" text="s = s + 1 / 4 * (8 - 6) = 2
"><code>s <span class="hljs-operator">=</span> s <span class="hljs-operator">+</span> <span class="hljs-number">1</span> <span class="hljs-operator">/</span> <span class="hljs-number">4</span> <span class="hljs-operator">*</span> (<span class="hljs-number">8</span> <span class="hljs-operator">-</span> <span class="hljs-number">6</span>) <span class="hljs-operator">=</span> <span class="hljs-number">2</span>
</code></pre><p>同时将 Alice 的单位累加值记为 <code>2</code>，此时 Alice 可以获得的奖励为：</p><pre data-type="codeBlock" text="4R = (2 - 0) * 2 * R
"><code>4R <span class="hljs-operator">=</span> (<span class="hljs-number">2</span> <span class="hljs-operator">-</span> <span class="hljs-number">0</span>) <span class="hljs-operator">*</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> R
</code></pre><p>其中，第一个 <code>2</code> 是最新累加值，<code>0</code> 是 Alice 的上次累加值，第二个<code>2</code>是 Alice 的质押数量。</p><p>第 10 秒时，Bob 离场，此刻之前的区间，总质押量为 <code>2</code>，因此单位数量获得的奖励为：</p><pre data-type="codeBlock" text="s = s + 1 / 2 * (10 - 8) = 3
"><code>s <span class="hljs-operator">=</span> s <span class="hljs-operator">+</span> <span class="hljs-number">1</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> (<span class="hljs-number">10</span> <span class="hljs-operator">-</span> <span class="hljs-number">8</span>) <span class="hljs-operator">=</span> <span class="hljs-number">3</span>
</code></pre><p>同时将 Bob 的单位累加值记为 <code>3</code>，此时 Bob 可以获得的奖励为：</p><pre data-type="codeBlock" text="3R = (3 - 1.5) * 2 * R
"><code>3R <span class="hljs-operator">=</span> (<span class="hljs-number">3</span> <span class="hljs-operator">-</span> <span class="hljs-number">1.5</span>) <span class="hljs-operator">*</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> R
</code></pre><p>与我们上面最原始的方法得出的答案相同，验证成功。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代码实现</h2><p>接下来我们看看代码怎么写。</p><p>首先定义几个变量：</p><pre data-type="codeBlock" text="// 质押奖励的发放速率
uint256 public rewardRate = 0;

// 每次有用户操作时，更新为当前时间
uint256 public lastUpdateTime;

// 我们前面说到的每单位数量获得奖励的累加值，这里是乘上奖励发放速率后的值
uint256 public rewardPerTokenStored;

// 在单个用户维度上，为每个用户记录每次操作的累加值，同样也是乘上奖励发放速率后的值
mapping(address =&gt; uint256) public userRewardPerTokenPaid;

// 用户到当前时刻可领取的奖励数量
mapping(address =&gt; uint256) public rewards;

// 池子中质押总量
uint256 private _totalSupply;

// 用户的余额
mapping(address =&gt; uint256) private _balances;
"><code><span class="hljs-comment">// 质押奖励的发放速率</span>
<span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> rewardRate <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

<span class="hljs-comment">// 每次有用户操作时，更新为当前时间</span>
<span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> lastUpdateTime;

<span class="hljs-comment">// 我们前面说到的每单位数量获得奖励的累加值，这里是乘上奖励发放速率后的值</span>
<span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> rewardPerTokenStored;

<span class="hljs-comment">// 在单个用户维度上，为每个用户记录每次操作的累加值，同样也是乘上奖励发放速率后的值</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">uint256</span>) <span class="hljs-keyword">public</span> userRewardPerTokenPaid;

<span class="hljs-comment">// 用户到当前时刻可领取的奖励数量</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">uint256</span>) <span class="hljs-keyword">public</span> rewards;

<span class="hljs-comment">// 池子中质押总量</span>
<span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> _totalSupply;

<span class="hljs-comment">// 用户的余额</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">uint256</span>) <span class="hljs-keyword">private</span> _balances;
</code></pre><p>接着按照前面讲解的数学原理实现代码：</p><pre data-type="codeBlock" text="// 计算当前时刻的累加值
function rewardPerToken() public view returns (uint256) {
    // 如果池子里的数量为0，说明上一个区间内没有必要发放奖励，因此累加值不变
    if (_totalSupply == 0) {
        return rewardPerTokenStored;
    }
    // 计算累加值，上一个累加值加上最近一个区间的单位数量可获得的奖励数量
    return
        rewardPerTokenStored.add(
            lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate)
                .mul(1e18).div(_totalSupply)
        );
}

// 获取当前有效时间，如果活动结束了，就用结束时间，否则就用当前时间
function lastTimeRewardApplicable() public view returns (uint256) {
    return block.timestamp &lt; periodFinish ? block.timestamp : periodFinish;
}

// 计算用户可以领取的奖励数量
// 质押数量 * （当前累加值 - 用户上次操作时的累加值）+ 上次更新的奖励数量
function earned(address account) public view returns (uint256) {
    return
        _balances[account].mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
            .div(1e18).add(rewards[account]);
}

modifier updateReward(address account) {
    // 更新累加值
    rewardPerTokenStored = rewardPerToken();
    // 更新最新有效时间戳
    lastUpdateTime = lastTimeRewardApplicable();
    if (account != address(0)) {
        // 更新奖励数量
        rewards[account] = earned(account);
        // 更新用户的累加值
        userRewardPerTokenPaid[account] = rewardPerTokenStored;
    }
    _;
}
"><code><span class="hljs-comment">// 计算当前时刻的累加值</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">rewardPerToken</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">uint256</span></span>) </span>{
    <span class="hljs-comment">// 如果池子里的数量为0，说明上一个区间内没有必要发放奖励，因此累加值不变</span>
    <span class="hljs-keyword">if</span> (_totalSupply <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">return</span> rewardPerTokenStored;
    }
    <span class="hljs-comment">// 计算累加值，上一个累加值加上最近一个区间的单位数量可获得的奖励数量</span>
    <span class="hljs-keyword">return</span>
        rewardPerTokenStored.add(
            lastTimeRewardApplicable().sub(lastUpdateTime).mul(rewardRate)
                .mul(<span class="hljs-number">1e18</span>).div(_totalSupply)
        );
}

<span class="hljs-comment">// 获取当前有效时间，如果活动结束了，就用结束时间，否则就用当前时间</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">lastTimeRewardApplicable</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">uint256</span></span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&#x3C;</span> periodFinish ? <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> : periodFinish;
}

<span class="hljs-comment">// 计算用户可以领取的奖励数量</span>
<span class="hljs-comment">// 质押数量 * （当前累加值 - 用户上次操作时的累加值）+ 上次更新的奖励数量</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">earned</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</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">uint256</span></span>) </span>{
    <span class="hljs-keyword">return</span>
        _balances[account].mul(rewardPerToken().sub(userRewardPerTokenPaid[account]))
            .div(<span class="hljs-number">1e18</span>).add(rewards[account]);
}

<span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">updateReward</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</span>) </span>{
    <span class="hljs-comment">// 更新累加值</span>
    rewardPerTokenStored <span class="hljs-operator">=</span> rewardPerToken();
    <span class="hljs-comment">// 更新最新有效时间戳</span>
    lastUpdateTime <span class="hljs-operator">=</span> lastTimeRewardApplicable();
    <span class="hljs-keyword">if</span> (account <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
        <span class="hljs-comment">// 更新奖励数量</span>
        rewards[account] <span class="hljs-operator">=</span> earned(account);
        <span class="hljs-comment">// 更新用户的累加值</span>
        userRewardPerTokenPaid[account] <span class="hljs-operator">=</span> rewardPerTokenStored;
    }
    <span class="hljs-keyword">_</span>;
}
</code></pre><p>上面的代码，实现了我们前面讲过的原理，同时将所有逻辑包装成了一个 <code>modifier</code>，这样与最基本的 <code>stake</code>，<code>withdraw</code> 逻辑抽离，使整个合约逻辑代码更清晰。</p><p>最后，实现 <code>stake</code>，<code>withdraw</code> 的逻辑，并用 <code>updateReward</code> 修饰：</p><pre data-type="codeBlock" text="function stake(uint256 amount) external nonReentrant notPaused updateReward(msg.sender) {
    require(amount &gt; 0, &quot;Cannot stake 0&quot;);
    _totalSupply = _totalSupply.add(amount);
    _balances[msg.sender] = _balances[msg.sender].add(amount);
    stakingToken.safeTransferFrom(msg.sender, address(this), amount);
    emit Staked(msg.sender, amount);
}

function withdraw(uint256 amount) public nonReentrant updateReward(msg.sender) {
    require(amount &gt; 0, &quot;Cannot withdraw 0&quot;);
    _totalSupply = _totalSupply.sub(amount);
    _balances[msg.sender] = _balances[msg.sender].sub(amount);
    stakingToken.safeTransfer(msg.sender, amount);
    emit Withdrawn(msg.sender, amount);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">stake</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">nonReentrant</span> <span class="hljs-title">notPaused</span> <span class="hljs-title">updateReward</span>(<span class="hljs-params"><span class="hljs-built_in">msg</span>.sender</span>) </span>{
    <span class="hljs-built_in">require</span>(amount <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"Cannot stake 0"</span>);
    _totalSupply <span class="hljs-operator">=</span> _totalSupply.add(amount);
    _balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> _balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>].add(amount);
    stakingToken.safeTransferFrom(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), amount);
    <span class="hljs-keyword">emit</span> Staked(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">nonReentrant</span> <span class="hljs-title">updateReward</span>(<span class="hljs-params"><span class="hljs-built_in">msg</span>.sender</span>) </span>{
    <span class="hljs-built_in">require</span>(amount <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"Cannot withdraw 0"</span>);
    _totalSupply <span class="hljs-operator">=</span> _totalSupply.sub(amount);
    _balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> _balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>].sub(amount);
    stakingToken.safeTransfer(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount);
    <span class="hljs-keyword">emit</span> Withdrawn(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount);
}
</code></pre><p>这段代码来自 <code>Synthetic</code> 的 <code>StakingRewards</code> 的合约（<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol">代码地址</a>），我们这里只截取了最核心的逻辑部分，建议大家在理解了上面代码之后去看看完整的代码。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>今天我们简单聊了聊流动性挖矿的原理，其实它本身是运用了一个很巧妙的数学原理来实现的。目前 DeFi 中比较流行的两个流动性挖矿合约 <code>StakingRewards</code> 和 <code>MasterChef</code> 都是运用了这个原理。建议大家好好理解一下这一块，看懂之后再看市面上的其他流动性挖矿就会发现基本上大同小异了。英语好的朋友建议也看看下面的视频，讲解得也很透彻。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="youtube" videoId="6ZO5aYg1GI8">
      <div class="youtube-player" data-id="6ZO5aYg1GI8" style="background-image: url('https://i.ytimg.com/vi/6ZO5aYg1GI8/hqdefault.jpg'); background-size: cover; background-position: center">
        <a href="https://www.youtube.com/watch?v=6ZO5aYg1GI8">
          <img src="{{DOMAIN}}/editor/youtube/play.png" class="play"/>
        </a>
      </div></div><div data-type="embedly" src="https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol" data="{&quot;provider_url&quot;:&quot;https://github.com&quot;,&quot;description&quot;:&quot;Synthetix Solidity smart contracts. Contribute to Synthetixio/synthetix development by creating an account on GitHub.&quot;,&quot;title&quot;:&quot;synthetix/contracts/StakingRewards.sol at develop · Synthetixio/synthetix&quot;,&quot;author_name&quot;:&quot;Synthetixio&quot;,&quot;thumbnail_width&quot;:2560,&quot;url&quot;:&quot;https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/3590fc60ddd281e9dd252f9b49bb9d036962034f95ff4050d16294e873d93e03.png&quot;,&quot;author_url&quot;:&quot;https://github.com/Synthetixio&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;GitHub&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:1280,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:2560,&quot;height&quot;:1280,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/3590fc60ddd281e9dd252f9b49bb9d036962034f95ff4050d16294e873d93e03.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/3590fc60ddd281e9dd252f9b49bb9d036962034f95ff4050d16294e873d93e03.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://github.com/Synthetixio/synthetix/blob/develop/contracts/StakingRewards.sol" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>synthetix/contracts/StakingRewards.sol at develop · Synthetixio/synthetix</h2><p>Synthetix Solidity smart contracts. Contribute to Synthetixio/synthetix development by creating an account on GitHub.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://github.com</span></div><img src="https://storage.googleapis.com/papyrus_images/3590fc60ddd281e9dd252f9b49bb9d036962034f95ff4050d16294e873d93e03.png"/></div></a></div></div><div data-type="embedly" src="https://etherscan.io/address/0xc2edad668740f1aa35e4d8f227fb8e17dca888cd" data="{&quot;provider_url&quot;:&quot;https://etherscan.io&quot;,&quot;description&quot;:&quot;Contract: Verified | Balance: $23,021,510.77 across 3 Chains | Transactions: 464,780 | As at Oct-24-2025 10:42:54 PM (UTC)&quot;,&quot;title&quot;:&quot;SushiSwap: MasterChef LP Staking Pool | Address: 0xc2edad66...7dca888cd | Etherscan&quot;,&quot;author_name&quot;:&quot;etherscan.io&quot;,&quot;url&quot;:&quot;https://etherscan.io/address/0xc2edad668740f1aa35e4d8f227fb8e17dca888cd&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/c3ceb1ec55f2d6c2b4eeb2df764e4bc8936caf048a981c893b5f220aa9d207a0.jpg&quot;,&quot;thumbnail_width&quot;:288,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Ethereum (ETH) Blockchain Explorer&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:288,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:288,&quot;height&quot;:288,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/c3ceb1ec55f2d6c2b4eeb2df764e4bc8936caf048a981c893b5f220aa9d207a0.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/c3ceb1ec55f2d6c2b4eeb2df764e4bc8936caf048a981c893b5f220aa9d207a0.jpg"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://etherscan.io/address/0xc2edad668740f1aa35e4d8f227fb8e17dca888cd" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>SushiSwap: MasterChef LP Staking Pool | Address: 0xc2edad66...7dca888cd | Etherscan</h2><p>Contract: Verified | Balance: $23,021,510.77 across 3 Chains | Transactions: 464,780 | As at Oct-24-2025 10:42:54 PM (UTC)</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://etherscan.io</span></div><img src="https://storage.googleapis.com/papyrus_images/c3ceb1ec55f2d6c2b4eeb2df764e4bc8936caf048a981c893b5f220aa9d207a0.jpg"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[EIP-1167 详解]]></title>
            <link>https://paragraph.com/@xyyme/eip-1167</link>
            <guid>MmAOMDxgN87c0uvx5YbR</guid>
            <pubDate>Fri, 26 Aug 2022 09:07:55 GMT</pubDate>
            <description><![CDATA[概述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 &#x3C; 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 =...]]></description>
            <content:encoded><![CDATA[<h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">概述</h2><p><code>EIP-1167</code>，又称 <code>Minimal Proxy Contract</code>，提供了一种低成本复制合约的方法。它有什么意义呢？我们先来看个例子：</p><pre data-type="codeBlock" text="function createPair(address tokenA, address tokenB) external returns (address pair) {
    require(tokenA != tokenB, &apos;UniswapV2: IDENTICAL_ADDRESSES&apos;);
    (address token0, address token1) = tokenA &lt; tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    require(token0 != address(0), &apos;UniswapV2: ZERO_ADDRESS&apos;);
    require(getPair[token0][token1] == address(0), &apos;UniswapV2: PAIR_EXISTS&apos;); // 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);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createPair</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> tokenA, <span class="hljs-keyword">address</span> tokenB</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">address</span> pair</span>) </span>{
    <span class="hljs-built_in">require</span>(tokenA <span class="hljs-operator">!</span><span class="hljs-operator">=</span> tokenB, <span class="hljs-string">'UniswapV2: IDENTICAL_ADDRESSES'</span>);
    (<span class="hljs-keyword">address</span> token0, <span class="hljs-keyword">address</span> token1) <span class="hljs-operator">=</span> tokenA <span class="hljs-operator">&#x3C;</span> tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
    <span class="hljs-built_in">require</span>(token0 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">'UniswapV2: ZERO_ADDRESS'</span>);
    <span class="hljs-built_in">require</span>(getPair[token0][token1] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">'UniswapV2: PAIR_EXISTS'</span>); <span class="hljs-comment">// single check is sufficient</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>(UniswapV2Pair).<span class="hljs-built_in">creationCode</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>(token0, token1));
    <span class="hljs-comment">// 通过原始 bytecode 创建合约</span>
    <span class="hljs-keyword">assembly</span> {
        pair <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>(bytecode, <span class="hljs-number">32</span>), <span class="hljs-built_in">mload</span>(bytecode), salt)
    }
    IUniswapV2Pair(pair).initialize(token0, token1);
    getPair[token0][token1] <span class="hljs-operator">=</span> pair;
    getPair[token1][token0] <span class="hljs-operator">=</span> pair; <span class="hljs-comment">// populate mapping in the reverse direction</span>
    allPairs.<span class="hljs-built_in">push</span>(pair);
    <span class="hljs-keyword">emit</span> PairCreated(token0, token1, pair, allPairs.<span class="hljs-built_in">length</span>);
}
</code></pre><p>在 Uniswap v2 的工厂合约中，需要创建交易对合约，这里的方法是直接使用交易对合约，即 <code>UniswapV2Pair</code> 合约的 <code>creationCode</code> 进行创建。由于是创建原始的合约内容，因此有一个缺点就是耗费的 gas 取决于 <code>Pair</code> 合约的大小，<code>Pair</code> 合约的内容越多，耗费的 gas 越多。</p><p>那么有没有什么改进方法呢？答案是有的，就是通过代理合约来转发调用。从创建原始合约变为创建代理合约，而代理合约只需要负责将调用转发给原始合约就行了，实际执行的逻辑仍然使用最原始的合约。接下来的内容需要大家了解内存布局和 <code>delegatecall</code> 相关的内容，不了解的朋友可以看看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec">这里</a>和<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/0gmFpVZVlHhwb2YlmaSY8Dyv5r3Z24sKIks38cyQRFk">这里</a>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">原理</h2><p>提到代理，可能大家想到的第一反应就是合约升级，但是我们今天说的代理并不涉及合约升级，它仅仅负责合约调用的转发。</p><p>我们先来看看可升级合约的代理合约架构：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1762f3357bd2853e25905e075ec5be92dfbf9e0ede84fe698229a32dd7a603f2.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>整个架构中存在一个代理合约和 N 个逻辑合约，只有一套数据，需要升级时则替换逻辑合约，同一时间只能存在一个逻辑合约。</p><p>再来看看今天提到的 <code>Minimal Proxy Contract</code> 架构：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b579b49fafe7b447b8328f1c0bbaf9f327ec3899e96ddb0f4701f70ea4010034.png" alt="Minimal Proxy Contract 架构" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Minimal Proxy Contract 架构</figcaption></figure><p>整个架构中存在 N 个代理合约和一个逻辑合约，有多套数据分别存储在不同的代理合约中，所有代理合约共享逻辑合约的执行逻辑，同一时间存在多个代理合约。<code>Minimal Proxy Contract</code> 的原理就是将代理合约作为逻辑合约的复制品，各个代理合约存储各自的数据，需要多少份复制品就创建多少个代理合约即可。而代理合约本身只负责请求转发，因此其内容很少，从而耗费更少的 gas。</p><p>我们来看一个例子（注意这里是为了简单起见直接使用了构造函数，实际应用中不应该使用构造函数，因为这部分不会在代理合约中运行，即不会被初始化。因此如果有初始化逻辑，需要放在 <code>initialize</code> 函数中额外调用）：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

contract Demo {
    uint256 public a;

    constructor() {
        a = 1000;
    }

    function setA(uint256 _a) external {
        a = _a;
    }
}
"><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.15;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> a;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        a <span class="hljs-operator">=</span> <span class="hljs-number">1000</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setA</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _a</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        a <span class="hljs-operator">=</span> _a;
    }
}
</code></pre><p>现在我们要对这个 <code>Demo</code> 合约进行复制，可以借助 OZ 的 <code>Clones</code> 合约来完成：</p><pre data-type="codeBlock" text="// 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), &quot;ERC1167: create failed&quot;);
    }
    ///....
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-comment">// OpenZeppelin Contracts v4.4.1 (proxy/Clones.sol)</span>

<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.0;</span>
<span class="hljs-class"><span class="hljs-keyword">library</span> <span class="hljs-title">Clones</span> </span>{
    <span class="hljs-comment">/**
     * @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.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clone</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> implementation</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">address</span> instance</span>) </span>{
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-keyword">let</span> ptr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)
            <span class="hljs-built_in">mstore</span>(ptr, <span class="hljs-number">0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000</span>)
            <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x14</span>), <span class="hljs-built_in">shl</span>(<span class="hljs-number">0x60</span>, implementation))
            <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(ptr, <span class="hljs-number">0x28</span>), <span class="hljs-number">0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000</span>)
            instance <span class="hljs-operator">:=</span> <span class="hljs-built_in">create</span>(<span class="hljs-number">0</span>, ptr, <span class="hljs-number">0x37</span>)
        }
        <span class="hljs-built_in">require</span>(instance <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">"ERC1167: create failed"</span>);
    }
    <span class="hljs-comment">///....</span>
}
</code></pre><p><code>clone</code> 方法需要接受一个原始逻辑合约的地址，然后返回新生成的克隆合约（也就是我们上面说的代理合约）地址。</p><p>我们来看看上面代码是什么意思，首先 <code>let ptr := mload(0x40)</code> 获取内存中空闲内存指针的位置，下一行的 <code>mstore(ptr, 0x3d602d80600a3d3981f33…)</code>，将该数据存入内存中，接下来的 <code>shl(0x60, implementation)</code> 将地址左移 <code>0x60</code>，即 <code>96</code> 位。由于 <code>address</code> 类型实际只占用 20 个字节，而传入的参数是通过 0 补齐到 32 字节的。假设传入的地址是</p><p><code>0xbebebebebebebebebebebebebebebebebebebebe</code>，则补齐的内容为：</p><p><code>0x000000000000000000000000bebebebebebebebebebebebebebebebebebebebe</code></p><p>左移 <code>96</code> 位之后恰好获得原始的地址数据。</p><p>接下来将地址数据 <code>mstore</code> 储存到内存中，内存位置为 <code>0x14</code>，即 <code>20</code>。这个数据恰好是上面一行 <code>0x3d602d80600a3d39</code> 中截断后面零值的长度。</p><p>第三个 <code>mstore</code> 再将 <code>0x5af43d82803e903d…</code> 拼接到前面的数据所在内存位置后面。三个 <code>mstore</code> 操作下来，此时内存中的数据为：</p><pre data-type="codeBlock" text="3d602d80600a3d3981f3363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3
"><code></code></pre><p>即三部分数据拼接到一起的结果。但是在合约地址前后拼接的这两部分是什么意思呢？</p><p>首先我们将前 <code>10</code> 个字节 <code>3d602d80600a3d3981f3</code> <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://ethervm.io/decompile?address=&amp;network=">反编译</a>一下得到：</p><pre data-type="codeBlock" text="contract Contract {
    function main() {
        var var0 = returndata.length;
        memory[returndata.length:returndata.length + 0x2d] = code[0x0a:0x37];
        return memory[var0:var0 + 0x2d];
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Contract</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">var</span> var0 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> <span class="hljs-number">0x2d</span>] <span class="hljs-operator">=</span> code[<span class="hljs-number">0x0a</span>:<span class="hljs-number">0x37</span>];
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">memory</span>[var0:var0 <span class="hljs-operator">+</span> <span class="hljs-number">0x2d</span>];
    }
}
</code></pre><p>即克隆合约的构造方法，内容是将整个克隆合约的字节码返回给 EVM。</p><p>再将后面的 <code>45</code> 字节数据 <code>363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3</code> 反编译：</p><pre data-type="codeBlock" text="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]); }
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Contract</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-keyword">var</span> temp0 <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> temp0] <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> temp0];
        <span class="hljs-keyword">var</span> temp1 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">var</span> temp2;
        temp2, <span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> returndata.<span class="hljs-built_in">length</span>] <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0xbebebebebebebebebebebebebebebebebebebebe</span>).<span class="hljs-built_in">delegatecall</span>.<span class="hljs-built_in">gas</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">gas</span>)(<span class="hljs-keyword">memory</span>[returndata.<span class="hljs-built_in">length</span>:returndata.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>.<span class="hljs-built_in">length</span>]);
        <span class="hljs-keyword">var</span> temp3 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
        <span class="hljs-keyword">memory</span>[temp1:temp1 <span class="hljs-operator">+</span> temp3] <span class="hljs-operator">=</span> returndata[temp1:temp1 <span class="hljs-operator">+</span> temp3];
        <span class="hljs-keyword">var</span> var1 <span class="hljs-operator">=</span> temp1;
        <span class="hljs-keyword">var</span> var0 <span class="hljs-operator">=</span> returndata.<span class="hljs-built_in">length</span>;
    
        <span class="hljs-keyword">if</span> (temp2) { <span class="hljs-keyword">return</span> <span class="hljs-keyword">memory</span>[var1:var1 <span class="hljs-operator">+</span> var0]; }
        <span class="hljs-keyword">else</span> { <span class="hljs-keyword">revert</span>(<span class="hljs-keyword">memory</span>[var1:var1 <span class="hljs-operator">+</span> var0]); }
    }
}
</code></pre><p>这部分内容是利用 <code>delegatecall</code> 将调用进行转发的逻辑。</p><p><code>clone</code> 方法的最后一行使用了 <code>create</code> 方法创建克隆合约，长度 <code>0x37</code> 即 <code>55</code> 是内存中克隆合约字节码的长度。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">实践</h2><p>现在我们来实际操作一下，首先编写克隆工厂合约:</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import &quot;./Clones.sol&quot;;

contract MyClonesFactory {
    using Clones for address;

    event ProxyGenerated(address proxy);

    function clone(address implementation) external {
        address proxy = implementation.clone();
        emit ProxyGenerated(proxy);
    }
}
"><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.0;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"./Clones.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">MyClonesFactory</span> </span>{
    <span class="hljs-keyword">using</span> <span class="hljs-title">Clones</span> <span class="hljs-title"><span class="hljs-keyword">for</span></span> <span class="hljs-title"><span class="hljs-keyword">address</span></span>;

    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">ProxyGenerated</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> proxy</span>)</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">clone</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> implementation</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-keyword">address</span> proxy <span class="hljs-operator">=</span> implementation.clone();
        <span class="hljs-keyword">emit</span> ProxyGenerated(proxy);
    }
}
</code></pre><p>接下来先部署原始 <code>Demo</code> 合约，然后部署 <code>MyClonesFactory</code> 合约，随后调用 <code>clone</code> 方法，传入 <code>Demo</code> 合约地址，即可创建克隆合约。</p><p>创建原始 <code>Demo</code> 合约时耗费了 14 万 gas：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cf78844389ad8061dc046220c4b883e3898cfae8857c56777e63054b644a8e41.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>而 <code>clone</code> 方法创建克隆合约只花费了 6 万 gas：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/47df04712caa2738270b064977183ccbbb155e681ee01efb3f9c8d3fe2a17326.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><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f97948aa5510578cdf810bbc34472c6f76e0b4b5bac786bdf43a3fe74bc2192c.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>Etherscan 也已经支持了 <code>EIP-1167</code> 的合约验证，只要原始合约代码已经验证，那么克隆合约代码自动验证。</p><p>原始合约验证代码之前的克隆合约：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2dc36f8e0c42bb0e63e6e0ea667a2155900b2224305fe1181031a7152f064a5d.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>可以看到克隆合约的字节码就是去除构造方法字节码之后其余的部分，然后我们验证一下原始合约的代码，这时克隆合约就已经自动验证了，并且标注了 <code>Minimal Proxy Contract</code>：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9d637f7876f2a55bbe752e5982ddc2388e407a78527cf847b69e38ec5343cc09.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>注意需要直接验证原始逻辑合约，如果验证克隆合约会失败。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p><code>EIP-1167</code> 提供了一种低成本克隆合约的方法，并且 Etherscan 也已经提供了支持，在需要创建多个合约的场景下，不失为一种好方法。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="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</a></p><div data-type="embedly" src="https://zenn.dev/yukista/articles/0bff3879ed0263" data="{&quot;provider_url&quot;:&quot;https://zenn.dev&quot;,&quot;description&quot;:&quot;OpenZeppelinのUpgradeable Contractsに関するドキュメントを読んでいて、Clonesの存在を知りました。 ClonesはUpgradable Contractsの仕組みの一部であるProxyの一種で、Implementation Contractの更新機能を省いたProxyだと言えます。（つまり、Implementation ...&quot;,&quot;title&quot;:&quot;💎🎉 OpenZeppelin Clones 解説 🎉💎&quot;,&quot;thumbnail_width&quot;:1200,&quot;url&quot;:&quot;https://zenn.dev/yukista/articles/0bff3879ed0263&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/287f4e9919f9513320a784f2534fb1a56fc784b61444733215b2c55daec24cb4.png&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Zenn&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:630,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1200,&quot;height&quot;:630,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/287f4e9919f9513320a784f2534fb1a56fc784b61444733215b2c55daec24cb4.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/287f4e9919f9513320a784f2534fb1a56fc784b61444733215b2c55daec24cb4.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://zenn.dev/yukista/articles/0bff3879ed0263" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>💎🎉 OpenZeppelin Clones 解説 🎉💎</h2><p>OpenZeppelinのUpgradeable Contractsに関するドキュメントを読んでいて、Clonesの存在を知りました。 ClonesはUpgradable Contractsの仕組みの一部であるProxyの一種で、Implementation Contractの更新機能を省いたProxyだと言えます。（つまり、Implementation ...</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://zenn.dev</span></div><img src="https://storage.googleapis.com/papyrus_images/287f4e9919f9513320a784f2534fb1a56fc784b61444733215b2c55daec24cb4.png"/></div></a></div></div><div data-type="embedly" src="https://medium.com/etherscan-blog/eip-1167-minimal-proxy-contract-on-etherscan-3eaedd85ef50" data="{&quot;provider_url&quot;:&quot;https://medium.com&quot;,&quot;description&quot;:&quot;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 ...&quot;,&quot;title&quot;:&quot;EIP- 1167, Minimal Proxy Contract on Etherscan&quot;,&quot;url&quot;:&quot;https://medium.com/etherscan-blog/eip-1167-minimal-proxy-contract-on-etherscan-3eaedd85ef50&quot;,&quot;author_name&quot;:&quot;Kaven Choi&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/9faca8c0548aafe308e13e6f829553e72c0dbd487e13cc3d05d9948fc44c989e.jpg&quot;,&quot;thumbnail_width&quot;:128,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Medium&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:128,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:128,&quot;height&quot;:128,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/9faca8c0548aafe308e13e6f829553e72c0dbd487e13cc3d05d9948fc44c989e.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/9faca8c0548aafe308e13e6f829553e72c0dbd487e13cc3d05d9948fc44c989e.jpg"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://medium.com/etherscan-blog/eip-1167-minimal-proxy-contract-on-etherscan-3eaedd85ef50" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>EIP- 1167, Minimal Proxy Contract on Etherscan</h2><p>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 ...</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://medium.com</span></div><img src="https://storage.googleapis.com/papyrus_images/9faca8c0548aafe308e13e6f829553e72c0dbd487e13cc3d05d9948fc44c989e.jpg"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[Audius 攻击事件分析]]></title>
            <link>https://paragraph.com/@xyyme/audius</link>
            <guid>r7LwA1vKq9FBfGk6gdea</guid>
            <pubDate>Tue, 23 Aug 2022 15:18:40 GMT</pubDate>
            <description><![CDATA[我们今天来研究一下前段时间 Audius 项目被黑的原因。这部分涉及到了内存槽位和合约升级方面的内容，如果有朋友不了解这一块，可以看看我之前写的这个系列。看完之后再来看这篇文章就比较容易理解了。代码分析我们先看一下 Audius 的合约架构，它采用了可升级合约的架构：Audius 合约架构我们知道，可升级合约架构中，代理合约存储了数据，逻辑合约中只是执行逻辑而已，执行过程中涉及到的内存变量会反向修改代理合约中的数据。我们来看看 Audius 的合约是怎么写的：AudiusAdminUpgradeabilityProxy（代理合约，节选）contract AudiusAdminUpgradeabilityProxy is UpgradeabilityProxy { address private proxyAdmin; string private constant ERROR_ONLY_ADMIN = ( "AudiusAdminUpgradeabilityProxy: Caller must be current proxy admin" ); } 在代理合约中，slot 0 ...]]></description>
            <content:encoded><![CDATA[<p>我们今天来研究一下前段时间 Audius 项目被黑的原因。这部分涉及到了内存槽位和合约升级方面的内容，如果有朋友不了解这一块，可以看看我之前写的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/RZscMYGkeGTY8z6ccHseY8HKu-ER3pX0mFYoWXRqXQ0">这个系列</a>。看完之后再来看这篇文章就比较容易理解了。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代码分析</h2><p>我们先看一下 Audius 的合约架构，它采用了可升级合约的架构：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fc7ca6519860e99782431017838ce9c0ad6553e1da1d15e962b18cf0f0465e7c.png" alt="Audius 合约架构" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">Audius 合约架构</figcaption></figure><p>我们知道，可升级合约架构中，代理合约存储了数据，逻辑合约中只是执行逻辑而已，执行过程中涉及到的内存变量会反向修改代理合约中的数据。我们来看看 Audius 的合约是怎么写的：</p><h3 id="h-audiusadminupgradeabilityproxy" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">AudiusAdminUpgradeabilityProxy（代理合约，节选）</h3><pre data-type="codeBlock" text="contract AudiusAdminUpgradeabilityProxy is UpgradeabilityProxy {
    address private proxyAdmin;
    string private constant ERROR_ONLY_ADMIN = (
        &quot;AudiusAdminUpgradeabilityProxy: Caller must be current proxy admin&quot;
    );
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">AudiusAdminUpgradeabilityProxy</span> <span class="hljs-keyword">is</span> <span class="hljs-title">UpgradeabilityProxy</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">private</span> proxyAdmin;
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> ERROR_ONLY_ADMIN <span class="hljs-operator">=</span> (
        <span class="hljs-string">"AudiusAdminUpgradeabilityProxy: Caller must be current proxy admin"</span>
    );
}
</code></pre><p>在代理合约中，<code>slot 0</code> 的位置是 <code>proxyAdmin</code>，这与我们之前讲过的合约升级不同。我们前面说过为了避免内存槽位冲突，<code>EIP-1967</code> 标准应运而生。它将 <code>implementation</code> 和 <code>admin</code> 这两个字段放在了两个特殊的插槽中：</p><pre data-type="codeBlock" text="# bytes32(uint256(keccak256(&apos;eip1967.proxy.implementation&apos;)) - 1)
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

# bytes32(uint256(keccak256(&apos;eip1967.proxy.admin&apos;)) - 1)
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
"><code># <span class="hljs-keyword">bytes32</span>(<span class="hljs-keyword">uint256</span>(<span class="hljs-built_in">keccak256</span>(<span class="hljs-string">'eip1967.proxy.implementation'</span>)) <span class="hljs-operator">-</span> <span class="hljs-number">1</span>)
<span class="hljs-number">0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc</span>

# <span class="hljs-keyword">bytes32</span>(<span class="hljs-keyword">uint256</span>(<span class="hljs-built_in">keccak256</span>(<span class="hljs-string">'eip1967.proxy.admin'</span>)) <span class="hljs-operator">-</span> <span class="hljs-number">1</span>)
<span class="hljs-number">0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103</span>
</code></pre><p>这样做的目的就是为了最大限度保证这两个保留数据不会和逻辑合约中的数据槽位冲突，而 Audius 这种写法正是造成这次 bug 的原因。</p><p>我们再来看看逻辑合约的内容：</p><h3 id="h-governance" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Governance（逻辑合约，节选）</h3><pre data-type="codeBlock" text="contract Initializable {
    /**
     * @dev Indicates that the contract has been initialized.
     */
    bool private initialized;

    /**
     * @dev Indicates that the contract is in the process of being initialized.
     */
    bool private initializing;

    /**
     * @dev Modifier to use in the initializer function of a contract.
     */
    modifier initializer() {
        require(
            initializing || isConstructor() || !initialized,
            &quot;Contract instance has already been initialized&quot;
        );

        bool isTopLevelCall = !initializing;
        if (isTopLevelCall) {
            initializing = true;
            initialized = true;
        }

        _;

        if (isTopLevelCall) {
            initializing = false;
        }
    }

    ///....

    // Reserved storage space to allow for layout changes in the future.
    uint256[50] private ______gap;
}

contract InitializableV2 is Initializable {
    /// ....
}

contract Governance is InitializableV2 {
    /// ....
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Initializable</span> </span>{
    <span class="hljs-comment">/**
     * @dev Indicates that the contract has been initialized.
     */</span>
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">private</span> initialized;

    <span class="hljs-comment">/**
     * @dev Indicates that the contract is in the process of being initialized.
     */</span>
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">private</span> initializing;

    <span class="hljs-comment">/**
     * @dev Modifier to use in the initializer function of a contract.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">initializer</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">require</span>(
            initializing <span class="hljs-operator">|</span><span class="hljs-operator">|</span> isConstructor() <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-operator">!</span>initialized,
            <span class="hljs-string">"Contract instance has already been initialized"</span>
        );

        <span class="hljs-keyword">bool</span> isTopLevelCall <span class="hljs-operator">=</span> <span class="hljs-operator">!</span>initializing;
        <span class="hljs-keyword">if</span> (isTopLevelCall) {
            initializing <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
            initialized <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        }

        <span class="hljs-keyword">_</span>;

        <span class="hljs-keyword">if</span> (isTopLevelCall) {
            initializing <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
        }
    }

    <span class="hljs-comment">///....</span>

    <span class="hljs-comment">// Reserved storage space to allow for layout changes in the future.</span>
    <span class="hljs-keyword">uint256</span>[<span class="hljs-number">50</span>] <span class="hljs-keyword">private</span> ______gap;
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">InitializableV2</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Initializable</span> </span>{
    <span class="hljs-comment">/// ....</span>
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Governance</span> <span class="hljs-keyword">is</span> <span class="hljs-title">InitializableV2</span> </span>{
    <span class="hljs-comment">/// ....</span>
}
</code></pre><p>按照合约继承的内存分布规则，<code>Initializable</code> 合约中的 <code>initialized</code> 和 <code>initializing</code> 这两个变量分别位于逻辑合约 <code>Governance</code> 的 <code>slot 0</code> 和 <code>slot 1</code> 中。</p><p>看到这里，大家是不是已经发现了问题。如果按照这种写法，那么代理合约和逻辑合约的内存槽位不是冲突了吗？没错，但还有一点要注意的是，由于 <code>initialized</code> 和 <code>initializing</code> 都是 <code>bool</code> 类型变量，因此他们各自都只占据一字节（注意，是 1 byte，不是 1 bit），所以说它们俩实际上是被打包放在了 <code>slot 0</code> 中。也就是说，<code>slot 0</code> 的结构是：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/759f2a45eb13b94b058ce2f2ef44d77d62933b36e78179c859a83c674b8f797e.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>上图是逻辑合约的 <code>slot 0</code> 内存分布。由于与代理合约的 <code>ProxyAdmin</code> 冲突，且 <code>ProxyAdmin</code> 的值为：</p><blockquote><p>0x4DEcA517D6817B6510798b7328F2314d3003AbAC</p></blockquote><p>因此，对应的 <code>slot 0</code> 槽位图示为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4e82136b266fcb29f2f35ebf0d98be349d64d38a199a29540105d95eb37f0b41.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>这说明 <code>initialized</code> 和 <code>initializing</code> 这两个变量的值使用了 <code>ProxyAdmin</code> 实际值的最后两个字节！而恰好最后两个字节（0xAB, 0xAC）都是非零值，这也就造成在实际可升级合约的数据读取中，<code>initialized</code> 和 <code>initializing</code> 的值总是 <code>true</code>。而这个巧合其实也取决于 <code>ProxyAdmin</code> 的最后两个字节是什么，如果它的地址最后两字节都是零： <code>0x4DEcA517D6817B6510798b7328F2314d30030000</code>，那么 <code>initialized</code> 和 <code>initializing</code> 便都是 <code>false</code> 了。</p><p>冲突原因已经找到了，我们来看看这个冲突会造成什么。再次看看逻辑合约：</p><pre data-type="codeBlock" text="contract Initializable {
    ///....
    modifier initializer() {
        require(
            initializing || isConstructor() || !initialized,
            &quot;Contract instance has already been initialized&quot;
        );

        bool isTopLevelCall = !initializing;
        if (isTopLevelCall) {
            initializing = true;
            initialized = true;
        }

        _;

        if (isTopLevelCall) {
            initializing = false;
        }
    }

    ///....

    // Reserved storage space to allow for layout changes in the future.
    uint256[50] private ______gap;
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Initializable</span> </span>{
    <span class="hljs-comment">///....</span>
    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">initializer</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-built_in">require</span>(
            initializing <span class="hljs-operator">|</span><span class="hljs-operator">|</span> isConstructor() <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-operator">!</span>initialized,
            <span class="hljs-string">"Contract instance has already been initialized"</span>
        );

        <span class="hljs-keyword">bool</span> isTopLevelCall <span class="hljs-operator">=</span> <span class="hljs-operator">!</span>initializing;
        <span class="hljs-keyword">if</span> (isTopLevelCall) {
            initializing <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
            initialized <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        }

        <span class="hljs-keyword">_</span>;

        <span class="hljs-keyword">if</span> (isTopLevelCall) {
            initializing <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
        }
    }

    <span class="hljs-comment">///....</span>

    <span class="hljs-comment">// Reserved storage space to allow for layout changes in the future.</span>
    <span class="hljs-keyword">uint256</span>[<span class="hljs-number">50</span>] <span class="hljs-keyword">private</span> ______gap;
}
</code></pre><p>对于 <code>initializer</code> 修饰符，由于 <code>initializing</code> 为 <code>true</code>，因此可以通过 <code>require</code> 校验。而下面的 <code>isTopLevelCall</code> 会被赋值为 <code>false</code>，造成 <code>if</code> 语句无法执行，那么 <code>initializing</code> 将永远为 <code>true</code>，也就是说 <code>initializer</code> 已经起不到限制作用了。</p><p>黑客就是利用了这个 bug，从而可以调用各种被 <code>initializer</code> 修饰的方法。这些方法中包含一些特权方法，本来只能被管理员调用一次，这下被黑客调用，损失惨重。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">解决办法</h2><p>解决这个 bug 的中心思想就是去除代理合约和逻辑合约的内存冲突，我们来看看官方的新版逻辑合约：</p><pre data-type="codeBlock" text="contract Initializable {
    address private proxyAdmin;

    uint256 private filler1;
    uint256 private filler2;

    /**
     * @dev Indicates that the contract has been initialized.
     */
    bool private initialized;

    /**
     * @dev Indicates that the contract is in the process of being initialized.
     */
    bool private initializing;

    /**
     * @dev Modifier to use in the initializer function of a contract.
     */
    modifier initializer() {
        require(msg.sender == proxyAdmin, &quot;Only proxy admin can initialize&quot;);
        require(
            initializing || isConstructor() || !initialized,
            &quot;Contract instance has already been initialized&quot;
        );

        bool isTopLevelCall = !initializing;
        if (isTopLevelCall) {
            initializing = true;
            initialized = true;
        }

        _;

        if (isTopLevelCall) {
            initializing = false;
        }
    }

    /// @dev Returns true if and only if the function is running in the constructor
    function isConstructor() private view returns (bool) {
        // extcodesize checks the size of the code stored in an address, and
        // address returns the current address. Since the code is still not
        // deployed when running a constructor, any checks on its code size will
        // yield zero, making it an effective way to detect if a contract is
        // under construction or not.
        address self = address(this);
        uint256 cs;
        assembly {
            cs := extcodesize(self)
        }
        return cs == 0;
    }

    // Reserved storage space to allow for layout changes in the future.
    uint256[47] private ______gap;
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Initializable</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">private</span> proxyAdmin;

    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> filler1;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> filler2;

    <span class="hljs-comment">/**
     * @dev Indicates that the contract has been initialized.
     */</span>
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">private</span> initialized;

    <span class="hljs-comment">/**
     * @dev Indicates that the contract is in the process of being initialized.
     */</span>
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">private</span> initializing;

    <span class="hljs-comment">/**
     * @dev Modifier to use in the initializer function of a contract.
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">initializer</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> proxyAdmin, <span class="hljs-string">"Only proxy admin can initialize"</span>);
        <span class="hljs-built_in">require</span>(
            initializing <span class="hljs-operator">|</span><span class="hljs-operator">|</span> isConstructor() <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-operator">!</span>initialized,
            <span class="hljs-string">"Contract instance has already been initialized"</span>
        );

        <span class="hljs-keyword">bool</span> isTopLevelCall <span class="hljs-operator">=</span> <span class="hljs-operator">!</span>initializing;
        <span class="hljs-keyword">if</span> (isTopLevelCall) {
            initializing <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
            initialized <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        }

        <span class="hljs-keyword">_</span>;

        <span class="hljs-keyword">if</span> (isTopLevelCall) {
            initializing <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
        }
    }

    <span class="hljs-comment">/// @dev Returns true if and only if the function is running in the constructor</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isConstructor</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">private</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">bool</span></span>) </span>{
        <span class="hljs-comment">// extcodesize checks the size of the code stored in an address, and</span>
        <span class="hljs-comment">// address returns the current address. Since the code is still not</span>
        <span class="hljs-comment">// deployed when running a constructor, any checks on its code size will</span>
        <span class="hljs-comment">// yield zero, making it an effective way to detect if a contract is</span>
        <span class="hljs-comment">// under construction or not.</span>
        <span class="hljs-keyword">address</span> <span class="hljs-built_in">self</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>);
        <span class="hljs-keyword">uint256</span> cs;
        <span class="hljs-keyword">assembly</span> {
            cs <span class="hljs-operator">:=</span> <span class="hljs-built_in">extcodesize</span>(self)
        }
        <span class="hljs-keyword">return</span> cs <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-comment">// Reserved storage space to allow for layout changes in the future.</span>
    <span class="hljs-keyword">uint256</span>[<span class="hljs-number">47</span>] <span class="hljs-keyword">private</span> ______gap;
}
</code></pre><ol><li><p>将 <code>initialized</code> 和 <code>initializing</code> 变量后移，预留出空间解决内存冲突</p></li><li><p><code>initializer</code> 修饰符中同时添加只能 <code>proxyAdmin</code> 调用的限制，双重保险</p></li><li><p>由于前面添加了三个变量，因此最后的 <code>______gap</code> 预留位置要减少，由之前的 50 个减少为 47 个，这样做是为了兼容之前的数据。避免更新后老数据又冲突了。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>一个内存插槽冲突引发的血案，警示我们在编写可升级合约时一定要注意这方面问题。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://blog.audius.co/article/audius-governance-takeover-post-mortem-7-23-22" data="{&quot;provider_url&quot;:&quot;https://blog.audius.co&quot;,&quot;description&quot;:&quot;Your hub for the latest happenings, feature releases, and important notices from Audius.&quot;,&quot;title&quot;:&quot;Audius Governance Takeover Post-Mortem 7/23/22 | Audius Blog&quot;,&quot;thumbnail_width&quot;:2000,&quot;url&quot;:&quot;https://blog.audius.co/article/audius-governance-takeover-post-mortem-7-23-22&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/b5d6d557bc501dcee16e22178c856408c14ae6ef8fe5604d826f9181e720e4f7.webp&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Audius&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:1000,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:2000,&quot;height&quot;:1000,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/b5d6d557bc501dcee16e22178c856408c14ae6ef8fe5604d826f9181e720e4f7.webp&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/b5d6d557bc501dcee16e22178c856408c14ae6ef8fe5604d826f9181e720e4f7.webp"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://blog.audius.co/article/audius-governance-takeover-post-mortem-7-23-22" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Audius Governance Takeover Post-Mortem 7/23/22 | Audius Blog</h2><p>Your hub for the latest happenings, feature releases, and important notices from Audius.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://blog.audius.co</span></div><img src="https://storage.googleapis.com/papyrus_images/b5d6d557bc501dcee16e22178c856408c14ae6ef8fe5604d826f9181e720e4f7.webp"/></div></a></div></div><div data-type="twitter" tweetId="1553105446180888577" tweetData="{&quot;__typename&quot;:&quot;Tweet&quot;,&quot;lang&quot;:&quot;en&quot;,&quot;favorite_count&quot;:340,&quot;possibly_sensitive&quot;:false,&quot;created_at&quot;:&quot;2022-07-29T19:49:28.000Z&quot;,&quot;display_text_range&quot;:[0,262],&quot;entities&quot;:{&quot;hashtags&quot;:[],&quot;urls&quot;:[],&quot;user_mentions&quot;:[],&quot;symbols&quot;:[],&quot;media&quot;:[{&quot;display_url&quot;:&quot;pic.x.com/gm0KP7XQ61&quot;,&quot;expanded_url&quot;:&quot;https://x.com/danielvf/status/1553105446180888577/photo/1&quot;,&quot;indices&quot;:[263,286],&quot;url&quot;:&quot;https://t.co/gm0KP7XQ61&quot;}]},&quot;id_str&quot;:&quot;1553105446180888577&quot;,&quot;text&quot;:&quot;The Audius hack this week involved two contracts that overlapped the same storage slot (for three different variables).\n\nThe end of the admin address on the proxy was used by the implementation contract as value of the initializing and initialized variables. 1/6 https://t.co/gm0KP7XQ61&quot;,&quot;user&quot;:{&quot;id_str&quot;:&quot;5678&quot;,&quot;name&quot;:&quot;Daniel Von Fange&quot;,&quot;screen_name&quot;:&quot;danielvf&quot;,&quot;is_blue_verified&quot;:true,&quot;profile_image_shape&quot;:&quot;Circle&quot;,&quot;verified&quot;:false,&quot;profile_image_url_https&quot;:&quot;https://storage.googleapis.com/papyrus_images/2cfe4ece0420cdf5b1b52459b804b6ad2bcf6432df858fb4bceb4530c714bd32.jpg&quot;},&quot;edit_control&quot;:{&quot;edit_tweet_ids&quot;:[&quot;1553105446180888577&quot;],&quot;editable_until_msecs&quot;:&quot;1659125968000&quot;,&quot;is_edit_eligible&quot;:false,&quot;edits_remaining&quot;:&quot;5&quot;},&quot;mediaDetails&quot;:[{&quot;display_url&quot;:&quot;pic.x.com/gm0KP7XQ61&quot;,&quot;expanded_url&quot;:&quot;https://x.com/danielvf/status/1553105446180888577/photo/1&quot;,&quot;ext_media_availability&quot;:{&quot;status&quot;:&quot;Available&quot;},&quot;indices&quot;:[263,286],&quot;media_url_https&quot;:&quot;https://pbs.twimg.com/media/FY29AxRXoAAmt-J.jpg&quot;,&quot;original_info&quot;:{&quot;height&quot;:1322,&quot;width&quot;:1618,&quot;focus_rects&quot;:[{&quot;x&quot;:0,&quot;y&quot;:0,&quot;w&quot;:1618,&quot;h&quot;:906},{&quot;x&quot;:269,&quot;y&quot;:0,&quot;w&quot;:1322,&quot;h&quot;:1322},{&quot;x&quot;:350,&quot;y&quot;:0,&quot;w&quot;:1160,&quot;h&quot;:1322},{&quot;x&quot;:600,&quot;y&quot;:0,&quot;w&quot;:661,&quot;h&quot;:1322},{&quot;x&quot;:0,&quot;y&quot;:0,&quot;w&quot;:1618,&quot;h&quot;:1322}]},&quot;sizes&quot;:{&quot;large&quot;:{&quot;h&quot;:1322,&quot;resize&quot;:&quot;fit&quot;,&quot;w&quot;:1618},&quot;medium&quot;:{&quot;h&quot;:980,&quot;resize&quot;:&quot;fit&quot;,&quot;w&quot;:1200},&quot;small&quot;:{&quot;h&quot;:556,&quot;resize&quot;:&quot;fit&quot;,&quot;w&quot;:680},&quot;thumb&quot;:{&quot;h&quot;:150,&quot;resize&quot;:&quot;crop&quot;,&quot;w&quot;:150}},&quot;type&quot;:&quot;photo&quot;,&quot;url&quot;:&quot;https://t.co/gm0KP7XQ61&quot;}],&quot;photos&quot;:[{&quot;backgroundColor&quot;:{&quot;red&quot;:204,&quot;green&quot;:214,&quot;blue&quot;:221},&quot;cropCandidates&quot;:[{&quot;x&quot;:0,&quot;y&quot;:0,&quot;w&quot;:1618,&quot;h&quot;:906},{&quot;x&quot;:269,&quot;y&quot;:0,&quot;w&quot;:1322,&quot;h&quot;:1322},{&quot;x&quot;:350,&quot;y&quot;:0,&quot;w&quot;:1160,&quot;h&quot;:1322},{&quot;x&quot;:600,&quot;y&quot;:0,&quot;w&quot;:661,&quot;h&quot;:1322},{&quot;x&quot;:0,&quot;y&quot;:0,&quot;w&quot;:1618,&quot;h&quot;:1322}],&quot;expandedUrl&quot;:&quot;https://x.com/danielvf/status/1553105446180888577/photo/1&quot;,&quot;url&quot;:&quot;https://storage.googleapis.com/papyrus_images/e36c95a01d1d1d971910ec9e57de1d131046cc0c4f521ee6727b2968cdf91ff0.jpg&quot;,&quot;width&quot;:1618,&quot;height&quot;:1322}],&quot;conversation_count&quot;:15,&quot;news_action_type&quot;:&quot;conversation&quot;,&quot;isEdited&quot;:false,&quot;isStaleEdit&quot;:false}"> 
  <div class="twitter-embed embed">
    <div class="twitter-header">
        <div style="display:flex">
          <a target="_blank" href="https://twitter.com/danielvf">
              <img alt="User Avatar" class="twitter-avatar" src="https://storage.googleapis.com/papyrus_images/2cfe4ece0420cdf5b1b52459b804b6ad2bcf6432df858fb4bceb4530c714bd32.jpg" />
            </a>
            <div style="margin-left:4px;margin-right:auto;line-height:1.2;">
              <a target="_blank" href="https://twitter.com/danielvf" class="twitter-displayname">Daniel Von Fange</a>
              <p><a target="_blank" href="https://twitter.com/danielvf" class="twitter-username">@danielvf</a></p>
    
            </div>
            <a href="https://twitter.com/danielvf/status/1553105446180888577" target="_blank">
              <img alt="Twitter Logo" class="twitter-logo" src="https://paragraph.com/editor/twitter/logo.png" />
            </a>
          </div>
        </div>
      
    <div class="twitter-body">
      The Audius hack this week involved two contracts that overlapped the same storage slot (for three different variables).<br /><br />The end of the admin address on the proxy was used by the implementation contract as value of the initializing and initialized variables. 1/6 
      <div class="twitter-media"><img class="twitter-image" src="https://storage.googleapis.com/papyrus_images/e36c95a01d1d1d971910ec9e57de1d131046cc0c4f521ee6727b2968cdf91ff0.jpg" /></div>
      
       
    </div>
    
     <div class="twitter-footer">
          <a target="_blank" href="https://twitter.com/danielvf/status/1553105446180888577" style="margin-right:16px; display:flex;">
            <img alt="Like Icon" class="twitter-heart" src="https://paragraph.com/editor/twitter/heart.png">
            340
          </a>
          <a target="_blank" href="https://twitter.com/danielvf/status/1553105446180888577"><p>2:49 PM • Jul 29, 2022</p></a>
        </div>
    
  </div> 
  </div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[深入理解 EVM（三）]]></title>
            <link>https://paragraph.com/@xyyme/evm-3</link>
            <guid>nmmeoTlCrxP3MGAwAoUc</guid>
            <pubDate>Mon, 22 Aug 2022 13:33:45 GMT</pubDate>
            <description><![CDATA[今天我们来聊聊调用合约方法在字节码层面是怎么实现的。同样地，我们以一个简单的合约作为例子：编译合约// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; contract Demo { constructor() {} function func1() public {} function func2() public {} } 该合约中一共有两个方法，分别是 func1 与 func2。我们这里着重于理解方法调用的过程，因此简单起见就将方法内容置为空。 使用 solc 进行编译：solc Demo.sol --bin得到的字节码为：利用 0xfe 操作符分割后，得到的各部分分别为：6080604052348015600f57600080fd5b5060818061001e6000396000f3（init bytecode） 6080604052348015600f57600080fd5b506004361060325760003560e01c806374135154146037578063b1ade4db1...]]></description>
            <content:encoded><![CDATA[<p>今天我们来聊聊调用合约方法在字节码层面是怎么实现的。同样地，我们以一个简单的合约作为例子：</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">编译合约</h2><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

contract Demo {
    constructor() {}

    function func1() public {}

    function func2() public {}
}
"><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.15;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{}

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">func1</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{}

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">func2</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{}
}
</code></pre><p>该合约中一共有两个方法，分别是 <code>func1</code> 与 <code>func2</code>。我们这里着重于理解方法调用的过程，因此简单起见就将方法内容置为空。</p><p>使用 <code>solc</code> 进行编译：</p><blockquote><p>solc Demo.sol --bin</p></blockquote><p>得到的字节码为：</p><pre data-type="codeBlock" text="6080604052348015600f57600080fd5b5060818061001e6000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c806374135154146037578063b1ade4db14603f575b600080fd5b603d6047565b005b60456049565b005b565b56fea264697066735822122069532a763e3f61abcfbb422ae7dc4587126c8f8b2264e73bb837a5924ebb1d4964736f6c634300080f0033
"><code></code></pre><p>利用 <code>0xfe</code> 操作符分割后，得到的各部分分别为：</p><pre data-type="codeBlock" text="6080604052348015600f57600080fd5b5060818061001e6000396000f3（init bytecode）

6080604052348015600f57600080fd5b506004361060325760003560e01c806374135154146037578063b1ade4db14603f575b600080fd5b603d6047565b005b60456049565b005b565b56（runtime bytecode）

a264697066735822122069532a763e3f61abcfbb422ae7dc4587126c8f8b2264e73bb837a5924ebb1d4964736f6c634300080f0033（metadata hash）
"><code>6080604052348015600f57600080fd5b5060818061001e6000396000f3（init bytecode）

6080604052348015600f57600080fd5b506004361060325760003560e01c806374135154146037578063b1ade4db14603f575b600080fd5b603d6047565b005b60456049565b005b565b56（runtime bytecode）

a264697066735822122069532a763e3f61abcfbb422ae7dc4587126c8f8b2264e73bb837a5924ebb1d4964736f6c634300080f0033（metadata <span class="hljs-built_in">hash</span>）
</code></pre><p><code>init bytecode</code> 是合约部署部分，我们在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/6vqE2DRsMzlPNmh3kYiwTdMBj-9hanmxyDuTHM7tZDU">上篇文章</a>中已经介绍过了。调用合约方法是与 <code>runtime bytecode</code> 这部分进行交互，我们主要来研究这一块。</p><p>我们知道，调用合约方法交易的 <code>data</code> 域就是合约签名与参数的拼接内容。具体来说，就是合约签名的 <code>keccak256</code> 哈希值的前 4 个字节再加上参数内容就构成了 <code>data</code> 部分。</p><p>在上面例子中，<code>func1</code> 与 <code>func2</code> 的哈希值前 4 个字节分别是 <code>0x74135154</code> 与 <code>0xb1ade4db</code>，同时由于这两个方法都没有参数，因此不用在后面拼接参数。如果 <code>fun1</code> 的方法签名为：</p><pre data-type="codeBlock" text="function func1(uint _a) public {}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">func1</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> _a</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{}
</code></pre><p>那么就需要添加参数，并且补齐 32 字节：</p><pre data-type="codeBlock" text="0x254e43db0000000000000000000000000000000000000000000000000000000000000001
"><code></code></pre><p>其中 <code>0x254e43db</code> 是 <code>func1(uint256)</code> 的哈希值前 4 个字节。</p><p>接下来我们就来看看在调用合约方法的过程中，在 EVM 字节码层面会发生什么。我们以调用 <code>func1</code> 方法为例，那么此时交易的 <code>data</code> 域内容就为 <code>0x74135154</code>。</p><p>我们先将 <code>runtime bytecode</code> 部分的字节码与其 opcodes 一一对应起来：</p><pre data-type="codeBlock" text="0 -&gt; 60 PUSH1
1 -&gt; 80 0x80
2 -&gt; 60 PUSH1
3 -&gt; 40 0x40
4 -&gt; 52 MSTORE
5 -&gt; 34 CALLVALUE
6 -&gt; 80 DUP1
7 -&gt; 15 ISZERO
8 -&gt; 60 PUSH1
9 -&gt; 0f 0xF
10 -&gt; 57 JUMPI
11 -&gt; 60 PUSH1
12 -&gt; 00 0x0
13 -&gt; 80 DUP1
14 -&gt; fd REVERT
15 -&gt; 5b JUMPDEST
16 -&gt; 50 POP
17 -&gt; 60 PUSH1
18 -&gt; 04 0x4
19 -&gt; 36 CALLDATASIZE
20 -&gt; 10 LT
21 -&gt; 60 PUSH1
22 -&gt; 32 0x32
23 -&gt; 57 JUMPI
24 -&gt; 60 PUSH1
25 -&gt; 00 0x0
26 -&gt; 35 CALLDATALOAD
27 -&gt; 60 PUSH1
28 -&gt; e0 0xE0
29 -&gt; 1c SHR
30 -&gt; 80 DUP1
31 -&gt; 63 PUSH4
(32 ~ 35) -&gt; 74135154 func1 方法签名哈希前四字节
36 -&gt; 14 EQ
37 -&gt; 60 PUSH1
38 -&gt; 37 0x37
39 -&gt; 57 JUMPI
40 -&gt; 80 DUP1
41 -&gt; 63 PUSH4
(42 ~ 45) -&gt; b1ade4db func2 方法签名哈希前四字节
46 -&gt; 14 EQ
47 -&gt; 60 PUSH1
48 -&gt; 3f 0x3F
49 -&gt; 57 JUMPI
50 -&gt; 5b JUMPDEST
51 -&gt; 60 PUSH1
52 -&gt; 00 0x0
53 -&gt; 80 DUP1
54 -&gt; fd REVERT
55 -&gt; 5b JUMPDEST
56 -&gt; 60 PUSH1
57 -&gt; 3d 0x3D
58 -&gt; 60 PUSH1
59 -&gt; 47 0x47
60 -&gt; 56 JUMP
61 -&gt; 5b JUMPDEST
62 -&gt; 00 STOP
63 -&gt; 5b JUMPDEST
64 -&gt; 60 PUSH1
65 -&gt; 45 0x45
66 -&gt; 60 PUSH1
67 -&gt; 49 0x49
68 -&gt; 56 JUMP
69 -&gt; 5b JUMPDEST
70 -&gt; 00 STOP
71 -&gt; 5b JUMPDEST
72 -&gt; 56 JUMP
73 -&gt; 5b JUMPDEST
74 -&gt; 56 JUMP
"><code><span class="hljs-number">0</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">1</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> <span class="hljs-number">0x80</span>
<span class="hljs-number">2</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">3</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">40</span> <span class="hljs-number">0x40</span>
<span class="hljs-number">4</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">52</span> MSTORE
<span class="hljs-number">5</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">34</span> CALLVALUE
<span class="hljs-number">6</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">7</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">15</span> ISZERO
<span class="hljs-number">8</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">9</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 0f <span class="hljs-number">0xF</span>
<span class="hljs-number">10</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">57</span> JUMPI
<span class="hljs-number">11</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">12</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">13</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">14</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> fd REVERT
<span class="hljs-number">15</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">16</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">50</span> POP
<span class="hljs-number">17</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">18</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 04 <span class="hljs-number">0x4</span>
<span class="hljs-number">19</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">36</span> CALLDATASIZE
<span class="hljs-number">20</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">10</span> LT
<span class="hljs-number">21</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">22</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">32</span> <span class="hljs-number">0x32</span>
<span class="hljs-number">23</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">57</span> JUMPI
<span class="hljs-number">24</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">25</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">26</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">35</span> CALLDATALOAD
<span class="hljs-number">27</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">28</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> e0 <span class="hljs-number">0xE0</span>
<span class="hljs-number">29</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 1c SHR
<span class="hljs-number">30</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">31</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">63</span> PUSH4
(<span class="hljs-number">32</span> <span class="hljs-operator">~</span> <span class="hljs-number">35</span>) <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">74135154</span> func1 方法签名哈希前四字节
<span class="hljs-number">36</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">14</span> EQ
<span class="hljs-number">37</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">38</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">37</span> <span class="hljs-number">0x37</span>
<span class="hljs-number">39</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">57</span> JUMPI
<span class="hljs-number">40</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">41</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">63</span> PUSH4
(<span class="hljs-number">42</span> <span class="hljs-operator">~</span> <span class="hljs-number">45</span>) <span class="hljs-operator">-</span><span class="hljs-operator">></span> b1ade4db func2 方法签名哈希前四字节
<span class="hljs-number">46</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">14</span> EQ
<span class="hljs-number">47</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">48</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 3f <span class="hljs-number">0x3F</span>
<span class="hljs-number">49</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">57</span> JUMPI
<span class="hljs-number">50</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">51</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">52</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">53</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">54</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> fd REVERT
<span class="hljs-number">55</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">56</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">57</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 3d <span class="hljs-number">0x3D</span>
<span class="hljs-number">58</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">59</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">47</span> <span class="hljs-number">0x47</span>
<span class="hljs-number">60</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">56</span> JUMP
<span class="hljs-number">61</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">62</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 STOP
<span class="hljs-number">63</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">64</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">65</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">45</span> <span class="hljs-number">0x45</span>
<span class="hljs-number">66</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">67</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">49</span> <span class="hljs-number">0x49</span>
<span class="hljs-number">68</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">56</span> JUMP
<span class="hljs-number">69</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">70</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 STOP
<span class="hljs-number">71</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">72</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">56</span> JUMP
<span class="hljs-number">73</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">74</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">56</span> JUMP
</code></pre><p>来看看这部分字节码都干了些什么。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">调用合约方法流程</h2><p>0 - 4 行我们已经非常熟悉了，加载空闲内存指针。</p><p>5 - 14 行是校验 <code>msg.value</code> 必须为零，我们在上篇文章已经看到过这部分了。由于合约中没有 <code>payable</code> 方法，因此要求所有的合约调用的 <code>callvalue</code> 都要为零，否则会在 14 行<code>REVERT</code> 失败。</p><p>如果传入的 <code>value</code> 为零，则进入正常流程 15 行，在 16 行将栈中无用数据 pop 出。</p><p>17 - 18 行将 <code>0x4</code> 放入栈中，它代表正常的方法调用签名长度。此时栈为：</p><pre data-type="codeBlock" text="| 4
"><code><span class="hljs-operator">|</span> <span class="hljs-number">4</span>
</code></pre><p>19 行获取到 <code>data</code> 域的长度并放入栈中，<code>0x74135154</code> 的长度为 4 个字节。此时栈为：</p><pre data-type="codeBlock" text="| 4 | 4
"><code><span class="hljs-operator">|</span> <span class="hljs-number">4</span> <span class="hljs-operator">|</span> <span class="hljs-number">4</span>
</code></pre><p>20 行从栈中获取两个数据，并判断第一个数字是否小于第二个数字，小于返回 1，否则返回 0。这里由于 4 = 4，因此返回 0。这部分是什么意思呢？上面我们说到第一次放入栈中的 4 代表正常的方法调用签名长度，也就是方法签名前四个字节的意思。无论我们调用了哪个方法，有没有参数，<code>data</code> 域的长度都至少应该是 4。如果某个交易的 <code>data</code> 域的长度小于 4，说明它并不是正常的方法调用，那么在接下来的流程中肯定就走不到正常的方法名匹配部分，要么交易失败，要么进入到 <code>fallback</code> 的调用流程中。我们这个例子中没有声明 <code>fallback</code> 方法，因此如果这时 <code>LT</code> 操作符返回 1（即 TRUE），交易就会失败。</p><p>在这里正常情况下，此时栈中为：</p><pre data-type="codeBlock" text="| 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>21 - 22 行将 <code>0x32</code> 放入栈中：</p><pre data-type="codeBlock" text="| 0 | 0x32
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span> <span class="hljs-number">0x32</span>
</code></pre><p>23 行 <code>JUMPI(0x32, 0)</code> 操作符根据栈中数据判断是否跳转。由于第二个参数是 0，因此不跳转，继续向下执行。这里假设第二个参数是 1，也就是上一步中的 <code>data</code> 域长度小于 4 字节，流程会跳转到 <code>0x32</code>，也就是 50 行，并最终在 54 行 <code>REVERT</code> 交易失败，验证了我们上面的说法。</p><p>24 - 25 行将 <code>0x0</code> 放入栈中：</p><pre data-type="codeBlock" text="| 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>26 行加载 32 字节长度的 <code>data</code> 域放入栈中，开始位置从栈中获取，这里即为 <code>calldata[0]</code>，当前的 <code>data</code> 域内容为 <code>0x74135154</code>，不够 32 字节因此需要用 0 补齐。此时栈中为：</p><pre data-type="codeBlock" text="| 7413515400000000000000000000000000000000000000000000000000000000
"><code><span class="hljs-operator">|</span> <span class="hljs-number">7413515400000000000000000000000000000000000000000000000000000000</span>
</code></pre><p>27 - 28 行将 <code>0xE0</code>，即 224 放入栈中：</p><pre data-type="codeBlock" text="| 0x7413515400000000000000000000000000000000000000000000000000000000 | 0xE0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x7413515400000000000000000000000000000000000000000000000000000000</span> <span class="hljs-operator">|</span> <span class="hljs-number">0xE0</span>
</code></pre><p>29 行 <code>SHR</code> 取出栈中数据，将 <code>7413515400000000000000000000000000000000000000000000000000000000</code> 右移 224 位，前者的长度是 256（即 64 * 4） 位，右移之后变成 <code>0x74135154</code>，刚好是四个字节，这四个字节恰好是方法签名哈希值的前四个字节，是用来匹配合约中的方法的。此时栈为：</p><pre data-type="codeBlock" text="| 0x74135154
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span>
</code></pre><p>30 行复制一份栈顶数据：</p><pre data-type="codeBlock" text="| 0x74135154 | 0x74135154
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span> <span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span>
</code></pre><p>31 - 35 将 <code>func1</code> 的方法签名哈希前四个字节放入栈中：</p><pre data-type="codeBlock" text="| 0x74135154 | 0x74135154 | 0x74135154
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span> <span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span> <span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span>
</code></pre><p>36 行 <code>EQ</code> 判断栈顶两个元素是否相等，这里相等，因此返回 1:</p><pre data-type="codeBlock" text="| 0x74135154 | 1
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span> <span class="hljs-operator">|</span> <span class="hljs-number">1</span>
</code></pre><p>37 - 38 将 <code>0x37</code> 放入栈中：</p><pre data-type="codeBlock" text="| 0x74135154 | 1 ｜ 0x37
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span> <span class="hljs-operator">|</span> <span class="hljs-number">1</span> ｜ <span class="hljs-number">0x37</span>
</code></pre><p>39 行 <code>JUMPI(0x37, 1)</code> 判断是否跳转，此时会跳转到 <code>0x37</code>，即 55 行。栈为：</p><pre data-type="codeBlock" text="| 0x74135154
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span>
</code></pre><p>这里如果第二个参数，也就是 <code>EQ</code> 的结果为 0，说明调用的方法与当前的方法不匹配，则不跳转，继续向下运行，寻找下一个方法签名进行匹配。</p><p>此时，我们已经找到了相匹配的合约方法，也就是说已经完成了匹配方法名的过程。接下来就是执行方法体内容了。</p><p>跳转到 55 行，56 - 59 将 <code>0x3D</code> 与 <code>0x47</code> 放入栈中：</p><pre data-type="codeBlock" text="| 0x74135154 | 0x3D | 0x47
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span> <span class="hljs-operator">|</span> <span class="hljs-number">0x3D</span> <span class="hljs-operator">|</span> <span class="hljs-number">0x47</span>
</code></pre><p>其中，<code>0x47</code>，即 71 是 <code>func1</code> 方法体所在的字节码开始位置。</p><p>60 行 <code>JUMP</code> 跳转到 71 行执行 <code>func1</code> 方法体，此时栈为：</p><pre data-type="codeBlock" text="| 0x74135154 | 0x3D
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0x74135154</span> <span class="hljs-operator">|</span> <span class="hljs-number">0x3D</span>
</code></pre><p>由于 <code>func1</code> 内容为空，因此不需要执行什么操作。</p><p>72 行获取栈顶元素 <code>0x3D</code>，即 61，并跳转。</p><p>61 - 62 行这里会进行方法调用的结尾工作，通过 <code>STOP</code> 结束。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">小结</h2><p>这篇文章我们学习了合约方法调用在字节码层面的实现。我们通过一个简单的合约，了解了合约方法调用过程中，用户的请求是如何匹配到具体的方法的。其实内容也比较简单，就是在栈中与所有的方法签名哈希值一个一个进行比对，如果相同，则跳转到相应的部分执行对应方法内容。这里还是建议大家都亲手去 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.evm.codes/playground">evm.codes</a> 这个网站去实际操作一下，感受整个流程中内存与栈内容的变化，会加深对整个流程的理解。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/" data="{&quot;provider_url&quot;:&quot;http://leftasexercise.com&quot;,&quot;description&quot;:&quot;In the last post, we have seen how the Solidity compiler creates code - the init bytecode - to prepare and deploy the actual bytecode executed at runtime. Today, we will look at a few standard patterns that we find when looking at this runtime bytecode.&quot;,&quot;title&quot;:&quot;A deep-dive into Solidity - function selectors, encoding and state variables&quot;,&quot;author_name&quot;:&quot;christianb93&quot;,&quot;thumbnail_width&quot;:440,&quot;url&quot;:&quot;https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp&quot;,&quot;author_url&quot;:&quot;https://leftasexercise.com/author/christianb93/&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;LeftAsExercise&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:293,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:440,&quot;height&quot;:293,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>A deep-dive into Solidity - function selectors, encoding and state variables</h2><p>In the last post, we have seen how the Solidity compiler creates code - the init bytecode - to prepare and deploy the actual bytecode executed at runtime. Today, we will look at a few standard patterns that we find when looking at this runtime bytecode.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>http://leftasexercise.com</span></div><img src="https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp"/></div></a></div></div><div data-type="embedly" src="https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy?utm_source=%2Fprofile%2F80455042-noxx&amp;utm_medium=reader2" data="{&quot;provider_url&quot;:&quot;https://noxx.substack.com&quot;,&quot;description&quot;:&quot;Digging deep into the EVM mechanics during contract function calls&quot;,&quot;title&quot;:&quot;EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 1&quot;,&quot;author_name&quot;:&quot;noxx&quot;,&quot;url&quot;:&quot;https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/76799b336644e7786cc394c6e2467c03e6080db3551e9f668dd5ddf769518b68.jpg&quot;,&quot;thumbnail_width&quot;:489,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Substack&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:538,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:489,&quot;height&quot;:538,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/76799b336644e7786cc394c6e2467c03e6080db3551e9f668dd5ddf769518b68.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/76799b336644e7786cc394c6e2467c03e6080db3551e9f668dd5ddf769518b68.jpg"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy?utm_source=%2Fprofile%2F80455042-noxx&amp;utm_medium=reader2" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 1</h2><p>Digging deep into the EVM mechanics during contract function calls</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://noxx.substack.com</span></div><img src="https://storage.googleapis.com/papyrus_images/76799b336644e7786cc394c6e2467c03e6080db3551e9f668dd5ddf769518b68.jpg"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[深入理解 EVM（二）]]></title>
            <link>https://paragraph.com/@xyyme/evm-2</link>
            <guid>tSCdpZHJd8eeXPpLZfC4</guid>
            <pubDate>Fri, 29 Jul 2022 14:09:37 GMT</pubDate>
            <description><![CDATA[上篇文章我们简要介绍了一下合约的字节码构造以及内存布局，今天我们来从字节码层面聊聊合约的部署过程。编译合约我们来看一个例子：// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; contract Demo { constructor() {} } 这个合约比上篇文章的还要简单，它什么内容都没有，仅仅有一个合约的框架而已，这样我们就可以不用关心一些额外内容比如变量赋值等，只需关心合约部署的最核心步骤。 先使用 solc 进行编译：solc Demo.sol --bin编译后的字节码为：6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220c6a9021ede10c2befd51f04c8bcd9294ea47f79868b9ad6d81af3e41d3b2c2bf64736f6c634300080f0033 按照 0xfe 操作符分割后的结果为：6080604052348...]]></description>
            <content:encoded><![CDATA[<p>上篇<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/GNVcUgKAOEiLyClKeqkmD35ctLu6_XomT3ZDIfV3tz8">文章</a>我们简要介绍了一下合约的字节码构造以及内存布局，今天我们来从字节码层面聊聊合约的部署过程。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">编译合约</h2><p>我们来看一个例子：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

contract Demo {
    constructor() {}
}
"><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.15;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{}
}
</code></pre><p>这个合约比上篇文章的还要简单，它什么内容都没有，仅仅有一个合约的框架而已，这样我们就可以不用关心一些额外内容比如变量赋值等，只需关心合约部署的最核心步骤。</p><p>先使用 <code>solc</code> 进行编译：</p><blockquote><p>solc Demo.sol --bin</p></blockquote><p>编译后的字节码为：</p><pre data-type="codeBlock" text="6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220c6a9021ede10c2befd51f04c8bcd9294ea47f79868b9ad6d81af3e41d3b2c2bf64736f6c634300080f0033
"><code>6080604052348015600f57600080fd5b50603f80601d6000396000f3fe6080604052600080fdfea2646970667358221220c6a9021ede10c2befd51f04c8bcd9294ea47f79868b9ad6d81af3e41d3b2c2b<span class="hljs-type">f64</span>736f6c634300080f0033
</code></pre><p>按照 <code>0xfe</code> 操作符分割后的结果为：</p><pre data-type="codeBlock" text="6080604052348015600f57600080fd5b50603f80601d6000396000f3（init bytecode）

6080604052600080fd（runtime bytecode）

a2646970667358221220c6a9021ede10c2befd51f04c8bcd9294ea47f79868b9ad6d81af3e41d3b2c2bf64736f6c634300080f0033（metadata hash）
"><code>6080604052348015600f57600080fd5b50603f80601d6000396000f3（init bytecode）

6080604052600080fd（runtime bytecode）

a2646970667358221220c6a9021ede10c2befd51f04c8bcd9294ea47f79868b9ad6d81af3e41d3b2c2bf64736f6c634300080f0033（metadata <span class="hljs-built_in">hash</span>）
</code></pre><p>其中 <code>init bytecode</code> 部分就是合约的部署流程，我们主要着重于这部分。</p><p>我们前面说过，这些字节码对应的其实都是 opcodes，也就是操作符和操作数，我们可以通过</p><blockquote><p>solc Demo.sol --opcodes</p></blockquote><p>来得到对应的 opcodes：</p><pre data-type="codeBlock" text="PUSH1 0x80 PUSH1 0x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 0xF JUMPI PUSH1 0x0 DUP1 REVERT JUMPDEST POP PUSH1 0x3F DUP1 PUSH1 0x1D PUSH1 0x0 CODECOPY PUSH1 0x0 RETURN INVALID PUSH1 0x80 PUSH1 0x40 MSTORE PUSH1 0x0 DUP1 REVERT INVALID LOG2 PUSH5 0x6970667358 0x22 SLT KECCAK256 0xC6 0xA9 MUL 0x1E 0xDE LT 0xC2 0xBE REVERT MLOAD CREATE 0x4C DUP12 0xCD SWAP3 SWAP5 0xEA SELFBALANCE 0xF7 SWAP9 PUSH9 0xB9AD6D81AF3E41D3B2 0xC2 0xBF PUSH5 0x736F6C6343 STOP ADDMOD 0xF STOP CALLER
"><code>PUSH1 <span class="hljs-number">0</span>x80 PUSH1 <span class="hljs-number">0</span>x40 MSTORE CALLVALUE DUP1 ISZERO PUSH1 <span class="hljs-number">0</span>xF JUMPI PUSH1 <span class="hljs-number">0</span>x0 DUP1 REVERT JUMPDEST POP PUSH1 <span class="hljs-number">0</span>x3F DUP1 PUSH1 <span class="hljs-number">0</span>x1D PUSH1 <span class="hljs-number">0</span>x0 CODECOPY PUSH1 <span class="hljs-number">0</span>x0 <span class="hljs-keyword">RETURN</span> INVALID PUSH1 <span class="hljs-number">0</span>x80 PUSH1 <span class="hljs-number">0</span>x40 MSTORE PUSH1 <span class="hljs-number">0</span>x0 DUP1 REVERT INVALID LOG2 PUSH5 <span class="hljs-number">0</span>x6970667358 <span class="hljs-number">0</span>x22 SLT KECCAK256 <span class="hljs-number">0</span>xC6 <span class="hljs-number">0</span>xA9 MUL <span class="hljs-number">0</span>x1E <span class="hljs-number">0</span>xDE LT <span class="hljs-number">0</span>xC2 <span class="hljs-number">0</span>xBE REVERT MLOAD CREATE <span class="hljs-number">0</span>x4C DUP12 <span class="hljs-number">0</span>xCD SWAP3 SWAP5 <span class="hljs-number">0</span>xEA SELFBALANCE <span class="hljs-number">0</span>xF7 SWAP9 PUSH9 <span class="hljs-number">0</span>xB9AD6D81AF3E41D3B2 <span class="hljs-number">0</span>xC2 <span class="hljs-number">0</span>xBF PUSH5 <span class="hljs-number">0</span>x736F6C6343 <span class="hljs-keyword">STOP</span> ADDMOD <span class="hljs-number">0</span>xF <span class="hljs-keyword">STOP</span> CALLER
</code></pre><p>我们将 <code>init bytecode</code> 部分的 16 进制字节码与其 opcodes 一一对应起来：</p><pre data-type="codeBlock" text="0  -&gt; 60 PUSH1
1  -&gt; 80 0x80
2  -&gt; 60 PUSH1
3  -&gt; 40 0x40
4  -&gt; 52 MSTORE
5  -&gt; 34 CALLVALUE
6  -&gt; 80 DUP1
7  -&gt; 15 ISZERO
8  -&gt; 60 PUSH1
9  -&gt; 0f 0xF
10 -&gt; 57 JUMPI
11 -&gt; 60 PUSH1
12 -&gt; 00 0x0
13 -&gt; 80 DUP1
14 -&gt; fd REVERT
15 -&gt; 5b JUMPDEST
16 -&gt; 50 POP
17 -&gt; 60 PUSH1
18 -&gt; 3f 0x3F
19 -&gt; 80 DUP1
20 -&gt; 60 PUSH1
21 -&gt; 1d 0x1D
22 -&gt; 60 PUSH1
23 -&gt; 00 0x0
24 -&gt; 39 CODECOPY
25 -&gt; 60 PUSH1
26 -&gt; 00 0x0
27 -&gt; f3 RETURN
"><code><span class="hljs-number">0</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">1</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> <span class="hljs-number">0x80</span>
<span class="hljs-number">2</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">3</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">40</span> <span class="hljs-number">0x40</span>
<span class="hljs-number">4</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">52</span> MSTORE
<span class="hljs-number">5</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">34</span> CALLVALUE
<span class="hljs-number">6</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">7</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">15</span> ISZERO
<span class="hljs-number">8</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">9</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> 0f <span class="hljs-number">0xF</span>
<span class="hljs-number">10</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">57</span> JUMPI
<span class="hljs-number">11</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">12</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">13</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">14</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> fd REVERT
<span class="hljs-number">15</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 5b JUMPDEST
<span class="hljs-number">16</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">50</span> POP
<span class="hljs-number">17</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">18</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 3f <span class="hljs-number">0x3F</span>
<span class="hljs-number">19</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">20</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">21</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 1d <span class="hljs-number">0x1D</span>
<span class="hljs-number">22</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">23</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">24</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">39</span> CODECOPY
<span class="hljs-number">25</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">26</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">27</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> f3 RETURN
</code></pre><p>我们今天的目标就是把这些字节码搞懂，看看它都干了些什么。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约部署流程</h2><p>开头的 0 → 4 是我们前面讲过的加载空闲内存指针的过程，即 <code>MSTORE(0x40, 0x80)</code>，此时内存中的数据为（第三行的最后为 80）：</p><pre data-type="codeBlock" text="0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000000
"><code></code></pre><p>每一行的长度是 64，即 32 个字节。我们前面说过 <code>0x40</code> - <code>0x5f</code> 内存区域是用来存储空闲内存指针的，对照上面结果，确实如此。</p><p>接下来，第 5 行的 <code>CALLVALUE</code>，获取创建合约时发送的 ETH 数量，并存入栈中。我们创建该合约并不需要同时发送 ETH，因此值为 0。此时的栈中数据为（左边是栈底，右边是栈顶，下同）：</p><pre data-type="codeBlock" text="| 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>第 6 行的 <code>DUP1</code>，会取栈顶第一个元素，也就是 0，并且复制一份，再放入栈中。此时，栈中数据为：</p><pre data-type="codeBlock" text="| 0 | 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>第 7 行 <code>ISZERO</code>，取出栈顶元素，判断是否为 0，若为 0，返回 1，否则返回 0，最后将结果放入栈中。此时，栈为：</p><pre data-type="codeBlock" text="| 0 | 1
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span> <span class="hljs-number">1</span>
</code></pre><p>第 8、9 行，将 0xF 放入栈中。此时，栈为：</p><pre data-type="codeBlock" text="| 0 | 1 | F
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span> <span class="hljs-number">1</span> <span class="hljs-operator">|</span> F
</code></pre><p>第 10 行，<code>JUMPI</code> 会取出栈顶的两个元素，并将其作为参数，即 <code>JUMPI(F, 1)</code>，如果第二个参数是 1，则跳转到第一个参数，也就是 <code>F</code> 位置，如果不是 1，则不跳转。这里需要跳转，<code>F</code> 是 16 进制中的 15，因此需要跳转到 15 行。这个 15 行，也就是从 0 开始第 15 个字节处。每个操作符都消耗一个字节，操作数也同样消耗字节。此时，栈为：</p><pre data-type="codeBlock" text="| 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>第 15 行是 <code>JUMPDEST</code>，是跳转的目标位，也就是说，凡是 <code>JUMPI</code> 指令进行跳转，跳转的目标必须是它，它本身并没有什么实际作用，仅仅用做标记位。如果跳转至此，没有该操作符，则流程失败。</p><p>第 16 行，<code>POP</code> 会弹出栈顶的元素，即弹出 0。此时栈为空：</p><pre data-type="codeBlock" text="|
"><code><span class="hljs-operator">|</span>
</code></pre><p>第 17、18 行，<code>PUSH1 3F</code>，即将 <code>0x3F</code> 放入栈中。此时，栈为：</p><pre data-type="codeBlock" text="| 3F
"><code><span class="hljs-operator">|</span> <span class="hljs-number">3</span><span class="hljs-built_in">F</span>
</code></pre><p>第 19 行，<code>DUP1</code> 复制栈顶元素。此时，栈为：</p><pre data-type="codeBlock" text="| 3F | 3F
"><code><span class="hljs-operator">|</span> <span class="hljs-number">3</span><span class="hljs-built_in">F</span> <span class="hljs-operator">|</span> <span class="hljs-number">3</span><span class="hljs-built_in">F</span>
</code></pre><p>第 20 到 23 行，连续将两个元素 <code>0x1D</code>、<code>0x0</code> 放入栈中。此时，栈为：</p><pre data-type="codeBlock" text="| 3F | 3F | 1D | 0
"><code><span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> 1D <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>第 24 行，<code>CODECOPY</code>，将当前运行环境中的字节码复制到内存中，接收 3 个参数，分别是目标内存位置、要复制数据的起始位置、复制长度。这里是 <code>CODECOPY(0, 1D, 3F)</code>，即从字节码中的 <code>1D</code> 起始，复制长度为 <code>3F</code> 的字节码到内存位置 0 处。</p><p>我们来看看这几个参数是什么意思，首先第一个参数是说将结果都放在 0 开始的内存处，这个好理解。第二个参数 <code>1D</code>，换成 10 进制是 <code>29</code>，我们前面看的 <code>init bytecode</code> 部分一共有 27 行（算上 0 行有 28 行），由于我们是按照 <code>0xfe</code> 来将其分割的，因此按照全部字节码来说，<code>0xfe</code> 是位于第 28 行的，而第 29 行开始的部分就是 <code>runtime bytecode</code> 了。也就是说，我们需要从 <code>runtime bytecode</code> 处开始复制代码，长度是 <code>0x3F</code>，即 10 进制的 <code>63</code>。我们再来看整体的字节码，从 <code>runtime bytecode</code> 处的 <code>6080604052…</code> 开始的字节码，一直到最后，长度是 <code>126</code>，恰好是 <code>63</code> 个字节。</p><p>也就是说，我们将除了 <code>init bytecode</code> 部分的 <code>runtime bytecode</code> 与 <code>metadata hash</code> 复制到了内存中。此时，内存中数据为：</p><pre data-type="codeBlock" text="6080604052600080fdfea2646970667358221220c6a9021ede10c2befd51f04c
8bcd9294ea47f79868b9ad6d81af3e41d3b2c2bf64736f6c634300080f003300
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000000
"><code>6080604052600080fdfea2646970667358221220c6a9021ede10c2befd51f04c
8bcd9294ea47f79868b9ad6d81af3e41d3b2c2b<span class="hljs-type">f64</span>736f6c634300080f003300
<span class="hljs-number">0000000000000000000000000000000000000000000000000000000000000080</span>
<span class="hljs-number">0000000000000000000000000000000000000000000000000000000000000000</span>
</code></pre><p>同时，栈中数据为：</p><pre data-type="codeBlock" text="| 3F
"><code><span class="hljs-operator">|</span> <span class="hljs-number">3</span><span class="hljs-built_in">F</span>
</code></pre><p>接下来，第 25、26 行，<code>PUSH1 0x0</code> 将 <code>0</code> 放入了栈中。此时，栈为：</p><pre data-type="codeBlock" text="| 3F | 0
"><code><span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>最后，第 27 行，<code>RETURN</code> 从内存中读取数据并返回给 EVM，并结束流程。它接收两个参数，分别是起始位置以及长度，即 <code>RETURN(0, 3F)</code>。前面我们说了 <code>0x3F</code> 是 <code>runtime bytecode</code> 与 <code>metadata hash</code> 的长度，也就说从内存中读取了这两部分的数据，并返回。即返回值为：</p><pre data-type="codeBlock" text="6080604052600080fdfea2646970667358221220c6a9021ede10c2befd51f04c
8bcd9294ea47f79868b9ad6d81af3e41d3b2c2bf64736f6c634300080f0033
"><code>6080604052600080fdfea2646970667358221220c6a9021ede10c2befd51f04c
8bcd9294ea47f79868b9ad6d81af3e41d3b2c2b<span class="hljs-type">f64</span>736f6c634300080f0033
</code></pre><p>此时，栈中数据已经清空。</p><p>现在，我们已经走完了部署的整个流程，可以看到，整个部署的流程主要就是简单的两步：</p><ol><li><p>运行构造函数的逻辑</p></li><li><p>获取 <code>runtime bytecode</code> 与 <code>metadata hash</code> 的内容并返回给 EVM</p></li></ol><p>接下来我们回到第 5 行，这里我们获取了 <code>CALLVALUE</code>，前面的流程中值为 0。现在我们假设在创建合约的同时也发送了 ETH，那么这里得到的就是非零值，这里我们假设是在创建合约时同时发送了 1 wei（只要是非零值结果都一样），那么栈中为：</p><pre data-type="codeBlock" text="| 1
"><code><span class="hljs-operator">|</span> <span class="hljs-number">1</span>
</code></pre><p>第 6 行 <code>DUP1</code>，复制后栈为：</p><pre data-type="codeBlock" text="| 1 | 1
"><code><span class="hljs-operator">|</span> <span class="hljs-number">1</span> <span class="hljs-operator">|</span> <span class="hljs-number">1</span>
</code></pre><p>第 7 行 <code>ISZERO</code>，由于栈顶非零，因此结果为 0，此时栈中为：</p><pre data-type="codeBlock" text="| 1 | 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">1</span> <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>第 8、9 行，将 0xF 放入栈中。此时，栈为：</p><pre data-type="codeBlock" text="| 1 | 0 | F
"><code><span class="hljs-operator">|</span> <span class="hljs-number">1</span> <span class="hljs-operator">|</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span> F
</code></pre><p>第 10 行，<code>JUMPI(F, 0)</code>，由于第二个参数是 0，因此不跳转，继续向下执行。</p><p>第 11、12 行，<code>PUSH1 0x0</code> 将 <code>0</code> 放入栈中：</p><pre data-type="codeBlock" text="| 1 | 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">1</span> <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>第 13 行 <code>DUP1</code>，此时栈为：</p><pre data-type="codeBlock" text="| 1 | 0 | 0
"><code><span class="hljs-operator">|</span> <span class="hljs-number">1</span> <span class="hljs-operator">|</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>第 14 行 <code>REVERT</code>，抛出错误，流程以失败结束。</p><p>那么我们说的这个 <code>CALLVALUE</code> 0 或者非 0，到底是什么意思呢？</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

contract Demo {
    constructor() {}
}
"><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.15;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{}
}
</code></pre><p>看看代码，其实就是因为合约中的构造函数没有 <code>payable</code> 关键字，禁止向合约中发送 ETH。因此在字节码中需要对此进行判断。那么我们就会想到，如果加上了 <code>payable</code> 之后，字节码会怎样呢？</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

contract Demo {
    constructor() 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.15;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{}
}
</code></pre><p>编译后的字节码：</p><pre data-type="codeBlock" text="6080604052603f8060116000396000f3（init bytecode）

6080604052600080fd（runtime bytecode）

a26469706673582212208c516264e3e6785d014f2db856266d96d155fdd72ef5e53ecc896b5837f13cc664736f6c634300080f0033（metadata hash）
"><code>6080604052603f8060116000396000f3（init bytecode）

6080604052600080fd（runtime bytecode）

a26469706673582212208c516264e3e6785d014f2db856266d96d155fdd72ef5e53ecc896b5837f13cc664736f6c634300080f0033（metadata <span class="hljs-built_in">hash</span>）
</code></pre><p>这时我们看到 <code>init bytecode</code> 部分短了很多，其实就是去掉了判断 <code>CALLVALUE</code> 的部分。我们再将其与 opcodes 一一对应：</p><pre data-type="codeBlock" text="0  -&gt; 60 PUSH1
1  -&gt; 80 0x80
2  -&gt; 60 PUSH1
3  -&gt; 40 0x40
4  -&gt; 52 MSTORE
5  -&gt; 60 PUSH1
6  -&gt; 3f 0x3F
7  -&gt; 80 DUP1
8  -&gt; 60 PUSH1
9  -&gt; 11 0x11
10 -&gt; 60 PUSH1
11 -&gt; 00 0x0
12 -&gt; 39 CODECOPY
13 -&gt; 60 PUSH1
14 -&gt; 00 0x0
15 -&gt; f3 RETURN
"><code><span class="hljs-number">0</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">1</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> <span class="hljs-number">0x80</span>
<span class="hljs-number">2</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">3</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">40</span> <span class="hljs-number">0x40</span>
<span class="hljs-number">4</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">52</span> MSTORE
<span class="hljs-number">5</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">6</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> 3f <span class="hljs-number">0x3F</span>
<span class="hljs-number">7</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">80</span> DUP1
<span class="hljs-number">8</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">9</span>  <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">11</span> <span class="hljs-number">0x11</span>
<span class="hljs-number">10</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">11</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">12</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">39</span> CODECOPY
<span class="hljs-number">13</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> <span class="hljs-number">60</span> PUSH1
<span class="hljs-number">14</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> 00 <span class="hljs-number">0x0</span>
<span class="hljs-number">15</span> <span class="hljs-operator">-</span><span class="hljs-operator">></span> f3 RETURN
</code></pre><p>我们再简单过一下流程。</p><p>0 → 4: <code>MSTORE(0x40, 0x80)</code>，内存为：</p><pre data-type="codeBlock" text="0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000000
"><code></code></pre><p>5 → 6: <code>PUSH1 0x3F</code></p><pre data-type="codeBlock" text="| 3F
"><code><span class="hljs-operator">|</span> <span class="hljs-number">3</span><span class="hljs-built_in">F</span>
</code></pre><p>7: <code>DUP1</code></p><pre data-type="codeBlock" text="| 3F | 3F
"><code><span class="hljs-operator">|</span> <span class="hljs-number">3</span><span class="hljs-built_in">F</span> <span class="hljs-operator">|</span> <span class="hljs-number">3</span><span class="hljs-built_in">F</span>
</code></pre><p>8 → 9: <code>PUSH1 0x11</code></p><pre data-type="codeBlock" text="| 3F | 3F | 11
"><code><span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> <span class="hljs-number">11</span>
</code></pre><p>10 → 11: <code>PUSH1 0x0</code></p><pre data-type="codeBlock" text="| 3F | 3F | 11 | 0
"><code><span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> <span class="hljs-number">11</span> <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>12: <code>CODECOPY(0, 11, 3F)</code>，此时内存为：</p><pre data-type="codeBlock" text="6080604052600080fdfea26469706673582212208c516264e3e6785d014f2db8
56266d96d155fdd72ef5e53ecc896b5837f13cc664736f6c634300080f003300
0000000000000000000000000000000000000000000000000000000000000080
0000000000000000000000000000000000000000000000000000000000000000
"><code></code></pre><p>13 → 14: <code>PUSH1 0x0</code></p><pre data-type="codeBlock" text="| 3F | 0
"><code><span class="hljs-operator">|</span> 3F <span class="hljs-operator">|</span> <span class="hljs-number">0</span>
</code></pre><p>15: <code>RETURN(0, 3F)</code>，内存中前 <code>3F</code> 个字节，即</p><pre data-type="codeBlock" text="6080604052600080fdfea26469706673582212208c516264e3e6785d014f2db8
56266d96d155fdd72ef5e53ecc896b5837f13cc664736f6c634300080f0033
"><code></code></pre><p>我们看到，由于构造函数中有 <code>payable</code> 关键字，也就是发不发送 ETH 都可以，对应于字节码的整个流程中确实没有了对于 <code>CALLVALUE</code> 的判断。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">实际部署合约</h2><p>我们从字节码方面走完了整个部署流程，那么我们来实际部署一个合约，来看看具体交易的细节。仍然使用最开始的合约：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

contract Demo {
    constructor() {}
}
"><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.15;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{}
}
</code></pre><p>通过 <code>Remix</code> 或者其它工具进行部署。我在 <code>Rinkeby</code> 上部署了合约，部署的交易哈希是：</p><pre data-type="codeBlock" text="0x039c306e0ac3ad5f8a972185d2094a977b1bef97694e2872f975f70e6db4a241
"><code></code></pre><p>利用 <code>Ethersjs</code> 读取交易详情：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7c6742798ab495af6bb38320a37005168034a3820932f892140eb43fbaf2bef9.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>从图中注意到两点，一个是 <code>to</code> 字段为空，一个是 <code>data</code> 字段恰好就是我们前面编译合约之后得到的字节码，这也正是部署合约交易的两个特点。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">小结</h2><p>这篇文章我们学习了字节码层面的合约部署流程，我们以最简单的合约为例，逐一走过了整个流程。对于有更复杂操作的合约，原理都一样，只不过是多了一些变量赋值等操作。这里强烈推荐 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.evm.codes/playground">evm.codes</a> 这个网站，将字节码放进去，我们就可以一步一步观察到实际的内存与栈中的数据，分析字节码会更加直观。</p><p>下篇文章我们来聊聊合约方法调用的过程，仍然是从字节码层面分析，相对会更加复杂一点，不过也更加有趣。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://leftasexercise.com/2021/09/05/a-deep-dive-into-solidity-contract-creation-and-the-init-code/" data="{&quot;provider_url&quot;:&quot;http://leftasexercise.com&quot;,&quot;description&quot;:&quot;In some of the previous posts in this series, we have already touched upon contract creation and referred to the fact that during contract creation, an init bytecode is sent as part of a transaction which is supposed to return the actual bytecode of the smart contract.&quot;,&quot;title&quot;:&quot;A deep-dive into Solidity - contract creation and the init code&quot;,&quot;author_name&quot;:&quot;christianb93&quot;,&quot;thumbnail_width&quot;:440,&quot;url&quot;:&quot;https://leftasexercise.com/2021/09/05/a-deep-dive-into-solidity-contract-creation-and-the-init-code/&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/54bca64a90c2b5db85a414d297cda0a9f1ff5d54018f3010634b30916290a872.webp&quot;,&quot;author_url&quot;:&quot;https://leftasexercise.com/author/christianb93/&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;LeftAsExercise&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:248,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:440,&quot;height&quot;:248,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/54bca64a90c2b5db85a414d297cda0a9f1ff5d54018f3010634b30916290a872.webp&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/54bca64a90c2b5db85a414d297cda0a9f1ff5d54018f3010634b30916290a872.webp"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://leftasexercise.com/2021/09/05/a-deep-dive-into-solidity-contract-creation-and-the-init-code/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>A deep-dive into Solidity - contract creation and the init code</h2><p>In some of the previous posts in this series, we have already touched upon contract creation and referred to the fact that during contract creation, an init bytecode is sent as part of a transaction which is supposed to return the actual bytecode of the smart contract.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>http://leftasexercise.com</span></div><img src="https://storage.googleapis.com/papyrus_images/54bca64a90c2b5db85a414d297cda0a9f1ff5d54018f3010634b30916290a872.webp"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[深入理解 EVM（一）]]></title>
            <link>https://paragraph.com/@xyyme/evm</link>
            <guid>pdnN9wV0HKo2B09B7spU</guid>
            <pubDate>Wed, 20 Jul 2022 09:47:15 GMT</pubDate>
            <description><![CDATA[今天我们来聊聊 EVM，那么什么是 EVM？EVM 其实就是执行 bytecode（字节码）的机器，它的全称是 Ethereum Virtual Machine（以太坊虚拟机），和 Java 的 JVM 很类似。我们平时写合约都是用 Solidity （或者 Vyper）编写的，但是这种语言机器是没有办法理解的，我们需要先使用编译器进行编译，编译后的结果是一串二进制码，EVM 可以理解这些二进制的东西，因此它就可以执行这些代码，从而完成一笔交易。合约编译我们用一个简单的图示来解释这个过程：我们从一个很简单的例子开始（文件命名为 Demo.sol）：// SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; contract Demo { uint256 a; constructor() { a = 1; } } 我们平时开发项目用的都是 hardhat，forge 这种框架，他们的底层都是通过 solc 来进行编译。这次我们就直接使用编译工具 solc 来进行编译，可以参考这里进行安装。这里列出 Mac 的安装方法...]]></description>
            <content:encoded><![CDATA[<p>今天我们来聊聊 EVM，那么什么是 EVM？EVM 其实就是执行 bytecode（字节码）的机器，它的全称是 Ethereum Virtual Machine（以太坊虚拟机），和 Java 的 JVM 很类似。我们平时写合约都是用 Solidity （或者 Vyper）编写的，但是这种语言机器是没有办法理解的，我们需要先使用编译器进行编译，编译后的结果是一串二进制码，EVM 可以理解这些二进制的东西，因此它就可以执行这些代码，从而完成一笔交易。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约编译</h2><p>我们用一个简单的图示来解释这个过程：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c559b03091f56aabbecc5b8338a139b4bc9f9cc129beb8d868c88c2b0cb4548d.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>我们从一个很简单的例子开始（文件命名为 Demo.sol）：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.15;

contract Demo {
    uint256 a;
    constructor() {
      a = 1;
    }
}
"><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.15;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> </span>{
    <span class="hljs-keyword">uint256</span> a;
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
      a <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
    }
}
</code></pre><p>我们平时开发项目用的都是 <code>hardhat</code>，<code>forge</code> 这种框架，他们的底层都是通过 <code>solc</code> 来进行编译。这次我们就直接使用编译工具 <code>solc</code> 来进行编译，可以参考<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/v0.8.15/installing-solidity.html">这里</a>进行安装。这里列出 Mac 的安装方法：</p><pre data-type="codeBlock" text="brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity
"><code>brew update
brew upgrade
brew tap ethereum/ethereum
brew install solidity
</code></pre><p>安装完成后，我们来编译试试，使用下面的命令：</p><blockquote><p>solc Demo.sol --bin</p></blockquote><p>输出以下内容：</p><blockquote><p>======= Demo.sol:Demo ======= Binary: 6080604052348015600f57600080fd5b506001600081905550603f8060256000396000f3fe6080604052600080fdfea26469706673582212204ca38d4a605f03f1487b9cb337c0853cca3c62a6c42f942ecb021fb7357002b564736f6c634300080f0033</p></blockquote><p>我们看到在结果中输出了一串 16 进制字符，这就是上面的合约经过编译过后的字节码。我们前面说过，字节码是一串二进制的字符，这里显示为 16 进制方便阅读。注意到我们在命令中指定了 <code>--bin</code> 参数，因此输出的是 16 进制。</p><p>第一眼看见这串字符是不是已经懵了，别慌，我们慢慢来研究。首先，我们需要知道，EVM 的核心实际上是一个 stack machine（栈机器），它会接受操作符和操作数，学过数据结构的朋友应该都了解栈的原理。其次，上面这些字符都是由操作符和操作数组成的。例如开头的 <code>60</code> 代表的是 <code>PUSH1</code>，也就是将后面的一个字节（这里是 <code>80</code>）压入栈中。后面又是一个 <code>60</code>，接着是 <code>40</code>，即代表将 <code>40</code> 压入栈中。后面是 <code>52</code>，代表 <code>MSTORE</code>，它需要消耗两个操作数，需要从栈中获取。也就是说，上面的 <code>6080604052</code>，就代表着：</p><pre data-type="codeBlock" text="PUSH1 0x80
PUSH1 0x40
MSTORE
"><code></code></pre><p>MSTORE 的两个操作数分别为 0x40、0x80，即 MSTORE(0x40, 0x80)，也就是在内存中地址 0x40 处存储了数据 0x80。（这一句不明白没关系，我们先往下看）</p><p>现在我们已经明白了字节码的基本逻辑，它就是由操作符和操作数组成的。其中两个字符代表一个字节，操作符都是一个字节，但是操作数可能有多个字节。像我们前面看到的 <code>PUSH1</code>，是将后面的一个字节压入栈中，如果是 <code>PUSH4</code>，就是将后面的四个字节压入栈中。一般将操作符称为 Opcodes，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.evm.codes/">这里</a>可以看到所有的 Opcodes。需要注意的是操作符和操作数有可能重复，例如判断 <code>60</code> 是操作符还是操作数，取决于它在字节码中的位置，并不是绝对的。例如 <code>6060</code>，前面的 <code>60</code> 是操作符，后面的就是操作数，代表 <code>PUSH1 60</code>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">字节码构造</h2><p>接下来我们看看字节码的构造，我们在上面的字节码中搜索 <code>fe</code>，可以看到其中有两个 <code>fe</code>，同时查询 Opcodes 对照表，可知 <code>fe</code> 是无效操作符（INVALID）。它的作用其实是分隔符，它将字节码分成了三部分：</p><ol><li><p>init bytecode（初始化字节码）</p></li><li><p>runtime bytecode （运行时字节码）</p></li><li><p>metadata hash（合约的一些 meta 信息哈希）</p></li></ol><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0452c9d45e8f4e895f0f8af0cb54d3fc03b9551e115fdc62214a61a5b5e7e8a1.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>那么我们前面编译的字节码就被分成了三部分：</p><pre data-type="codeBlock" text="6080604052348015600f57600080fd5b506001600081905550603f8060256000396000f3（init bytecode）

6080604052600080fd（runtime bytecode）

a26469706673582212204ca38d4a605f03f1487b9cb337c0853cca3c62a6c42f942ecb021fb7357002b564736f6c634300080f0033（metadata hash）
"><code>6080604052348015600f57600080fd5b506001600081905550603f8060256000396000f3（init bytecode）

6080604052600080fd（runtime bytecode）

a26469706673582212204ca38d4a605f03f1487b9cb337c0853cca3c62a6c42f942ecb021fb7357002b564736f6c634300080f0033（metadata <span class="hljs-built_in">hash</span>）
</code></pre><p>我们先来看看最后这里的 metadata hash，它默认是合约 metadata 文件的 IPFS 哈希值，我们可以使用：</p><blockquote><p>solc Demo.sol --metadata</p></blockquote><p>来获取到其 metadata：</p><pre data-type="codeBlock" text="{
    &quot;compiler&quot;: {
        &quot;version&quot;: &quot;0.8.15+commit.e14f2714&quot;
    },
    &quot;language&quot;: &quot;Solidity&quot;,
    &quot;output&quot;: {
        &quot;abi&quot;: [
            {
                &quot;inputs&quot;: [],
                &quot;stateMutability&quot;: &quot;nonpayable&quot;,
                &quot;type&quot;: &quot;constructor&quot;
            }
        ],
        &quot;devdoc&quot;: {
            &quot;kind&quot;: &quot;dev&quot;,
            &quot;methods&quot;: {},
            &quot;version&quot;: 1
        },
        &quot;userdoc&quot;: {
            &quot;kind&quot;: &quot;user&quot;,
            &quot;methods&quot;: {},
            &quot;version&quot;: 1
        }
    },
    &quot;settings&quot;: {
        &quot;compilationTarget&quot;: {
            &quot;Demo.sol&quot;: &quot;Demo&quot;
        },
        &quot;evmVersion&quot;: &quot;london&quot;,
        &quot;libraries&quot;: {},
        &quot;metadata&quot;: {
            &quot;bytecodeHash&quot;: &quot;ipfs&quot;
        },
        &quot;optimizer&quot;: {
            &quot;enabled&quot;: false,
            &quot;runs&quot;: 200
        },
        &quot;remappings&quot;: []
    },
    &quot;sources&quot;: {
        &quot;Demo.sol&quot;: {
            &quot;keccak256&quot;: &quot;0xf6e99f20fac61b16466088a9996227f35c4ca82119a846ba19a83698e8e126b1&quot;,
            &quot;license&quot;: &quot;UNLICENSED&quot;,
            &quot;urls&quot;: [
                &quot;bzz-raw://3fda90691cfa365f68a59d5b6bb76f8a0189153cebf93143879521ea309751b8&quot;,
                &quot;dweb:/ipfs/QmP2dVkfPWy3tAriX17tar9phGzC8gqYzutmhEur6dwdiw&quot;
            ]
        }
    },
    &quot;version&quot;: 1
}
"><code><span class="hljs-punctuation">{</span>
    <span class="hljs-attr">"compiler"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">"version"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0.8.15+commit.e14f2714"</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">"language"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"Solidity"</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">"output"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">"abi"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
            <span class="hljs-punctuation">{</span>
                <span class="hljs-attr">"inputs"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">"stateMutability"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"nonpayable"</span><span class="hljs-punctuation">,</span>
                <span class="hljs-attr">"type"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"constructor"</span>
            <span class="hljs-punctuation">}</span>
        <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"devdoc"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">"kind"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"dev"</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"methods"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"version"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"userdoc"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">"kind"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"user"</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"methods"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"version"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span>
        <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">"settings"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">"compilationTarget"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">"Demo.sol"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"Demo"</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"evmVersion"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"london"</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"libraries"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span><span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"metadata"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">"bytecodeHash"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"ipfs"</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"optimizer"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">"enabled"</span><span class="hljs-punctuation">:</span> <span class="hljs-literal"><span class="hljs-keyword">false</span></span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"runs"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">200</span>
        <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
        <span class="hljs-attr">"remappings"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">"sources"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
        <span class="hljs-attr">"Demo.sol"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
            <span class="hljs-attr">"keccak256"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"0xf6e99f20fac61b16466088a9996227f35c4ca82119a846ba19a83698e8e126b1"</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"license"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"UNLICENSED"</span><span class="hljs-punctuation">,</span>
            <span class="hljs-attr">"urls"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
                <span class="hljs-string">"bzz-raw://3fda90691cfa365f68a59d5b6bb76f8a0189153cebf93143879521ea309751b8"</span><span class="hljs-punctuation">,</span>
                <span class="hljs-string">"dweb:/ipfs/QmP2dVkfPWy3tAriX17tar9phGzC8gqYzutmhEur6dwdiw"</span>
            <span class="hljs-punctuation">]</span>
        <span class="hljs-punctuation">}</span>
    <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
    <span class="hljs-attr">"version"</span><span class="hljs-punctuation">:</span> <span class="hljs-number">1</span>
<span class="hljs-punctuation">}</span>
</code></pre><p>可以看到其中主要包含了编译器版本，ABI，IPFS 等信息。这部分了解即可，我们平时也用不到这些，详细内容可以查看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/latest/metadata.html">文档</a>。</p><p>我们前面提到了一些操作数，例如 0x40、0x80，这些数字是什么意思呢。要了解这些，我们就得先明白 EVM 中的内存布局。前面的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec">文章</a>中，我们讲解过内存布局，但是当时讲的实际上是 Storage Layout，也就是状态变量的布局结构。而我们现在要讲的是 Memory Layout。区别在于 Storage 的数据是永久存在于区块链上的，类似于计算机的硬盘数据。而 Memory 的数据只有在发起交易的时候才有，交易完毕，数据全部消失，类似于计算机的内存数据。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约内存</h2><p>Memory 的数据结构就是一个简单的字节数组，数据可以以 1 字节（8 位）或者 32 字节（256 位）为单位进行存储，读取时只能以 32 字节为单位读取，但是读取时可以从任意字节处开始读取，不限定于 32 的倍数字节。图示：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f184f12ba28424cacbd1492c35ea1d7d610e943310074e64b3fef236173b8534.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>用于操作内存的一共有 3 个操作符：</p><ul><li><p>MSTORE (x, y) - 在内存 x 处开始存储 32 字节的数据 y</p></li><li><p>MLOAD (x) - 将内存 x 处开始的 32 字节数据加载到栈中</p></li><li><p>MSTORE8 (x, y) - 在内存 x 处存储 1 字节数据 y（32字节栈值中的最低有效字节）</p></li></ul><p>Solidity 中预留了 4 个 32 字节的插槽（slot），分别是：</p><ul><li><p><code>0x00</code> - <code>0x3f</code> (64 字节): 哈希方法的暂存空间</p></li><li><p><code>0x40</code> - <code>0x5f</code> (32 字节): 当前已分配内存大小 (也称为空闲内存指针)</p></li><li><p><code>0x60</code> - <code>0x7f</code> (32 字节): 零槽，用作动态内存数组的初始值，永远不能写入值</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/49a14371575dd4f319e9eae52d81863d4813d5b58f1f445a5c019a5a7b1d0f64.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>这里面最重要的就是中间这一项，也就是空闲指针。它会指向空闲空间的开始位置，也就是说，要将一个新变量写入内存，给它分配的位置就是空闲指针所指向的位置。需要注意的是，Solidity 中的内存是不会被释放（free）的。</p><p>对于空闲指针，它的更新遵守了很简单的原则：</p><blockquote><p>新的空闲指针位置 = 旧的空闲指针位置 + 分配的数据大小</p></blockquote><p>上图中我们看到，Solidity 的预留空间已经占据了 128 个字节，因此空闲指针的起始位置就只能从 0x80（128字节） 开始。空闲指针本身是存在于 0x40 位置的。由于我们在函数中的操作均需要在内存中进行，因此首要任务就是要通过空闲指针分配内存，所以我们前面才需要使用 <code>6080604052</code>，也就是 <code>MSTORE(0x40, 0x80)</code>，来加载空闲指针。此时是不是已经有些明白为什么所有的合约都是以 <code>6080604052</code> 开头了（有些老版本合约以 <code>6060604052</code> 开头）。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">小结</h2><p>这篇文章我们就先介绍到这里，我们学习了合约的编译过程，字节码的构造，以及合约的内存分布。可以多看几遍消化消化。下篇文章我们将介绍合约的部署，也就是 init bytecode 部分，了解 EVM 在合约部署时的运行逻辑。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/" data="{&quot;provider_url&quot;:&quot;http://leftasexercise.com&quot;,&quot;description&quot;:&quot;In the last post, we have seen how the Solidity compiler creates code - the init bytecode - to prepare and deploy the actual bytecode executed at runtime. Today, we will look at a few standard patterns that we find when looking at this runtime bytecode.&quot;,&quot;title&quot;:&quot;A deep-dive into Solidity - function selectors, encoding and state variables&quot;,&quot;author_name&quot;:&quot;christianb93&quot;,&quot;thumbnail_width&quot;:440,&quot;url&quot;:&quot;https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp&quot;,&quot;author_url&quot;:&quot;https://leftasexercise.com/author/christianb93/&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;LeftAsExercise&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:293,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:440,&quot;height&quot;:293,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://leftasexercise.com/2021/09/08/a-deep-dive-into-solidity-function-selectors-encoding-and-state-variables/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>A deep-dive into Solidity - function selectors, encoding and state variables</h2><p>In the last post, we have seen how the Solidity compiler creates code - the init bytecode - to prepare and deploy the actual bytecode executed at runtime. Today, we will look at a few standard patterns that we find when looking at this runtime bytecode.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>http://leftasexercise.com</span></div><img src="https://storage.googleapis.com/papyrus_images/10d90edfc0c38d43ffdf6c2314576bd9096bee4ff0da1bfd79139eacd83dd55b.webp"/></div></a></div></div><div data-type="embedly" src="https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy-d6b" data="{&quot;provider_url&quot;:&quot;https://noxx.substack.com&quot;,&quot;description&quot;:&quot;Let&apos;s take a trip down memory lane&quot;,&quot;title&quot;:&quot;EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 2&quot;,&quot;author_name&quot;:&quot;noxx&quot;,&quot;url&quot;:&quot;https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy-d6b&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/da564bc9eda253d84b99e5eb96bcf57af98a64055a9ad7017f064a7b6291cc24.jpg&quot;,&quot;thumbnail_width&quot;:1066,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Substack&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:600,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1066,&quot;height&quot;:600,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/da564bc9eda253d84b99e5eb96bcf57af98a64055a9ad7017f064a7b6291cc24.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/da564bc9eda253d84b99e5eb96bcf57af98a64055a9ad7017f064a7b6291cc24.jpg"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://noxx.substack.com/p/evm-deep-dives-the-path-to-shadowy-d6b" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>EVM Deep Dives: The Path to Shadowy Super Coder 🥷 💻 - Part 2</h2><p>Let&#x27;s take a trip down memory lane</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://noxx.substack.com</span></div><img src="https://storage.googleapis.com/papyrus_images/da564bc9eda253d84b99e5eb96bcf57af98a64055a9ad7017f064a7b6291cc24.jpg"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[XCarnival 攻击事件分析及攻击模拟重现]]></title>
            <link>https://paragraph.com/@xyyme/xcarnival</link>
            <guid>v78njqKiVoOu36npqZzg</guid>
            <pubDate>Fri, 01 Jul 2022 11:31:46 GMT</pubDate>
            <description><![CDATA[XCarnival 是一个 NFT 借贷协议，用户可以将 NFT 抵押给协议，从而借出 token。同时也可以将 token 抵押给协议，获得利息收益。 这次攻击事件的原理是，用户在将 NFT 从协议中取出后，仍然可以借出 token。黑客利用这个 bug，不断新建合约，将其作为抵押人，抵押 NFT 并取出，然后再去借出 token，从而实现无本套利。代码分析XCarnival 的核心在于三个合约：XNFT，用户抵押 NFT 入口XToken，用户借款入口P2Controller，校验用户的权限，例如订单是否可借出，用户是否可赎回等这次的 bug 在于，用户可以将 NFT 抵押到协议之后，再取出 NFT，而由于合约借贷逻辑中，没有订单的状态进行判断，因此仍然可以调用借出方法进行借贷，造成资产流失。 我们先来看看抵押的代码：注意到抵押方法的 xToken 地址是作为参数传进去的，指定要借哪个 token。 再来看看取出 NFT 的方法（由于这段代码太长，我们只截取最重要的部分）：取出 NFT （前部分）取出 NFT （后部分）我们看到，在取出 NFT 的最后，将订单的 isWith...]]></description>
            <content:encoded><![CDATA[<p>XCarnival 是一个 NFT 借贷协议，用户可以将 NFT 抵押给协议，从而借出 token。同时也可以将 token 抵押给协议，获得利息收益。</p><p>这次攻击事件的原理是，用户在将 NFT 从协议中取出后，仍然可以借出 token。黑客利用这个 bug，不断新建合约，将其作为抵押人，抵押 NFT 并取出，然后再去借出 token，从而实现无本套利。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代码分析</h2><p>XCarnival 的核心在于三个合约：</p><ul><li><p><code>XNFT</code>，用户抵押 NFT 入口</p></li><li><p><code>XToken</code>，用户借款入口</p></li><li><p><code>P2Controller</code>，校验用户的权限，例如订单是否可借出，用户是否可赎回等</p></li></ul><p>这次的 bug 在于，用户可以将 NFT 抵押到协议之后，再取出 NFT，而由于合约借贷逻辑中，没有订单的状态进行判断，因此仍然可以调用借出方法进行借贷，造成资产流失。</p><p>我们先来看看抵押的代码：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ff5e1f1765c5376e2dae0bc7e9c1d57b67365d1dcee2babc44a73b4c93259acd.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>注意到抵押方法的 xToken 地址是作为参数传进去的，指定要借哪个 token。</p><p>再来看看取出 NFT 的方法（由于这段代码太长，我们只截取最重要的部分）：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d1428a035a94bfcede38a07641f107455383ac2e5249c729b34e76ba84069bcd.png" alt="取出 NFT （前部分）" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">取出 NFT （前部分）</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6920c8dea839bf3f79df34f6e2923a57fbbe490d858df9b6ec8086a0ee4f2b93.png" alt="取出 NFT （后部分）" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">取出 NFT （后部分）</figcaption></figure><p>我们看到，在取出 NFT 的最后，将订单的 <code>isWithdraw</code> 置为了 true，指明订单已经被取出。</p><p>接下来我们看看借出 xToken 的部分：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/23b9dd4158e0b76016dad16c3ec04f87ba8f0bc9459cf7c7c1a51da14a247312.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>xToken 中会调用 <code>controller</code> 的各种 <code>allow</code>，<code>verify</code> 方法等来判断该 token 是否可借出。那么我们再看看 <code>controller</code> 中是如何实现的：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/9e885147a0ed1bf0a5b484e05420681ce10ce07f26d790d05da0d3e888504ce0.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>注意到没有，这里并没有对订单中的 NFT 是否取出，也就是上面的 <code>isWithdraw</code> 进行检查。如果我们先去抵押 NFT，这时已经生成了订单，然后再取出。此时再去调用 xToken 的 <code>borrow</code> 方法，那么就可以借款，而此时是没有任何抵押的。此次黑客事件就是利用了这个 bug。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">攻击步骤</h2><p>我们来看看黑客的几笔交易都在干什么：</p><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/tx/0x16bb7799cf4e919bcb81f3ed531743ea6a6857e9a5121500fa1e3619bb2b82cf">从 OpenSea 中购买 BAYC （tokenId：5110）作为抵押物</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/tx/0xe4f99b2fb86a317eb16f7f288fda74ab07f0ffcbf645fb3b1a6490ca23206d09">创建攻击合约</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/tx/0x7cd094bc34c6700090f88950ab0095a95eb0d54c8e5012f1f46266c8871027ff">将 BAYC 转到攻击合约中</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/tx/0x422e7b0a449deba30bfe922b5c34282efbdbf860205ff04b14fd8129c5b91433">新建内部合约并将其作为抵押人，抵押 NFT 后取出</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/tx/0xabfcfaf3620bbb2d41a3ffea6e31e93b9b5f61c061b9cfc5a53c74ebe890294d">内部合约行使借贷权利，从协议中借出 token</a></p></li></ol><p>其中第四步的详细逻辑为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2cb784caeebec6bdb2a22725472b2de214279f2bf5ddc47b37fb5fce50e45e3c.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>这样在一笔交易中就可以构建多个抵押人（即新建的合约）。在实际的攻击中每一笔交易新建了四个内部合约，然后多次调用该方法，就可以构建出许多的抵押人。并且这些抵押人在协议中已经没有了抵押，但是仍然有借出 token 的权利。</p><p>接下来第五步，利用内部合约抵押人的身份，在没有任何抵押的情况下，直接从协议中借出 token，完成套利。</p><p>注意黑客在调用抵押方法 <code>pledgeAndBorrow</code> 的时候，传入了自定义的 xToken 地址。由于在这一步只需要实现抵押，不需要借贷，因此传入自定义的 xToken，可以避免 xToken 自带的一堆校验。</p><p>整体的逻辑还是比较简单的，我们也来尝试写写攻击合约。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">攻击重现</h2><p>要重现攻击，就需要模拟主网的各种状态，这时我们可以利用 hardhat 的 fork 功能，不熟悉的朋友可以看看我之前写的一篇<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/Z2qjTJJtaQHcwLc-9yHOGpXbeE7RHGdm5EafGjq7qhw">文章</a>，对其用法有详细的解释。</p><p>黑客的第一笔交易发生在区块 15028718 中，是购买 BAYC 的交易，那么我们就 fork 区块 15028720，并且使用 <code>hardhat_impersonateAccount</code> 模拟黑客的地址，那么此时在本地我们就拥有了 BAYC（5110），只需要关注攻击逻辑本身即可。</p><p>根绝我们前面的分析，在第四步中，需要新建内部的辅助合约，并具有抵押、取出、转移 NFT 的功能，最重要的，需要有借出 xToken 的功能。同时，在外部的攻击合约中，需要维护一个内部合约的地址列表，方便后续集中调用借款方法。</p><p>综上，我们最终实现的内部辅助合约为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/893995e1c21239f3aab868d2b4f3ddc27ced1f153a46f1298b566d10076b1efd.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><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c1a778be83a574cc7a7220d42687e5f189769279fdf6ad1319d5d39e4f6fee70.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><p>接着我们在本地自测，调用两次 <code>prepare</code> 之后调用 <code>attack</code>，测试结果为：</p><blockquote><p>balance before hack is 27.69746933937151467 balance after hack is 315.51434111624879089</p></blockquote><p>差值恰好是 <code>36 * 4 * 2 = 288</code>，说明我们模拟成功。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这次的 bug 原因在于在一个很小的细节，也就是 NFT 取出后没有修改状态。这种小问题，审计人员也没有查出来，说明细节真的很重要。我们在编写合约的时候，对各个状态的变更都要仔细考虑几遍。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://mp.weixin.qq.com/s/F2hpBNRzhZmfCn7BQanGOA" data="{&quot;provider_url&quot;:&quot;https://mp.weixin.qq.com&quot;,&quot;description&quot;:&quot;慢雾安全团队建议在进行借款操作时应做好订单状态中是否已经提走抵押品的判断，避免再次出现此类问题。&quot;,&quot;title&quot;:&quot;熊市新考验 -- XCarnival NFT 借贷协议漏洞分析&quot;,&quot;author_name&quot;:&quot;慢雾安全团队&quot;,&quot;url&quot;:&quot;https://mp.weixin.qq.com/s/F2hpBNRzhZmfCn7BQanGOA&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg&quot;,&quot;thumbnail_width&quot;:960,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;微信公众平台&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:409,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:140,&quot;height&quot;:140,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://mp.weixin.qq.com/s/F2hpBNRzhZmfCn7BQanGOA" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>熊市新考验 -- XCarnival NFT 借贷协议漏洞分析</h2><p>慢雾安全团队建议在进行借款操作时应做好订单状态中是否已经提走抵押品的判断，避免再次出现此类问题。</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://mp.weixin.qq.com</span></div><img src="https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg"/></div></a></div></div><div data-type="embedly" src="https://mp.weixin.qq.com/s/WEzNmTDGI2GUNrF7DJuZ6A" data="{&quot;provider_url&quot;:&quot;https://mp.weixin.qq.com&quot;,&quot;description&quot;:&quot;这个安全检查，一言难尽&quot;,&quot;title&quot;:&quot;门有锁，但钥匙在锁孔上 - 详解 XCarnival 攻击&quot;,&quot;author_name&quot;:&quot;yudan&quot;,&quot;url&quot;:&quot;https://mp.weixin.qq.com/s/WEzNmTDGI2GUNrF7DJuZ6A&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg&quot;,&quot;thumbnail_width&quot;:1080,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;微信公众平台&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:460,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:140,&quot;height&quot;:140,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://mp.weixin.qq.com/s/WEzNmTDGI2GUNrF7DJuZ6A" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>门有锁，但钥匙在锁孔上 - 详解 XCarnival 攻击</h2><p>这个安全检查，一言难尽</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://mp.weixin.qq.com</span></div><img src="https://storage.googleapis.com/papyrus_images/115ad2eeaccb91bd7497ce79ffd5ac450c8214e7fe611ef3076cfe38748ebb94.jpg"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[LooksRare 合约代码解读（二）]]></title>
            <link>https://paragraph.com/@xyyme/looksrare-2</link>
            <guid>vHYvSa55b1xC9RPthxha</guid>
            <pubDate>Mon, 27 Jun 2022 15:32:56 GMT</pubDate>
            <description><![CDATA[我们继续来学习 LooksRare 的代码，上篇文章中我们学习了它的主合约 LooksRareExchange，这篇文章我们着重于其它的辅助合约，包括各种管理合约以及交易策略合约等。源码解读TransferSelectorNFT上篇文章的最后，我们谈到了 NFT 的转账操作：NFT 转账可以看到，需要先根据 NFT 地址获取相应的转账管理器，然后采用对应的管理器来进行转账。其内部的实现为：获取 NFT 对应的转账管理合约这段代码中先查询该 NFT 有没有特定的转账管理器。如果没有，则利用 ERC165 规范，查询 NFT 合约实现了哪种规范（ERC721 或 ERC1155），然后使用对应的通用管理器。转账管理器本身的合约代码很简单，例如 ERC721 的转账代码为：TransferManagerERC721ERC721 转账管理器ManagerCurrencyManager，ExecutionManager 这两个管理器合约内容比较简单，包含基本的 add，remove，view 等方法，我们这里不再介绍。RoyaltyFeeRegistry该合约用于存储版税的相关信息，包括设...]]></description>
            <content:encoded><![CDATA[<p>我们继续来学习 LooksRare 的代码，<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mlRnXcw-eg2Xeoctg54mDU08msmUt4LLnOy4-4WTQyo">上篇文章</a>中我们学习了它的主合约 <code>LooksRareExchange</code>，这篇文章我们着重于其它的辅助合约，包括各种管理合约以及交易策略合约等。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">源码解读</h2><h3 id="h-transferselectornft" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">TransferSelectorNFT</h3><p>上篇文章的最后，我们谈到了 NFT 的转账操作：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b16ffcefcf28f8f61cd941917b8603213683aa98265732dc351de023dcc41ceb.png" alt="NFT 转账" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">NFT 转账</figcaption></figure><p>可以看到，需要先根据 NFT 地址获取相应的转账管理器，然后采用对应的管理器来进行转账。其内部的实现为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f58cf5ac75db0dee4d5e980eb9eb8f57286629c80df13b85b437aa5e4b05a80e.png" alt="获取 NFT 对应的转账管理合约" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">获取 NFT 对应的转账管理合约</figcaption></figure><p>这段代码中先查询该 NFT 有没有特定的转账管理器。如果没有，则利用 ERC165 规范，查询 NFT 合约实现了哪种规范（ERC721 或 ERC1155），然后使用对应的通用管理器。转账管理器本身的合约代码很简单，例如 ERC721 的转账代码为：</p><h3 id="h-transfermanagererc721" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">TransferManagerERC721</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8667ae61fa5cbd17beace401bb3568e2774a584b0eced8bd3da014aac4575ea5.png" alt="ERC721 转账管理器" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">ERC721 转账管理器</figcaption></figure><h3 id="h-manager" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Manager</h3><p><code>CurrencyManager</code>，<code>ExecutionManager</code> 这两个管理器合约内容比较简单，包含基本的 <code>add</code>，<code>remove</code>，<code>view</code> 等方法，我们这里不再介绍。</p><h3 id="h-royaltyfeeregistry" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">RoyaltyFeeRegistry</h3><p>该合约用于存储版税的相关信息，包括设置，读取等方法。</p><p>其中设置方法只能由 owner，即 <code>RoyaltyFeeSetter</code> 合约调用。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/592c351a1a748e82cf04fb3e9061692fc21ad072b3e2691ecd3ab4c29cffa3ce.png" alt="版税数据结构" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">版税数据结构</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2536cc15c6074833916a5263f81d8aa6a0d24194ce954e9a2e7a297eeeab75ab.png" alt="版税对应的相关方法" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">版税对应的相关方法</figcaption></figure><h3 id="h-royaltyfeesetter" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">RoyaltyFeeSetter</h3><p>设置版税信息，可以理解为将上面的 <code>RoyaltyFeeRegistry</code> 合约包装了一层，这里是设置的入口。包含下面几个主要方法：</p><ul><li><p><code>updateRoyaltyInfoForCollectionIfAdmin</code></p></li><li><p><code>updateRoyaltyInfoForCollectionIfOwner</code></p></li><li><p><code>updateRoyaltyInfoForCollectionIfSetter</code></p></li><li><p><code>updateRoyaltyInfoForCollection</code></p></li><li><p><code>_updateRoyaltyInfoForCollectionIfOwnerOrAdmin</code>（内部方法，被上面的 <code>IfAdmin</code> 和 <code>IfOwner</code> 方法调用）</p></li></ul><p>前四个方法均为设置版税功能，区别在于，前两个是由 NFT 的管理员设置，由于每个 NFT 合约的写法都不同，因此会区分 <code>admin</code>，<code>owner</code> 等角色。第三个方法是由前面设置版税时的 setter 设置。第四个是由 <code>RoyaltyFeeSetter</code> 合约本身的 owner 调用。最后一个内部方法调用上面的 <code>registry</code> 合约，进行设置操作。</p><p>我对这几个方法的理解是，如果 NFT 合约本身实现了 <code>owner</code> 或者 <code>admin</code> 方法，那么就有自主设置版税的权利。如果没有实现，可以找到系统管理员协助设置，此时同时会设置一个 setter 角色，以后 NFT 的版税就可以通过 setter 地址调用 <code>updateRoyaltyInfoForCollectionIfSetter</code> 来管理。系统更加推荐使用 setter 角色来管理版税。</p><p>我们来看看其中一个设置方法：</p><h4 id="h-updateroyaltyinfoforcollectionifadmin" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">updateRoyaltyInfoForCollectionIfAdmin</h4><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b826d765f481d7248fdca69eba1d9b3a116c39652b052c6ebd82f9ac0b3a1df9.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>逻辑比较简单。要求只有 NFT 的 admin 可以调用。我们看到第一步要求 NFT 不能实现 ERC2981，奇怪了，ERC2981 不就是版税的标准呢，为什么不能实现这个接口呢，我们后面会解答这个问题。</p><h4 id="h-updateroyaltyinfoforcollectionifowneroradmin" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">_updateRoyaltyInfoForCollectionIfOwnerOrAdmin</h4><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/64c193dc8e3d992c2738d4e1e538ba9acef55fa938cd4380351e1e46c74348fb.png" alt="更新版税内部方法" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">更新版税内部方法</figcaption></figure><h3 id="h-royaltyfeemanager" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">RoyaltyFeeManager</h3><p>该合约是最外层的接口，主合约在这里获取版税信息，只有一个主要方法：</p><h4 id="h-calculateroyaltyfeeandgetrecipient" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">calculateRoyaltyFeeAndGetRecipient</h4><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/66e1c32e46cac513d86bc80a2dfd2ac7e6b3d474d5df267ecfc59ab0299a72b4.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>我们看到，首先要检查系统中是否设置了该 NFT 的版税信息，如果没有设置，再去检查 NFT 本身是否包含版税信息。反过来想，如果合约本身就已经包含了版税信息，那么我们就没有必要在系统中设置。这也就解答了我们前面提出的问题：在 <code>RoyaltyFeeSetter</code> 合约中设置版税时，要求 NFT 合约本身不能实现 ERC2981。因为实现了 ERC2981 的合约，我们直接使用其自身的版税信息即可，不必再次设置。</p><p>接下来我们看看交易策略，LooksRare 合约库中一共包含 5 个交易策略，分别是：</p><ul><li><p><code>StrategyStandardSaleForFixedPrice</code>，标准固定价格交易</p></li><li><p><code>StrategyPrivateSale</code>，卖家指定的买家才能购买</p></li><li><p><code>StrategyDutchAuction</code>，荷兰拍卖</p></li><li><p><code>StrategyAnyItemFromCollectionForFixedPrice</code>，买家出价购买一个 NFT 合集中任意一项。例如，买家想购买 BAYC，任意一个都可以</p></li><li><p><code>StrategyAnyItemInASetForFixedPrice</code>，买家出价购买一个 NFT 合集中某些特定 tokenId 中任意一项。例如，买家只想购买蓝色背景的 BAYC</p></li></ul><p>我们回想一下，在上篇文章中我们看到每个 <code>match</code> 成单方法都会调用策略的方法来校验该交易是否有效。例如：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d07be3a453aeea2923b9680d47b25dfbfaf64e31feddad599c99643bfe28d795.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><ul><li><p><code>canExecuteTakerAsk</code>，校验 taker 卖单与 maker 买单能否有效匹配</p></li><li><p><code>canExecuteTakerBid</code>，校验 taker 买单与 maker 卖单能否有效匹配</p></li></ul><p>接下来我们分别来介绍每个策略的代码，看看他们的具体实现。</p><h3 id="h-strategystandardsaleforfixedprice" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">StrategyStandardSaleForFixedPrice</h3><p>最标准的买卖策略，固定价格，买单与卖单均指定 tokenId。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fd29bf09da07fbd3447bd16cbc2b0f13ea39de3e72b93503c677b2e4dbf1bc97.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>可以看到标准策略中需要校验 <code>price</code>、<code>tokenId</code> 是否匹配，并检查时间戳是否有效，逻辑比较简单。只要满足这些规则，就是符合标准策略。</p><h3 id="h-strategyprivatesale" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">StrategyPrivateSale</h3><p>当卖家指定由某位固定的买家购买时，使用该策略。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f69e1115bba48f11fb51a74b142eb161d7f80e90601897bacd9e6685abf68483.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>卖家指定买家，此时卖单挂单中会指定买家的地址，将其存放在 MakerOrder 的 <code>params</code> 字段。因为这个策略的执行只能由买家触发，所以 <code>canExecuteTakerAsk</code> 方法没有意义，需要返回 false。在下面的 <code>canExecuteTakerBid</code> 中，需要将卖家指定的买家地址从 <code>params</code> 中解析出来，然后进行校验，同时也要校验价格，tokenId，时间戳等信息。</p><h3 id="h-strategydutchauction" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">StrategyDutchAuction</h3><p>荷兰拍卖，价格会不断下降，买家为主动方。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/a4cf969c51fba367d9088cabc82bc89af772fece53719b2a186b8509834f698d.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>荷兰拍卖同样由买家触发，因此 taker 卖单没有意义，<code>canExecuteTakerAsk</code> 需要返回 false。在 <code>canExecuteTakerBid</code> 方法中，根据开始价格，结束价格，拍卖时常来计算当前实时价格，最后对参数进行校验。</p><h3 id="h-strategyanyitemfromcollectionforfixedprice" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">StrategyAnyItemFromCollectionForFixedPrice</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/898aa7034df7200a16b33dbbc1317b26a52b97c0b01f70db036c4d77a2666566.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>买家购买合集中任意一项均可，因此为买家挂单。此时卖家为 taker 主动方，因此 <code>canExecuteTakerBid</code> 没有意义，返回 false。我们看到，在 <code>canExecuteTakerAsk</code> 中仅仅校验了价格与时间，没有 tokenId，这是为何呢？因为买家购买任意一款均可，那么就不用指定 tokenId，只要价格与时间等信息匹配，任意一款 tokenId 都可以，因此这里不用校验。</p><h3 id="h-strategyanyiteminasetforfixedprice" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">StrategyAnyItemInASetForFixedPrice</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/51bc7f857f2a5f8512d853ee97bc760c4efe3f7608e01f29fc966ce66b85889a.png" alt="买家购买某些特定 id" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">买家购买某些特定 id</figcaption></figure><p>买家指定购买某些特定的 tokenId，此时买家挂单，卖家为 taker 主动方，因此 <code>canExecuteTakerBid</code> 没有意义，返回 false。<code>canExecuteTakerAsk</code> 中利用了 Merkle Tree 的技术，maker 买单中的 <code>params</code> 字段存储所有想要购买 tokenId 合集的 Merkle Root，taker 卖单中的 <code>params</code> 存储了该 tokenId 对应的 Proof。最后通过 verify 校验 taker 卖单中的 tokenId 是否匹配。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>现在我们就已经介绍完了 LooksRare 的所有主要合约，是不是感觉还挺简单的。我个人认为 LooksRare 的文档和代码写得都比较简洁易懂，比较适合开发者来学习 NFT 市场的原理。希望大家能够结合代码库和我的文章，多理解几遍，必然会对自己的能力有一些提升。</p><h2 id="h-looksrare" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">LooksRare 代码系列文章</h2><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mlRnXcw-eg2Xeoctg54mDU08msmUt4LLnOy4-4WTQyo">LooksRare 合约代码解读（一）</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mdlzrw_UEl_ea6svtJ3zoF7gDdrnEp67hVDMr1hlvho">LooksRare 合约代码解读（二）</a></p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://docs.looksrare.org/developers/looksrare-exchange-overview" data="{&quot;provider_url&quot;:&quot;https://docs.looksrare.org&quot;,&quot;description&quot;:&quot;Understand the LooksRare blockchain architecture with the technical documentation for smart contracts of the exchange protocol.&quot;,&quot;title&quot;:&quot;LooksRare Exchange v1 Overview | LooksRare Docs&quot;,&quot;url&quot;:&quot;https://docs.looksrare.org/developers/looksrare-exchange-overview&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Looksrare&quot;,&quot;type&quot;:&quot;link&quot;}" format="small"><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://docs.looksrare.org/developers/looksrare-exchange-overview" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>LooksRare Exchange v1 Overview | LooksRare Docs</h2><p>Understand the LooksRare blockchain architecture with the technical documentation for smart contracts of the exchange protocol.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://docs.looksrare.org</span></div></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[LooksRare 合约代码解读（一）]]></title>
            <link>https://paragraph.com/@xyyme/looksrare</link>
            <guid>qaBTa9EyDGmbLpFLNsLQ</guid>
            <pubDate>Thu, 23 Jun 2022 07:26:34 GMT</pubDate>
            <description><![CDATA[最近研究了 LooksRare 的合约代码，他们的代码写得比较简单易懂，同时文档内容也比较丰富。学习了几天，基本算是把整个合约代码都研究明白了，因此写篇文章来做做笔记，同时也希望能够帮助到有需要的朋友。系统架构名词解释ask 代表卖家卖出，bid 代表买家买入。maker 代表主动挂单的人，例如卖家主动挂单卖 NFT ，此时卖家为 maker。或者买家对某 NFT 出价，此时买家为 maker。taker 代表撮合完成订单的人，例如买家看到某 NFT 的价格合适，对其买入，此时买家为 taker。或者卖家看到某买家的出价合适，对其卖出，此时卖家为 taker。结合上面的描述，合约中一共有四个角色，分别是：makerAsk，挂单的卖家makerBid，出价的买家takerAsk，撮合完成订单的卖家takerBid，撮合完成订单的买家链下挂单（出价）行为是在链下完成的，即 maker 操作不上链，只是在链下签名。链上成单行为是在链上完成的，即 taker 操作上链。因此代码中只有成单，没有挂单的逻辑。合约架构LooksRareExchange，主合约，用户的所有操作都在这里管理合约C...]]></description>
            <content:encoded><![CDATA[<p>最近研究了 LooksRare 的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/LooksRare/contracts-exchange-v1">合约代码</a>，他们的代码写得比较简单易懂，同时文档内容也比较丰富。学习了几天，基本算是把整个合约代码都研究明白了，因此写篇文章来做做笔记，同时也希望能够帮助到有需要的朋友。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">系统架构</h2><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">名词解释</h3><ul><li><p><code>ask</code> 代表卖家卖出，<code>bid</code> 代表买家买入。</p></li><li><p><code>maker</code> 代表主动挂单的人，例如卖家主动挂单卖 NFT ，此时卖家为 maker。或者买家对某 NFT 出价，此时买家为 maker。</p></li><li><p><code>taker</code> 代表撮合完成订单的人，例如买家看到某 NFT 的价格合适，对其买入，此时买家为 taker。或者卖家看到某买家的出价合适，对其卖出，此时卖家为 taker。</p></li></ul><p>结合上面的描述，合约中一共有四个角色，分别是：</p><ul><li><p><code>makerAsk</code>，挂单的卖家</p></li><li><p><code>makerBid</code>，出价的买家</p></li><li><p><code>takerAsk</code>，撮合完成订单的卖家</p></li><li><p><code>takerBid</code>，撮合完成订单的买家</p></li></ul><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">链下</h3><p>挂单（出价）行为是在链下完成的，即 <code>maker</code> 操作不上链，只是在链下签名。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">链上</h3><p>成单行为是在链上完成的，即 <code>taker</code> 操作上链。因此代码中只有成单，没有挂单的逻辑。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约架构</h3><ul><li><p><code>LooksRareExchange</code>，主合约，用户的所有操作都在这里</p></li><li><p>管理合约</p><ul><li><p><code>CurrencyManager</code>，管理协议支持的支付币种</p></li><li><p><code>ExecutionManager</code>，管理协议支持的交易策略</p></li><li><p><code>RoyaltyFeeManager</code>，管理 NFT 对应的版税信息</p></li><li><p>TransferManager，均继承于 <code>ITransferManagerNFT</code></p><ul><li><p><code>TransferManagerERC721</code>，执行 ERC-721 的转移操作（使用 <code>safeTransferFrom</code>）</p></li><li><p><code>TransferManagerERC1155</code>，执行 ERC-1155 的转移操作</p></li><li><p><code>TransferManagerNonCompliantERC721</code>，执行 ERC-721 的转移操作（<strong>不</strong>使用 <code>safeTransferFrom</code>，而是 <code>transferFrom</code>）</p></li></ul></li></ul></li><li><p><code>OrderTypes</code>，包含 <code>MakerOrder</code> 与 <code>TakerOrder</code> 的订单数据结构</p></li><li><p><code>TransferSelectorNFT</code>，管理 NFT 对应的 TransferManager</p></li><li><p>交易策略合约</p><ul><li><p><code>StrategyStandardSaleForFixedPrice</code>，标准固定价格交易</p></li><li><p><code>StrategyPrivateSale</code>，卖家指定的买家才能购买</p></li><li><p><code>StrategyDutchAuction</code>，荷兰拍卖</p></li><li><p><code>StrategyAnyItemFromCollectionForFixedPrice</code>，买家出价购买一个 NFT 合集中任意一项。例如，买家想购买 BAYC，任意一个都可以</p></li><li><p><code>StrategyAnyItemInASetForFixedPrice</code>，买家出价购买一个 NFT 合集中某些特定 tokenId 中任意一项。例如，买家只想购买蓝色背景的 BAYC</p></li></ul></li><li><p><code>RoyaltyFeeRegistry</code>，设置 NFT 的版税信息，存储版税信息</p></li><li><p><code>RoyaltyFeeSetter</code>，设置 NFT 的版税信息，该合约为入口，调用上面的合约</p></li></ul><p>看到这么多合约，是不是已经晕了。不用怕，我们要关心的只有 <code>LooksRareExchange</code> 合约，其他的合约都是为了它服务的。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">源码解读</h2><blockquote><p>注：由于 Mirror 的排版原因，长代码的阅读性很差，因此为了统一起见，所有代码将会截图展示。同时，只着重于主要业务逻辑，对于例如 setter 等比较简单的部分，不再介绍。</p></blockquote><h3 id="h-ordertypes" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">OrderTypes</h3><p>包含 <code>MakerOrder</code> 与 <code>TakerOrder</code>，分别为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b4998f620990a39d5269ac0d8994ed5097f567221d0bdb8a130bbffa750d0791.png" alt="订单数据结构" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">订单数据结构</figcaption></figure><h3 id="h-looksrareexchange" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">LooksRareExchange</h3><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/900ae735cdddfde843a32d34dfda52fcc342ceaa3bea08083ccaa1950a791bfe.png" alt="nonce 数据结构" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">nonce 数据结构</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f3b71f35bbf9f8238d7d6e281096f2125d0284f88f9291d6c66388e1fcbd7fdd.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>我们知道，挂单是在通过签名在链下进行的，每个挂单都包含 nonce。但是如果要取消订单，必须要上链，为什么呢？因为在成单的时候，要用到挂单的签名信息，而一旦签名了，签名信息是一直存在的。即使链下再怎么操作，链上也可以把这份签名拿过来用。因此 maker 需要在链上将这个 nonce 的订单取消，让其在链上失效。这样即使有人拿着签名信息来用，那么在链上这个 nonce 的订单已经失效了。</p><p>对于上面两个函数，<code>cancelAllOrdersForSender</code> 属于一刀切，传入一个 nonce，该 nonce 以下的订单就全部失效。<code>cancelMultipleMakerOrders</code> 则是传入特定的 nonce 列表，只有这些 nonce 的订单失效。</p><h4 id="h-matchaskwithtakerbidusingethandweth" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">matchAskWithTakerBidUsingETHAndWETH</h4><p>买家发起撮合，使用 WETH 购买</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d5613d452e73ef6507f8153dfc014c7d3c02c07e50883c031de6962854717ec2.png" alt="买家使用 WETH 购买" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">买家使用 WETH 购买</figcaption></figure><h4 id="h-matchaskwithtakerbid" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">matchAskWithTakerBid</h4><p>买家发起撮合，使用指定币种（makerAsk.currency）购买</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0259659fe34dd265117f2b6532050cd03d37da9b628f47ef28a88632730a497b.png" alt="买家使用非 WETH 币种购买" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">买家使用非 WETH 币种购买</figcaption></figure><p>可以看到上面两个函数的逻辑大同小异，区别比较大的地方就是使用 WETH 购买的函数中，需要对 <code>msg.value</code> 以及 ETH 的转换进行处理。</p><h4 id="h-matchbidwithtakerask" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">matchBidWithTakerAsk</h4><p>卖家发起撮合</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/acd2d47f182158ce8dbac6b5fb0efc4ac2a1d253b2a382ca25f332c75eedffc1.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><p>用户所有的操作就是这些，我们再来小结一下：</p><ul><li><p><code>cancelAllOrdersForSender</code> → 取消指定 nonce 以下所有订单</p></li><li><p><code>cancelMultipleMakerOrders</code> → 批量取消指定订单</p></li><li><p><code>matchAskWithTakerBidUsingETHAndWETH</code> → 买家发起撮合，使用 WETH 购买</p></li><li><p><code>matchAskWithTakerBid</code> → 买家发起撮合，使用指定币种(makerAsk.currency)购买</p></li><li><p><code>matchBidWithTakerAsk</code> → 卖家发起撮合</p></li></ul><p>读懂这些逻辑，我们就已经掌握了 LooksRare 合约的核心内容。</p><p>接下来，我们看看几个重要的内部函数。</p><h4 id="h-transferfeesandfunds" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">_transferFeesAndFunds</h4><p>分发买家的款项（非 ETH 支付）</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/470e9116ed20c436419354d797491ddafa9494f983c4bc6866694dfd07c9b09f.png" alt="分发买家的款项（非 ETH 支付）" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">分发买家的款项（非 ETH 支付）</figcaption></figure><h4 id="h-transferfeesandfundswithweth" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">_transferFeesAndFundsWithWETH</h4><p>分发买家的款项（WETH 支付）</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cbe9d7d477eef10ddb321ff20c28bf128d358fae2bc8e31a82a07e6ce46c57ab.png" alt="分发买家的款项（WETH 支付）" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">分发买家的款项（WETH 支付）</figcaption></figure><p>我们可以看到，上面两个函数的内容基本相同，主要逻辑都是对买家的款项进行了分发，计算了协议费用和版税费用，最后将剩余款项转给卖家。</p><p>还记得我们前面看到的 <code>MakerOrder</code> 中的 <code>minPercentageToAsk</code> 字段吗，当时看是不是有点迷茫，这个字段是什么意思？因为有版税和协议费用的存在，且它俩都是变量，可能卖家在挂单之后，各种费用的值被管理员修改了，那么在挂单的时候就需要设置一个最小可接受的值，如果最后盈利的数量小于该值，那么就不交易了，类似于 DEX 中滑点的概念。</p><p>注意到两个函数最大的区别就是转账这块，在非 ETH 函数中，使用的是 <code>safeTransferFrom</code>，而在 ETH 函数中，使用的是 <code>safeTransfer</code>。因为一个是 ERC20，一个是原生货币，后者在调用函数的同时就已经转入款项了。同时这也是为什么我们前面的入口函数那里，ETH 支付的函数需要对 <code>msg.value</code> 进行处理，而 ERC20 不需要这一步。</p><h4 id="h-transfernonfungibletoken" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">_transferNonFungibleToken</h4><p>转账 NFT</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2f05de5be33e82da4f0e046b4c0703d4422df6f110124437d61f2a505875b01b.png" alt="转账 NFT" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">转账 NFT</figcaption></figure><p>由于目前 NFT 流行的标准有 ERC-721 和 ERC-1155，两者的转账方法略有不同。因此，这里会根据 NFT 合集对应的标准，选取相应的转账管理器来进行转账。转账管理器的内部逻辑其实很简单，我们后面再介绍，这里先着重于主逻辑。</p><h4 id="h-validateorder" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">_validateOrder</h4><p>校验订单信息</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7367469218c771cc735f8643b719b8fb07ff9644e014dfffb54a1a49a4a468cb.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>这里，先校验订单的 nonce 是否有效，我们最开始看到的两个关于 nonce 的数据结构可以用来验证。然后再根据 maker 的签名和 makerOrder 的哈希值来判断其是否为正确的订单信息，这里利用了 EIP-712 的相关内容，不熟悉的朋友可以看看我之前的写的这篇<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/cJX3zqiiUg2dxB1nmbXbDcQ1DSdajHP5iNgBc6wEZz4">文章</a>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">小结</h2><p>到这里，所有的主逻辑我们就已经看完了，是不是还算比较简单。</p><p>我们先休息一下，没有完全看懂的朋友可以再多看两遍消化一下。这篇文章就先介绍到这里，看完这篇我们基本上就已经对 LooksRare 的主要逻辑了解了个大概了。剩下的合约基本上都是辅助合约了，例如各种管理器合约，我们放在下篇文章介绍。</p><h2 id="h-looksrare" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">LooksRare 代码系列文章</h2><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mlRnXcw-eg2Xeoctg54mDU08msmUt4LLnOy4-4WTQyo">LooksRare 合约代码解读（一）</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/mdlzrw_UEl_ea6svtJ3zoF7gDdrnEp67hVDMr1hlvho">LooksRare 合约代码解读（二）</a></p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">关于我</h2><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">参考</h2><div data-type="embedly" src="https://docs.looksrare.org/developers/looksrare-exchange-overview" data="{&quot;provider_url&quot;:&quot;https://docs.looksrare.org&quot;,&quot;description&quot;:&quot;Understand the LooksRare blockchain architecture with the technical documentation for smart contracts of the exchange protocol.&quot;,&quot;title&quot;:&quot;LooksRare Exchange v1 Overview | LooksRare Docs&quot;,&quot;url&quot;:&quot;https://docs.looksrare.org/developers/looksrare-exchange-overview&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Looksrare&quot;,&quot;type&quot;:&quot;link&quot;}" format="small"><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://docs.looksrare.org/developers/looksrare-exchange-overview" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>LooksRare Exchange v1 Overview | LooksRare Docs</h2><p>Understand the LooksRare blockchain architecture with the technical documentation for smart contracts of the exchange protocol.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://docs.looksrare.org</span></div></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[Solidity Events 详解]]></title>
            <link>https://paragraph.com/@xyyme/solidity-events</link>
            <guid>zpItyFkT7ObhpzrIfey1</guid>
            <pubDate>Wed, 08 Jun 2022 11:14:52 GMT</pubDate>
            <description><![CDATA[Events 是 Solidity 中记录事件的工具，可以简单理解为日志。Events 的优点在于，一是能够利用较少的 Gas 就能将数据记录在区块链上，二是可以方便链下对链上数据进行监听。代码示例先来看一段简单的代码：pragma solidity 0.8.10; contract EventsDemo { // 定义 Events event Transfer( address indexed from, address indexed to, uint256 amount ); function transfer(address to, uint256 amount) external { // 发送 Events emit Transfer(msg.sender, to, amount); } } 在上述代码中，我们通过 event 关键字定义事件，通过 emit 关键字发送事件。这样，在调用 transfer 函数的时候，就会发送 Transfer 事件，将其数据记录在链上。 接下来我们实际部署一下，并调用 transfer 看看会发生什么。 调用 transfer 时...]]></description>
            <content:encoded><![CDATA[<p>Events 是 Solidity 中记录事件的工具，可以简单理解为日志。Events 的优点在于，一是能够利用较少的 Gas 就能将数据记录在区块链上，二是可以方便链下对链上数据进行监听。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">代码示例</h3><p>先来看一段简单的代码：</p><pre data-type="codeBlock" text="pragma solidity 0.8.10;

contract EventsDemo {
    // 定义 Events
    event Transfer(
        address indexed from, 
        address indexed to, 
        uint256 amount
    );

    function transfer(address to, uint256 amount) external {
        // 发送 Events
        emit Transfer(msg.sender, to, amount);
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.10;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">EventsDemo</span> </span>{
    <span class="hljs-comment">// 定义 Events</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Transfer</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> <span class="hljs-keyword">from</span>, 
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> to, 
        <span class="hljs-keyword">uint256</span> amount
    </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> to, <span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// 发送 Events</span>
        <span class="hljs-keyword">emit</span> Transfer(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, to, amount);
    }
}
</code></pre><p>在上述代码中，我们通过 <code>event</code> 关键字定义事件，通过 <code>emit</code> 关键字发送事件。这样，在调用 <code>transfer</code> 函数的时候，就会发送 <code>Transfer</code> 事件，将其数据记录在链上。</p><p>接下来我们实际部署一下，并调用 <code>transfer</code> 看看会发生什么。</p><p>调用 <code>transfer</code> 时的参数我们分别设为：</p><ul><li><p><code>to</code> → 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4</p></li><li><p><code>amount</code> → 123</p></li></ul><p>调用成功后，查看 Etherscan 的 Logs 页面：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/cf2fc7341c858ba146e6f80a42cd119790a2b0fd2ea28a7e5f033c92ba01cac0.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>我们可以看到页面中有三部分，分别是：</p><ul><li><p>Address</p></li><li><p>Topics</p></li><li><p>Data</p></li></ul><p>其中 <code>Address</code> 是合约地址，这个很容易理解，那么剩下的两个字段分别代表什么呢？</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">基本数据结构</h3><p>Events 打印出的日志中包含两种数据结构，分别是 <code>topic</code> 和 <code>data</code>。每个日志中最多可以包含 4 个 <code>topic</code>，<code>data</code> 则没有限制。<code>topic</code> 的第一个字段默认是事件签名，在上例中，<code>Topics[0]</code> 中的 <code>0xddf25…b3ef</code> 哈希值就是 <code>Transfer</code> 事件的签名：</p><pre data-type="codeBlock" text="keccak256(bytes(&quot;Transfer(address,address,uint256)&quot;))
"><code><span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(<span class="hljs-string">"Transfer(address,address,uint256)"</span>))
</code></pre><p>图示即为（图片来自于<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378">这里</a>）：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/af3ed3af032c440b52b7cc7d90bd853a923942c3f5b30c6efdadb45d09c2f7e5.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>我们注意到，前面定义的 <code>Transfer</code> 事件中，<code>from</code> 和 <code>to</code> 字段都带有一个 <code>indexed</code> 关键字，它的作用就是将该字段列为 <code>topic</code>。由于最多限制 4 个 <code>topic</code>，且第一个 <code>topic</code> 默认是事件签名，因此在合约中定义事件的时候，最多只能有 3 个字段可以使用 <code>indexed</code>。那么 <code>topic</code> 到底有什么用呢？</p><p>将字段列为 <code>topic</code> 可以方便检索。例如，我们想要在链下监听 <code>Transfer</code> 事件，但是并不想监听每一笔事件，只想监听 <code>from</code> 是我的地址的事件，那么就必须将 <code>from</code> 设置为 <code>topic</code>，否则 <code>data</code> 类型的日志是没有办法做到的。</p><p>在上例中，我们将 <code>from</code> 和 <code>to</code> 设置为 <code>indexed</code>，因此我们可以在上图中看到，有三个 <code>topic</code>，分别是</p><ul><li><p>事件签名</p></li><li><p><code>from</code>，这里是调用合约的地址</p></li><li><p><code>to</code></p></li></ul><p>那么剩下的 <code>data</code> 部分就是 <code>amount</code> 字段的数据了。</p><p><code>data</code> 的内容是将数据经过 <code>abi-encoded</code> 的结果。</p><p>我们在 Etherscan 上传代码，刷新页面：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/deeffb0c96cf15c8b41986b2f37e6fb546e47ccd480992afe1a037a5369ebed6.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>这时 Etherscan 就可以识别出事件的 <code>name</code>，并且可以将 <code>topic</code> 和 <code>data</code> 数据解码。</p><h4 id="h-topic" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">Topic 的限制</h4><p>前面我们说到，一个事件日志中，最多只能有 4 个 <code>topic</code>，也就是说最多有 3 个 <code>indexed</code>。同时，<code>topic</code> 本身也有长度限制，每个 <code>topic</code> 只能容纳 32 个字节的数据。那么像 <code>string</code> 或者 <code>bytes</code> 这种非定长的数据能否设置为 <code>topic</code> 呢？</p><p>答案是可以的，但是需要对其内容进行哈希。我们来看一段代码：</p><pre data-type="codeBlock" text="pragma solidity 0.8.10;

contract EventsDemo {
    event Message(
        address indexed from,
        address indexed to,
        string message
    );

    function sendMessage(address to, string memory message) public {
        emit Message(msg.sender, to, message);
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.10;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">EventsDemo</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Message</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> <span class="hljs-keyword">from</span>,
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> to,
        <span class="hljs-keyword">string</span> message
    </span>)</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sendMessage</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span> message</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">emit</span> Message(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, to, message);
    }
}
</code></pre><p>首先，我们先将 <code>Message</code> 事件中的 <code>message</code> 字段设置非 <code>indexed</code>，部署并调用，调用的参数分别是：</p><ul><li><p><code>to</code> → 0x5b38da6a701c568545dcfcb03fcb875f56beddc4</p></li><li><p><code>message</code> → Hello World</p></li></ul><p>结果为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c3b0085bae2c061dee06a418d42135807338a464e64a9134426173fe4e256907.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><code>message</code> 字段位于 <code>data</code> 中，可以被正常解码。</p><p>接下来，我们给 <code>message</code> 加上 <code>indexed</code>：</p><pre data-type="codeBlock" text="event Message(
    address indexed from,
    address indexed to,
    string indexed message
);
"><code><span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Message</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> <span class="hljs-keyword">from</span>,
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">indexed</span> to,
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">indexed</span> message
</span>)</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/d34a00a942909c29e59972cc6dcc232a85182d9697d625779040f7de847f2d59.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>此时，<code>data</code> 中内容为空，而 <code>topic[3]</code> 的值就是 <code>message</code> 的哈希值：</p><pre data-type="codeBlock" text="keccak256(bytes(&quot;Hello World&quot;))
"><code><span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(<span class="hljs-string">"Hello World"</span>))
</code></pre><h3 id="h-events-gas" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Events 的 Gas 花费</h3><p>根据以太坊黄皮书的内容，日志的基础费用是 375 Gas。另外每个 <code>topic</code> 同样需要花费 375 Gas，<code>data</code> 中每个字节需要花费 8 Gas。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/df1a401a106678e860b6052d9aaa923573c2aebd0ba0caca1ee6642271842709.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><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5a9a7fbfbfb2306eedd046918c423426152b6a8ceb87a4f23be22e871f92ad07.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>因此，我们前面的 <code>Transfer</code> 事件花费的 Gas 为：</p><blockquote><p>1756 = 375（基础费用）+ 375 * 3（3 个 <code>topic</code>）+ 32 * 8（<code>data</code> 中共 32 字节）</p></blockquote><h3 id="h-low-level-log" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Low-level log</h3><p>Solidity 老版本中还存在一种底层调用，例如：</p><pre data-type="codeBlock" text="pragma solidity &gt;=0.4.10 &lt;0.7.0;

contract C {
    function f() public payable {
        uint256 _id = 0x420042;
        log2(
            bytes32(msg.value),
            bytes32(uint256(msg.sender)),
            bytes32(_id)
        );
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> >=0.4.10 &#x3C;0.7.0;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">C</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">f</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">payable</span></span> </span>{
        <span class="hljs-keyword">uint256</span> _id <span class="hljs-operator">=</span> <span class="hljs-number">0x420042</span>;
        <span class="hljs-built_in">log2</span>(
            <span class="hljs-keyword">bytes32</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span>),
            <span class="hljs-keyword">bytes32</span>(<span class="hljs-keyword">uint256</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>)),
            <span class="hljs-keyword">bytes32</span>(_id)
        );
    }
}
</code></pre><p>包括 <code>log0</code>、<code>log1</code>、<code>log2</code>、<code>log3</code>、<code>log4</code>，不过新版文档中已经去除了这部分内容，因此我们也不再介绍，了解其存在即可，感兴趣的朋友可以查看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/v0.4.21/contracts.html#low-level-interface-to-logs">这里</a>。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">监听事件</h3><p>链下监听事件有很多种方法，几乎所有主流语言都有对应的 web3 库可以实现。我前面写的一篇 <code>ethers</code> 使用教程的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/hT78s8EI3wYp-IixUC951fQNWTON-EhNcGd2jXEtcdM">文章</a>中包含了这部分内容，感兴趣的朋友可以前往查看，这里就不再赘述了。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">总结</h3><p>Solidity 中 Events 是一个很实用的日志工具，花费 Gas 少，利于链下来监听链上交易。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">关于我</h3><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">参考</h3><div data-type="embedly" src="https://blog.chain.link/events-and-logging-in-solidity/" data="{&quot;provider_url&quot;:&quot;https://blog.chain.link&quot;,&quot;description&quot;:&quot;In this technical tutorial, learn all about events and logging in Solidity. This is everything you need to know.&quot;,&quot;title&quot;:&quot;Events and Logging in SoliditySolidity Events and Logging [With Examples] | Chainlink&quot;,&quot;mean_alpha&quot;:161.780097734,&quot;author_name&quot;:&quot;Chainlink&quot;,&quot;url&quot;:&quot;https://blog.chain.link/events-and-logging-in-solidity/&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/b218cea228e9e79992cea48e52f57b281ff46211a42030990faf87e8537db154.png&quot;,&quot;thumbnail_width&quot;:4000,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Chainlink Blog&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:2251,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:4000,&quot;height&quot;:2251,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/b218cea228e9e79992cea48e52f57b281ff46211a42030990faf87e8537db154.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/b218cea228e9e79992cea48e52f57b281ff46211a42030990faf87e8537db154.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://blog.chain.link/events-and-logging-in-solidity/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Events and Logging in SoliditySolidity Events and Logging [With Examples] | Chainlink</h2><p>In this technical tutorial, learn all about events and logging in Solidity. This is everything you need to know.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://blog.chain.link</span></div><img src="https://storage.googleapis.com/papyrus_images/b218cea228e9e79992cea48e52f57b281ff46211a42030990faf87e8537db154.png"/></div></a></div></div><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.soliditylang.org/en/latest/contracts.html#events">https://docs.soliditylang.org/en/latest/contracts.html#events</a></p><div data-type="embedly" src="https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378" data="{&quot;provider_url&quot;:&quot;https://medium.com&quot;,&quot;description&quot;:&quot;Understanding event logs on the Ethereum blockchain Most transactions have an event log, but those event logs can be hard to read. Preface: If you haven&apos;t read my article on the Ethereum Virtual ...&quot;,&quot;title&quot;:&quot;Understanding event logs on the Ethereum blockchain&quot;,&quot;mean_alpha&quot;:247.5,&quot;author_name&quot;:&quot;Luit&quot;,&quot;url&quot;:&quot;https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/c12c6c4c5fe872b13178930c95b00c154d5e7ff59cb67d8eb99fd4d4236b893d.png&quot;,&quot;thumbnail_width&quot;:1200,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Medium&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:535,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1200,&quot;height&quot;:535,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/c12c6c4c5fe872b13178930c95b00c154d5e7ff59cb67d8eb99fd4d4236b893d.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/c12c6c4c5fe872b13178930c95b00c154d5e7ff59cb67d8eb99fd4d4236b893d.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://medium.com/mycrypto/understanding-event-logs-on-the-ethereum-blockchain-f4ae7ba50378" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Understanding event logs on the Ethereum blockchain</h2><p>Understanding event logs on the Ethereum blockchain Most transactions have an event log, but those event logs can be hard to read. Preface: If you haven&#x27;t read my article on the Ethereum Virtual ...</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://medium.com</span></div><img src="https://storage.googleapis.com/papyrus_images/c12c6c4c5fe872b13178930c95b00c154d5e7ff59cb67d8eb99fd4d4236b893d.png"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[Hardhat fork 简明使用教程]]></title>
            <link>https://paragraph.com/@xyyme/hardhat-fork</link>
            <guid>Bnw48l0UDWoEGFBOOQM5</guid>
            <pubDate>Mon, 06 Jun 2022 08:39:53 GMT</pubDate>
            <description><![CDATA[Hardhat fork 可以使我们将主网的区块 fork 到本地，这样我们就可以在本地与真实的链上数据进行交互，同时也可以模拟任意账户，方便我们进行一些测试，速度快，并且不用花费 Gas。基本命令首先，我们先创建一个 Hardhat 项目，不熟悉的朋友可以看看这里。创建完毕之后，就可以在本地 node 中 fork 区块了。使用命令（替换自己的 key）：npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/ 这个命令会将当前最新的一个区块数据 fork 到本地，也就是说现在本地 node 链中存在的就是这个区块的数据。当然也可以指定区块号进行 fork：npx hardhat node --fork https://eth-mainnet.alchemyapi.io/v2/ --fork-block-number 14913700 每次 fork 都使用命令行指定这一堆参数很麻烦，因此可以将其放在 Hardhat 的配置文件 hardhat.config.js 中：module.exports = { sol...]]></description>
            <content:encoded><![CDATA[<p>Hardhat fork 可以使我们将主网的区块 fork 到本地，这样我们就可以在本地与真实的链上数据进行交互，同时也可以模拟任意账户，方便我们进行一些测试，速度快，并且不用花费 Gas。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">基本命令</h3><p>首先，我们先创建一个 Hardhat 项目，不熟悉的朋友可以看看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hardhat.org/tutorial/creating-a-new-hardhat-project">这里</a>。创建完毕之后，就可以在本地 node 中 fork 区块了。使用命令（替换自己的 key）：</p><blockquote><p>npx hardhat node --fork <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eth-mainnet.alchemyapi.io/v2/">https://eth-mainnet.alchemyapi.io/v2/</a></p><p>这个命令会将当前最新的一个区块数据 fork 到本地，也就是说现在本地 node 链中存在的就是这个区块的数据。当然也可以指定区块号进行 fork：</p><blockquote><p>npx hardhat node --fork <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eth-mainnet.alchemyapi.io/v2/">https://eth-mainnet.alchemyapi.io/v2/</a> --fork-block-number 14913700</p><p>每次 fork 都使用命令行指定这一堆参数很麻烦，因此可以将其放在 Hardhat 的配置文件 <code>hardhat.config.js</code> 中：</p><pre data-type="codeBlock" text="module.exports = {
  solidity: &quot;0.8.4&quot;,

  networks: {
    hardhat: {
      // 添加 forking 内容
      forking: {
        url: &quot;https://eth-mainnet.alchemyapi.io/v2/&lt;key&gt;&quot;,
        // 如果不指定区块，则默认 fork 当前最新区块
        blockNumber: 14913700
      }
    }
  }
};
"><code>module.exports <span class="hljs-operator">=</span> {
  solidity: <span class="hljs-string">"0.8.4"</span>,

  networks: {
    hardhat: {
      <span class="hljs-comment">// 添加 forking 内容</span>
      forking: {
        url: <span class="hljs-string">"https://eth-mainnet.alchemyapi.io/v2/&#x3C;key>"</span>,
        <span class="hljs-comment">// 如果不指定区块，则默认 fork 当前最新区块</span>
        blockNumber: <span class="hljs-number">14913700</span>
      }
    }
  }
};
</code></pre><p>这样配置之后，就可以直接使用</p><blockquote><p>npx hardhat node</p></blockquote><p>此时本地 node 的数据就是 fork 的区块数据。</p><p>需要注意的一点是，在执行 <code>npx hardhat test</code> 运行单元测试时，测试内容运行的链不是本地 <code>node</code>，而是沙盒模式。因此如果想要在单元测试的沙盒模式中使用 fork 的数据，需要在配置文件中显式配置 forking 内容。如果希望单元测试在本地 node 中执行，需要指定网络：</p><blockquote><p>npx hardhat test --network localhost</p></blockquote><h3 id="h-fork" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">测试 fork 数据</h3><p>接下来，我们利用单元测试来实际验证一下我们的 fork 是否成功。在 <code>test</code> 文件夹下创建 <code>fork.test.js</code> 文件，编写代码：</p><pre data-type="codeBlock" text="const { ethers } = require(&quot;hardhat&quot;);

describe(&quot;Fork&quot;, function () {
  it(&quot;Testing fork data&quot;, async function () {
    console.log((await ethers.provider.getBlockNumber()).toString());
  });
});
"><code>const { ethers } <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"hardhat"</span>);

describe(<span class="hljs-string">"Fork"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  it(<span class="hljs-string">"Testing fork data"</span>, async <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    console.log((await ethers.provider.getBlockNumber()).toString());
  });
});
</code></pre><p>执行测试，输出 <code>14913700</code>，说明 fork 成功。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/4fda163b65156c264630022b337646188be08746a88188c8f5c9d9c59f425b7f.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>现在我们来试试，实际调用主网的数据。我们以 USDT 合约为例，调用它的数据：</p><pre data-type="codeBlock" text="// 需要将 usdt 的 abi 保存在本地
const USDT_ABI = require(&quot;./usdt_abi.json&quot;);
// usdt 合约的主网地址
const USDT_ADDRESS = &quot;0xdAC17F958D2ee523a2206206994597C13D831ec7&quot;;
const { ethers } = require(&quot;hardhat&quot;);

describe(&quot;Fork&quot;, function () {
  it(&quot;Testing fork data&quot;, async function () {
    const provider = ethers.provider;
    // 构造 usdt 合约对象
    const USDT = new ethers.Contract(USDT_ADDRESS, USDT_ABI, provider);
    // 调用 usdt 的 totalSupply
    let totalSupply = await USDT.totalSupply();
    console.log(totalSupply.toString());
  });
});
"><code><span class="hljs-comment">// 需要将 usdt 的 abi 保存在本地</span>
const USDT_ABI <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"./usdt_abi.json"</span>);
<span class="hljs-comment">// usdt 合约的主网地址</span>
const USDT_ADDRESS <span class="hljs-operator">=</span> <span class="hljs-string">"0xdAC17F958D2ee523a2206206994597C13D831ec7"</span>;
const { ethers } <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"hardhat"</span>);

describe(<span class="hljs-string">"Fork"</span>, <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
  it(<span class="hljs-string">"Testing fork data"</span>, async <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
    const provider <span class="hljs-operator">=</span> ethers.provider;
    <span class="hljs-comment">// 构造 usdt 合约对象</span>
    const USDT <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.Contract(USDT_ADDRESS, USDT_ABI, provider);
    <span class="hljs-comment">// 调用 usdt 的 totalSupply</span>
    let totalSupply <span class="hljs-operator">=</span> await USDT.totalSupply();
    console.log(totalSupply.toString());
  });
});
</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/64c33a01816f3484d7f153d3cf911d494d549f7c8c042dbe6ca9d68427fe720f.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>再去查看 USDT 的实际发行量：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e399a81235a00ea6b7ccb091ea5ae68169dde24088375f09eb063382dcf7c65f.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>验证数据正确。</p><p>fork 功能还有一个很有用的功能是，可以本地模拟任意账户，那么就意味着可以在本地拥有该地址的任意资产，这对我们做一些测试很有帮助。在测试文件中编写如下代码：</p><pre data-type="codeBlock" text="const mockAddress = &quot;0x4c8CFE078a5B989CeA4B330197246ceD82764c63&quot;;

await network.provider.request({
  method: &quot;hardhat_impersonateAccount&quot;,
  params: [mockAddress],
});

const signer = await ethers.provider.getSigner(mockAddress);
"><code>const <span class="hljs-attr">mockAddress</span> = <span class="hljs-string">"0x4c8CFE078a5B989CeA4B330197246ceD82764c63"</span><span class="hljs-comment">;</span>

await network.provider.request({
  method: "hardhat_impersonateAccount",
  params: <span class="hljs-section">[mockAddress]</span>,
})<span class="hljs-comment">;</span>

const <span class="hljs-attr">signer</span> = await ethers.provider.getSigner(mockAddress)<span class="hljs-comment">;</span>
</code></pre><p>这样就可以模拟地址 <code>0x4c8CFE078a5B989CeA4B330197246ceD82764c63</code>，此时 <code>signer</code> 就是该地址的信息。先来查看该地址的账户信息：</p><pre data-type="codeBlock" text="let ETHBalance = await signer.getBalance();
console.log(`ETH balance is ${ETHBalance.toString() / 1e18}`);

let USDTBalance = await USDT.balanceOf(signer.getAddress()) / 1e6;
console.log(`USDT balance is ${USDTBalance.toString()}`);
"><code>let ETHBalance <span class="hljs-operator">=</span> await signer.getBalance();
console.log(`ETH balance <span class="hljs-keyword">is</span> ${ETHBalance.toString() <span class="hljs-operator">/</span> <span class="hljs-number">1e18</span>}`);

let USDTBalance <span class="hljs-operator">=</span> await USDT.balanceOf(signer.getAddress()) <span class="hljs-operator">/</span> <span class="hljs-number">1e6</span>;
console.log(`USDT balance <span class="hljs-keyword">is</span> ${USDTBalance.toString()}`);
</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/00190f883654a9fe567815830d2801a61eb5053ef68fbfdbdf95fdd383bbe04b.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>主网查询其资产为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f34b2edba86014dfd382f6d2d1286c7b01b13a89e5a14ad60249b820549ce38b.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>验证数据正确。我们再来看看如何模拟该账户发送交易，假设需要向其他地址发送一万 USDT，代码为：</p><pre data-type="codeBlock" text="// 打印转账前的账户余额
let USDTBalanceA = await USDT.balanceOf(signer.getAddress()) / 1e6;
console.log(`USDT balance before transfer is ${USDTBalanceA.toString()}`);

const recipient = &quot;0x652361ED2a8FB7E9b15Fe073AAb9fE2cFacb0B52&quot;;

let USDTBalanceB = await USDT.balanceOf(recipient) / 1e6;
console.log(`USDT balance of recipient before transfer is ${USDTBalanceB.toString()}`);

console.log(&quot;========Transfering========&quot;);

// 转账操作
await USDT.connect(signer).transfer(
  &quot;0x652361ED2a8FB7E9b15Fe073AAb9fE2cFacb0B52&quot;,
  ethers.utils.parseUnits(&quot;10000&quot;, 6)
);

// 打印转账后的账户余额
USDTBalanceA = await USDT.balanceOf(signer.getAddress()) / 1e6;
console.log(`USDT balance after transfer is ${USDTBalanceA.toString()}`);

USDTBalanceB = await USDT.balanceOf(recipient) / 1e6;
console.log(`USDT balance of recipient after transfer is ${USDTBalanceB.toString()}`);
"><code><span class="hljs-comment">// 打印转账前的账户余额</span>
let USDTBalanceA <span class="hljs-operator">=</span> await USDT.balanceOf(signer.getAddress()) <span class="hljs-operator">/</span> <span class="hljs-number">1e6</span>;
console.log(`USDT balance before transfer <span class="hljs-keyword">is</span> ${USDTBalanceA.toString()}`);

const recipient <span class="hljs-operator">=</span> <span class="hljs-string">"0x652361ED2a8FB7E9b15Fe073AAb9fE2cFacb0B52"</span>;

let USDTBalanceB <span class="hljs-operator">=</span> await USDT.balanceOf(recipient) <span class="hljs-operator">/</span> <span class="hljs-number">1e6</span>;
console.log(`USDT balance of recipient before transfer <span class="hljs-keyword">is</span> ${USDTBalanceB.toString()}`);

console.log(<span class="hljs-string">"========Transfering========"</span>);

<span class="hljs-comment">// 转账操作</span>
await USDT.connect(signer).<span class="hljs-built_in">transfer</span>(
  <span class="hljs-string">"0x652361ED2a8FB7E9b15Fe073AAb9fE2cFacb0B52"</span>,
  ethers.utils.parseUnits(<span class="hljs-string">"10000"</span>, <span class="hljs-number">6</span>)
);

<span class="hljs-comment">// 打印转账后的账户余额</span>
USDTBalanceA <span class="hljs-operator">=</span> await USDT.balanceOf(signer.getAddress()) <span class="hljs-operator">/</span> <span class="hljs-number">1e6</span>;
console.log(`USDT balance after transfer <span class="hljs-keyword">is</span> ${USDTBalanceA.toString()}`);

USDTBalanceB <span class="hljs-operator">=</span> await USDT.balanceOf(recipient) <span class="hljs-operator">/</span> <span class="hljs-number">1e6</span>;
console.log(`USDT balance of recipient after transfer <span class="hljs-keyword">is</span> ${USDTBalanceB.toString()}`);
</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/54f3075285499f83e6e1e778fd5a73c3d24ac4c8a8d3c1c050e02a542d6ceadf.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>验证转账成功。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">拓展</h3><p>fork 下还有一些功能我们这里没有介绍，例如</p><pre data-type="codeBlock" text="// 设置账户余额
await network.provider.send(&quot;hardhat_setBalance&quot;, [
  &quot;0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B&quot;,
  &quot;0x1000&quot;,
]);

// 设置账户 nonce
await network.provider.send(&quot;hardhat_setNonce&quot;, [
  &quot;0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B&quot;,
  &quot;0x21&quot;,
]);
"><code><span class="hljs-comment">// 设置账户余额</span>
await network.provider.<span class="hljs-built_in">send</span>(<span class="hljs-string">"hardhat_setBalance"</span>, [
  <span class="hljs-string">"0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B"</span>,
  <span class="hljs-string">"0x1000"</span>,
]);

<span class="hljs-comment">// 设置账户 nonce</span>
await network.provider.<span class="hljs-built_in">send</span>(<span class="hljs-string">"hardhat_setNonce"</span>, [
  <span class="hljs-string">"0x0d2026b3EE6eC71FC6746ADb6311F6d3Ba1C000B"</span>,
  <span class="hljs-string">"0x21"</span>,
]);
</code></pre><p>感兴趣的朋友可以查看<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hardhat.org/hardhat-network/reference">文档</a>自行了解。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">总结</h3><p>Hardhat fork 在平时的测试中非常方便，例如我们想要测试闪电贷，套利合约等，如果在主网测试，一是很慢，二是会花费 Gas。不过这个功能也不是 Hardhat 的专属，Ganache 和 Foundry 也有 fork 功能，感兴趣的朋友可以了解一下。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">关于我</h3><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">参考</h3><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hardhat.org/hardhat-network/guides/mainnet-forking">https://hardhat.org/hardhat-network/guides/mainnet-forking</a></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://hardhat.org/hardhat-network/reference">https://hardhat.org/hardhat-network/reference</a></p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://stackoverflow.com/questions/71106843/check-balance-of-erc20-token-in-hardhat-using-ethers-js">https://stackoverflow.com/questions/71106843/check-balance-of-erc20-token-in-hardhat-using-ethers-js</a></p></blockquote></blockquote>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[EIP-712 使用详解]]></title>
            <link>https://paragraph.com/@xyyme/eip-712</link>
            <guid>8rvo00d07r3tY3PEb4Bc</guid>
            <pubDate>Fri, 03 Jun 2022 13:31:19 GMT</pubDate>
            <description><![CDATA[之前的文章我们介绍过如何对数据进行签名，利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单，一般就是给一串字符串，或者一串哈希签名，如果我们想为更复杂的数据签名就无法实现了。 EIP-712 的出现就是为了解决这个问题，利用 EIP-712，我们可以对更大的数据集，例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap，PancakeSwap 等 DEX 的朋友应该有印象，在移除 LP 流动性的时候，我们需要先签名，然后再发送一笔交易移除流动性。正常情况下，其实应该我们先调用 LP 代币的授权方法，授权 DEX 合约可以转移我们的 LP，然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次，就可以将两步交易合并为一步交易，从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。基本结构EIP712Domain顾名思义，是一个与域相关的结构体，总共包含五个字段：name，合约或者协议的名称version，合约的版本chainId，合约部署的链 Id，一般使用 ...]]></description>
            <content:encoded><![CDATA[<p>之前的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/-e1FodE7HZcwhw60VuGnbUue2SfCN4kn6JVg0JjCFS4">文章</a>我们介绍过如何对数据进行签名，利用签名技术我们可以实现一些功能例如白名单校验等。但是这种签名技术的应用场景比较简单，一般就是给一串字符串，或者一串哈希签名，如果我们想为更复杂的数据签名就无法实现了。</p><p>EIP-712 的出现就是为了解决这个问题，利用 EIP-712，我们可以对更大的数据集，例如对结构体进行签名。那么这种签名格式有什么实际的应用场景呢。使用过 Uniswap，PancakeSwap 等 DEX 的朋友应该有印象，在移除 LP 流动性的时候，我们需要先签名，然后再发送一笔交易移除流动性。正常情况下，其实应该我们先调用 LP 代币的授权方法，授权 DEX 合约可以转移我们的 LP，然后再去移除流动性。而这种二合一的实现正是应用了 EIP-712。它帮助我们仅仅签名一次，就可以将两步交易合并为一步交易，从而节省 Gas 费用。这篇文章我们就来看看 EIP-712 到底是怎么使用的。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">基本结构</h3><h4 id="h-eip712domain" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">EIP712Domain</h4><p>顾名思义，是一个与<code>域</code>相关的结构体，总共包含五个字段：</p><ul><li><p><code>name</code>，合约或者协议的名称</p></li><li><p><code>version</code>，合约的版本</p></li><li><p><code>chainId</code>，合约部署的链 Id，一般使用 <code>block.chainid</code>，即当前链 Id</p></li><li><p><code>verifyingContract</code>，签名的合约地址，一般使用 <code>address(this)</code>，即当前合约</p></li><li><p><code>salt</code>，随机数盐，一般不常用</p></li></ul><h4 id="h-domainseparator" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">DOMAIN_SEPARATOR</h4><p>EIP712Domain 数据的哈希值，即：</p><pre data-type="codeBlock" text="DOMAIN_SEPARATOR = keccak256(
    abi.encode(
        EIP712DOMAIN_TYPEHASH,
        keccak256(name，即合约名称),
        keccak256(version，即合约版本),
        chainId,
        verifyingContract
    )
);
"><code>DOMAIN_SEPARATOR <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
    <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
        EIP712DOMAIN_TYPEHASH,
        <span class="hljs-built_in">keccak256</span>(name，即合约名称),
        <span class="hljs-built_in">keccak256</span>(version，即合约版本),
        chainId,
        verifyingContract
    )
);
</code></pre><h4 id="h-eip712domaintypehash" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">EIP712DOMAIN_TYPEHASH</h4><pre data-type="codeBlock" text="bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
    &quot;EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)&quot;
);
"><code><span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">constant</span> EIP712DOMAIN_TYPEHASH <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
    <span class="hljs-string">"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"</span>
);
</code></pre><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">签名对象</h4><p>这里我们以签名 <code>Mail</code> 对象为例：</p><pre data-type="codeBlock" text="struct Mail {
    address from;
    address to;
    string contents;
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">Mail</span> {
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>;
    <span class="hljs-keyword">address</span> to;
    <span class="hljs-keyword">string</span> contents;
}
</code></pre><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">签名对象类型哈希</h4><pre data-type="codeBlock" text="bytes32 internal constant TYPE_HASH = keccak256(
    &quot;Mail(address from,address to,string contents)&quot;
);
"><code><span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">internal</span> <span class="hljs-keyword">constant</span> TYPE_HASH <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
    <span class="hljs-string">"Mail(address from,address to,string contents)"</span>
);
</code></pre><p>注意对象名要首字母大写，结构体字段按照函数签名编写。这是规范，套用即可。</p><p>如果结构体中还包含其他结构体，例如：</p><pre data-type="codeBlock" text="struct Transaction {
    Person from;
    Person to;
    Asset tx;
}

struct Asset {
    address token;
    uint256 amount;
}

struct Person {
    address wallet;
    string name;
}
"><code><span class="hljs-keyword">struct</span> <span class="hljs-title">Transaction</span> {
    Person <span class="hljs-keyword">from</span>;
    Person to;
    Asset <span class="hljs-built_in">tx</span>;
}

<span class="hljs-keyword">struct</span> <span class="hljs-title">Asset</span> {
    <span class="hljs-keyword">address</span> token;
    <span class="hljs-keyword">uint256</span> amount;
}

<span class="hljs-keyword">struct</span> <span class="hljs-title">Person</span> {
    <span class="hljs-keyword">address</span> wallet;
    <span class="hljs-keyword">string</span> name;
}
</code></pre><p>那么需要写成（按照首字母排序，因此 <code>Asset</code> 在 <code>Person</code> 前面）：</p><pre data-type="codeBlock" text="Transaction(Person from,Person to,Asset tx)Asset(address token,uint256 amount)Person(address wallet,string name)
"><code><span class="hljs-built_in">Transaction</span>(Person from,Person to,Asset tx)<span class="hljs-built_in">Asset</span>(address token,uint256 amount)<span class="hljs-built_in">Person</span>(address wallet,string name)
</code></pre><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">签名对象值哈希</h4><p>计算值哈希的格式为：</p><pre data-type="codeBlock" text="keccak256(
    abi.encode(
        TYPE_HASH, 
        mail.from,
        mail.to,
        keccak256(bytes(mail.contents))
    )
);
"><code><span class="hljs-built_in">keccak256</span>(
    <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
        TYPE_HASH, 
        mail.from,
        mail.to,
        <span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(mail.contents))
    )
);
</code></pre><p>其中第一个参数为 <code>TYPE_HASH</code>，即签名对象类型的哈希。接下来依次是对象的各个字段，如果是变长类型例如 <code>string</code>，<code>bytes</code>，则需要对其进行哈希。例如，这里的 <code>mail.contents</code> 是 <code>string</code> 类型，因此需要进行哈希。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">代码实践</h3><p>看了这么多概念，是不是已经懵了。我们马上来看看代码：</p><h4 id="h-" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">合约</h4><pre data-type="codeBlock" text="pragma solidity 0.8.10;

contract EIP712Mail {
    // Mail 是待签名的结构体
    struct Mail {
        address from;
        address to;
        string contents;
    }

    struct EIP712Domain {
        string  name;
        string  version;
        uint256 chainId;
        address verifyingContract;
    }

    bytes32 public immutable DOMAIN_SEPARATOR;

    bytes32 public constant EIP712DOMAIN_TYPEHASH = keccak256(
        &quot;EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)&quot;
    );

    // 签名对象哈希
    bytes32 internal constant TYPE_HASH = keccak256(
        &quot;Mail(address from,address to,string contents)&quot;
    );

    constructor() {
        DOMAIN_SEPARATOR = keccak256(
            // 计算 DOMAIN_SEPARATOR 哈希
            // 这里的 name 为 EIP712Mail，即合约名
            // version 为 1
            abi.encode(
                EIP712DOMAIN_TYPEHASH,
                keccak256(&quot;EIP712Mail&quot;),
                keccak256(&quot;1&quot;),
                block.chainid,
                address(this)
            )
        );
    }

    // 计算待签名的结构体的哈希
    function hashStruct(Mail memory mail) public pure returns (bytes32) {
        return keccak256(
            abi.encode(
                TYPE_HASH,
                mail.from,
                mail.to,
                keccak256(bytes(mail.contents))
            )
        );
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.10;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">EIP712Mail</span> </span>{
    <span class="hljs-comment">// Mail 是待签名的结构体</span>
    <span class="hljs-keyword">struct</span> <span class="hljs-title">Mail</span> {
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>;
        <span class="hljs-keyword">address</span> to;
        <span class="hljs-keyword">string</span> contents;
    }

    <span class="hljs-keyword">struct</span> <span class="hljs-title">EIP712Domain</span> {
        <span class="hljs-keyword">string</span>  name;
        <span class="hljs-keyword">string</span>  version;
        <span class="hljs-keyword">uint256</span> chainId;
        <span class="hljs-keyword">address</span> verifyingContract;
    }

    <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">immutable</span> DOMAIN_SEPARATOR;

    <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> EIP712DOMAIN_TYPEHASH <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
        <span class="hljs-string">"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"</span>
    );

    <span class="hljs-comment">// 签名对象哈希</span>
    <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">internal</span> <span class="hljs-keyword">constant</span> TYPE_HASH <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
        <span class="hljs-string">"Mail(address from,address to,string contents)"</span>
    );

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) </span>{
        DOMAIN_SEPARATOR <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
            <span class="hljs-comment">// 计算 DOMAIN_SEPARATOR 哈希</span>
            <span class="hljs-comment">// 这里的 name 为 EIP712Mail，即合约名</span>
            <span class="hljs-comment">// version 为 1</span>
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
                EIP712DOMAIN_TYPEHASH,
                <span class="hljs-built_in">keccak256</span>(<span class="hljs-string">"EIP712Mail"</span>),
                <span class="hljs-built_in">keccak256</span>(<span class="hljs-string">"1"</span>),
                <span class="hljs-built_in">block</span>.<span class="hljs-built_in">chainid</span>,
                <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)
            )
        );
    }

    <span class="hljs-comment">// 计算待签名的结构体的哈希</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashStruct</span>(<span class="hljs-params">Mail <span class="hljs-keyword">memory</span> mail</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bytes32</span></span>) </span>{
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">keccak256</span>(
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
                TYPE_HASH,
                mail.from,
                mail.to,
                <span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(mail.contents))
            )
        );
    }
}
</code></pre><p>这就是基本的代码逻辑，接下来再看看验证签名的代码：</p><pre data-type="codeBlock" text="function verify(Mail memory mail, address signer, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {

    // Note: we need to use `encodePacked` here instead of `encode`.
    // 这里是固定格式，套用即可
    bytes32 digest = keccak256(abi.encodePacked(
        &quot;\x19\x01&quot;,
        DOMAIN_SEPARATOR,
        hashStruct(mail)
    ));
    
    return ecrecover(digest, v, r, s) == signer;
    
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verify</span>(<span class="hljs-params">Mail <span class="hljs-keyword">memory</span> mail, <span class="hljs-keyword">address</span> signer, <span class="hljs-keyword">uint8</span> v, <span class="hljs-keyword">bytes32</span> r, <span class="hljs-keyword">bytes32</span> s</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">bool</span></span>) </span>{

    <span class="hljs-comment">// Note: we need to use `encodePacked` here instead of `encode`.</span>
    <span class="hljs-comment">// 这里是固定格式，套用即可</span>
    <span class="hljs-keyword">bytes32</span> digest <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-string">"\x19\x01"</span>,
        DOMAIN_SEPARATOR,
        hashStruct(mail)
    ));
    
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">ecrecover</span>(digest, v, r, s) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> signer;
    
}
</code></pre><p><code>verify</code> 函数接收三个参数，分别是待签名结构体，签名地址，v，r，s。其中 v，r，s 是构成签名的三部分，签名一共有 65 个字节，前 32 个字节是 r，接下来 32 个字节是 s，最后一个字节是 v。<code>ecrecover</code> 是 Solidity 内置函数，可以用于验证签名，它会根据 digest 以及签名内容 v，r，s 来计算出签名人的地址。如果结果等于传入的签名地址，则说明验证签名正确。</p><p>接下来我们看看在链下如何进行签名：</p><h4 id="h-javascript" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">使用 JavaScript 进行签名：</h4><pre data-type="codeBlock" text="const {ethers} = require(&quot;ethers&quot;);
// 将合约部署在 hardhat node 本地链上
const provider = new ethers.providers.JsonRpcProvider();

// 这里我们使用 hardhat node 自带的地址进行签名
const privateKey = `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`
const wallet = new ethers.Wallet(privateKey, provider);

async function sign() {
    // 获取 chainId
    const { chainId } = await provider.getNetwork();

    // 构造 domain 结构体
    // 最后一个地址字段，由于我们在合约中使用了 address(this)
    // 因此需要在部署合约之后，将合约地址粘贴到这里
    const domain = {
        name: &apos;EIP712Mail&apos;,
        version: &apos;1&apos;,
        chainId: 4,
        verifyingContract: &apos;0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0&apos;,
    };
    // The named list of all type definitions
    // 构造签名结构体类型对象
    const types = {
        Mail: [
            {name: &apos;from&apos;, type: &apos;address&apos;},
            {name: &apos;to&apos;, type: &apos;address&apos;},
            {name: &apos;contents&apos;, type: &apos;string&apos;}
        ]
    };
    // The data to sign
    // 自行构造一个结构体的值
    const value = {
        from: &apos;0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266&apos;,
        to: &apos;0x70997970c51812dc3a010c7d01b50e0d17dc79c8&apos;,
        contents: &apos;xyyme&apos;
    };
    const signature = await wallet._signTypedData(
        domain,
        types,
        value
    );

    // 将签名分割成 v r s 的格式
    let signParts = ethers.utils.splitSignature(signature);
    console.log(&quot;&gt;&gt;&gt; Signature:&quot;, signParts);
    // 打印签名本身
    console.log(signature);
}

sign()
"><code>const {ethers} <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"ethers"</span>);
<span class="hljs-comment">// 将合约部署在 hardhat node 本地链上</span>
const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.JsonRpcProvider();

<span class="hljs-comment">// 这里我们使用 hardhat node 自带的地址进行签名</span>
const privateKey <span class="hljs-operator">=</span> `<span class="hljs-number">0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80</span>`
const wallet <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.Wallet(privateKey, provider);

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sign</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// 获取 chainId</span>
    const { chainId } <span class="hljs-operator">=</span> await provider.getNetwork();

    <span class="hljs-comment">// 构造 domain 结构体</span>
    <span class="hljs-comment">// 最后一个地址字段，由于我们在合约中使用了 address(this)</span>
    <span class="hljs-comment">// 因此需要在部署合约之后，将合约地址粘贴到这里</span>
    const domain <span class="hljs-operator">=</span> {
        name: <span class="hljs-string">'EIP712Mail'</span>,
        version: <span class="hljs-string">'1'</span>,
        chainId: <span class="hljs-number">4</span>,
        verifyingContract: <span class="hljs-string">'0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0'</span>,
    };
    <span class="hljs-comment">// The named list of all type definitions</span>
    <span class="hljs-comment">// 构造签名结构体类型对象</span>
    const types <span class="hljs-operator">=</span> {
        Mail: [
            {name: <span class="hljs-string">'from'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'address'</span>},
            {name: <span class="hljs-string">'to'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'address'</span>},
            {name: <span class="hljs-string">'contents'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'string'</span>}
        ]
    };
    <span class="hljs-comment">// The data to sign</span>
    <span class="hljs-comment">// 自行构造一个结构体的值</span>
    const value <span class="hljs-operator">=</span> {
        <span class="hljs-keyword">from</span>: <span class="hljs-string">'0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'</span>,
        to: <span class="hljs-string">'0x70997970c51812dc3a010c7d01b50e0d17dc79c8'</span>,
        contents: <span class="hljs-string">'xyyme'</span>
    };
    const signature <span class="hljs-operator">=</span> await wallet._signTypedData(
        domain,
        types,
        value
    );

    <span class="hljs-comment">// 将签名分割成 v r s 的格式</span>
    let signParts <span class="hljs-operator">=</span> ethers.utils.splitSignature(signature);
    console.log(<span class="hljs-string">">>> Signature:"</span>, signParts);
    <span class="hljs-comment">// 打印签名本身</span>
    console.log(signature);
}

sign()
</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/a2a212ce3b2ddccc34174cfb6aec83bdd7a1ced7da3b2e0d95d5901c065a82c3.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>我们将 rsv 签名，vaule 值，以及签名地址传给 <code>verify</code> 函数进行验证，结果为 true，说明验证成功。</p><h4 id="h-python" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">使用 Python 进行签名：</h4><p>也可以使用 Python 进行签名，不过语法稍微有些复杂。我们这里作展示，但是我个人还是推荐使用 JavaScript 进行签名。</p><pre data-type="codeBlock" text="# 需要安装 web3, eth-account 依赖
import eth_account
from web3 import Web3
from eth_account.messages import encode_structured_data

web3 = Web3(Web3.HTTPProvider())

private_key = &apos;0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80&apos;
account = web3.eth.account.privateKeyToAccount(private_key)

domain = {
    &quot;name&quot;: &quot;EIP712Mail&quot;,
    &quot;version&quot;: &quot;1&quot;,
    &quot;chainId&quot;: web3.eth.chain_id,
    &quot;verifyingContract&quot;: &quot;0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0&quot;,
}

value = {
    &quot;from&quot;: &apos;0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266&apos;,
    &quot;to&quot;: &apos;0x70997970c51812dc3a010c7d01b50e0d17dc79c8&apos;,
    &quot;contents&quot;: &apos;xyyme&apos;
}

// 这里是固定格式，套用即可
msg = {
    &quot;types&quot;: {
        &quot;EIP712Domain&quot;: [
            {&quot;name&quot;: &quot;name&quot;, &quot;type&quot;: &quot;string&quot;},
            {&quot;name&quot;: &quot;version&quot;, &quot;type&quot;: &quot;string&quot;},
            {&quot;name&quot;: &quot;chainId&quot;, &quot;type&quot;: &quot;uint256&quot;},
            {&quot;name&quot;: &quot;verifyingContract&quot;, &quot;type&quot;: &quot;address&quot;}
        ],
        &quot;Mail&quot;: [
            {&quot;name&quot;: &apos;from&apos;, &quot;type&quot;: &apos;address&apos;},
            {&quot;name&quot;: &apos;to&apos;, &quot;type&quot;: &apos;address&apos;},
            {&quot;name&quot;: &apos;contents&apos;, &quot;type&quot;: &apos;string&apos;}
        ]
    },
    &quot;domain&quot;: domain,
    &quot;primaryType&quot;: &apos;Mail&apos;,
    &quot;message&quot;: value
}

// 需要先对结构数据进行编码
encoded_data = encode_structured_data(msg)

// 再进行签名
signed_message = web3.eth.account.sign_message(encoded_data, private_key)

print(signed_message)
r = signed_message[&apos;r&apos;]
s = signed_message[&apos;s&apos;]
v = signed_message[&apos;v&apos;]

print(f&apos;r =&gt; {hex(r)}&apos;)
print(f&apos;s =&gt; {hex(s)}&apos;)
print(f&apos;v =&gt; {hex(v)}&apos;)
"><code># 需要安装 web3, eth<span class="hljs-operator">-</span>account 依赖
<span class="hljs-keyword">import</span> <span class="hljs-title">eth_account</span>
<span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-title">web3</span> <span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">Web3</span>
<span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-title">eth_account</span>.<span class="hljs-title">messages</span> <span class="hljs-title"><span class="hljs-keyword">import</span></span> <span class="hljs-title">encode_structured_data</span>

<span class="hljs-title">web3</span> <span class="hljs-operator">=</span> <span class="hljs-title">Web3</span>(<span class="hljs-title">Web3</span>.<span class="hljs-title">HTTPProvider</span>())

<span class="hljs-title">private_key</span> <span class="hljs-operator">=</span> <span class="hljs-string">'0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'</span>
<span class="hljs-title">account</span> <span class="hljs-operator">=</span> <span class="hljs-title">web3</span>.<span class="hljs-title">eth</span>.<span class="hljs-title">account</span>.<span class="hljs-title">privateKeyToAccount</span>(<span class="hljs-title">private_key</span>)

<span class="hljs-title">domain</span> <span class="hljs-operator">=</span> {
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"EIP712Mail"</span>,
    <span class="hljs-string">"version"</span>: <span class="hljs-string">"1"</span>,
    <span class="hljs-string">"chainId"</span>: <span class="hljs-title">web3</span>.<span class="hljs-title">eth</span>.<span class="hljs-title">chain_id</span>,
    <span class="hljs-string">"verifyingContract"</span>: <span class="hljs-string">"0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0"</span>,
}

<span class="hljs-title">value</span> <span class="hljs-operator">=</span> {
    <span class="hljs-string">"from"</span>: <span class="hljs-string">'0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'</span>,
    <span class="hljs-string">"to"</span>: <span class="hljs-string">'0x70997970c51812dc3a010c7d01b50e0d17dc79c8'</span>,
    <span class="hljs-string">"contents"</span>: <span class="hljs-string">'xyyme'</span>
}

<span class="hljs-comment">// 这里是固定格式，套用即可</span>
<span class="hljs-title"><span class="hljs-built_in">msg</span></span> <span class="hljs-operator">=</span> {
    <span class="hljs-string">"types"</span>: {
        <span class="hljs-string">"EIP712Domain"</span>: [
            {<span class="hljs-string">"name"</span>: <span class="hljs-string">"name"</span>, <span class="hljs-string">"type"</span>: <span class="hljs-string">"string"</span>},
            {<span class="hljs-string">"name"</span>: <span class="hljs-string">"version"</span>, <span class="hljs-string">"type"</span>: <span class="hljs-string">"string"</span>},
            {<span class="hljs-string">"name"</span>: <span class="hljs-string">"chainId"</span>, <span class="hljs-string">"type"</span>: <span class="hljs-string">"uint256"</span>},
            {<span class="hljs-string">"name"</span>: <span class="hljs-string">"verifyingContract"</span>, <span class="hljs-string">"type"</span>: <span class="hljs-string">"address"</span>}
        ],
        <span class="hljs-string">"Mail"</span>: [
            {<span class="hljs-string">"name"</span>: <span class="hljs-string">'from'</span>, <span class="hljs-string">"type"</span>: <span class="hljs-string">'address'</span>},
            {<span class="hljs-string">"name"</span>: <span class="hljs-string">'to'</span>, <span class="hljs-string">"type"</span>: <span class="hljs-string">'address'</span>},
            {<span class="hljs-string">"name"</span>: <span class="hljs-string">'contents'</span>, <span class="hljs-string">"type"</span>: <span class="hljs-string">'string'</span>}
        ]
    },
    <span class="hljs-string">"domain"</span>: <span class="hljs-title">domain</span>,
    <span class="hljs-string">"primaryType"</span>: <span class="hljs-string">'Mail'</span>,
    <span class="hljs-string">"message"</span>: <span class="hljs-title">value</span>
}

<span class="hljs-comment">// 需要先对结构数据进行编码</span>
<span class="hljs-title">encoded_data</span> <span class="hljs-operator">=</span> <span class="hljs-title">encode_structured_data</span>(<span class="hljs-title"><span class="hljs-built_in">msg</span></span>)

<span class="hljs-comment">// 再进行签名</span>
<span class="hljs-title">signed_message</span> <span class="hljs-operator">=</span> <span class="hljs-title">web3</span>.<span class="hljs-title">eth</span>.<span class="hljs-title">account</span>.<span class="hljs-title">sign_message</span>(<span class="hljs-title">encoded_data</span>, <span class="hljs-title">private_key</span>)

<span class="hljs-title">print</span>(<span class="hljs-title">signed_message</span>)
<span class="hljs-title">r</span> <span class="hljs-operator">=</span> <span class="hljs-title">signed_message</span>[<span class="hljs-string">'r'</span>]
<span class="hljs-title">s</span> <span class="hljs-operator">=</span> <span class="hljs-title">signed_message</span>[<span class="hljs-string">'s'</span>]
<span class="hljs-title">v</span> <span class="hljs-operator">=</span> <span class="hljs-title">signed_message</span>[<span class="hljs-string">'v'</span>]

<span class="hljs-title">print</span>(<span class="hljs-title">f</span><span class="hljs-string">'r => {hex(r)}'</span>)
<span class="hljs-title">print</span>(<span class="hljs-title">f</span><span class="hljs-string">'s => {hex(s)}'</span>)
<span class="hljs-title">print</span>(<span class="hljs-title">f</span><span class="hljs-string">'v => {hex(v)}'</span>)
</code></pre><p>同样可以打印出 rsv 签名。不过我在使用 Python 的时候遇到了一个问题，就是如果待签名结构体中存在 <code>bytes</code> 字段，需要使用：</p><pre data-type="codeBlock" text="bytes(&quot;...xxx...&quot;, &quot;utf-8&quot;)
"><code><span class="hljs-built_in">bytes</span>(<span class="hljs-string">"...xxx..."</span>, <span class="hljs-string">"utf-8"</span>)
</code></pre><p>对内容进行编码，而这种类型无法进行 JSON 序列化，这是当前版本（5.29.1）存在的问题，更新到测试版（6.0.0b2）则可以成功签名，但是签名结果错误。这里我没有深究，也可能是我的使用方法有误。个人还是推荐使用 JavaScript 进行签名，更加简单易用。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">应用</h3><p>最开始我们提到过，Uniswap 中运用了 EIP-712，使得移除流动性的操作由两步变成一步，减少了 Gas 的使用。由于 Uniswap 的操作比较复杂，需要组 LP 等，用于演示的话会占用比较长的篇幅。我们这里使用 Dai 的合约进行演示，Dai 的合约中有一个 <code>permit</code> 函数，用于第三方授权，同样也是应用了 EIP-712 标准。</p><p>我们知道在 ERC20 币种中，A 可以调用 <code>approve</code> 来对 B 进行授权，而 Dai 合约中的 <code>permit</code> 函数的目的就是，A 提前在链下对授权对象进行签名，这样第三方就可以拿着 A 的签名去调用 <code>permit</code> 来实现 A 的授权操作，从而使 A 在不发送交易的情况下就能够完成授权操作。</p><p>我们来看看 Dai 的核心代码（仅包含了签名相关）：</p><pre data-type="codeBlock" text="contract Dai is LibNote {
    // ERC20 信息，name 和 version 用于 domain 签名
    string  public constant name     = &quot;Dai Stablecoin&quot;;
    string  public constant symbol   = &quot;DAI&quot;;
    string  public constant version  = &quot;1&quot;;
    uint8   public constant decimals = 18;
    uint256 public totalSupply;

    mapping (address =&gt; uint)                      public balanceOf;
    mapping (address =&gt; mapping (address =&gt; uint)) public allowance;
    // nonces 用于避免重放攻击
    mapping (address =&gt; uint)                      public nonces;

    // --- EIP712 niceties ---
    bytes32 public DOMAIN_SEPARATOR;

    // 计算签名结构体 Permit 的哈希
    // bytes32 public constant PERMIT_TYPEHASH = keccak256(&quot;Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)&quot;);

    bytes32 public constant PERMIT_TYPEHASH = 0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb;

    constructor(uint256 chainId_) public {
        wards[msg.sender] = 1;
        // 计算 domain 哈希
        DOMAIN_SEPARATOR = keccak256(abi.encode(
            keccak256(&quot;EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)&quot;),
            keccak256(bytes(name)),
            keccak256(bytes(version)),
            chainId_,
            address(this)
        ));
    }

    // 常规授权方法
    function approve(address usr, uint wad) 
        external returns (bool) {
        allowance[msg.sender][usr] = wad;
        emit Approval(msg.sender, usr, wad);
        return true;
    }

    // --- Approve by signature ---
    // 重点是这里的 permit 函数
    function permit(address holder, address spender, uint256 nonce, uint256 expiry,
                    bool allowed, uint8 v, bytes32 r, bytes32 s) external
    {
        bytes32 digest =
            keccak256(abi.encodePacked(
                &quot;\x19\x01&quot;,
                DOMAIN_SEPARATOR,
                keccak256(abi.encode(PERMIT_TYPEHASH,
                                     holder,
                                     spender,
                                     nonce,
                                     expiry,
                                     allowed))
        ));

        require(holder != address(0), &quot;Dai/invalid-address-0&quot;);
        require(holder == ecrecover(digest, v, r, s), &quot;Dai/invalid-permit&quot;);
        require(expiry == 0 || now &lt;= expiry, &quot;Dai/permit-expired&quot;);
        // 用于防止重放攻击
        require(nonce == nonces[holder]++, &quot;Dai/invalid-nonce&quot;);
        uint wad = allowed ? uint(-1) : 0;
        allowance[holder][spender] = wad;
        emit Approval(holder, spender, wad);
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Dai</span> <span class="hljs-keyword">is</span> <span class="hljs-title">LibNote</span> </span>{
    <span class="hljs-comment">// ERC20 信息，name 和 version 用于 domain 签名</span>
    <span class="hljs-keyword">string</span>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> name     <span class="hljs-operator">=</span> <span class="hljs-string">"Dai Stablecoin"</span>;
    <span class="hljs-keyword">string</span>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> symbol   <span class="hljs-operator">=</span> <span class="hljs-string">"DAI"</span>;
    <span class="hljs-keyword">string</span>  <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> version  <span class="hljs-operator">=</span> <span class="hljs-string">"1"</span>;
    <span class="hljs-keyword">uint8</span>   <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> decimals <span class="hljs-operator">=</span> <span class="hljs-number">18</span>;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> totalSupply;

    <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">uint</span>)                      <span class="hljs-keyword">public</span> balanceOf;
    <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">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">></span> <span class="hljs-keyword">uint</span>)) <span class="hljs-keyword">public</span> allowance;
    <span class="hljs-comment">// nonces 用于避免重放攻击</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">uint</span>)                      <span class="hljs-keyword">public</span> nonces;

    <span class="hljs-comment">// --- EIP712 niceties ---</span>
    <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">public</span> DOMAIN_SEPARATOR;

    <span class="hljs-comment">// 计算签名结构体 Permit 的哈希</span>
    <span class="hljs-comment">// bytes32 public constant PERMIT_TYPEHASH = keccak256("Permit(address holder,address spender,uint256 nonce,uint256 expiry,bool allowed)");</span>

    <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> PERMIT_TYPEHASH <span class="hljs-operator">=</span> <span class="hljs-number">0xea2aa0a1be11a07ed86d755c93467f4f82362b452371d1ba94d1715123511acb</span>;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> chainId_</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        wards[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
        <span class="hljs-comment">// 计算 domain 哈希</span>
        DOMAIN_SEPARATOR <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
            <span class="hljs-built_in">keccak256</span>(<span class="hljs-string">"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"</span>),
            <span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(name)),
            <span class="hljs-built_in">keccak256</span>(<span class="hljs-keyword">bytes</span>(version)),
            chainId_,
            <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)
        ));
    }

    <span class="hljs-comment">// 常规授权方法</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> usr, <span class="hljs-keyword">uint</span> wad</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>{
        allowance[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>][usr] <span class="hljs-operator">=</span> wad;
        <span class="hljs-keyword">emit</span> Approval(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, usr, wad);
        <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
    }

    <span class="hljs-comment">// --- Approve by signature ---</span>
    <span class="hljs-comment">// 重点是这里的 permit 函数</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">permit</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> holder, <span class="hljs-keyword">address</span> spender, <span class="hljs-keyword">uint256</span> nonce, <span class="hljs-keyword">uint256</span> expiry,
                    <span class="hljs-keyword">bool</span> allowed, <span class="hljs-keyword">uint8</span> v, <span class="hljs-keyword">bytes32</span> r, <span class="hljs-keyword">bytes32</span> s</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span>
    </span>{
        <span class="hljs-keyword">bytes32</span> digest <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-string">"\x19\x01"</span>,
                DOMAIN_SEPARATOR,
                <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(PERMIT_TYPEHASH,
                                     holder,
                                     spender,
                                     nonce,
                                     expiry,
                                     allowed))
        ));

        <span class="hljs-built_in">require</span>(holder <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>), <span class="hljs-string">"Dai/invalid-address-0"</span>);
        <span class="hljs-built_in">require</span>(holder <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-built_in">ecrecover</span>(digest, v, r, s), <span class="hljs-string">"Dai/invalid-permit"</span>);
        <span class="hljs-built_in">require</span>(expiry <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> <span class="hljs-built_in">now</span> <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> expiry, <span class="hljs-string">"Dai/permit-expired"</span>);
        <span class="hljs-comment">// 用于防止重放攻击</span>
        <span class="hljs-built_in">require</span>(nonce <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nonces[holder]<span class="hljs-operator">+</span><span class="hljs-operator">+</span>, <span class="hljs-string">"Dai/invalid-nonce"</span>);
        <span class="hljs-keyword">uint</span> wad <span class="hljs-operator">=</span> allowed ? <span class="hljs-keyword">uint</span>(<span class="hljs-number">-1</span>) : <span class="hljs-number">0</span>;
        allowance[holder][spender] <span class="hljs-operator">=</span> wad;
        <span class="hljs-keyword">emit</span> Approval(holder, spender, wad);
    }
}
</code></pre><p>可以看到这些代码与我们前面的代码实践大同小异。<code>permit</code> 函数的参数中，<code>holder</code> 地址需要在链下进行签名，<code>spender</code> 即为被授权地址，<code>nonce</code> 用于防止重放攻击，这是什么意思呢？</p><p>假设 A 之前有一个对 B 进行授权的签名，后来 A 又取消了授权，也就是将授权额度减为 0。或者说 B 在一段时间内已经耗费完了所有授权。假设没有防止重放攻击，那么在这时，如果 B 是有恶意的，那么 B 就可以再次使用之前 A 的授权签名来进行授权，从而花费 A 的 token。如果加上了 <code>nonce</code> 字段，那么 A 在每次签名的时候，也对 <code>nonce</code> 进行签名，同时合约中对 <code>nonce</code> 进行记录，且是递增的，这样就可以确保每次的签名只能够使用一次，防止发生重放攻击。</p><p>接下来，我们来部署 Dai 的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/address/0x6b175474e89094c44da98b954eedeac495271d0f#contracts">代码</a>进行测试，部署时 <code>chainId</code> 使用 31337，这是 hardhat node 本地链的 chainId。然后使用 JavaScript 在链下进行签名：</p><pre data-type="codeBlock" text="const {ethers} = require(&quot;ethers&quot;);
const provider = new ethers.providers.JsonRpcProvider()

const privateKey1 = `0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80` // Private key of account 1
const wallet = new ethers.Wallet(privateKey1, provider)

async function sign() {
    const { chainId } = await provider.getNetwork();
    const domain = {
        name: &apos;Dai Stablecoin&apos;,
        version: &apos;1&apos;,
        chainId: chainId,
        verifyingContract: &apos;0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9&apos;,
    };
    const types = {
        Permit: [
            {name: &apos;holder&apos;, type: &apos;address&apos;},
            {name: &apos;spender&apos;, type: &apos;address&apos;},
            {name: &apos;nonce&apos;, type: &apos;uint256&apos;},
            {name: &apos;expiry&apos;, type: &apos;uint256&apos;},
            {name: &apos;allowed&apos;, type: &apos;bool&apos;},
        ]
    };

    // 这里 expiry 需要使用 0 或者比当前时间大的时间戳
    // 由于是初次授权，因此 nonce 为 0，下次递增
    const value = {
        holder: &apos;0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266&apos;,
        spender: &apos;0x70997970c51812dc3a010c7d01b50e0d17dc79c8&apos;,
        nonce: 0,
        expiry: 2208963661,
        allowed: true
    };
    const signature = await wallet._signTypedData(
        domain,
        types,
        value
    );

    let signParts = ethers.utils.splitSignature(signature);
    console.log(&quot;&gt;&gt;&gt; Signature:&quot;, signParts);
    console.log(signature);
}

sign()
"><code>const {ethers} <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"ethers"</span>);
const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.JsonRpcProvider()

const privateKey1 <span class="hljs-operator">=</span> `<span class="hljs-number">0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80</span>` <span class="hljs-comment">// Private key of account 1</span>
const wallet <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.Wallet(privateKey1, provider)

async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">sign</span>(<span class="hljs-params"></span>) </span>{
    const { chainId } <span class="hljs-operator">=</span> await provider.getNetwork();
    const domain <span class="hljs-operator">=</span> {
        name: <span class="hljs-string">'Dai Stablecoin'</span>,
        version: <span class="hljs-string">'1'</span>,
        chainId: chainId,
        verifyingContract: <span class="hljs-string">'0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9'</span>,
    };
    const types <span class="hljs-operator">=</span> {
        Permit: [
            {name: <span class="hljs-string">'holder'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'address'</span>},
            {name: <span class="hljs-string">'spender'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'address'</span>},
            {name: <span class="hljs-string">'nonce'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'uint256'</span>},
            {name: <span class="hljs-string">'expiry'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'uint256'</span>},
            {name: <span class="hljs-string">'allowed'</span>, <span class="hljs-keyword">type</span>: <span class="hljs-string">'bool'</span>},
        ]
    };

    <span class="hljs-comment">// 这里 expiry 需要使用 0 或者比当前时间大的时间戳</span>
    <span class="hljs-comment">// 由于是初次授权，因此 nonce 为 0，下次递增</span>
    const value <span class="hljs-operator">=</span> {
        holder: <span class="hljs-string">'0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'</span>,
        spender: <span class="hljs-string">'0x70997970c51812dc3a010c7d01b50e0d17dc79c8'</span>,
        nonce: <span class="hljs-number">0</span>,
        expiry: <span class="hljs-number">2208963661</span>,
        allowed: <span class="hljs-literal">true</span>
    };
    const signature <span class="hljs-operator">=</span> await wallet._signTypedData(
        domain,
        types,
        value
    );

    let signParts <span class="hljs-operator">=</span> ethers.utils.splitSignature(signature);
    console.log(<span class="hljs-string">">>> Signature:"</span>, signParts);
    console.log(signature);
}

sign()
</code></pre><p>打印出签名结果，使用第三方地址调用 <code>permit</code> 函数传入对应的参数。接下来我们调用 <code>allowance</code> 函数传入 <code>holder</code> 与 <code>spender</code>，结果非零，为 <code>type(uint).max</code>，说明我们操作成功。</p><p>对于 Uniswap 的签名操作，感兴趣的朋友可以自己实践操作一下，我们这里不再演示。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">总结</h3><p>EIP-712 初次看上去比较复杂，其实只要掌握了用法，基本都是套用即可，难度不高。在业界的一些应用也确实能够提高用户的体验。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">关于我</h3><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">参考</h3><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.8btc.com/article/6669785">https://www.8btc.com/article/6669785</a></p><div data-type="embedly" src="https://eips.ethereum.org/EIPS/eip-712" data="{&quot;provider_url&quot;:&quot;https://eips.ethereum.org&quot;,&quot;description&quot;:&quot;A procedure for hashing and signing of typed structured data as opposed to just bytestrings.&quot;,&quot;title&quot;:&quot;EIP-712: Typed structured data hashing and signing&quot;,&quot;url&quot;:&quot;https://eips.ethereum.org/EIPS/eip-712&quot;,&quot;author_name&quot;:&quot;Ethereum Improvement Proposals&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;author_url&quot;:&quot;https://eips.ethereum.org/&quot;,&quot;provider_name&quot;:&quot;Ethereum Improvement Proposals&quot;,&quot;type&quot;:&quot;link&quot;}" format="small"><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://eips.ethereum.org/EIPS/eip-712" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>EIP-712: Typed structured data hashing and signing</h2><p>A procedure for hashing and signing of typed structured data as opposed to just bytestrings.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://eips.ethereum.org</span></div></div></a></div></div><div data-type="embedly" src="https://learnblockchain.cn/article/1496" data="{&quot;provider_url&quot;:&quot;https://learnblockchain.cn&quot;,&quot;description&quot;:&quot;本文介绍了一种通过线下签名的进行授权的方式，来转移 gas 费用。&quot;,&quot;title&quot;:&quot;EIP2612: 通过链下签名授权实现更少 Gas 的 ERC20代币&quot;,&quot;author_name&quot;:&quot;LBC Team&quot;,&quot;url&quot;:&quot;https://learnblockchain.cn/article/1496&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/65bbda98b502850f5aff8924df3c0bdb2090c82a1369c8e9f7a74d2100f984ea.webp&quot;,&quot;thumbnail_width&quot;:705,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;登链社区&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:300,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:705,&quot;height&quot;:300,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/65bbda98b502850f5aff8924df3c0bdb2090c82a1369c8e9f7a74d2100f984ea.webp&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/65bbda98b502850f5aff8924df3c0bdb2090c82a1369c8e9f7a74d2100f984ea.webp"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://learnblockchain.cn/article/1496" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>EIP2612: 通过链下签名授权实现更少 Gas 的 ERC20代币</h2><p>本文介绍了一种通过线下签名的进行授权的方式，来转移 gas 费用。</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://learnblockchain.cn</span></div><img src="https://storage.googleapis.com/papyrus_images/65bbda98b502850f5aff8924df3c0bdb2090c82a1369c8e9f7a74d2100f984ea.webp"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[Ethers.js 简明使用教程]]></title>
            <link>https://paragraph.com/@xyyme/ethers-js</link>
            <guid>AbkSMVDP1BaiKWqvk41O</guid>
            <pubDate>Wed, 25 May 2022 07:40:27 GMT</pubDate>
            <description><![CDATA[Ethers.js 是一个与 evm-compitable 区块链交互的 JavaScript 库。用户通过编写 JavaScript 代码就可以与区块链进行交互，包括读写等操作。同时还有一个名为 web3.js 的库也可以与区块链交互，与之相比，Ethers.js 更加轻量级，上手更加简单。安装npm install --save ethers基本使用方法// 需要导入 ethers 包 const { ethers } = require("ethers"); // 构建 provider，可以理解为与区块链交互的桥梁 // 1. 可使用最基础的 JsonRpcProvider 进行构建 provider const INFURA_ID = 'xxx..'; const provider = new ethers.providers.JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_ID}`); // 2. 也可使用封装好的特定 provider，例如 AlchemyProvider，InfuraProvider /...]]></description>
            <content:encoded><![CDATA[<p><code>Ethers.js</code> 是一个与 evm-compitable 区块链交互的 JavaScript 库。用户通过编写 JavaScript 代码就可以与区块链进行交互，包括读写等操作。同时还有一个名为 <code>web3.js</code> 的库也可以与区块链交互，与之相比，<code>Ethers.js</code> 更加轻量级，上手更加简单。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">安装</h3><blockquote><p>npm install --save ethers</p></blockquote><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">基本使用方法</h3><pre data-type="codeBlock" text="// 需要导入 ethers 包
const { ethers } = require(&quot;ethers&quot;);

// 构建 provider，可以理解为与区块链交互的桥梁
// 1. 可使用最基础的 JsonRpcProvider 进行构建 provider
const INFURA_ID = &apos;xxx..&apos;;
const provider = new ethers.providers.JsonRpcProvider(`https://mainnet.infura.io/v3/${INFURA_ID}`);

// 2. 也可使用封装好的特定 provider，例如 AlchemyProvider，InfuraProvider
// 使用 alchemy 的时候，第一个参数若为 null 则代表主网
const ALCHEMY_ID = `xxx...`;
const provider = new ethers.providers.AlchemyProvider(&apos;null&apos;, ALCHEMY_ID);
"><code><span class="hljs-comment">// 需要导入 ethers 包</span>
const { ethers } <span class="hljs-operator">=</span> <span class="hljs-built_in">require</span>(<span class="hljs-string">"ethers"</span>);

<span class="hljs-comment">// 构建 provider，可以理解为与区块链交互的桥梁</span>
<span class="hljs-comment">// 1. 可使用最基础的 JsonRpcProvider 进行构建 provider</span>
const INFURA_ID <span class="hljs-operator">=</span> <span class="hljs-string">'xxx..'</span>;
const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.JsonRpcProvider(`https:<span class="hljs-comment">//mainnet.infura.io/v3/${INFURA_ID}`);</span>

<span class="hljs-comment">// 2. 也可使用封装好的特定 provider，例如 AlchemyProvider，InfuraProvider</span>
<span class="hljs-comment">// 使用 alchemy 的时候，第一个参数若为 null 则代表主网</span>
const ALCHEMY_ID <span class="hljs-operator">=</span> `xxx...`;
const provider <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.providers.AlchemyProvider(<span class="hljs-string">'null'</span>, ALCHEMY_ID);
</code></pre><p>接下来我们看看一些常用的函数：</p><pre data-type="codeBlock" text="// 获取当前网络最新的区块号 
let blockNumber = await provider.getBlockNumber(); 

// 获取余额，参数为地址，返回值是 bignumber 格式，单位为 wei
let balance = await provider.getBalance(&quot;0x...&quot;);

// 将 wei 格式转化为 ether 格式，bignumber 和 toString 格式都可以作为参数
let balance_in_ether = ethers.utils.formatEther(balance);

// 将 ether 转换为 wei 格式，返回值为 bignumber 格式
let result = ethers.utils.parseEther(&quot;1.0&quot;);

// 获取 gas price，返回值为 bignumber 格式
let gasPrice = await provider.getGasPrice();

// 获取内存 storage slot
// 第一个参数是合约地址，第二个参数是插槽位置
// 插槽位置可以使用十进制或者十六进制，十六进制需要加引号
await provider.getStorageAt(&quot;0x...&quot;, 3);
await provider.getStorageAt(&quot;0x...&quot;, &quot;0x121&quot;);

// 获取地址的 nonce
await provider.getTransactionCount(&quot;0x...&quot;);
"><code><span class="hljs-comment">// 获取当前网络最新的区块号 </span>
let blockNumber <span class="hljs-operator">=</span> await provider.getBlockNumber(); 

<span class="hljs-comment">// 获取余额，参数为地址，返回值是 bignumber 格式，单位为 wei</span>
let balance <span class="hljs-operator">=</span> await provider.getBalance(<span class="hljs-string">"0x..."</span>);

<span class="hljs-comment">// 将 wei 格式转化为 ether 格式，bignumber 和 toString 格式都可以作为参数</span>
let balance_in_ether <span class="hljs-operator">=</span> ethers.utils.formatEther(balance);

<span class="hljs-comment">// 将 ether 转换为 wei 格式，返回值为 bignumber 格式</span>
let result <span class="hljs-operator">=</span> ethers.utils.parseEther(<span class="hljs-string">"1.0"</span>);

<span class="hljs-comment">// 获取 gas price，返回值为 bignumber 格式</span>
let gasPrice <span class="hljs-operator">=</span> await provider.getGasPrice();

<span class="hljs-comment">// 获取内存 storage slot</span>
<span class="hljs-comment">// 第一个参数是合约地址，第二个参数是插槽位置</span>
<span class="hljs-comment">// 插槽位置可以使用十进制或者十六进制，十六进制需要加引号</span>
await provider.getStorageAt(<span class="hljs-string">"0x..."</span>, <span class="hljs-number">3</span>);
await provider.getStorageAt(<span class="hljs-string">"0x..."</span>, <span class="hljs-string">"0x121"</span>);

<span class="hljs-comment">// 获取地址的 nonce</span>
await provider.getTransactionCount(<span class="hljs-string">"0x..."</span>);
</code></pre><p>注意上面返回值为 <code>bignumber</code> 格式的数据，如果要在控制台打印，需要使用 <code>toString()</code> 进行转换。同时，由于 JavaScript 中的语法限制，所有 <code>await</code> 的语句都要放在 <code>async</code> 函数中。也就是说，要执行带有 <code>await</code> 的语句，必须为：</p><pre data-type="codeBlock" text="const main = async () =&gt; {
    let gasPrice = await provider.getGasPrice();
}

main();
"><code>const <span class="hljs-attr">main</span> = async () => {
    let <span class="hljs-attr">gasPrice</span> = await provider.getGasPrice()<span class="hljs-comment">;</span>
}

main()<span class="hljs-comment">;</span>
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">构建钱包信息</h3><pre data-type="codeBlock" text="// 构建钱包有两种方法
// 1. 直接通过 provider 和 私钥 构建
const privateKey1 = `0x...` // Private key of account 1
const wallet = new ethers.Wallet(privateKey1, provider);

// 2. 先通过 私钥 构建钱包，然后连接 provider
wallet = new ethers.Wallet(privateKey1);
walletSigner = wallet.connect(provider);

// 获取钱包地址
let address = await wallet.getAddress();
"><code><span class="hljs-comment">// 构建钱包有两种方法</span>
<span class="hljs-comment">// 1. 直接通过 provider 和 私钥 构建</span>
const privateKey1 <span class="hljs-operator">=</span> `0x...` <span class="hljs-comment">// Private key of account 1</span>
const wallet <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.Wallet(privateKey1, provider);

<span class="hljs-comment">// 2. 先通过 私钥 构建钱包，然后连接 provider</span>
wallet <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> ethers.Wallet(privateKey1);
walletSigner <span class="hljs-operator">=</span> wallet.connect(provider);

<span class="hljs-comment">// 获取钱包地址</span>
let <span class="hljs-keyword">address</span> <span class="hljs-operator">=</span> await wallet.getAddress();
</code></pre><h3 id="h-ethbnb" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">发送链上原生货币（例如 ETH，BNB）</h3><pre data-type="codeBlock" text="// eth 转账
const tx = await wallet.sendTransaction({
    to: wallet2.getAddress(),
    value: ethers.utils.parseEther(&quot;100&quot;)
});

// 等待交易上链
await tx.wait();
// 打印交易信息
console.log(tx);
"><code><span class="hljs-comment">// eth 转账</span>
const <span class="hljs-built_in">tx</span> <span class="hljs-operator">=</span> await wallet.sendTransaction({
    to: wallet2.getAddress(),
    <span class="hljs-built_in">value</span>: ethers.utils.parseEther(<span class="hljs-string">"100"</span>)
});

<span class="hljs-comment">// 等待交易上链</span>
await <span class="hljs-built_in">tx</span>.wait();
<span class="hljs-comment">// 打印交易信息</span>
console.log(<span class="hljs-built_in">tx</span>);
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">构建合约</h3><pre data-type="codeBlock" text="// 可以直接通过 函数签名 构建 abi
// 需要用到哪个函数就写哪个，不需要写出全部的函数签名
const ERC20_ABI = [
    &quot;function name() public view returns (string)&quot;,
    &quot;function symbol() view returns (string)&quot;,
    &quot;function totalSupply() view returns (uint256)&quot;,
    &quot;function balanceOf(address) view returns (uint)&quot;,

    &quot;event Transfer(address indexed from, address indexed to, uint amount)&quot;
];

// 合约地址
const address = &apos;0x...&apos;;
// 通过 地址，abi，provider 构建合约对象
const contract = new ethers.Contract(address, ERC20_ABI, provider);
"><code><span class="hljs-comment">// 可以直接通过 函数签名 构建 abi</span>
<span class="hljs-comment">// 需要用到哪个函数就写哪个，不需要写出全部的函数签名</span>
const ERC20_ABI <span class="hljs-operator">=</span> [
    <span class="hljs-string">"function name() public view returns (string)"</span>,
    <span class="hljs-string">"function symbol() view returns (string)"</span>,
    <span class="hljs-string">"function totalSupply() view returns (uint256)"</span>,
    <span class="hljs-string">"function balanceOf(address) view returns (uint)"</span>,

    <span class="hljs-string">"event Transfer(address indexed from, address indexed to, uint amount)"</span>
];

<span class="hljs-comment">// 合约地址</span>
const <span class="hljs-keyword">address</span> <span class="hljs-operator">=</span> <span class="hljs-string">'0x...'</span>;
<span class="hljs-comment">// 通过 地址，abi，provider 构建合约对象</span>
const <span class="hljs-class"><span class="hljs-keyword">contract</span> = <span class="hljs-title"><span class="hljs-keyword">new</span></span> <span class="hljs-title">ethers</span>.<span class="hljs-title">Contract</span>(<span class="hljs-params"><span class="hljs-keyword">address</span>, ERC20_ABI, provider</span>);
</span></code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">调用合约只读方法</h3><pre data-type="codeBlock" text="// 读取 name() 的值
const name = await contract.name();
console.log(name.toString());

// 读取 symbol() 的值
const symbol = await contract.symbol();
console.log(symbol);

// 读取 ERC20 合约中 balanceOf 的值
const balance = await contract.balanceOf(&apos;0x....&apos;);
console.log(balance.toString());
"><code><span class="hljs-comment">// 读取 name() 的值</span>
const name <span class="hljs-operator">=</span> await <span class="hljs-keyword">contract</span>.<span class="hljs-built_in">name</span>();
console.log(name.toString());

<span class="hljs-comment">// 读取 symbol() 的值</span>
const symbol <span class="hljs-operator">=</span> await <span class="hljs-keyword">contract</span>.symbol();
console.log(symbol);

<span class="hljs-comment">// 读取 ERC20 合约中 balanceOf 的值</span>
const balance <span class="hljs-operator">=</span> await <span class="hljs-keyword">contract</span>.balanceOf(<span class="hljs-string">'0x....'</span>);
console.log(balance.toString());
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">调用合约的写方法</h3><pre data-type="codeBlock" text="// 获取钱包的 ERC20 余额
const balance = await contract.balanceOf(wallet.getAddress());

// 合约连接钱包对象
const contractWithWallet = contract.connect(wallet);

// 调用合约的 transfer 方法向其他账户转账
// 注意这里是调用 ERC20 合约的 transfer 函数，而不是原生货币转账
// 如果要调用 approve 函数，则为 contractWithWallet.approve
const tx = await contractWithWallet.transfer(&apos;0x...&apos;, balance);
// 等待交易上链
await tx.wait();

console.log(tx);
"><code><span class="hljs-comment">// 获取钱包的 ERC20 余额</span>
const balance <span class="hljs-operator">=</span> await <span class="hljs-keyword">contract</span>.balanceOf(wallet.getAddress());

<span class="hljs-comment">// 合约连接钱包对象</span>
const contractWithWallet <span class="hljs-operator">=</span> <span class="hljs-keyword">contract</span>.connect(wallet);

<span class="hljs-comment">// 调用合约的 transfer 方法向其他账户转账</span>
<span class="hljs-comment">// 注意这里是调用 ERC20 合约的 transfer 函数，而不是原生货币转账</span>
<span class="hljs-comment">// 如果要调用 approve 函数，则为 contractWithWallet.approve</span>
const <span class="hljs-built_in">tx</span> <span class="hljs-operator">=</span> await contractWithWallet.<span class="hljs-built_in">transfer</span>(<span class="hljs-string">'0x...'</span>, balance);
<span class="hljs-comment">// 等待交易上链</span>
await <span class="hljs-built_in">tx</span>.wait();

console.log(<span class="hljs-built_in">tx</span>);
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">实时监听合约事件</h3><pre data-type="codeBlock" text="// Transfer 事件要在 abi 中声明
// 括号中的参数分别对应 Transfer 事件的参数
contract.on(&quot;Transfer&quot;, (from, to, amount, event) =&gt; {
    console.log(`${from} sent ${ethers.utils.formatEther(amount)} to ${to}`);
});

// 这里是指定参数的监听行为
// 例如，我只想监听接收人是 `0x1234....` 的事件，那么就这样指定地址
// 构造一个 filter，然后通过 filter 筛选
filter = contract.filters.Transfer(null, &apos;0x1234....&apos;);

// Receive an event when that filter occurs
contract.on(filter, (from, to, amount, event) =&gt; {
    // The to will always be &quot;address&quot;
    console.log(`I got ${ethers.utils.formatEther(amount)} from ${from}.`);
});
"><code><span class="hljs-comment">// Transfer 事件要在 abi 中声明</span>
<span class="hljs-comment">// 括号中的参数分别对应 Transfer 事件的参数</span>
<span class="hljs-keyword">contract</span>.on(<span class="hljs-string">"Transfer"</span>, (<span class="hljs-keyword">from</span>, to, amount, <span class="hljs-function"><span class="hljs-keyword">event</span>) => </span>{
    console.log(`${<span class="hljs-keyword">from</span>} sent ${ethers.utils.formatEther(amount)} to ${to}`);
});

<span class="hljs-comment">// 这里是指定参数的监听行为</span>
<span class="hljs-comment">// 例如，我只想监听接收人是 `0x1234....` 的事件，那么就这样指定地址</span>
<span class="hljs-comment">// 构造一个 filter，然后通过 filter 筛选</span>
filter <span class="hljs-operator">=</span> <span class="hljs-keyword">contract</span>.filters.Transfer(null, <span class="hljs-string">'0x1234....'</span>);

<span class="hljs-comment">// Receive an event when that filter occurs</span>
<span class="hljs-keyword">contract</span>.on(filter, (<span class="hljs-keyword">from</span>, to, amount, <span class="hljs-function"><span class="hljs-keyword">event</span>) => </span>{
    <span class="hljs-comment">// The to will always be "address"</span>
    console.log(`I got ${ethers.utils.formatEther(amount)} <span class="hljs-keyword">from</span> ${<span class="hljs-keyword">from</span>}.`);
});
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">设置范围，扫描指定区块范围的事件</h3><pre data-type="codeBlock" text="// 创建 指定发送人是 myAddress 的 filter
const myAddress = `0x...`;
filterFrom = contract.filters.Transfer(myAddress, null);

// 创建 指定接收人是 myAddress 的 filter
filterTo = contract.filters.Transfer(null, myAddress);
// 扫描指定区块的范围

console.log(await contract.queryFilter(filterFrom, 14692250, 14693250))

// 扫描最近 1000 个区块
console.log(await contract.queryFilter(filterFrom, -1000))

// 扫描所有区块
await daiContract.queryFilter(filterTo)
"><code><span class="hljs-comment">// 创建 指定发送人是 myAddress 的 filter</span>
const myAddress <span class="hljs-operator">=</span> `0x...`;
filterFrom <span class="hljs-operator">=</span> <span class="hljs-keyword">contract</span>.filters.Transfer(myAddress, null);

<span class="hljs-comment">// 创建 指定接收人是 myAddress 的 filter</span>
filterTo <span class="hljs-operator">=</span> <span class="hljs-keyword">contract</span>.filters.Transfer(null, myAddress);
<span class="hljs-comment">// 扫描指定区块的范围</span>

console.log(await <span class="hljs-keyword">contract</span>.queryFilter(filterFrom, <span class="hljs-number">14692250</span>, <span class="hljs-number">14693250</span>))

<span class="hljs-comment">// 扫描最近 1000 个区块</span>
console.log(await <span class="hljs-keyword">contract</span>.queryFilter(filterFrom, <span class="hljs-number">-1000</span>))

<span class="hljs-comment">// 扫描所有区块</span>
await daiContract.queryFilter(filterTo)
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">签名</h3><pre data-type="codeBlock" text="// 签名 utf-8
const signature = await wallet.signMessage(&apos;hello world&apos;);
console.log(signature);

// 更常见的 case 是签名 hash，长度为 32 字节
// 注意签名十六进制时，必须要将其转换为数组格式

// This string is 66 characters long
message = &quot;0x4c8f18581c0167eb90a761b4a304e009b924f03b619a0c0e8ea3adfce20aee64&quot;;
// This array representation is 32 bytes long
messageBytes = ethers.utils.arrayify(message);

// To sign a hash, you most often want to sign the bytes
const signature2 = await wallet.signMessage(messageBytes);
console.log(signature2);
"><code><span class="hljs-comment">// 签名 utf-8</span>
const signature <span class="hljs-operator">=</span> await wallet.signMessage(<span class="hljs-string">'hello world'</span>);
console.log(signature);

<span class="hljs-comment">// 更常见的 case 是签名 hash，长度为 32 字节</span>
<span class="hljs-comment">// 注意签名十六进制时，必须要将其转换为数组格式</span>

<span class="hljs-comment">// This string is 66 characters long</span>
message <span class="hljs-operator">=</span> <span class="hljs-string">"0x4c8f18581c0167eb90a761b4a304e009b924f03b619a0c0e8ea3adfce20aee64"</span>;
<span class="hljs-comment">// This array representation is 32 bytes long</span>
messageBytes <span class="hljs-operator">=</span> ethers.utils.arrayify(message);

<span class="hljs-comment">// To sign a hash, you most often want to sign the bytes</span>
const signature2 <span class="hljs-operator">=</span> await wallet.signMessage(messageBytes);
console.log(signature2);
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">指定调用合约时的一些参数</h3><pre data-type="codeBlock" text="// 我们上面在做一些写操作，例如转 ETH，写合约等操作时
// 使用的 gasLimit、gasPrice，都是由链上获取的默认数值
// 如果想要手动指定，需要添加一个额外的参数
// 声明一个对象，填写需要覆盖的字段，例如
let overrides = {
    gasLimit: 230000,
    maxFeePerGas: ethers.utils.parseUnits(&apos;12&apos;, &apos;gwei&apos;),
    maxPriorityFeePerGas: ethers.utils.parseUnits(&apos;3&apos;, &apos;gwei&apos;),
    nonce: (await provider.getTransactionCount(wallet.getAddress()))
}
// 将 overrides 作为最后一个参数
await walletWithSigner.transfer(wallet2.getAddress(), amount, overrides)
"><code><span class="hljs-comment">// 我们上面在做一些写操作，例如转 ETH，写合约等操作时</span>
<span class="hljs-comment">// 使用的 gasLimit、gasPrice，都是由链上获取的默认数值</span>
<span class="hljs-comment">// 如果想要手动指定，需要添加一个额外的参数</span>
<span class="hljs-comment">// 声明一个对象，填写需要覆盖的字段，例如</span>
let overrides <span class="hljs-operator">=</span> {
    gasLimit: <span class="hljs-number">230000</span>,
    maxFeePerGas: ethers.utils.parseUnits(<span class="hljs-string">'12'</span>, <span class="hljs-string">'gwei'</span>),
    maxPriorityFeePerGas: ethers.utils.parseUnits(<span class="hljs-string">'3'</span>, <span class="hljs-string">'gwei'</span>),
    nonce: (await provider.getTransactionCount(wallet.getAddress()))
}
<span class="hljs-comment">// 将 overrides 作为最后一个参数</span>
await walletWithSigner.<span class="hljs-built_in">transfer</span>(wallet2.getAddress(), amount, overrides)
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">总结</h3><p>本文列出了一些常用的用法，作为基本的使用基本够用了。查询更多方法可以查询 <code>Ethers.js</code> 的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.ethers.io/v5/">官方文档</a>。总的来说，<code>Ethers.js</code> 还是比较简单的，上手也比较快。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">关于我</h3><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">参考</h3><div data-type="embedly" src="https://docs.ethers.io/v5/" data="{&quot;provider_url&quot;:&quot;https://docs.ethers.org&quot;,&quot;description&quot;:&quot;Documentation for ethers, a complete, tiny and simple Ethereum library.&quot;,&quot;title&quot;:&quot;Documentation&quot;,&quot;thumbnail_width&quot;:1280,&quot;url&quot;:&quot;https://docs.ethers.org/v5/&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/8d250b70269d5cc279fc8f5d00fda99086933fe57d001fc87efbd30595cc0097.jpg&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Ethers&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:640,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1280,&quot;height&quot;:640,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/8d250b70269d5cc279fc8f5d00fda99086933fe57d001fc87efbd30595cc0097.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/8d250b70269d5cc279fc8f5d00fda99086933fe57d001fc87efbd30595cc0097.jpg"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://docs.ethers.io/v5/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Documentation</h2><p>Documentation for ethers, a complete, tiny and simple Ethereum library.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://docs.ethers.org</span></div><img src="https://storage.googleapis.com/papyrus_images/8d250b70269d5cc279fc8f5d00fda99086933fe57d001fc87efbd30595cc0097.jpg"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[深入理解合约升级(5) - 部署一个可升级合约]]></title>
            <link>https://paragraph.com/@xyyme/5</link>
            <guid>YBk3iD3kVOTlhjzm6vs6</guid>
            <pubDate>Mon, 23 May 2022 14:35:05 GMT</pubDate>
            <description><![CDATA[前面的文章我们基本把合约升级的原理介绍完了，这篇文章我们来实际操作一下，部署一个可升级合约。我们将会使用到 hardhat 框架和 OpenZeppelin 的可升级合约库。这个库和 OZ 的普通合约库的区别是，所有的合约中都没有构造函数，作为代替的是 initialize 函数，用来作初始化操作。初始化首先执行下面的命令来做一些初始化的工作：mkdir upgradeable_demo && cd upgradeable_demonpm init -ynpm install --save-dev hardhatnpx hardhat，创建实例项目，并且按照步骤进行npm install --save-dev @openzeppelin/hardhat-upgrades，安装 hardhat 的升级组件npm install --save-dev @nomiclabs/hardhat-ethers ethers，这两个包主要用于合约部署和测试npm install --save @openzeppelin/contracts-upgradeable，安装可升级合约库接下来我们配置...]]></description>
            <content:encoded><![CDATA[<p>前面的文章我们基本把合约升级的原理介绍完了，这篇文章我们来实际操作一下，部署一个可升级合约。我们将会使用到 <code>hardhat</code> 框架和 <code>OpenZeppelin</code> 的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable">可升级合约库</a>。这个库和 OZ 的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/openzeppelin-contracts">普通合约库</a>的区别是，所有的合约中都没有构造函数，作为代替的是 <code>initialize</code> 函数，用来作初始化操作。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">初始化</h3><p>首先执行下面的命令来做一些初始化的工作：</p><ol><li><p><code>mkdir upgradeable_demo &amp;&amp; cd upgradeable_demo</code></p></li><li><p><code>npm init -y</code></p></li><li><p><code>npm install --save-dev hardhat</code></p></li><li><p><code>npx hardhat</code>，创建实例项目，并且按照步骤进行</p></li><li><p><code>npm install --save-dev @openzeppelin/hardhat-upgrades</code>，安装 hardhat 的升级组件</p></li><li><p><code>npm install --save-dev @nomiclabs/hardhat-ethers ethers</code>，这两个包主要用于合约部署和测试</p></li><li><p><code>npm install --save @openzeppelin/contracts-upgradeable</code>，安装可升级合约库</p></li></ol><p>接下来我们配置 <code>hardhat</code> 的配置文件：</p><pre data-type="codeBlock" text="// hardhat.config.js
require(&quot;@nomiclabs/hardhat-ethers&quot;);
require(&apos;@openzeppelin/hardhat-upgrades&apos;);

/**
 * @type import(&apos;hardhat/config&apos;).HardhatUserConfig
 */
module.exports = {
  solidity: &quot;0.8.4&quot;,
  networks: {
    rinkeby: {
      url: // 这里填写对应网络的 rpc 地址,
      accounts: [这里填写私钥]
    }
  }
};
"><code><span class="hljs-comment">// hardhat.config.js</span>
<span class="hljs-built_in">require</span>(<span class="hljs-string">"@nomiclabs/hardhat-ethers"</span>);
<span class="hljs-built_in">require</span>(<span class="hljs-string">'@openzeppelin/hardhat-upgrades'</span>);

<span class="hljs-comment">/**
 * @type import('hardhat/config').HardhatUserConfig
 */</span>
module.exports <span class="hljs-operator">=</span> {
  solidity: <span class="hljs-string">"0.8.4"</span>,
  networks: {
    rinkeby: {
      url: <span class="hljs-comment">// 这里填写对应网络的 rpc 地址,</span>
      accounts: [这里填写私钥]
    }
  }
};
</code></pre><p>然后我们再来编写可升级合约，在 <code>contracts</code> 文件夹下创建 <code>Demo.sol</code> 文件：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;

import &quot;@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol&quot;;

contract Demo is Initializable {
    uint256 public a;

    // 初始化函数，后面的修饰符 initializer 来自 Initializable.sol
    // 用于限制该函数只能调用一次
    function initialize(uint256 _a) public initializer {
        a = _a;
    }

    function increaseA() external {
        ++a;
    }
}
"><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.4;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Initializable</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> a;

    <span class="hljs-comment">// 初始化函数，后面的修饰符 initializer 来自 Initializable.sol</span>
    <span class="hljs-comment">// 用于限制该函数只能调用一次</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initialize</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _a</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">initializer</span> </span>{
        a <span class="hljs-operator">=</span> _a;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increaseA</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-operator">+</span><span class="hljs-operator">+</span>a;
    }
}
</code></pre><p>编译合约 <code>npx hardhat compile</code>，没有问题。</p><h3 id="h-transparent-proxy" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">Transparent Proxy</h3><p>接着我们编写部署脚本，在 <code>scripts</code> 文件夹下创建 <code>deploy.js</code> 文件：</p><pre data-type="codeBlock" text="async function main() {
    const Demo = await ethers.getContractFactory(&quot;Demo&quot;);
    console.log(&quot;Deploying Demo...&quot;);
    // initializer 后面的参数为初始化函数的名字，这里为 initialize
    // 中括号的参数为初始化函数的参数
    const demo = await upgrades.deployProxy(Demo, [101], { initializer: &apos;initialize&apos; });
    // 这里打印的地址为代理合约的地址
    console.log(&quot;Demo deployed to:&quot;, demo.address);
}

// 这里也可以简化为 main()，后面的都省略也可以
main()
    .then(() =&gt; process.exit(0))
    .catch(error =&gt; {
        console.error(error);
        process.exit(1);
    });
"><code>async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
    const Demo <span class="hljs-operator">=</span> await ethers.getContractFactory(<span class="hljs-string">"Demo"</span>);
    console.log(<span class="hljs-string">"Deploying Demo..."</span>);
    <span class="hljs-comment">// initializer 后面的参数为初始化函数的名字，这里为 initialize</span>
    <span class="hljs-comment">// 中括号的参数为初始化函数的参数</span>
    const demo <span class="hljs-operator">=</span> await upgrades.deployProxy(Demo, [<span class="hljs-number">101</span>], { initializer: <span class="hljs-string">'initialize'</span> });
    <span class="hljs-comment">// 这里打印的地址为代理合约的地址</span>
    console.log(<span class="hljs-string">"Demo deployed to:"</span>, demo.<span class="hljs-built_in">address</span>);
}

<span class="hljs-comment">// 这里也可以简化为 main()，后面的都省略也可以</span>
main()
    .then(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> process.exit(<span class="hljs-number">0</span>))
    .catch(<span class="hljs-function"><span class="hljs-keyword">error</span> => </span>{
        console.error(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
        process.exit(<span class="hljs-number">1</span>);
    });
</code></pre><p>我们来运行部署脚本，这里我们使用本地测试网络进行部署：</p><p>注意，本地测试网络需要运行 <code>npx hardhat node</code>。本地网络无需在配置文件中配置，如果使用真实网络，需要进行配置，并且在 <code>--network</code> 后面指定网络名称即可。</p><blockquote><p>npx hardhat run scripts/deploy.js --network localhost</p></blockquote><p>可以观察到一共部署了三个合约，对应的部署顺序分别是</p><ol><li><p>逻辑合约</p></li><li><p><code>ProxyAdmin</code> 合约</p></li><li><p>代理合约（名为 <code>TransparentUpgradeableProxy</code>）</p></li></ol><p>注意，一个项目中只会有一个 <code>ProxyAdmin</code> 合约，管理着所有的代理合约。也就是说，我们在同一个项目中再去部署另外的合约，那么只会有步骤 1、3，<code>ProxyAdmin</code> 只在部署第一个合约时会部署。</p><p>这时，假设由于业务场景变化，需要修改合约，将 <code>increaseA</code> 函数修改为：</p><pre data-type="codeBlock" text="function increaseA() external {
    a += 10;
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increaseA</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    a <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-number">10</span>;
}
</code></pre><p>再次编译，没有问题。接着编写升级脚本，在 <code>scripts</code> 文件夹下创建 <code>upgrade.js</code> 文件：</p><pre data-type="codeBlock" text="async function main() {
    // 这里的地址为前面部署的代理合约地址
    const proxyAddress = &apos;0x...&apos;;

    const Demo = await ethers.getContractFactory(&quot;Demo&quot;);
    console.log(&quot;Preparing upgrade...&quot;);
    // 升级合约
    await upgrades.upgradeProxy(proxyAddress, Demo);
}

main()
    .then(() =&gt; process.exit(0))
    .catch(error =&gt; {
        console.error(error);
        process.exit(1);
    });
"><code>async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// 这里的地址为前面部署的代理合约地址</span>
    const proxyAddress <span class="hljs-operator">=</span> <span class="hljs-string">'0x...'</span>;

    const Demo <span class="hljs-operator">=</span> await ethers.getContractFactory(<span class="hljs-string">"Demo"</span>);
    console.log(<span class="hljs-string">"Preparing upgrade..."</span>);
    <span class="hljs-comment">// 升级合约</span>
    await upgrades.upgradeProxy(proxyAddress, Demo);
}

main()
    .then(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> process.exit(<span class="hljs-number">0</span>))
    .catch(<span class="hljs-function"><span class="hljs-keyword">error</span> => </span>{
        console.error(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
        process.exit(<span class="hljs-number">1</span>);
    });
</code></pre><p>接着运行</p><blockquote><p>npx hardhat run scripts/upgrade.js --network localhost</p></blockquote><p>升级合约时， <code>upgradeProxy</code> 中一共有两个步骤：</p><ol><li><p>部署新的逻辑合约</p></li><li><p>调用 <code>ProxyAdmin</code> 合约的 <code>upgrade</code> 函数来更换新合约，两个参数分别是代理合约和新逻辑合约的地址</p></li></ol><p>这样我们就完成了可升级合约的部署与升级，注意到我们的部署过程中有 <code>ProxyAdmin</code> 合约，说明这是 <code>TransparentProxy</code> 的合约升级模式。接下来我们看看 <code>UUPS</code> 的模式如何部署及升级。</p><h3 id="h-uups" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">UUPS</h3><p>若要支持 <code>UUPS</code> 的升级模式，需要做以下几点改动：</p><ol><li><p>逻辑合约需继承 <code>UUPSUpgradeable</code> 合约</p></li><li><p>覆写 <code>_authorizeUpgrade</code> 函数</p></li><li><p>部署脚本需要添加 <code>kind: &apos;uups&apos;</code> 参数</p></li></ol><p>此时，逻辑合约变为：</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.4;

import &quot;@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol&quot;;
import &quot;@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol&quot;;

// 需要继承 UUPSUpgradeable 合约
contract Demo is Initializable, UUPSUpgradeable {
    uint256 public a;

    function initialize(uint256 _a) public initializer {
        a = _a;
    }

    function increaseA() external {
        a += 10;
    }

    // 覆写 _authorizeUpgrade 函数
    function _authorizeUpgrade(address) internal override {}
}
"><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.4;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"</span>;

<span class="hljs-comment">// 需要继承 UUPSUpgradeable 合约</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Demo</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Initializable</span>, <span class="hljs-title">UUPSUpgradeable</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> a;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initialize</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _a</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">initializer</span> </span>{
        a <span class="hljs-operator">=</span> _a;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increaseA</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        a <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-number">10</span>;
    }

    <span class="hljs-comment">// 覆写 _authorizeUpgrade 函数</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_authorizeUpgrade</span>(<span class="hljs-params"><span class="hljs-keyword">address</span></span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{}
}
</code></pre><p>部署脚本变为：</p><pre data-type="codeBlock" text="async function main() {
    const Demo = await ethers.getContractFactory(&quot;Demo&quot;);
    console.log(&quot;Deploying Demo...&quot;);
    // 这里添加了参数 =&gt; kind: &apos;uups&apos;
    const demo = await upgrades.deployProxy(Demo, [101], { initializer: &apos;initialize&apos;, kind: &apos;uups&apos; });
    console.log(&quot;Demo deployed to:&quot;, demo.address);
}

main()
    .then(() =&gt; process.exit(0))
    .catch(error =&gt; {
        console.error(error);
        process.exit(1);
    });
"><code>async <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">main</span>(<span class="hljs-params"></span>) </span>{
    const Demo <span class="hljs-operator">=</span> await ethers.getContractFactory(<span class="hljs-string">"Demo"</span>);
    console.log(<span class="hljs-string">"Deploying Demo..."</span>);
    <span class="hljs-comment">// 这里添加了参数 => kind: 'uups'</span>
    const demo <span class="hljs-operator">=</span> await upgrades.deployProxy(Demo, [<span class="hljs-number">101</span>], { initializer: <span class="hljs-string">'initialize'</span>, kind: <span class="hljs-string">'uups'</span> });
    console.log(<span class="hljs-string">"Demo deployed to:"</span>, demo.<span class="hljs-built_in">address</span>);
}

main()
    .then(() <span class="hljs-operator">=</span><span class="hljs-operator">></span> process.exit(<span class="hljs-number">0</span>))
    .catch(<span class="hljs-function"><span class="hljs-keyword">error</span> => </span>{
        console.error(<span class="hljs-function"><span class="hljs-keyword">error</span>)</span>;
        process.exit(<span class="hljs-number">1</span>);
    });
</code></pre><p>编译合约并运行部署脚本（注意，这时最好删除 <code>.openzeppelin</code> 文件夹下的对应网络配置文件，因为其包含了我们上面测试的 <code>TransparentProxy</code> 模式的一些运行配置，可能会有影响），可以观察到一共部署了两个合约，分别是：</p><ol><li><p>逻辑合约</p></li><li><p>代理合约（名为 <code>ERC1967Proxy</code>）</p></li></ol><p>此时，假设我们需要升级合约，在改动了合约之后，我们可以继续使用上面的 <code>upgrade.js</code> 脚本进行升级，这时的升级步骤是：</p><ol><li><p>部署新的逻辑合约</p></li><li><p>调用<strong>代理合约</strong>的 <code>upgradeTo</code> 函数进行升级，参数是新的逻辑合约地址</p></li></ol><p>我们可以看到，两种升级模式有所区别。<code>TransparentProxy</code> 模式在升级的时候，需要调用 <code>ProxyAdmin</code> 的升级函数。而 <code>UUPS</code> 模式在升级时，需要调用代理合约的升级函数。后者相比于前者少部署一个合约。</p><h4 id="h-uups" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">UUPS 中需要注意的权限管理问题</h4><p>这里有一个重点是，由于 <code>TransparentProxy</code> 模式是由 <code>ProxyAdmin</code> 进行管理，也就是说只有 <code>ProxyAdmin</code> 有权限进行升级，那么我们只要保证 <code>ProxyAdmin</code> 合约的管理员权限安全即可保证整个可升级架构安全。而对于 <code>UUPS</code> 模式来说，升级合约的逻辑是需要调用代理合约的，这时的权限管理就需要开发者手动处理。具体来说，就是对于我们覆写的 <code>_authorizeUpgrade</code> 函数，需要加上权限管理：</p><pre data-type="codeBlock" text="function _authorizeUpgrade(address) internal 
    override onlyOwner {}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_authorizeUpgrade</span>(<span class="hljs-params"><span class="hljs-keyword">address</span></span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> 
    <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">onlyOwner</span> </span>{}
</code></pre><p>这里加上了 <code>onlyOwner</code> 用于限制升级权限，否则任何人都可调用代理合约的 <code>upgradeTo</code> 进行升级。但要注意的是，我们这里只是简单加上了 <code>onlyOwner</code> 做为示例的权限管理，在实际开发中，由于升级的逻辑和业务逻辑都在逻辑合约中，因此需要区分业务场景的 <code>owner</code> 和合约升级架构的 <code>owner</code>。这里可能会对开发者带来困扰，因此需要多加注意。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">总结</h3><p><code>TransparentProxy</code> 和 <code>UUPS</code> 是目前阶段比较流行的成熟的合约升级解决方案。OZ 建议开发者使用 <code>UUPS</code>，更加轻量级，节省 gas。我个人还是比较倾向于前者，因为我觉得前者架构清晰，权限管理简单。后者将业务逻辑和升级组件都放在逻辑合约中，当需要多次升级合约时，是否节省 gas 仍需探讨。不过建议大家两个都能够熟练掌握，毕竟难度不高，上手也很简单。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约升级系列文章</h3><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/RZscMYGkeGTY8z6ccHseY8HKu-ER3pX0mFYoWXRqXQ0">深入理解合约升级(1) - 概括</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec">深入理解合约升级(2) - Solidity 内存布局</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/0gmFpVZVlHhwb2YlmaSY8Dyv5r3Z24sKIks38cyQRFk">深入理解合约升级(3) - call 与 delegatecall</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/VSyU0JfmVrcqN-F28tX5mzYjxFFAosl8tDAQX3vB5Dg">深入理解合约升级(4) - 合约升级原理的代码实现</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/kM9ld2u0D1BpHAfXTiaSPGPtDnOd6vrxJ5_tW4wZVBk">深入理解合约升级(5) - 部署一个可升级合约</a></p></li></ol><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">关于我</h3><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">参考</h3><div data-type="embedly" src="https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786" data="{&quot;provider_url&quot;:&quot;https://forum.openzeppelin.com&quot;,&quot;description&quot;:&quot;UUPS Proxies: A Tutorial In this tutorial we will deploy an upgradeable contract using the UUPS proxy pattern. We assume some familiarity with Ethereum upgradeable proxies. Introduction to UUPS The original proxies included in OpenZeppelin followed the Transparent Proxy Pattern. While this pattern is still provided, our recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile.&quot;,&quot;title&quot;:&quot;UUPS Proxies: Tutorial (Solidity + JavaScript)&quot;,&quot;author_name&quot;:&quot;frangio&quot;,&quot;thumbnail_width&quot;:1200,&quot;url&quot;:&quot;https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png&quot;,&quot;author_url&quot;:&quot;https://forum.openzeppelin.com/u/frangio&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;OpenZeppelin Forum&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:600,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1200,&quot;height&quot;:600,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>UUPS Proxies: Tutorial (Solidity + JavaScript)</h2><p>UUPS Proxies: A Tutorial In this tutorial we will deploy an upgradeable contract using the UUPS proxy pattern. We assume some familiarity with Ethereum upgradeable proxies. Introduction to UUPS The original proxies included in OpenZeppelin followed the Transparent Proxy Pattern. While this pattern is still provided, our recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://forum.openzeppelin.com</span></div><img src="https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png"/></div></a></div></div><div data-type="embedly" src="https://forum.openzeppelin.com/t/openzeppelin-upgrades-step-by-step-tutorial-for-hardhat/3580" data="{&quot;provider_url&quot;:&quot;https://forum.openzeppelin.com&quot;,&quot;description&quot;:&quot;OpenZeppelin Hardhat Upgrades Smart contracts deployed with the OpenZeppelin Upgrades plugins can be upgraded to modify their code, while preserving their address, state, and balance. This allows you to iteratively add new features to your project, or fix any bugs you may find in production.&quot;,&quot;title&quot;:&quot;OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat&quot;,&quot;author_name&quot;:&quot;abcoathup&quot;,&quot;thumbnail_width&quot;:1024,&quot;url&quot;:&quot;https://forum.openzeppelin.com/t/openzeppelin-upgrades-step-by-step-tutorial-for-hardhat/3580&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/d3f090b346fcf28cb20ea6239b93c12d34da5ee50f7dde4afd9b82a5d2f811da.png&quot;,&quot;author_url&quot;:&quot;https://forum.openzeppelin.com/u/abcoathup&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;OpenZeppelin Forum&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:576,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1024,&quot;height&quot;:576,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/d3f090b346fcf28cb20ea6239b93c12d34da5ee50f7dde4afd9b82a5d2f811da.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/d3f090b346fcf28cb20ea6239b93c12d34da5ee50f7dde4afd9b82a5d2f811da.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://forum.openzeppelin.com/t/openzeppelin-upgrades-step-by-step-tutorial-for-hardhat/3580" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>OpenZeppelin Upgrades: Step by Step Tutorial for Hardhat</h2><p>OpenZeppelin Hardhat Upgrades Smart contracts deployed with the OpenZeppelin Upgrades plugins can be upgraded to modify their code, while preserving their address, state, and balance. This allows you to iteratively add new features to your project, or fix any bugs you may find in production.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://forum.openzeppelin.com</span></div><img src="https://storage.googleapis.com/papyrus_images/d3f090b346fcf28cb20ea6239b93c12d34da5ee50f7dde4afd9b82a5d2f811da.png"/></div></a></div></div><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.npmjs.com/package/@openzeppelin/hardhat-upgrades">https://www.npmjs.com/package/@openzeppelin/hardhat-upgrades</a></p><div data-type="embedly" src="https://blog.logrocket.com/using-uups-proxy-pattern-upgrade-smart-contracts/" data="{&quot;provider_url&quot;:&quot;https://blog.logrocket.com&quot;,&quot;description&quot;:&quot;UUPS is a gas-efficient proxy pattern that allows underlying logic to be upgraded when needed, without losing previous data.&quot;,&quot;title&quot;:&quot;Using the UUPS proxy pattern to upgrade smart contracts - LogRocket Blog&quot;,&quot;author_name&quot;:&quot;Pranesh A. S.&quot;,&quot;url&quot;:&quot;https://blog.logrocket.com/using-uups-proxy-pattern-upgrade-smart-contracts/&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/e90f80f34494d5a952fedd90e77151f97bf8c207eef868226efb7f72760fdcae.avif&quot;,&quot;thumbnail_width&quot;:730,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;LogRocket Blog&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:487,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:100,&quot;height&quot;:100,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/e90f80f34494d5a952fedd90e77151f97bf8c207eef868226efb7f72760fdcae.avif&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/e90f80f34494d5a952fedd90e77151f97bf8c207eef868226efb7f72760fdcae.avif"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://blog.logrocket.com/using-uups-proxy-pattern-upgrade-smart-contracts/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Using the UUPS proxy pattern to upgrade smart contracts - LogRocket Blog</h2><p>UUPS is a gas-efficient proxy pattern that allows underlying logic to be upgraded when needed, without losing previous data.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://blog.logrocket.com</span></div><img src="https://storage.googleapis.com/papyrus_images/e90f80f34494d5a952fedd90e77151f97bf8c207eef868226efb7f72760fdcae.avif"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[深入理解合约升级(4) - 合约升级原理的代码实现]]></title>
            <link>https://paragraph.com/@xyyme/4</link>
            <guid>Cn3RryVlXg7MUokesENd</guid>
            <pubDate>Mon, 23 May 2022 08:04:55 GMT</pubDate>
            <description><![CDATA[前面的文章我们提到，合约升级的原理是将合约架构分为 代理合约 与 逻辑合约，通过前面对于内存结构以及 delegatecall 的学习，我们已经基本掌握的合约升级的基础。这篇文章我们就从代码层面来看看合约升级到底应该如何实现。这是我们在第一篇文章中的图例，当我们学习了内存结构以及 delegatecall 之后，我们再来看这幅图，就能够很好地理解了：数据都存放在代理合约的内存插槽中，而由于代理合约使用了 delegatecall，因此函数执行都在逻辑合约中运行，修改的却是代理合约中的数据。这样就可以方便替换逻辑合约，实现合约升级。合约升级实现过程简版 Proxy首先我们考虑，对于代理合约而言，如何将请求转发到逻辑合约。最简单的方法就是对于逻辑合约中的每个函数，都在代理合约中包装一层，然后通过 delegatecall 来分别调用各个函数。这种方法是不可行的，因为既然我们都用到了可升级合约，那就说明我们后期会对合约做改动，我们总不能每添加一个函数，都在代理合约中添加一个包装层。一是这样很冗余，二是这样无法实现，因为代理合约也是区块链上的智能合约，它本身是不可变的。 这时我们想到，能...]]></description>
            <content:encoded><![CDATA[<p>前面的文章我们提到，合约升级的原理是将合约架构分为 <code>代理合约</code> 与 <code>逻辑合约</code>，通过前面对于内存结构以及 <code>delegatecall</code> 的学习，我们已经基本掌握的合约升级的基础。这篇文章我们就从代码层面来看看合约升级到底应该如何实现。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/974ca2373be2f15029b1e817dab22df9632e609d26d1c7f6bf466b8d44f07c21.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>这是我们在第一篇文章中的图例，当我们学习了内存结构以及 <code>delegatecall</code> 之后，我们再来看这幅图，就能够很好地理解了：数据都存放在代理合约的内存插槽中，而由于代理合约使用了 <code>delegatecall</code>，因此函数执行都在逻辑合约中运行，修改的却是代理合约中的数据。这样就可以方便替换逻辑合约，实现合约升级。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约升级实现过程</h3><h4 id="h-proxy" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">简版 Proxy</h4><p>首先我们考虑，对于代理合约而言，如何将请求转发到逻辑合约。最简单的方法就是对于逻辑合约中的每个函数，都在代理合约中包装一层，然后通过 <code>delegatecall</code> 来分别调用各个函数。这种方法是不可行的，因为既然我们都用到了可升级合约，那就说明我们后期会对合约做改动，我们总不能每添加一个函数，都在代理合约中添加一个包装层。一是这样很冗余，二是这样无法实现，因为代理合约也是区块链上的智能合约，它本身是不可变的。</p><p>这时我们想到，能不能够利用 Solidity 中的 <code>fallback</code> 与 <code>receive</code> 函数，它们的作用就是接收并处理一切未实现的函数（两者的区别是，<code>receive</code> 接收所有 <code>msg.data</code> 为空的调用，<code>fallback</code> 接收所有未匹配函数的调用）。如果我们将所有的函数调用都通过 <code>fallback</code> 转发给逻辑合约，那么是否就达到目标了呢？我们来看看代码：</p><pre data-type="codeBlock" text="// 注：这个实现有问题（后文有描述），不要直接使用！
contract Proxy {
    address public implementation;
    address public admin;

    constructor() public {
        admin = msg.sender;
    }
    function setImplementation(address newImplementation) external {
        require(msg.sender == admin, &quot;must called by admin&quot;);
        implementation = newImplementation;
    }
    function changeAdmin(address newAdmin) external {
        require(msg.sender == admin, &quot;must called by admin&quot;);
        admin = newAdmin;
    }

    function _delegate() internal {
        require(msg.sender != admin, &quot;admin cannot fallback to proxy target&quot;);
        address _implementation = implementation;
        // 下面代码是利用 delegatecall 把请求转发给 _implementation 所指定地址的合约中去
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())

            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    fallback () payable external {
        _delegate();
    }
    // Will run if call data is empty.
    receive () payable external {
        _delegate();
    }
}
"><code><span class="hljs-comment">// 注：这个实现有问题（后文有描述），不要直接使用！</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> implementation;
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> admin;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        admin <span class="hljs-operator">=</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">setImplementation</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> newImplementation</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> admin, <span class="hljs-string">"must called by admin"</span>);
        implementation <span class="hljs-operator">=</span> newImplementation;
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">changeAdmin</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> newAdmin</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> admin, <span class="hljs-string">"must called by admin"</span>);
        admin <span class="hljs-operator">=</span> newAdmin;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_delegate</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">internal</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> admin, <span class="hljs-string">"admin cannot fallback to proxy target"</span>);
        <span class="hljs-keyword">address</span> _implementation <span class="hljs-operator">=</span> implementation;
        <span class="hljs-comment">// 下面代码是利用 delegatecall 把请求转发给 _implementation 所指定地址的合约中去</span>
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-built_in">calldatacopy</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-built_in">calldatasize</span>())
            <span class="hljs-keyword">let</span> result <span class="hljs-operator">:=</span> <span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">gas</span>(), _implementation, <span class="hljs-number">0</span>, <span class="hljs-built_in">calldatasize</span>(), <span class="hljs-number">0</span>, <span class="hljs-number">0</span>)
            <span class="hljs-built_in">returndatacopy</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-built_in">returndatasize</span>())

            <span class="hljs-keyword">switch</span> result
            <span class="hljs-keyword">case</span> <span class="hljs-number">0</span> { <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">returndatasize</span>()) }
            <span class="hljs-keyword">default</span> { <span class="hljs-keyword">return</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">returndatasize</span>()) }
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">fallback</span> (<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">payable</span></span> <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        _delegate();
    }
    <span class="hljs-comment">// Will run if call data is empty.</span>
    <span class="hljs-function"><span class="hljs-keyword">receive</span> (<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">payable</span></span> <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        _delegate();
    }
}
</code></pre><p>这段代码中，我们将 <code>fallback</code> 和 <code>receive</code> 函数都指向了 <code>_delegate</code> 函数，它会将有请求都转发给逻辑合约。乍一看没有什么问题，但是需要注意：</p><ol><li><p>此合约中有两个字段，<code>implementation</code> 和 <code>admin</code>，分别存储逻辑合约地址和管理员地址（管理员地址用户更换逻辑合约升级）。它们分别占据了插槽 0 和 1 的位置，那么这就有可能和我们的逻辑合约中的内存发生冲突，如果逻辑合约中有对这俩插槽的修改，那就直接把这两个很重要的变量给改掉了，那就乱套了。</p></li><li><p>还有一个问题就是代理合约本身也存在一些自身的方法，比如 <code>changeAdmin</code> 等，如果逻辑合约中恰好也有这些方法，那么用户的请求就不会转发到逻辑合约。</p></li></ol><h4 id="h-eip-1967" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">EIP-1967</h4><p>该如何解决这个问题呢？<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://eips.ethereum.org/EIPS/eip-1967">EIP-1967</a> 提出了一个解决办法，它把 <code>implementation</code> 和 <code>admin</code> 这两个字段放在了两个特殊的插槽中：</p><pre data-type="codeBlock" text="# bytes32(uint256(keccak256(&apos;eip1967.proxy.implementation&apos;)) - 1)
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc

# bytes32(uint256(keccak256(&apos;eip1967.proxy.admin&apos;)) - 1)
0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
"><code># <span class="hljs-keyword">bytes32</span>(<span class="hljs-keyword">uint256</span>(<span class="hljs-built_in">keccak256</span>(<span class="hljs-string">'eip1967.proxy.implementation'</span>)) <span class="hljs-operator">-</span> <span class="hljs-number">1</span>)
<span class="hljs-number">0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc</span>

# <span class="hljs-keyword">bytes32</span>(<span class="hljs-keyword">uint256</span>(<span class="hljs-built_in">keccak256</span>(<span class="hljs-string">'eip1967.proxy.admin'</span>)) <span class="hljs-operator">-</span> <span class="hljs-number">1</span>)
<span class="hljs-number">0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103</span>
</code></pre><p>这两个插槽是经过哈希计算出来的值，根据概率来看，不可能与逻辑合约中的其他内存位置发生冲突。此时，我们的代码就变成了：</p><pre data-type="codeBlock" text="bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

bytes32 private constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;
"><code><span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> _IMPLEMENTATION_SLOT <span class="hljs-operator">=</span> <span class="hljs-number">0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc</span>;

<span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> _ADMIN_SLOT <span class="hljs-operator">=</span> <span class="hljs-number">0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103</span>;
</code></pre><p>注意这里指用了 <code>constant</code> 来保存插槽位置，<code>constant</code> 关键字保存的是常量，并不保存在插槽中。这样我们就解决了前面提到的第一个问题。</p><p>我们再来看看第二个问题，这个问题看似很好解决，比如我们给代理合约中的函数都起一些不常用的名字就行。例如，把上面的 <code>changeAdmin</code> 改成 <code>changeAdmin12345</code>。但是问题没有这么简单，我们要知道，智能合约匹配请求是根据函数签名 <code>keccak256</code> 值的前 4 个字节来判断的，如果两个函数的哈希值前 4 位相同，那么它们就被判断为同一个函数，例如：</p><pre data-type="codeBlock" text="# keccak256(&quot;proxyOwner()&quot;) 前 4 字节为 025313a2
proxyOwner()

# keccak256(&quot;clash550254402()&quot;) 前 4 字节为 025313a2
clash550254402()
"><code># <span class="hljs-built_in">keccak256</span>("proxyOwner()") 前 <span class="hljs-number">4</span> 字节为 <span class="hljs-number">025313</span>a2
<span class="hljs-built_in">proxyOwner</span>()

# <span class="hljs-built_in">keccak256</span>("clash550254402()") 前 <span class="hljs-number">4</span> 字节为 <span class="hljs-number">025313</span>a2
<span class="hljs-built_in">clash550254402</span>()
</code></pre><p>这个问题被称为 <code>Proxy selector clashing</code>。这可能会造成一些问题，如果我们的逻辑合约中恰好有函数的哈希值前 4 位与代理合约中的某个函数相同，那就会造成用户的请求实际上是在代理合约中执行的，而执行结果必然是我们不希望发生的。</p><h4 id="h-transparent-proxy" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">Transparent Proxy</h4><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://blog.openzeppelin.com/the-transparent-proxy-pattern/">Transparent Proxy</a> 提出了解决方案，它主要从两方面解决这个问题：</p><ol><li><p>来自普通用户的请求全部转发给逻辑合约，即使代理合约与逻辑合约发生了名称冲突，也要转发</p></li><li><p>来自管理员 <code>admin</code> 的请求，全部不转发，由代理合约处理</p></li></ol><p>主要的代码如下：</p><pre data-type="codeBlock" text="contract Proxy {
    bytes32 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
    bytes32 private constant _ADMIN_SLOT = 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103;

    modifier ifAdmin() {
        // 如果调用者是 admin，就执行；否则转发到 Implementation 合约
        if (msg.sender == admin()) {
            _;
        } else {
            _delegate();
        }
    }

    constructor() public {
        address admin = msg.sender;
        bytes32 slot = _ADMIN_SLOT;
        assembly {
            sstore(slot, admin)
        }
    }
    function implementation() public ifAdmin returns (address) {
        return _implementation();
    }

    function _implementation() internal view returns (address impl) {
        bytes32 slot = _IMPLEMENTATION_SLOT;
        assembly {
            impl := sload(slot)
        }
    }

    function setImplementation(address newImplementation) external ifAdmin {
        bytes32 slot = _IMPLEMENTATION_SLOT;
        assembly {
            sstore(slot, newImplementation)
        }
    }
    function admin() public ifAdmin returns (address) {
        return _admin();
    }

    function _admin() internal view returns (address adm) {
        bytes32 slot = _ADMIN_SLOT;
        assembly {
            adm := sload(slot)
        }
    }
    function changeAdmin(address newAdmin) external ifAdmin {
        bytes32 slot = _ADMIN_SLOT;
        assembly {
            sstore(slot, newAdmin)
        }
    }

    function _delegate() internal {
        address _implementation = _implementation();
        // 下面代码是利用 delegatecall 把请求转发给 _implementation 所指定地址的合约中去
        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())

            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }

    fallback () payable external {
        // 来自 admin 的请求不转发
        require(msg.sender != _admin(), &quot;admin cannot fallback to proxy target&quot;);
        _delegate();
    }

    // 来自 admin 的请求不转发
    receive () payable external {
        require(msg.sender != _admin(), &quot;admin cannot fallback to proxy target&quot;);
        _delegate();
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Proxy</span> </span>{
    <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> _IMPLEMENTATION_SLOT <span class="hljs-operator">=</span> <span class="hljs-number">0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc</span>;
    <span class="hljs-keyword">bytes32</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> _ADMIN_SLOT <span class="hljs-operator">=</span> <span class="hljs-number">0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103</span>;

    <span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">ifAdmin</span>(<span class="hljs-params"></span>) </span>{
        <span class="hljs-comment">// 如果调用者是 admin，就执行；否则转发到 Implementation 合约</span>
        <span class="hljs-keyword">if</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> admin()) {
            <span class="hljs-keyword">_</span>;
        } <span class="hljs-keyword">else</span> {
            _delegate();
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">address</span> admin <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        <span class="hljs-keyword">bytes32</span> slot <span class="hljs-operator">=</span> _ADMIN_SLOT;
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-built_in">sstore</span>(slot, admin)
        }
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">implementation</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">ifAdmin</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-keyword">return</span> _implementation();
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_implementation</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">internal</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> impl</span>) </span>{
        <span class="hljs-keyword">bytes32</span> slot <span class="hljs-operator">=</span> _IMPLEMENTATION_SLOT;
        <span class="hljs-keyword">assembly</span> {
            impl <span class="hljs-operator">:=</span> <span class="hljs-built_in">sload</span>(slot)
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setImplementation</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> newImplementation</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">ifAdmin</span> </span>{
        <span class="hljs-keyword">bytes32</span> slot <span class="hljs-operator">=</span> _IMPLEMENTATION_SLOT;
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-built_in">sstore</span>(slot, newImplementation)
        }
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">admin</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">ifAdmin</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-keyword">return</span> _admin();
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_admin</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">internal</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> adm</span>) </span>{
        <span class="hljs-keyword">bytes32</span> slot <span class="hljs-operator">=</span> _ADMIN_SLOT;
        <span class="hljs-keyword">assembly</span> {
            adm <span class="hljs-operator">:=</span> <span class="hljs-built_in">sload</span>(slot)
        }
    }
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">changeAdmin</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> newAdmin</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">ifAdmin</span> </span>{
        <span class="hljs-keyword">bytes32</span> slot <span class="hljs-operator">=</span> _ADMIN_SLOT;
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-built_in">sstore</span>(slot, newAdmin)
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_delegate</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
        <span class="hljs-keyword">address</span> _implementation <span class="hljs-operator">=</span> _implementation();
        <span class="hljs-comment">// 下面代码是利用 delegatecall 把请求转发给 _implementation 所指定地址的合约中去</span>
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-built_in">calldatacopy</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-built_in">calldatasize</span>())
            <span class="hljs-keyword">let</span> result <span class="hljs-operator">:=</span> <span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">gas</span>(), _implementation, <span class="hljs-number">0</span>, <span class="hljs-built_in">calldatasize</span>(), <span class="hljs-number">0</span>, <span class="hljs-number">0</span>)
            <span class="hljs-built_in">returndatacopy</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-built_in">returndatasize</span>())

            <span class="hljs-keyword">switch</span> result
            <span class="hljs-keyword">case</span> <span class="hljs-number">0</span> { <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">returndatasize</span>()) }
            <span class="hljs-keyword">default</span> { <span class="hljs-keyword">return</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">returndatasize</span>()) }
        }
    }

    <span class="hljs-function"><span class="hljs-keyword">fallback</span> (<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">payable</span></span> <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// 来自 admin 的请求不转发</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> _admin(), <span class="hljs-string">"admin cannot fallback to proxy target"</span>);
        _delegate();
    }

    <span class="hljs-comment">// 来自 admin 的请求不转发</span>
    <span class="hljs-function"><span class="hljs-keyword">receive</span> (<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">payable</span></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> _admin(), <span class="hljs-string">"admin cannot fallback to proxy target"</span>);
        _delegate();
    }
}
</code></pre><p>可以看到，合约中添加了 <code>ifAdmin</code> 修饰符，用于判断请求的来源。对于代理合约自身的一些函数如 <code>changeAdmin</code> 等，均使用了该修饰符。这样即使出现了签名冲突的情况，只要是来自于普通用户的请求，均直接转发给了逻辑合约执行。这样就解决了前面的第二个问题。</p><p>不过，仍然存在一个小问题，就是 <code>admin</code> 用户无法作为普通用户的视角正常调用。这个问题也比较好解决，一般可以准备一个特殊账户作为 <code>admin</code> 用户，仅调用管理员方法即可。也有另一个解决方法，就是使用一个 <code>ProxyAdmin</code> 合约来作为管理员，这样所有的账户都可以正常调用合约了。</p><p><code>ProxyAdmin</code> 的合约代码片段如下：</p><pre data-type="codeBlock" text="contract ProxyAdmin is Ownable {
    // 管理员EOA地址通过调用该方法来更换逻辑合约
    function upgrade(IProxy proxy, address newImplementation) public onlyOwner {
        proxy.setImplementation(implementation);
    }
    // ......
}

interface IProxy {
    function setImplementation(address newImplementation);
    // ......
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ProxyAdmin</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Ownable</span> </span>{
    <span class="hljs-comment">// 管理员EOA地址通过调用该方法来更换逻辑合约</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">upgrade</span>(<span class="hljs-params">IProxy proxy, <span class="hljs-keyword">address</span> newImplementation</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        proxy.setImplementation(implementation);
    }
    <span class="hljs-comment">// ......</span>
}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IProxy</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setImplementation</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> newImplementation</span>)</span>;
    <span class="hljs-comment">// ......</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/22f4e80beed7646d2a2390a974c05b74e0cff3907e79b62314a207f3fbad0a65.png" alt="可升级合约架构" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">可升级合约架构</figcaption></figure><h4 id="h-universal-upgradeable-proxy-standard-uups" class="text-xl font-header !mt-6 !mb-3 first:!mt-0 first:!mb-0">Universal Upgradeable Proxy Standard (UUPS)</h4><p>UUPS 是 OpenZeppelin 在近期推出的一种新的合约升级模式，与上面的 <code>Transparent</code> 代理模式原理相同，都是利用 <code>delegatecall</code> 通过代理合约调用逻辑合约。所不同的是，<code>Transparent</code> 模式是将替换逻辑合约的函数放在代理合约中，而 <code>UUPS</code> 则是将其放在了逻辑合约中。也就是说，前者模式中，每个代理合约中都有一个类似 <code>upgradeTo</code> 这样的函数，用来更换逻辑合约。而在后者模式中，这样的函数是放在了逻辑合约的实现中。</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups">官方文档</a>中对于两者的对比，主要表述了 <code>UUPS</code> 模式更加轻量级，更加节省 gas，并且更推荐使用这种模式。由于代理合约中不用再包含替换逻辑合约的函数，因此节省了 gas。我个人目前对于这种模式持保留意见，因为对于可升级合约来说，代理合约和逻辑合约是一对多的关系。在之前的模式中，把替换逻辑合约的部分放在代理合约中，这也只需要部署一份代理合约。而新模式中，每个逻辑合约都要包含这部分升级组件，这样是否更加耗费 gas。而且将这部分逻辑放在逻辑合约中，是否会造成逻辑合约变得更加臃肿。毕竟在之前的模式中，开发逻辑合约时只需要关注业务逻辑即可。也许是因为目前我对于 <code>UUPS</code> 模式的理解不够深入，暂时先将疑问抛出，待后续继续深入学习。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">可升级合约的一些限制</h3><p>要实现合约升级，有一些限制需要我们注意</p><p><strong>第一个</strong>就是我们在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/0gmFpVZVlHhwb2YlmaSY8Dyv5r3Z24sKIks38cyQRFk">上篇文章</a>最后提到的，在升级合约，也就是更换逻辑合约的时候，新合约的新增状态变量必须添加在当前所有变量之后，不能在前面的变量中插入，否则会更改内存插槽对应关系。同理，如果合约涉及到继承关系，不能在基类中添加变量（在基类中添加变量就相当于在基类和子类的状态变量之前插入变量）。也不能够更改或删除之前的状态变量。</p><p><strong>第二个</strong>是不能使用构造函数 <code>constructor</code>，由于合约的构造函数是在合约初始化的时候就被调用，这时它的一些赋值操作会直接影响到自身的内存。合约升级的前提是代理合约通过 <code>delegatecall</code> 调用逻辑合约来影响代理的内存布局，如果逻辑合约自己使用了构造函数去初始化一些变量，那么对于代理合约而言，内存是没有任何变化的。对于该问题，替代方法是使用 <code>initialize</code> 函数来代替构造函数。在合约部署完成后需要手动调用 <code>initialize</code> 函数。同时要记得，逻辑合约中实现的 <code>initialize</code> 函数中要手动实现调用基类的 <code>initialize</code> 方法。例如：</p><pre data-type="codeBlock" text="pragma solidity ^0.6.0;

import &quot;@openzeppelin/upgrades/contracts/Initializable.sol&quot;;

contract BaseContract is Initializable {
    uint256 public y;

    function initialize() public initializer {
        y = 42;
    }
}

contract MyContract is BaseContract {
    uint256 public x;

    function initialize(uint256 _x) public initializer {
        // 手动调用基类中的初始化方法
        BaseContract.initialize();
        x = _x;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.6.0;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/upgrades/contracts/Initializable.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">BaseContract</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Initializable</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> y;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initialize</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">initializer</span> </span>{
        y <span class="hljs-operator">=</span> <span class="hljs-number">42</span>;
    }
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">MyContract</span> <span class="hljs-keyword">is</span> <span class="hljs-title">BaseContract</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> x;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initialize</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _x</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">initializer</span> </span>{
        <span class="hljs-comment">// 手动调用基类中的初始化方法</span>
        BaseContract.initialize();
        x <span class="hljs-operator">=</span> _x;
    }
}
</code></pre><p><strong>第三个</strong>是所有状态变量不能在声明时就赋初始值，例如：</p><pre data-type="codeBlock" text="contract MyContract {
    uint256 public hasInitialValue = 42;
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">MyContract</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> hasInitialValue <span class="hljs-operator">=</span> <span class="hljs-number">42</span>;
}
</code></pre><p>这种行为类似于在构造函数中赋值，不可行。需要改为在 <code>initialize</code> 函数中赋值。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">代码</h3><p>Openzeppelin 库已经实现了完善的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable">可升级合约库</a>，我们在开发过程中可以直接使用现有的合约进行部署，避免重复造轮子出现错误。同时也提供了相应的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.openzeppelin.com/contracts/4.x/upgradeable">文档</a>以供参考。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约升级系列文章</h3><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/RZscMYGkeGTY8z6ccHseY8HKu-ER3pX0mFYoWXRqXQ0">深入理解合约升级(1) - 概括</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec">深入理解合约升级(2) - Solidity 内存布局</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/0gmFpVZVlHhwb2YlmaSY8Dyv5r3Z24sKIks38cyQRFk">深入理解合约升级(3) - call 与 delegatecall</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/VSyU0JfmVrcqN-F28tX5mzYjxFFAosl8tDAQX3vB5Dg">深入理解合约升级(4) - 合约升级原理的代码实现</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/kM9ld2u0D1BpHAfXTiaSPGPtDnOd6vrxJ5_tW4wZVBk">深入理解合约升级(5) - 部署一个可升级合约</a></p></li></ol><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">关于我</h3><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">参考</h3><div data-type="embedly" src="http://aandds.com/blog/eth-delegatecall.html" data="{&quot;provider_url&quot;:&quot;http://aandds.com&quot;,&quot;description&quot;:&quot;UUPS ( EIP-1822) 和 Transparent Proxy Pattern 基本相似，区别只在于： 对于 UUPS 来说，升级相关代码位于 Implementation 合约（或称 Logic 合约）中，而对于 Transparent Proxy Pattern 来说，升级相关代码位于 Proxy 合约中。 下面是 UUPS 的例子（摘自 https://eips.ethereum.org/EIPS/eip-1822#erc-20-token ）。其中 Proxy 合约为： pragma solidity ^0.5.1; contract Proxy { // Code position in storage is keccak256(\&quot;PROXIABLE\&quot;) = \&quot;0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7\&quot; constructor(bytes memory constructData, address contractLogic) public {&quot;,&quot;title&quot;:&quot;Delegatecall and Upgradeable Contract&quot;,&quot;url&quot;:&quot;http://aandds.com/blog/eth-delegatecall.html&quot;,&quot;author_name&quot;:&quot;cig01&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Aandds&quot;,&quot;type&quot;:&quot;link&quot;}" format="small"><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="http://aandds.com/blog/eth-delegatecall.html" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Delegatecall and Upgradeable Contract</h2><p>UUPS ( EIP-1822) 和 Transparent Proxy Pattern 基本相似，区别只在于： 对于 UUPS 来说，升级相关代码位于 Implementation 合约（或称 Logic 合约）中，而对于 Transparent Proxy Pattern 来说，升级相关代码位于 Proxy 合约中。 下面是 UUPS 的例子（摘自 https://eips.ethereum.org/EIPS/eip-1822#erc-20-token ）。其中 Proxy 合约为： pragma solidity ^0.5.1; contract Proxy { // Code position in storage is keccak256(&quot;PROXIABLE&quot;) = &quot;0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7&quot; constructor(bytes memory constructData, address contractLogic) public {</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>http://aandds.com</span></div></div></a></div></div><div data-type="embedly" src="https://blog.openzeppelin.com/the-transparent-proxy-pattern/" data="{&quot;provider_url&quot;:&quot;https://www.openzeppelin.com&quot;,&quot;description&quot;:&quot;Much has been discussed around proxy patterns and how to best achieve upgradeability in Ethereum smart contracts. The underlying idea is quite simple: instead of interacting with your smart contract directly... | Smart Contracts Audit&quot;,&quot;title&quot;:&quot;The transparent proxy pattern - OpenZeppelin blog&quot;,&quot;thumbnail_width&quot;:2560,&quot;url&quot;:&quot;https://www.openzeppelin.com/news/the-transparent-proxy-pattern&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/ceff6a73c065e00cf5d9a7b0d89aceb76639e4b0ec6b322cf90eec8cac812453.png&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Openzeppelin&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:1350,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:2560,&quot;height&quot;:1350,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/ceff6a73c065e00cf5d9a7b0d89aceb76639e4b0ec6b322cf90eec8cac812453.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/ceff6a73c065e00cf5d9a7b0d89aceb76639e4b0ec6b322cf90eec8cac812453.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://blog.openzeppelin.com/the-transparent-proxy-pattern/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>The transparent proxy pattern - OpenZeppelin blog</h2><p>Much has been discussed around proxy patterns and how to best achieve upgradeability in Ethereum smart contracts. The underlying idea is quite simple: instead of interacting with your smart contract directly... | Smart Contracts Audit</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://www.openzeppelin.com</span></div><img src="https://storage.googleapis.com/papyrus_images/ceff6a73c065e00cf5d9a7b0d89aceb76639e4b0ec6b322cf90eec8cac812453.png"/></div></a></div></div><div data-type="embedly" src="https://ethereum.stackexchange.com/questions/81994/what-is-the-receive-keyword-in-solidity" data="{&quot;provider_url&quot;:&quot;https://ethereum.stackexchange.com&quot;,&quot;description&quot;:&quot;Solidity has a receive keyword. What is it and how do I use it?&quot;,&quot;title&quot;:&quot;What is the receive keyword in solidity?&quot;,&quot;mean_alpha&quot;:0,&quot;author_name&quot;:&quot;Shane Fontaine&quot;,&quot;url&quot;:&quot;https://ethereum.stackexchange.com/questions/81994/what-is-the-receive-keyword-in-solidity&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/dc7b2a51adecb9025c3c259ca1207f5f2fc8ebc314cfd83fd79e6d0cba01a644.png&quot;,&quot;thumbnail_width&quot;:316,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Ethereum Stack Exchange&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:316,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:316,&quot;height&quot;:316,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/dc7b2a51adecb9025c3c259ca1207f5f2fc8ebc314cfd83fd79e6d0cba01a644.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/dc7b2a51adecb9025c3c259ca1207f5f2fc8ebc314cfd83fd79e6d0cba01a644.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://ethereum.stackexchange.com/questions/81994/what-is-the-receive-keyword-in-solidity" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>What is the receive keyword in solidity?</h2><p>Solidity has a receive keyword. What is it and how do I use it?</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://ethereum.stackexchange.com</span></div><img src="https://storage.googleapis.com/papyrus_images/dc7b2a51adecb9025c3c259ca1207f5f2fc8ebc314cfd83fd79e6d0cba01a644.png"/></div></a></div></div><div data-type="embedly" src="https://docs.openzeppelin.com/contracts/4.x/upgradeable" data="{&quot;provider_url&quot;:&quot;https://docs.openzeppelin.com&quot;,&quot;description&quot;:&quot;The official documentation for OpenZeppelin Libraries and Tools&quot;,&quot;title&quot;:&quot;Using with Upgrades&quot;,&quot;mean_alpha&quot;:89.5066666667,&quot;thumbnail_width&quot;:1200,&quot;url&quot;:&quot;https://docs.openzeppelin.com/contracts/4.x/upgradeable&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/0f782755196baf7d7c54bc0aa1bff0f75fa123c45dd4c557b0b1c2c75bf40bb6.png&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;OpenZeppelin Docs&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:600,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1200,&quot;height&quot;:600,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/0f782755196baf7d7c54bc0aa1bff0f75fa123c45dd4c557b0b1c2c75bf40bb6.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/0f782755196baf7d7c54bc0aa1bff0f75fa123c45dd4c557b0b1c2c75bf40bb6.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://docs.openzeppelin.com/contracts/4.x/upgradeable" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Using with Upgrades</h2><p>The official documentation for OpenZeppelin Libraries and Tools</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://docs.openzeppelin.com</span></div><img src="https://storage.googleapis.com/papyrus_images/0f782755196baf7d7c54bc0aa1bff0f75fa123c45dd4c557b0b1c2c75bf40bb6.png"/></div></a></div></div><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups">https://docs.openzeppelin.com/contracts/4.x/api/proxy#transparent-vs-uups</a></p><div data-type="embedly" src="https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786" data="{&quot;provider_url&quot;:&quot;https://forum.openzeppelin.com&quot;,&quot;description&quot;:&quot;UUPS Proxies: A Tutorial In this tutorial we will deploy an upgradeable contract using the UUPS proxy pattern. We assume some familiarity with Ethereum upgradeable proxies. Introduction to UUPS The original proxies included in OpenZeppelin followed the Transparent Proxy Pattern. While this pattern is still provided, our recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile.&quot;,&quot;title&quot;:&quot;UUPS Proxies: Tutorial (Solidity + JavaScript)&quot;,&quot;author_name&quot;:&quot;frangio&quot;,&quot;thumbnail_width&quot;:1200,&quot;url&quot;:&quot;https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png&quot;,&quot;author_url&quot;:&quot;https://forum.openzeppelin.com/u/frangio&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;OpenZeppelin Forum&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:600,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1200,&quot;height&quot;:600,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://forum.openzeppelin.com/t/uups-proxies-tutorial-solidity-javascript/7786" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>UUPS Proxies: Tutorial (Solidity + JavaScript)</h2><p>UUPS Proxies: A Tutorial In this tutorial we will deploy an upgradeable contract using the UUPS proxy pattern. We assume some familiarity with Ethereum upgradeable proxies. Introduction to UUPS The original proxies included in OpenZeppelin followed the Transparent Proxy Pattern. While this pattern is still provided, our recommendation is now shifting towards UUPS proxies, which are both lightweight and versatile.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://forum.openzeppelin.com</span></div><img src="https://storage.googleapis.com/papyrus_images/f06adb8e2003c8abe57d4cb8b8e417953dad2ec493cbdfbe73358cfb5b7a1887.png"/></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
        <item>
            <title><![CDATA[深入理解合约升级(3) - call 与 delegatecall]]></title>
            <link>https://paragraph.com/@xyyme/3-call-delegatecall</link>
            <guid>P3KynOiLAYFPAC7lEqIy</guid>
            <pubDate>Wed, 18 May 2022 13:16:45 GMT</pubDate>
            <description><![CDATA[call 与 delegatecall 的区别call 和 delegatecall 是 Solidity 中调用外部合约的方法，但是它俩却有挺大的区别。假设 A 合约调用 B 合约，当在 A 合约中使用 call 调用 B 合约时，使用的是 B 合约的上下文，修改的是 B 合约的内存插槽值。而在如果在 A 合约中使用 delegatecall 调用 B 合约，那么在 B 合约的函数执行过程中，使用的是 A 合约的上下文，同时修改的也是 A 合约的内存插槽值。这么说有些抽象，我们来看一个简单的示意图：通过 call 调用通过 delegatecall 调用从上面的图中我们可以看出，在使用 call 调用时，B 合约使用的上下文数据均是 B 本身的。而当使用 delegatecall 调用时，B 合约使用了 A 合约中的上下文数据。我们来写段代码测试一下：pragma solidity 0.8.13; contract A { address public b; constructor(address _b) { b = _b; } function foo() external ...]]></description>
            <content:encoded><![CDATA[<h3 id="h-call-delegatecall" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">call 与 delegatecall 的区别</h3><p><code>call</code> 和 <code>delegatecall</code> 是 Solidity 中调用外部合约的方法，但是它俩却有挺大的区别。假设 A 合约调用 B 合约，当在 A 合约中使用 <code>call</code> 调用 B 合约时，使用的是 B 合约的上下文，修改的是 B 合约的内存插槽值。而在如果在 A 合约中使用 <code>delegatecall</code> 调用 B 合约，那么在 B 合约的函数执行过程中，使用的是 A 合约的上下文，同时修改的也是 A 合约的内存插槽值。这么说有些抽象，我们来看一个简单的示意图：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ee9e40fe735e66c390fd21f913fbde6778b3eb4b41ad02d23bf418cc1078c348.png" alt="通过 call 调用" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">通过 call 调用</figcaption></figure><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/29f6745969e2d350fd85fecc28eb820684e645e21ae0aa6b22356de572fb2bba.png" alt="通过 delegatecall 调用" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">通过 delegatecall 调用</figcaption></figure><p>从上面的图中我们可以看出，在使用 <code>call</code> 调用时，B 合约使用的上下文数据均是 B 本身的。而当使用 <code>delegatecall</code> 调用时，B 合约使用了 A 合约中的上下文数据。我们来写段代码测试一下：</p><pre data-type="codeBlock" text="pragma solidity 0.8.13;


contract A {
    address public b;
    constructor(address _b) {
        b = _b;
    }

    function foo() external {
        (bool success, bytes memory data) = 
            b.call(abi.encodeWithSignature(&quot;foo()&quot;));
        require(success, &quot;Tx failed&quot;);
    }
}

contract B {
    event Log(address sender, address me);
    function foo() external {
        emit Log(msg.sender, address(this));
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.13;</span>


<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">A</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> b;
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _b</span>) </span>{
        b <span class="hljs-operator">=</span> _b;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">foo</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        (<span class="hljs-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> 
            b.<span class="hljs-built_in">call</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodeWithSignature</span>(<span class="hljs-string">"foo()"</span>));
        <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Tx failed"</span>);
    }
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">B</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">Log</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> sender, <span class="hljs-keyword">address</span> me</span>)</span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">foo</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-keyword">emit</span> Log(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    }
}
</code></pre><p>上面代码中，我们在 A 合约中使用 <code>call</code> 调用 B 合约，通过 <code>Log</code> 事件记录一些信息。先部署 B 合约，然后将其地址作为参数部署 A 合约，接着我们调用 <code>foo</code> 函数，可以获取到 <code>Log</code> 事件的内容为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d0c141bc9a7a3e2861adef21f43d9f1533d2916d2fc44565061bd069ff598fa6.png" alt="通过 call 调用" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">通过 call 调用</figcaption></figure><p>与我们前面的说的规则一致，使用 <code>call</code> 调用时，使用的是 B 本身的上下文。接下来我们将 <code>call</code> 改成 <code>delegatecall</code>：</p><pre data-type="codeBlock" text="function foo() external {
    (bool success, bytes memory data) = 
        b.delegatecall(abi.encodeWithSignature(&quot;foo()&quot;));
    require(success, &quot;Tx failed&quot;);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">foo</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    (<span class="hljs-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> 
        b.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodeWithSignature</span>(<span class="hljs-string">"foo()"</span>));
    <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Tx 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/a172e4eeb2a77b6be1a00ad40f272345f58940cf8e8a7671c25ca20971d61cce.png" alt="通过 delegatecall 调用" blurdataurl="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=" nextheight="600" nextwidth="800" class="image-node embed"><figcaption HTMLAttributes="[object Object]" class="">通过 delegatecall 调用</figcaption></figure><p>可以看到当使用了 <code>delegatecall</code> 调用时，使用了 A 合约的上下文。</p><p>上面我们还提到，当使用 <code>delegatecall</code> 时，修改的是调用合约的内存插槽值，这是什么意思呢，我们来看一个例子：</p><pre data-type="codeBlock" text="pragma solidity 0.8.13;


contract A {
    uint256 public alice;
    uint256 public bob;

    address public b;
    constructor(address _b) {
        b = _b;
    }

    function foo(uint256 _alice, uint256 _bob) external {
        (bool success, bytes memory data) = 
            b.delegatecall(abi.encodeWithSignature(&quot;foo(uint256,uint256)&quot;, 
            _alice, _bob));
        require(success, &quot;Tx failed&quot;);
    }
}

contract B {
    uint256 public alice;
    uint256 public bob;
    function foo(uint256 _alice, uint256 _bob) external {
        alice = _alice;
        bob = _bob;
    }
}
"><code><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.13;</span>


<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">A</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> alice;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> bob;

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

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">foo</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _alice, <span class="hljs-keyword">uint256</span> _bob</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        (<span class="hljs-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> 
            b.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodeWithSignature</span>(<span class="hljs-string">"foo(uint256,uint256)"</span>, 
            _alice, _bob));
        <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Tx failed"</span>);
    }
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">B</span> </span>{
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> alice;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> bob;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">foo</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _alice, <span class="hljs-keyword">uint256</span> _bob</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        alice <span class="hljs-operator">=</span> _alice;
        bob <span class="hljs-operator">=</span> _bob;
    }
}
</code></pre><p>这段代码中，我们使用 <code>delegatecall</code> 来调用 <code>foo</code> 函数，<code>foo</code> 函数的作用是给 B 合约的两个变量赋值。但是实际调用后的结果是，A 合约的两个变量被赋值，而 B 中的变量仍为空。这就是我们前面说的，<code>delegatecall</code> 会修改调用合约的内存插槽值，我们来看一个图示：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5353ed8f2691916ed2ce0139f228cf6263e5aba02ea1fae8646249f6e6f09e51.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>在 A 合约中有三个状态变量，B 合约中有两个状态变量。当 A 合约使用 <code>delegatecall</code> 调用 B 合约时，对 B 合约状态变量的赋值会通过插槽顺序分别影响 A 合约的各个变量。也就是说，对 B 合约插槽 0 的变量 <code>alice</code> 赋值，实际上是把值赋给了 A 合约插槽 0 的变量 <code>alice</code>。同理，对 B 合约的第 n 个插槽赋值，实际上会对 A 合约的第 n 个插槽赋值。注意，这里仅仅和插槽顺序有关，而和变量名无关。如果我们将 B 合约改为：</p><pre data-type="codeBlock" text="contract B {
    // 调换了变量声明顺序
    uint256 public bob;
    uint256 public alice;
    function foo(uint256 _alice, uint256 _bob) external {
        // 调换了赋值内容
        bob = _alice;
        alice = _bob;
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">B</span> </span>{
    <span class="hljs-comment">// 调换了变量声明顺序</span>
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> bob;
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> alice;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">foo</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _alice, <span class="hljs-keyword">uint256</span> _bob</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// 调换了赋值内容</span>
        bob <span class="hljs-operator">=</span> _alice;
        alice <span class="hljs-operator">=</span> _bob;
    }
}
</code></pre><p>这段代码中，虽然变量声明以及赋值的顺序调换，但是 <code>foo</code> 的内容仍然是将 <code>_alice</code> 赋值给插槽 0 的变量，将 <code>_bob</code> 赋值给插槽 1 的变量，因此 A 合约的结果不变。</p><h3 id="h-delegatecall" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">delegatecall 在合约升级方面的应用</h3><p>学习理解 <code>delegatecall</code> 是我们后面学习合约升级的基础，合约升级的原理就是代理合约通过 <code>delegatecall</code> 调用逻辑合约，此时逻辑合约的上下文以及数据都是来自于代理合约，那么即使升级，更换了逻辑合约，所有的数据仍然存在于代理合约中，没有影响。可升级合约还有一个限制是，在升级合约时，不能更改已有的状态变量的顺序，如果需要新添变量，只能放在当前所有变量之后，不能在其中插入，原因就是这会改变插槽对应关系，使变量内容混乱。例如，若升级前的插槽为：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/18c28501674ea0cc61c969d549f95dea9fde77b868a8691c119960ef162ecaa0.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>此时，变量 a 和 b 的值分别存储在代理合约的插槽 0，1 中。若添加变量 c，将其放在 a 和 b 中间，那么后续对于 c 的修改实际修改的是 b 的插槽，而对于 b 的修改则是在一个新的插槽上操作，造成数据混乱。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">总结</h3><p><code>delegatecall</code> 会在被调用合约中使用调用合约的上下文，同时影响的是调用合约的内存插槽，这有时会对合约开发带来一些困扰。在使用时，一定要多考虑各方面的影响。同时，<code>delegatecall</code> 也是代理合约升级模式的基石，要理解合约升级，必须要明白这种调用方式的方方面面。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">合约升级系列文章</h3><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/RZscMYGkeGTY8z6ccHseY8HKu-ER3pX0mFYoWXRqXQ0">深入理解合约升级(1) - 概括</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/5eu3_7f7275rqY-fNMUP5BKS8izV9Tshmv8Z5H9bsec">深入理解合约升级(2) - Solidity 内存布局</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/0gmFpVZVlHhwb2YlmaSY8Dyv5r3Z24sKIks38cyQRFk">深入理解合约升级(3) - call 与 delegatecall</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/VSyU0JfmVrcqN-F28tX5mzYjxFFAosl8tDAQX3vB5Dg">深入理解合约升级(4) - 合约升级原理的代码实现</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://mirror.xyz/xyyme.eth/kM9ld2u0D1BpHAfXTiaSPGPtDnOd6vrxJ5_tW4wZVBk">深入理解合约升级(5) - 部署一个可升级合约</a></p></li></ol><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">关于我</h3><p>欢迎<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://linktr.ee/xyymeeth">和我交流</a></p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">参考</h3><div data-type="embedly" src="https://blockchain-academy.hs-mittweida.de/courses/solidity-coding-beginners-to-intermediate/lessons/solidity-5-calling-other-contracts-visibility-state-access/topic/delegatecall/" data="{&quot;provider_url&quot;:&quot;https://blockchain-academy.hs-mittweida.de&quot;,&quot;description&quot;:&quot;This topic focuses on the functionality of delegate calls in Solidity.&quot;,&quot;title&quot;:&quot;DelegateCall() - Blockchain Academy&quot;,&quot;author_name&quot;:&quot;Mario Oettler&quot;,&quot;url&quot;:&quot;https://blockchain-academy.hs-mittweida.de/courses/solidity-coding-beginners-to-intermediate/lessons/solidity-5-calling-other-contracts-visibility-state-access/topic/delegatecall/&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/577cddb808bb986f7f7533ef8a68ee416aa9986b9a646ae020027020e079a23a.webp&quot;,&quot;thumbnail_width&quot;:1024,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Blockchain Academy&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:422,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:1024,&quot;height&quot;:422,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/577cddb808bb986f7f7533ef8a68ee416aa9986b9a646ae020027020e079a23a.webp&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/577cddb808bb986f7f7533ef8a68ee416aa9986b9a646ae020027020e079a23a.webp"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://blockchain-academy.hs-mittweida.de/courses/solidity-coding-beginners-to-intermediate/lessons/solidity-5-calling-other-contracts-visibility-state-access/topic/delegatecall/" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>DelegateCall() - Blockchain Academy</h2><p>This topic focuses on the functionality of delegate calls in Solidity.</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://blockchain-academy.hs-mittweida.de</span></div><img src="https://storage.googleapis.com/papyrus_images/577cddb808bb986f7f7533ef8a68ee416aa9986b9a646ae020027020e079a23a.webp"/></div></a></div></div><div data-type="embedly" src="https://www.anquanke.com/post/id/152590" data="{&quot;provider_url&quot;:&quot;https://www.anquanke.com&quot;,&quot;description&quot;:&quot;安全KER - 安全资讯平台&quot;,&quot;title&quot;:&quot;Solidity中的delegatecall杂谈-安全KER - 安全资讯平台&quot;,&quot;url&quot;:&quot;https://www.anquanke.com/post/id/152590&quot;,&quot;thumbnail_width&quot;:2088,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/812af69f9a537c10cf532c46338d77049202b2d5aaf550500d7653a1330e0451.png&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Anquanke&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:603,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:2088,&quot;height&quot;:603,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/812af69f9a537c10cf532c46338d77049202b2d5aaf550500d7653a1330e0451.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/812af69f9a537c10cf532c46338d77049202b2d5aaf550500d7653a1330e0451.png"/><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="https://www.anquanke.com/post/id/152590" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Solidity中的delegatecall杂谈-安全KER - 安全资讯平台</h2><p>安全KER - 安全资讯平台</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>https://www.anquanke.com</span></div><img src="https://storage.googleapis.com/papyrus_images/812af69f9a537c10cf532c46338d77049202b2d5aaf550500d7653a1330e0451.png"/></div></a></div></div><div data-type="embedly" src="http://aandds.com/blog/eth-delegatecall.html" data="{&quot;provider_url&quot;:&quot;http://aandds.com&quot;,&quot;description&quot;:&quot;UUPS ( EIP-1822) 和 Transparent Proxy Pattern 基本相似，区别只在于： 对于 UUPS 来说，升级相关代码位于 Implementation 合约（或称 Logic 合约）中，而对于 Transparent Proxy Pattern 来说，升级相关代码位于 Proxy 合约中。 下面是 UUPS 的例子（摘自 https://eips.ethereum.org/EIPS/eip-1822#erc-20-token ）。其中 Proxy 合约为： pragma solidity ^0.5.1; contract Proxy { // Code position in storage is keccak256(\&quot;PROXIABLE\&quot;) = \&quot;0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7\&quot; constructor(bytes memory constructData, address contractLogic) public {&quot;,&quot;title&quot;:&quot;Delegatecall and Upgradeable Contract&quot;,&quot;url&quot;:&quot;http://aandds.com/blog/eth-delegatecall.html&quot;,&quot;author_name&quot;:&quot;cig01&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Aandds&quot;,&quot;type&quot;:&quot;link&quot;}" format="small"><div class="react-component embed my-5" data-drag-handle="true" data-node-view-wrapper="" style="white-space:normal"><a class="link-embed-link" href="http://aandds.com/blog/eth-delegatecall.html" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Delegatecall and Upgradeable Contract</h2><p>UUPS ( EIP-1822) 和 Transparent Proxy Pattern 基本相似，区别只在于： 对于 UUPS 来说，升级相关代码位于 Implementation 合约（或称 Logic 合约）中，而对于 Transparent Proxy Pattern 来说，升级相关代码位于 Proxy 合约中。 下面是 UUPS 的例子（摘自 https://eips.ethereum.org/EIPS/eip-1822#erc-20-token ）。其中 Proxy 合约为： pragma solidity ^0.5.1; contract Proxy { // Code position in storage is keccak256(&quot;PROXIABLE&quot;) = &quot;0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7&quot; constructor(bytes memory constructData, address contractLogic) public {</p></div><span><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-link h-3 w-3 my-auto inline mr-1"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>http://aandds.com</span></div></div></a></div></div>]]></content:encoded>
            <author>xyyme@newsletter.paragraph.com (xyyme.eth)</author>
        </item>
    </channel>
</rss>