<?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>Shurong Shen</title>
        <link>https://paragraph.com/@shurong-shen</link>
        <description>undefined</description>
        <lastBuildDate>Mon, 27 Jul 2026 16:51:18 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[Solidity mapping 变量的生日碰撞风险]]></title>
            <link>https://paragraph.com/@shurong-shen/solidity-mapping</link>
            <guid>FuyDolxXmNby6rjHM0wF</guid>
            <pubDate>Wed, 21 Sep 2022 09:57:01 GMT</pubDate>
            <description><![CDATA[生日碰撞生日攻击是一种密码攻击，它利用概率论中生日问题背后的数学原理，极大地降低发生碰撞的计算复杂度。利用生日攻击进行 hash 碰撞便称为生日碰撞。 一个好的设计应该尽量避免生日碰撞的风险，不幸的是 Solidity 的 mapping 变量便存在该风险，截至到本文发表时，所有 Solidity 版本（最新版本为 0.8.17）都存在该问题。这将给数以万亿美元计的链上资产带来潜在的安全隐患。Solidity 的 mapping 实现在 Solidity 中，mapping 变量会根据变量的索引位置（该变量在源文件中定义的顺序位置）和 key 值计算出存储 slot 位置，然后将 value 存储在该位置（注：当 value 类型是 string 等变长数组时，该位置是起始位置）。 设变量索引位置为 i，则一级 mapping(k) 的 slot 位置算法（伪代码，下同）如下：storage_slot = keccak(memory_concat(k,i)); 二级 mapping(k1, k2) 的 slot 位置算法如下：ii = keccak(memory_concat(k...]]></description>
            <content:encoded><![CDATA[<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://en.wikipedia.org/wiki/Birthday_attack">生日攻击</a>是一种密码攻击，它利用概率论中生日问题背后的数学原理，极大地降低发生碰撞的计算复杂度。利用生日攻击进行 hash 碰撞便称为生日碰撞。</p><p>一个好的设计应该尽量避免生日碰撞的风险，不幸的是 Solidity 的 mapping 变量便存在该风险，截至到本文发表时，所有 Solidity 版本（最新版本为 0.8.17）都存在该问题。这将给数以万亿美元计的链上资产带来潜在的安全隐患。</p><h2 id="h-solidity-mapping" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Solidity 的 mapping 实现</h2><p>在 Solidity 中，mapping 变量会根据变量的索引位置（该变量在源文件中定义的顺序位置）和 key 值计算出存储 slot 位置，然后将 value 存储在该位置（注：当 value 类型是 string 等变长数组时，该位置是起始位置）。</p><p>设变量索引位置为 i，则一级 mapping(k) 的 slot 位置算法（伪代码，下同）如下：</p><pre data-type="codeBlock" text="storage_slot = keccak(memory_concat(k,i));
"><code><span class="hljs-attr">storage_slot</span> = keccak(memory_concat(k,i))<span class="hljs-comment">;</span>
</code></pre><p>二级 mapping(k1, k2) 的 slot 位置算法如下：</p><pre data-type="codeBlock" text="ii = keccak(memory_concat(k1,i));
storage_slot = keccak(memory_concat(k2, ii));
"><code><span class="hljs-attr">ii</span> = keccak(memory_concat(k1,i))<span class="hljs-comment">;</span>
<span class="hljs-attr">storage_slot</span> = keccak(memory_concat(k2, ii))<span class="hljs-comment">;</span>
</code></pre><p>多级 mapping 可以以此类推。</p><p>显而易见，如果有两个变量，其存储 slot 位置相同，则会发生存储位置碰撞。一旦发生存储位置碰撞，便可以通过修改一个变量的值来改变另一个变量的值。</p><h2 id="h-erc20" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">ERC20 中的生日碰撞风险</h2><p>在 ERC20 中通常有如下两个变量，前者表示一个地址的余额，后者表示地址A授予地址B操作其账户的额度。</p><pre data-type="codeBlock" text="mapping(address =&gt; uint) public balanceOf;
mapping(address =&gt; mapping(address =&gt; uint)) public allowance;
"><code><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;
</code></pre><p>特别注意，allowance 变量具有如下两个特性：</p><ul><li><p>授予的地址可以任意</p></li><li><p>授予的额度可以任意</p></li></ul><p><strong>我们只需找到一组地址 a1、a2、a3，使得 balanceOf(a1) 的存储 slot 地址等于 allowance(a2, a3)，便可以随意改写 a1 的账户余额！！！</strong></p><p>具体细节，这里不进一步展开，稍作思考便可领会。</p><p>这明显是一个生日碰撞问题，以 Ethereum 256 位强度为例，碰撞的复杂度为 O(2**128)。</p><p>此外，这里虽然只分析了 ERC20，但其他合约显然也很容易存在类似问题。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">改进建议</h2><p>通过前面的分析，我们可以看出，该问题的根源在于 Solidity mapping 变量的底层实现没有从技术上规避生日碰撞风险。</p><p>这里提<strong>一个简单的改进思路</strong>：从 slot 位置划出若干位来存放变量索引值，具体划多少位可以固定一个数，也可以根据合约的 storage 变量总数动态决定。改进后的 slot 位置算法如下：</p><pre data-type="codeBlock" text="p = keccak(memory_concat(k,i);
m1 = bits(i)[x:256-x];
m2 = bits(p)[0:x];
storage_slot = bits_concat(m1, m2));
"><code><span class="hljs-attr">p</span> = keccak(memory_concat(k,i)<span class="hljs-comment">;</span>
<span class="hljs-attr">m1</span> = bits(i)[x:<span class="hljs-number">256</span>-x]<span class="hljs-comment">;</span>
<span class="hljs-attr">m2</span> = bits(p)[<span class="hljs-number">0</span>:x]<span class="hljs-comment">;</span>
<span class="hljs-attr">storage_slot</span> = bits_concat(m1, m2))<span class="hljs-comment">;</span>
</code></pre>]]></content:encoded>
            <author>shurong-shen@newsletter.paragraph.com (Shurong Shen)</author>
        </item>
    </channel>
</rss>